├── .devcontainer └── devcontainer.json ├── .env.sample ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── _assets └── cosine-similarity-search-result.png ├── _docs ├── rag.png ├── session-recommender-architecture.png └── session-recommender.png ├── azure.yaml ├── client ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── Main.tsx │ ├── api │ │ ├── chat.ts │ │ └── sessions.ts │ ├── components │ │ ├── FancyText.tsx │ │ ├── Header.tsx │ │ ├── Navigation.tsx │ │ ├── NoSessions.tsx │ │ ├── PrimaryButton.tsx │ │ ├── Session.tsx │ │ └── SessionsList.tsx │ ├── models.ts │ ├── pages │ │ ├── About.tsx │ │ ├── Chat.tsx │ │ ├── Root.tsx │ │ └── Search.tsx │ ├── site.ts │ └── user.ts ├── staticwebapp.config.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── database ├── Database.Deploy.csproj ├── Program.cs └── sql │ ├── 010-database.sql │ ├── 020-security.sql │ ├── 030-sequence.sql │ ├── 040-tables.sql │ ├── 050-get_sessions_count.sql │ ├── 060-get_embedding.sql │ ├── 070-find_sessions.sql │ ├── 080-update_session_embeddings.sql │ └── 090-update_speaker_embeddings.sql ├── func ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── ChatHandler.cs ├── Program.cs ├── RequestHandler.csproj ├── SessionProcessor.cs ├── host.json └── local.settings.json.sample ├── infra ├── abbreviations.json ├── app │ ├── functions.bicep │ ├── openai.bicep │ ├── sqlserver.bicep │ └── staticwebapp.bicep ├── core │ ├── host │ │ ├── appservice-appsettings.bicep │ │ ├── appservice.bicep │ │ ├── appserviceplan.bicep │ │ └── functions.bicep │ ├── monitor │ │ ├── applicationinsights-dashboard.bicep │ │ ├── applicationinsights.bicep │ │ └── loganalytics.bicep │ ├── security │ │ ├── keyvault-access.bicep │ │ ├── keyvault.bicep │ │ └── role.bicep │ └── storage │ │ └── storage-account.bicep ├── main.bicep └── main.parameters.json ├── scripts ├── install-dev-tools.sh └── ms-repo.pref ├── swa-cli.config.json └── swa-db-connections └── staticwebapp.database.config.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure Developer CLI", 3 | "image": "mcr.microsoft.com/devcontainers/dotnet:8.0-bookworm", 4 | "features": { 5 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 6 | }, 7 | "ghcr.io/devcontainers/features/node:1": { 8 | "version": "18", 9 | "nodeGypDependencies": false 10 | }, 11 | "ghcr.io/azure/azure-dev/azd:latest": {} 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "extensions": [ 16 | "GitHub.vscode-github-actions", 17 | "ms-azuretools.azure-dev", 18 | "ms-azuretools.vscode-azurefunctions", 19 | "ms-azuretools.vscode-bicep", 20 | "ms-azuretools.vscode-docker", 21 | "ms-dotnettools.csharp", 22 | "ms-dotnettools.vscode-dotnet-runtime", 23 | "ms-dotnettools.csdevkit", 24 | "ms-vscode.vscode-node-azure-pack" 25 | ] 26 | } 27 | }, 28 | "postCreateCommand": "bash scripts/install-dev-tools.sh", 29 | "remoteUser": "vscode", 30 | "hostRequirements": { 31 | "memory": "8gb" 32 | } 33 | } -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | MSSQL='Server=.database.windows.net;Initial Catalog=;Persist Security Info=False;User ID=session_recommender_app;Password=unEno!h5!&*KP420xds&@P901afb$^M;MultipleActiveResultSets=False;Encrypt=True;Connection Timeout=30;' 2 | OPENAI_URL='https://.openai.azure.com' 3 | OPENAI_KEY='' 4 | OPENAI_MODEL='text-embedding-ada-002' -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Thanks to: https://rehansaeed.com/gitattributes-best-practices/ 2 | 3 | # Set default behavior to automatically normalize line endings. 4 | * text=auto 5 | 6 | # Force batch scripts to always use CRLF line endings so that if a repo is accessed 7 | # in Windows via a file share from Linux, the scripts will work. 8 | *.{cmd,[cC][mM][dD]} text eol=crlf 9 | *.{bat,[bB][aA][tT]} text eol=crlf 10 | 11 | # Force bash scripts to always use LF line endings so that if a repo is accessed 12 | # in Unix via a file share from Windows, the scripts will work. 13 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | 400 | # Custom 401 | .env 402 | azuredeploy.parameters.json 403 | *.zip 404 | .azure/ 405 | .mono/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureFunctions.pickProcess}" 9 | }, 10 | { 11 | "name": "Run web app", 12 | "type": "node", 13 | "request": "launch", 14 | "cwd": "${workspaceFolder}", 15 | "runtimeExecutable": "swa", 16 | "runtimeArgs": ["start"], 17 | "presentation": { 18 | "hidden": false, 19 | "group": "Frontend", 20 | "order": 1 21 | }, 22 | "preLaunchTask": "npm: install" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "api/bin/Release/net8.0/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~4", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish (functions)" 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean (functions)", 6 | "command": "dotnet", 7 | "args": [ 8 | "clean", 9 | "/property:GenerateFullPaths=true", 10 | "/consoleloggerparameters:NoSummary" 11 | ], 12 | "type": "process", 13 | "problemMatcher": "$msCompile", 14 | "options": { 15 | "cwd": "${workspaceFolder}/api" 16 | } 17 | }, 18 | { 19 | "label": "build (functions)", 20 | "command": "dotnet", 21 | "args": [ 22 | "build", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "type": "process", 27 | "dependsOn": "clean (functions)", 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | }, 32 | "problemMatcher": "$msCompile", 33 | "options": { 34 | "cwd": "${workspaceFolder}/api" 35 | } 36 | }, 37 | { 38 | "label": "clean release (functions)", 39 | "command": "dotnet", 40 | "args": [ 41 | "clean", 42 | "--configuration", 43 | "Release", 44 | "/property:GenerateFullPaths=true", 45 | "/consoleloggerparameters:NoSummary" 46 | ], 47 | "type": "process", 48 | "problemMatcher": "$msCompile", 49 | "options": { 50 | "cwd": "${workspaceFolder}/api" 51 | } 52 | }, 53 | { 54 | "label": "publish (functions)", 55 | "command": "dotnet", 56 | "args": [ 57 | "publish", 58 | "--configuration", 59 | "Release", 60 | "/property:GenerateFullPaths=true", 61 | "/consoleloggerparameters:NoSummary" 62 | ], 63 | "type": "process", 64 | "dependsOn": "clean release (functions)", 65 | "problemMatcher": "$msCompile", 66 | "options": { 67 | "cwd": "${workspaceFolder}/api" 68 | } 69 | }, 70 | { 71 | "type": "func", 72 | "dependsOn": "build (functions)", 73 | "options": { 74 | "cwd": "${workspaceFolder}/api/bin/Debug/net8.0" 75 | }, 76 | "command": "host start", 77 | "isBackground": true, 78 | "problemMatcher": "$func-dotnet-watch" 79 | }, 80 | { 81 | "type": "npm", 82 | "options": { 83 | "cwd": "${workspaceFolder}/client" 84 | }, 85 | "script": "install", 86 | "label": "npm: install" 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - azdeveloper 5 | - csharp 6 | - sql 7 | - tsql 8 | - javascript 9 | - html 10 | - bicep 11 | products: 12 | - azure-functions 13 | - azure-sql-database 14 | - static-web-apps 15 | - sql-server 16 | - azure-sql-managed-instance 17 | - azure-sqlserver-vm 18 | - azure-openai 19 | urlFragment: azure-sql-db-session-recommender-v2 20 | name: Retrieval Augmented Generation with Azure SQL DB and OpenAI 21 | description: Build a session recommender using Jamstack and Event-Driven architecture, using Azure SQL DB to store and search vectors embeddings generated using OpenAI 22 | --- 23 | # Session Assistant Sample - Retrieval Augmented Generation with Azure SQL DB and OpenAI 24 | 25 | This sample demonstrates how to build a session recommender using Jamstack and Event-Driven architecture, using Azure SQL DB to store and search vectors embeddings generated using OpenAI. The solution is built using Azure Static Web Apps, Azure Functions, Azure SQL Database, and Azure OpenAI. 26 | 27 | A fully working, production ready, version of this sample, that has been used at [VS Live](https://vslive.com/) conferences, is available here: https://ai.microsofthq.vslive.com/ 28 | 29 | ![Retrieval Augmented Generator flow](./_docs/rag.png) 30 | 31 | This repository is a evoution of the [Session Recommender](https://github.com/azure-samples/azure-sql-db-session-recommender) sample. In addition to vector search, also Retrieval Augmented Generation (RAG) is used to generate the response to the user query. If you are completely new to this topic, you may want to start there, and then come back here. 32 | 33 | ![Architecture Diagram](./_docs/session-recommender-architecture.png) 34 | 35 | A session recommender built using 36 | 37 | - [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/overview) 38 | - [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/) 39 | - [Azure Functions](https://learn.microsoft.com/azure/azure-functions/functions-overview?pivots=programming-language-csharp) 40 | - [Azure Functions SQL Trigger Binding](https://learn.microsoft.com/azure/azure-functions/functions-bindings-azure-sql-trigger) 41 | - [Azure SQL Database](https://www.sqlservercentral.com/articles/the-sql-developer-experience-beyond-rdbms) 42 | - [Data API builder](https://aka.ms/dab) 43 | 44 | For more details on the solution check also the following articles: 45 | 46 | - [How I built a session recommender in 1 hour using Open AI](https://dev.to/azure/how-i-built-a-session-recommender-in-1-hour-using-open-ai-5419) 47 | - [Vector Similarity Search with Azure SQL database and OpenAI](https://devblogs.microsoft.com/azure-sql/vector-similarity-search-with-azure-sql-database-and-openai/) 48 | 49 | # Native or Classic ? 50 | Azure SQL database can be used to easily and quickly perform vector similarity search. There are two options for this: a native option and a classic option. 51 | 52 | The **native option** uses the new Vector Functions, recently introduced in Azure SQL database. Vector Functions are a set of functions that can be used to perform vector operations directly in the database. 53 | 54 | > [!NOTE] 55 | > Vector Functions are in Public Preview. Learn the details about vectors in Azure SQL here: https://aka.ms/azure-sql-vector-public-preview 56 | 57 | ```sql 58 | DECLARE @embedding VECTOR(1536) 59 | 60 | EXEC [web].[get_embedding] 'I want to learn about security in SQL', @embedding OUTPUT 61 | 62 | SELECT TOP(10) 63 | s.id, 64 | s.title, 65 | s.abstract, 66 | VECTOR_DISTANCE('cosine', @embedding, s.embeddings) AS cosine_distance 67 | FROM 68 | [web].[sessions] s 69 | ORDER BY 70 | cosine_distance 71 | ``` 72 | 73 | The **classic option** uses the classic T-SQL to perform vector operations, with the support for columnstore indexes for getting good performances. 74 | 75 | > [!IMPORTANT] 76 | > This branch (the `main` branch) uses the native vector support in Azure SQL. If you want to use the classic T-SQL, switch to the `classic` branch. 77 | 78 | # Deploy the sample using the Azure Developer CLI (azd) template 79 | 80 | The Azure Developer CLI (`azd`) is a developer-centric command-line interface (CLI) tool for creating Azure applications. 81 | 82 | ## Prerequisites 83 | 84 | - Install [AZD CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd). 85 | - Install [.NET SDK](https://dotnet.microsoft.com/download). 86 | - Install [Node.js](https://nodejs.org/download/). 87 | - Install [SWA CLI](https://azure.github.io/static-web-apps-cli/docs/use/install#installing-the-cli). 88 | 89 | ## Install AZD CLI 90 | 91 | You need to install it before running and deploying with the Azure Developer CLI. 92 | 93 | ### Windows 94 | 95 | ```powershell 96 | powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" 97 | ``` 98 | 99 | ### Linux/MacOS 100 | 101 | ```bash 102 | curl -fsSL https://aka.ms/install-azd.sh | bash 103 | ``` 104 | 105 | After logging in with the following command, you will be able to use azd cli to quickly provision and deploy the application. 106 | 107 | ## Authenticate with Azure 108 | 109 | Make sure AZD CLI can access Azure resources. You can use the following command to log in to Azure: 110 | 111 | ```bash 112 | azd auth login 113 | ``` 114 | 115 | ## Initialize the template 116 | 117 | Then, execute the `azd init` command to initialize the environment (You do not need to run this command if you already have the code or have opened this in a Codespace or DevContainer). 118 | 119 | ```bash 120 | azd init -t Azure-Samples/azure-sql-db-session-recommender-v2 121 | ``` 122 | 123 | Enter an environment name. 124 | 125 | ## Deploy the sample 126 | 127 | Run `azd up` to provision all the resources to Azure and deploy the code to those resources. 128 | 129 | ```bash 130 | azd up 131 | ``` 132 | 133 | Select your desired `subscription` and `location`. Then choose a resource group or create a new resource group. Wait a moment for the resource deployment to complete, click the Website endpoint and you will see the web app page. 134 | 135 | **Note**: Make sure to pick a region where all services are available like, for example, *West Europe* or *East US 2* 136 | 137 | ## GitHub Actions 138 | 139 | Using the Azure Developer CLI, you can setup your pipelines, monitor your application, test and debug locally. 140 | 141 | ```bash 142 | azd pipeline config 143 | ``` 144 | 145 | ## Deploy the database 146 | 147 | Since the database is using features that are in Private Preview, it must be deployed manually. After all resources have been deployed, get the database connection string and OpenAI endpoint and key and create a `.env` file from the `.env.sample` file. Once that is done, go into the `database` folder and run the following command: 148 | 149 | ```bash 150 | dotnet run 151 | ``` 152 | 153 | The .NET application will create the database schema and the required objects. 154 | 155 | ## Test the solution 156 | 157 | Add a new row to the `Sessions` table using the following SQL statement (you can use tools like [Azure Data Studio](https://learn.microsoft.com/en-us/azure-data-studio/quickstart-sql-database) or [SQL Server Management Studio](https://learn.microsoft.com/en-us/azure/azure-sql/database/connect-query-ssms?view=azuresql) to connect to the database. No need to install them if you don't want. In that case you can use the [SQL Editor in the Azure Portal](https://learn.microsoft.com/en-us/azure/azure-sql/database/connect-query-portal?view=azuresql)): 158 | 159 | ```sql 160 | insert into web.speakers 161 | (id, full_name, require_embeddings_update) 162 | values 163 | (5000, 'John Doe', 1) 164 | go 165 | 166 | insert into web.sessions 167 | (id, title, abstract, external_id, start_time, end_time, require_embeddings_update) 168 | values 169 | ( 170 | 1000, 171 | 'Building a session recommender using OpenAI and Azure SQL', 172 | 'In this fun and demo-driven session you''ll learn how to integrate Azure SQL with OpenAI to generate text embeddings, store them in the database, index them and calculate cosine distance to build a session recommender. And once that is done, you’ll publish it as a REST and GraphQL API to be consumed by a modern JavaScript frontend. Sounds pretty cool, uh? Well, it is!', 173 | 'S1', 174 | '2024-06-01 10:00:00', 175 | '2024-06-01 11:00:00', 176 | 1 177 | ) 178 | go 179 | 180 | insert into web.sessions_speakers 181 | (session_id, speaker_id) 182 | values 183 | (1000, 5000) 184 | go 185 | 186 | insert into web.sessions 187 | (id, title, abstract, external_id, start_time, end_time, require_embeddings_update) 188 | values 189 | ( 190 | 1001, 191 | 'Unlock the Art of Pizza Making with John Doe!', 192 | 'Whether you’re an avid home pizza oven enthusiast, contemplating a purchase, or nurturing dreams of launching your very own pizza venture, this course is tailor-made for you! Join John Doe, the visionary behind Great Pizza, as he guides you through the captivating world of pizza craftsmanship. With over six years of experience running his thriving pizza business, John has honed his skills to perfection, earning the title of a master pizzaiolo. Before embarking on his entrepreneurial journey, John—a former chef—also completed a pizza-making course at The School. Now, he’s excited to share his expertise with you in this hands-on workshop. During the course, you’ll learn to create three distinct pizza styles: Neapolitan, thin Roman “Tonda,” and Calzone. Dive into the art of dough preparation, experimenting with both high and low hydration doughs, all while adjusting temperatures to achieve pizza perfection. Don’t miss this opportunity to elevate your pizza-making game and impress your taste buds! ', 193 | 'S2', 194 | '2024-06-01 11:00:00', 195 | '2024-06-01 12:00:00', 196 | 1 197 | ) 198 | go 199 | 200 | insert into web.sessions_speakers 201 | (session_id, speaker_id) 202 | values 203 | (1001, 5000) 204 | go 205 | 206 | ``` 207 | 208 | immediately the deployed Azure Function will get executed in response to the `INSERT` statement. The Azure Function will call the OpenAI service to generate the text embedding for the session title and abstract, and then store the embedding in the database, specifically in the `web.sessions` table. 209 | 210 | ```sql 211 | select * from web.sessions 212 | ``` 213 | 214 | You can now open the URL associated with the created Static Web App to see the session recommender in action. You can get the URL from the Static Web App overview page in the Azure portal. 215 | 216 | ![Website running](./_docs/session-recommender.png) 217 | 218 | ## Run the solution locally 219 | 220 | The whole solution can be executed locally, using [Static Web App CLI](https://github.com/Azure/static-web-apps-cli) and [Azure Function CLI](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-csharp). 221 | 222 | Install the required node packages needed by the fronted: 223 | 224 | ```bash 225 | cd client 226 | npm install 227 | ``` 228 | 229 | once finished, create a `./func/local.settings.json` and `.env` starting from provided samples files, and fill out the settings using the correct values for your environment. 230 | 231 | Go back to the sample root folder and then run: 232 | 233 | ```bash 234 | swa build 235 | ``` 236 | 237 | to build the fronted and then start everything with: 238 | 239 | ```bash 240 | swa start 241 | ``` 242 | 243 | and once the local Static Web App environment is running, you can connect to 244 | 245 | ```text 246 | http://localhost:4280/ 247 | ``` 248 | 249 | and test the solution locally. 250 | 251 | ## Fluent UI 252 | 253 | The solution uses Fluent UI for the UI components. The Fluent UI is a collection of UX frameworks from Microsoft that provides a consistent design language for web, mobile, and desktop applications. More details about Fluent UI can be found at the following links: 254 | 255 | - https://github.com/microsoft/fluentui 256 | - https://react.fluentui.dev/ 257 | 258 | ## Credits 259 | 260 | Thanks a lot to [Aaron Powell](https://www.aaron-powell.com/) for having helped in building the RAG sample, doing a complete UI revamp using the Fluent UI and for the implementaiton of the `ask` endpoint. 261 | -------------------------------------------------------------------------------- /_assets/cosine-similarity-search-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-sql-db-session-recommender-v2/ad8389940e01913a7572c97bb425aefe4ecad8e0/_assets/cosine-similarity-search-result.png -------------------------------------------------------------------------------- /_docs/rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-sql-db-session-recommender-v2/ad8389940e01913a7572c97bb425aefe4ecad8e0/_docs/rag.png -------------------------------------------------------------------------------- /_docs/session-recommender-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-sql-db-session-recommender-v2/ad8389940e01913a7572c97bb425aefe4ecad8e0/_docs/session-recommender-architecture.png -------------------------------------------------------------------------------- /_docs/session-recommender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-sql-db-session-recommender-v2/ad8389940e01913a7572c97bb425aefe4ecad8e0/_docs/session-recommender.png -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: azure-sql-db-session-recommender-v2 4 | metadata: 5 | template: azure-sql-db-session-recommender-v2 6 | services: 7 | web: 8 | project: ./client 9 | language: js 10 | host: staticwebapp 11 | dist: dist 12 | functionapp: 13 | project: ./func 14 | language: dotnet 15 | host: function -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Conference AI Assistant 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@fluentui/react": "^8.115.5", 14 | "@fluentui/react-components": "^9.38.0", 15 | "dayjs": "^1.11.10", 16 | "localforage": "^1.10.0", 17 | "localstorage-slim": "^2.7.0", 18 | "match-sorter": "^6.3.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-markdown": "^9.0.1", 22 | "react-router-dom": "^6.16.0", 23 | "sort-by": "^1.2.0" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "^18.2.15", 27 | "@types/react-dom": "^18.2.7", 28 | "@typescript-eslint/eslint-plugin": "^6.10.0", 29 | "@typescript-eslint/parser": "^6.10.0", 30 | "@vitejs/plugin-react": "^4.0.3", 31 | "eslint": "^8.45.0", 32 | "eslint-plugin-react": "^7.32.2", 33 | "eslint-plugin-react-hooks": "^4.6.0", 34 | "eslint-plugin-react-refresh": "^0.4.3", 35 | "typescript": "^5.2.2", 36 | "vite": "^4.4.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 4 | import { FluentProvider, webLightTheme } from "@fluentui/react-components"; 5 | 6 | import Root from "./pages/Root"; 7 | import SessionSearch, { loader as sessionsListLoader } from "./pages/Search"; 8 | import { Chat, action as chatAction } from "./pages/Chat"; 9 | import { About, loader as aboutLoader } from "./pages/About"; 10 | 11 | const router = createBrowserRouter([ 12 | { 13 | path: "/", 14 | element: , 15 | children: [ 16 | { 17 | index: true, 18 | element: , 19 | action: chatAction, 20 | }, 21 | { 22 | index: false, 23 | element: , 24 | path: "/search", 25 | loader: sessionsListLoader, 26 | }, 27 | { 28 | index: false, 29 | element: , 30 | path: "/about", 31 | loader: aboutLoader, 32 | }, 33 | ], 34 | }, 35 | ]); 36 | 37 | ReactDOM.createRoot(document.getElementById("root")!).render( 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | -------------------------------------------------------------------------------- /client/src/api/chat.ts: -------------------------------------------------------------------------------- 1 | import { AskResponse } from "../models"; 2 | import { json } from 'react-router-dom'; 3 | 4 | type ChatTurn = { 5 | userPrompt: string; 6 | responseMessage?: string; 7 | }; 8 | 9 | type UserQuestion = { 10 | question: string; 11 | askedOn: Date; 12 | }; 13 | 14 | let questionAndAnswers: Record = {}; 15 | 16 | export const ask = async (prompt: string) => { 17 | const history: ChatTurn[] = []; 18 | const currentMessageId = Date.now(); 19 | const currentQuestion = { 20 | question: prompt, 21 | askedOn: new Date(), 22 | }; 23 | questionAndAnswers[currentMessageId] = [currentQuestion, undefined]; 24 | 25 | history.push({ 26 | userPrompt: currentQuestion.question 27 | }); 28 | 29 | const response = await fetch("/api/ask", { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | }, 34 | body: JSON.stringify(history), 35 | }) 36 | 37 | if (response.ok) { 38 | const askResponse: AskResponse = await response.json(); 39 | questionAndAnswers[currentMessageId] = [ 40 | currentQuestion, 41 | { 42 | answer: askResponse.answer, 43 | thoughts: askResponse.thoughts, 44 | dataPoints: askResponse.dataPoints, 45 | citationBaseUrl: askResponse.citationBaseUrl, 46 | } 47 | ]; 48 | } else { 49 | throw json(response.statusText, response.status); 50 | } 51 | 52 | return await Promise.resolve(questionAndAnswers); 53 | }; 54 | -------------------------------------------------------------------------------- /client/src/api/sessions.ts: -------------------------------------------------------------------------------- 1 | export type ErrorInfo = { 2 | errorSource?: string; 3 | errorCode?: number; 4 | errorMessage: string; 5 | }; 6 | 7 | export type SessionInfo = { 8 | id: string; 9 | external_id: string; 10 | title: string; 11 | abstract: string; 12 | start_time: string; 13 | end_time: string; 14 | cosine_similarity: number; 15 | speakers: string; 16 | }; 17 | 18 | export type SessionsResponse = { 19 | sessions: SessionInfo[]; 20 | errorInfo?: ErrorInfo; 21 | }; 22 | 23 | export async function getSessions(content: string): Promise { 24 | const settings = { 25 | method: "post", 26 | headers: { 27 | Accept: "application/json", 28 | "Content-Type": "application/json", 29 | }, 30 | body: JSON.stringify({ 31 | text: content, 32 | }), 33 | }; 34 | 35 | const response = await fetch("/data-api/rest/find", settings); 36 | if (!response.ok) { 37 | return { 38 | sessions: [], 39 | errorInfo: { 40 | errorSource: "Server", 41 | errorCode: response.status, 42 | errorMessage: response.statusText, 43 | }, 44 | }; 45 | } 46 | 47 | var sessions = []; 48 | var errorInfo = undefined; 49 | const data = await response.json(); 50 | 51 | if (data.value.length > 0) { 52 | if (data.value[0].error_code) { 53 | errorInfo = { 54 | errorSource: data.value[0].error_source as string, 55 | errorCode: data.value[0].error_code as number, 56 | errorMessage: data.value[0].error_message as string, 57 | }; 58 | } else { 59 | sessions = data.value; 60 | } 61 | } 62 | 63 | return { sessions: sessions, errorInfo: errorInfo }; 64 | } 65 | 66 | export async function getSessionsCount(): Promise { 67 | const response = await fetch("/data-api/rest/sessions-count"); 68 | if (!response.ok) return "n/a"; 69 | const data = await response.json(); 70 | const totalCount = data ? data.value[0].total_sessions : "n/a"; 71 | return totalCount; 72 | } 73 | -------------------------------------------------------------------------------- /client/src/components/FancyText.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Text, TextProps } from "@fluentui/react-components"; 2 | 3 | const useStyles = makeStyles({ 4 | fancy: { 5 | fontSize: "1.125rem", 6 | fontFamily: "var(--base-font-family)", 7 | fontWeight: 600, 8 | fontStyle: "normal", 9 | lineHeight: "1.688rem", 10 | marginTop: "-0.1rem", 11 | textDecorationColor: "none", 12 | textDecorationLine: "none", 13 | textTransform: "none", 14 | color: "var(--color-title-font)", 15 | }, 16 | }); 17 | 18 | export const FancyText = ({ 19 | children, 20 | className, 21 | block, 22 | as, 23 | ...rest 24 | }: TextProps) => { 25 | const classes = useStyles(); 26 | return ( 27 | 33 | {children} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Title1 } from "@fluentui/react-components"; 2 | import siteConfig from "../site"; 3 | 4 | export const Header = () => { 5 | return ( 6 | 7 | {siteConfig.name} 🤖 RAG Sample 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /client/src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, Tab, TabList } from "@fluentui/react-components"; 2 | import { SearchRegular, ChatRegular, InfoRegular } from "@fluentui/react-icons"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | export const Navigation = () => { 6 | const navigate = useNavigate(); 7 | 8 | return ( 9 | <> 10 | { 12 | navigate(data.value === "chat" ? "/" : `/${data.value}`); 13 | }} 14 | selectedValue={ 15 | window.location.pathname === "/" ? "chat" : window.location.pathname.substring(1) 16 | } 17 | > 18 | }> 19 | Ask 20 | 21 | }> 22 | Search 23 | 24 | }> 25 | About 26 | 27 | 28 |
29 | 30 |
31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /client/src/components/NoSessions.tsx: -------------------------------------------------------------------------------- 1 | export function NoSessions() { 2 | return ( 3 |
4 |

5 | No session found 6 |

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /client/src/components/PrimaryButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps, makeStyles } from "@fluentui/react-components"; 2 | 3 | const useStyles = makeStyles({ 4 | button: { 5 | boxShadow: "0 0 1px #0009, 0 1px 2px #0003", 6 | }, 7 | }); 8 | 9 | export const PrimaryButton = ({ children, ...rest }: ButtonProps) => { 10 | const classes = useStyles(); 11 | return ( 12 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/components/Session.tsx: -------------------------------------------------------------------------------- 1 | import { SessionInfo } from "../api/sessions"; 2 | import { Text, Title2 } from "@fluentui/react-components"; 3 | import { FancyText } from "./FancyText"; 4 | import dayjs from "dayjs"; 5 | import siteConfig from "../site"; 6 | 7 | function formatSubtitle(session: SessionInfo) { 8 | const speakers = JSON.parse(session.speakers).join(", "); 9 | 10 | const startTime = dayjs(session.start_time); 11 | const endTime = dayjs(session.end_time); 12 | 13 | const day = startTime.format("dddd") 14 | const start = startTime.format("hh:mm A"); 15 | const end = endTime.format("hh:mm A"); 16 | 17 | return `${speakers} | ${day}, ${start}-${end} | Similarity: ${session.cosine_similarity.toFixed(6)}`; 18 | } 19 | 20 | function formatSessionLink(session: SessionInfo) { 21 | const url = new URL(`#${session.external_id}`, siteConfig.sessionUrl); 22 | 23 | return url.toString(); 24 | } 25 | 26 | export const Session = ({ session }: { session: SessionInfo }) => { 27 | return ( 28 |
29 | 30 | 31 | {session.title} 32 | 33 | 34 | 35 | {formatSubtitle(session)} 36 | 37 | 38 | {session.abstract} 39 | 40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/components/SessionsList.tsx: -------------------------------------------------------------------------------- 1 | import { SessionInfo } from "../api/sessions"; 2 | import { Session } from "./Session"; 3 | 4 | export const SessionList = ({ sessions }: { sessions: SessionInfo[] }) => { 5 | return ( 6 |
7 | {sessions.map((session) => ( 8 | 9 | ))} 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/models.ts: -------------------------------------------------------------------------------- 1 | export type SupportingContentRecord = { 2 | title: string; 3 | content: string; 4 | url: string; 5 | similarity: number; 6 | }; 7 | 8 | export type AskResponse = { 9 | answer: string; 10 | thoughts?: string; 11 | dataPoints: SupportingContentRecord[]; 12 | citationBaseUrl: string; 13 | error?: string | null; 14 | }; 15 | -------------------------------------------------------------------------------- /client/src/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import { Await, LoaderFunction, defer, useLoaderData } from "react-router-dom"; 3 | import ls from "localstorage-slim"; 4 | import { getSessionsCount } from "../api/sessions"; 5 | import { FancyText } from "../components/FancyText"; 6 | import siteConfig from "../site"; 7 | 8 | function showSessionCount( 9 | sessionsCount: string | undefined | null = undefined 10 | ) { 11 | var sc = sessionsCount; 12 | if (sc === undefined) { 13 | sc = ls.get("sessionsCount"); 14 | console.log("sessionsCount", sc); 15 | } else { 16 | ls.set("sessionsCount", sc, { ttl: 60 * 60 * 24 * 7 }); 17 | } 18 | if (sc == null) { 19 | return Loading session count...; 20 | } 21 | return ( 22 | 23 | There are {sc} sessions indexed so far. 24 | 25 | ); 26 | } 27 | 28 | export const loader: LoaderFunction = async () => { 29 | const sessionsCount = getSessionsCount(); 30 | return defer({ sessionsCount }); 31 | }; 32 | 33 | export const About = () => { 34 | const { sessionsCount } = useLoaderData() as { 35 | sessionsCount: string | number; 36 | }; 37 | 38 | return ( 39 | <> 40 | 41 | Source code and and related articles are available on GitHub.{" "} 42 | The AI model used generate embeddings is the text-embedding-ada-002 and the AI model used to process and generate natural language content is gpt-35-turbo. 43 | 44 | 45 | Unable to load session count 😥... 49 | } 50 | > 51 | {(sessionsCount) => showSessionCount(sessionsCount)} 52 | 53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /client/src/pages/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Textarea, 4 | TextareaProps, 5 | makeStyles, 6 | Spinner, 7 | Title2 8 | } from "@fluentui/react-components"; 9 | import { SendRegular } from "@fluentui/react-icons"; 10 | import { useState } from "react"; 11 | import { ActionFunctionArgs, isRouteErrorResponse, useFetcher, useRouteError } from "react-router-dom"; 12 | import { ask } from "../api/chat"; 13 | import { FancyText } from "../components/FancyText"; 14 | import { PrimaryButton } from "../components/PrimaryButton"; 15 | import ReactMarkdown from "react-markdown"; 16 | 17 | var isThinking:boolean = false; 18 | var intervalId = 0 19 | var thinkingTicker = 0; 20 | var thinkingMessages:string[] = [ 21 | "Analyzing the question...", 22 | "Thinking...", 23 | "Querying the database...", 24 | "Extracting embeddings...", 25 | "Finding vectors in the latent space...", 26 | "Identifying context...", 27 | "Analyzing results...", 28 | "Finding the best answer...", 29 | "Formulating response...", 30 | "Double checking the answer...", 31 | "Correcting spelling...", 32 | "Doing an internal review...", 33 | "Checking for errors...", 34 | "Validating the answer...", 35 | "Adding more context...", 36 | "Analyzing potential response...", 37 | "Re-reading the original question...", 38 | "Adding more details...", 39 | "Improving the answer...", 40 | "Making it nice and polished...", 41 | "Removing typos...", 42 | "Adding punctuation...", 43 | "Checking grammar...", 44 | "Adding context...", 45 | "Sending response..." 46 | ] 47 | 48 | const useClasses = makeStyles({ 49 | container: {}, 50 | chatArea: {}, 51 | card: {}, 52 | rm: { marginBottom: "-1em", marginTop: "-1em"}, 53 | answersArea: { marginTop: "1em"}, 54 | textarea: { width: "100%", marginBottom: "1rem" }, 55 | }); 56 | 57 | export async function action({ request }: ActionFunctionArgs) { 58 | let formData = await request.formData(); 59 | const prompt = formData.get("prompt"); 60 | if (!prompt) { 61 | return null; 62 | } 63 | 64 | const data = await ask(prompt.toString()); 65 | return data; 66 | } 67 | 68 | const Answers = ({ data }: { data: Awaited> }) => { 69 | if (!data) { 70 | return null; 71 | } 72 | const components = []; 73 | const classes = useClasses(); 74 | 75 | var cid:number = 0 76 | for (const id in data) { cid = Number(id) } 77 | const [question, answer] = data[cid]; 78 | 79 | components.push( 80 | 81 | Your question 82 | 83 | {question.question} 84 | 85 | My answer 86 | 87 | {answer?.answer} 88 | 89 | My thoughts 90 | 91 | {answer?.thoughts} 92 | 93 | 94 | ); 95 | 96 | return <>{components}; 97 | }; 98 | 99 | export const Chat = () => { 100 | const fetcher = useFetcher>>(); 101 | const classes = useClasses(); 102 | 103 | const [thinking, setThinking] = useState(thinkingMessages[0]); 104 | const [prompt, setPrompt] = useState(""); 105 | 106 | const submitting = fetcher.state !== "idle"; 107 | const data = fetcher.data; 108 | 109 | const onChange: TextareaProps["onChange"] = (_, data) => 110 | setPrompt(() => data.value); 111 | 112 | const onKeyDown: TextareaProps["onKeyDown"] = (e) => { 113 | if (!prompt) { 114 | return; 115 | } 116 | 117 | if (e.key === "Enter" && !e.shiftKey) { 118 | const formData = new FormData(); 119 | formData.append("prompt", prompt); 120 | fetcher.submit(formData, { method: "POST" }); 121 | } 122 | }; 123 | 124 | if (submitting && !isThinking) { 125 | isThinking = true; 126 | thinkingTicker = 0; 127 | setThinking(thinkingMessages[thinkingTicker]); 128 | const updateThinking = () => { 129 | thinkingTicker += 1; 130 | var i = thinkingTicker > thinkingMessages.length - 1 ? 0 : thinkingTicker; 131 | setThinking(thinkingMessages[i]); 132 | } 133 | intervalId = setInterval(updateThinking, 2000); 134 | } 135 | 136 | if (!submitting && isThinking) { 137 | isThinking = false; 138 | clearInterval(intervalId); 139 | setThinking(thinkingMessages[0]); 140 | } 141 | 142 | return ( 143 |
144 |
145 | 146 | <> 147 | Ask questions to the AI model in natural language and get meaningful answers 148 | to help you navigate the conferences sessions and find the best ones for you. 149 | Thanks to Prompt Engineering and Retrieval Augmented Generation (RAG) finding 150 | details and recommendations on what session to attend is easier than ever. 151 | 152 | 153 |
154 |
155 | 156 | 168 | } 170 | disabled={submitting || !prompt} 171 | > 172 | Ask 173 | 174 | {submitting && } 175 | 176 |
177 |
178 | {!submitting && data && } 179 |
180 |
181 | ); 182 | }; 183 | 184 | export const ChatError = () => { 185 | const error = useRouteError(); 186 | console.error(error); 187 | if (isRouteErrorResponse(error)) { 188 | return( 189 |
190 | 191 | {error.status} - {error.statusText} {error.data.statusText} 192 | 193 | 194 | Sorry, there was a problem while processing your request. Please try again. 195 | 196 |
197 | ) 198 | } 199 | else { 200 | throw error; 201 | } 202 | } -------------------------------------------------------------------------------- /client/src/pages/Root.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import { makeStyles, shorthands } from "@fluentui/react-components"; 3 | import { Header } from "../components/Header"; 4 | import { Navigation } from "../components/Navigation"; 5 | 6 | const margin = shorthands.margin("1rem", "3rem", "1rem"); 7 | const useStyles = makeStyles({ 8 | root: { 9 | display: "grid", 10 | gridTemplateRows: "auto 1fr", 11 | gridTemplateAreas: ` 12 | "header" 13 | "main" 14 | `, 15 | height: `calc(100vh - ${margin.marginTop} - ${margin.marginBottom})`, 16 | ...margin, 17 | }, 18 | }); 19 | 20 | export default function Root() { 21 | const classes = useStyles(); 22 | return ( 23 | <> 24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /client/src/pages/Search.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Spinner } from "@fluentui/react-components"; 2 | import { Search24Regular } from "@fluentui/react-icons"; 3 | import { 4 | Form, 5 | LoaderFunction, 6 | useLoaderData, 7 | useNavigation, 8 | } from "react-router-dom"; 9 | import { NoSessions } from "../components/NoSessions"; 10 | import { SessionList } from "../components/SessionsList"; 11 | import type { ErrorInfo, SessionInfo } from "../api/sessions"; 12 | import { getSessions } from "../api/sessions"; 13 | import { FancyText } from "../components/FancyText"; 14 | import { PrimaryButton } from "../components/PrimaryButton"; 15 | 16 | type LoaderData = { 17 | sessions: SessionInfo[]; 18 | searchQuery: string; 19 | isSearch: boolean; 20 | errorInfo: ErrorInfo | null; 21 | }; 22 | 23 | const SEARCH_INPUT_ID = "q"; 24 | 25 | export const loader: LoaderFunction = async ({ request }) => { 26 | const url = new URL(request.url); 27 | const searchQuery = url.searchParams.get(SEARCH_INPUT_ID) ?? ""; 28 | const isSearch = searchQuery !== ""; 29 | 30 | if (!isSearch) { 31 | return { sessions: [] }; 32 | } 33 | 34 | let { sessions, errorInfo } = await getSessions(searchQuery); 35 | if (!Array.isArray(sessions)) { 36 | errorInfo = { errorMessage: "Error: sessions is not an array" }; 37 | sessions = []; 38 | } 39 | return { sessions, searchQuery, isSearch, errorInfo }; 40 | }; 41 | 42 | export default function SessionSearch() { 43 | const { sessions, searchQuery, isSearch, errorInfo } = useLoaderData() as LoaderData; 44 | const navigation = useNavigation(); 45 | 46 | const searching = 47 | navigation.location && 48 | new URLSearchParams(navigation.location.search).has(SEARCH_INPUT_ID); 49 | 50 | return ( 51 | <> 52 | 53 | <> 54 | Use OpenAI to search for interesting sessions. Write the topic you're 55 | interested in, and (up to) the top ten most interesting and related 56 | session will be returned. The search is done using text embeddings and 57 | then using cosine similarity to find the most similar sessions. 58 | 59 | 60 | 85 |
89 | {!errorInfo ? ( 90 | "" 91 | ) : ( 92 |

93 | {"Error" + errorInfo.errorMessage} 94 |

95 | )} 96 | {sessions.length > 0 && } 97 | {sessions.length === 0 && isSearch && } 98 |
99 | 100 | ); 101 | } 102 | 103 | -------------------------------------------------------------------------------- /client/src/site.ts: -------------------------------------------------------------------------------- 1 | const siteConfig = { 2 | name: 'Cool SQL+AI Conference', 3 | website: location.origin, 4 | sessionUrl: location.origin 5 | } 6 | 7 | export default siteConfig; -------------------------------------------------------------------------------- /client/src/user.ts: -------------------------------------------------------------------------------- 1 | export async function getUserInfo() 2 | { 3 | const response = await fetch('/.auth/me'); 4 | const payload = await response.json(); 5 | const { clientPrincipal } = payload; 6 | return clientPrincipal; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /client/staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationFallback": { 3 | "rewrite": "/" 4 | } 5 | } -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /database/Database.Deploy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /database/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using DbUp; 5 | using DbUp.ScriptProviders; 6 | using DotNetEnv; 7 | using Microsoft.Data.SqlClient; 8 | 9 | namespace Database.Deploy 10 | { 11 | class Program 12 | { 13 | static int Main(string[] args) 14 | { 15 | // This will load the content of .env and create related environment variables 16 | DotNetEnv.Env.Load("../.env"); 17 | 18 | // Connection string for deploying the database (high-privileged account as it needs to be able to CREATE/ALTER/DROP) 19 | var connectionString = Env.GetString("MSSQL"); 20 | 21 | if (string.IsNullOrEmpty(connectionString)) { 22 | Console.WriteLine("ERROR: 'MSSQL' enviroment variable not set or empty."); 23 | Console.WriteLine("You can create an .env file in parent folder that sets the 'MSSQL' environment variable; then run this app again."); 24 | return 1; 25 | } 26 | 27 | var csb = new SqlConnectionStringBuilder(connectionString); 28 | Console.WriteLine($"Deploying database: {csb.InitialCatalog}"); 29 | 30 | Console.WriteLine("Testing connection..."); 31 | var conn = new SqlConnection(csb.ToString()); 32 | conn.Open(); 33 | conn.Close(); 34 | 35 | FileSystemScriptOptions options = new() { 36 | IncludeSubDirectories = false, 37 | Extensions = ["*.sql"], 38 | Encoding = Encoding.UTF8 39 | }; 40 | 41 | Dictionary variables = new() { 42 | {"OPENAI_URL", Env.GetString("OPENAI_URL")}, 43 | {"OPENAI_KEY", Env.GetString("OPENAI_KEY")}, 44 | {"OPENAI_MODEL", Env.GetString("OPENAI_MODEL")} 45 | }; 46 | 47 | Console.WriteLine("Starting deployment..."); 48 | var dbup = DeployChanges.To 49 | .SqlDatabase(csb.ConnectionString) 50 | .WithVariables(variables) 51 | .WithScriptsFromFileSystem("sql", options) 52 | .JournalToSqlTable("dbo", "$__dbup_journal") 53 | .LogToConsole() 54 | .Build(); 55 | 56 | var result = dbup.PerformUpgrade(); 57 | 58 | if (!result.Successful) 59 | { 60 | Console.WriteLine(result.Error); 61 | return -1; 62 | } 63 | 64 | Console.WriteLine("Success!"); 65 | return 0; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /database/sql/010-database.sql: -------------------------------------------------------------------------------- 1 | ALTER DATABASE CURRENT 2 | SET CHANGE_TRACKING = ON 3 | (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON) -------------------------------------------------------------------------------- /database/sql/020-security.sql: -------------------------------------------------------------------------------- 1 | if not exists(select * from sys.symmetric_keys where [name] = '##MS_DatabaseMasterKey##') 2 | begin 3 | create master key encryption by password = N'V3RYStr0NGP@ssw0rd!'; 4 | end 5 | go 6 | 7 | if exists(select * from sys.[database_scoped_credentials] where name = '$OPENAI_URL$') 8 | begin 9 | drop database scoped credential [$OPENAI_URL$]; 10 | end 11 | go 12 | 13 | create database scoped credential [$OPENAI_URL$] 14 | with identity = 'HTTPEndpointHeaders', secret = '{"api-key":"$OPENAI_KEY$"}'; 15 | go 16 | 17 | create schema [web] AUTHORIZATION [dbo]; 18 | go 19 | 20 | -------------------------------------------------------------------------------- /database/sql/030-sequence.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE [web].[global_id] 2 | AS INT 3 | START WITH 1 4 | INCREMENT BY 1; 5 | GO 6 | 7 | -------------------------------------------------------------------------------- /database/sql/040-tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE [web].[searched_text] 2 | ( 3 | [id] INT IDENTITY (1, 1) NOT NULL, 4 | [searched_text] NVARCHAR (MAX) NOT NULL, 5 | [search_datetime] DATETIME2 (7) DEFAULT (sysdatetime()) NOT NULL, 6 | [ms_rest_call] INT NULL, 7 | [ms_vector_search] INT NULL, 8 | [found_sessions] INT NULL, 9 | 10 | PRIMARY KEY CLUSTERED ([id] ASC) 11 | ); 12 | GO 13 | 14 | CREATE TABLE [web].[sessions] 15 | ( 16 | [id] INT DEFAULT (NEXT VALUE FOR [web].[global_id]) NOT NULL, 17 | [title] NVARCHAR (200) NOT NULL, 18 | [abstract] NVARCHAR (MAX) NOT NULL, 19 | [external_id] VARCHAR (100) COLLATE Latin1_General_100_BIN2 NOT NULL, 20 | [last_fetched] DATETIME2 (7) NULL, 21 | [start_time] DATETIME2 (0) NOT NULL, 22 | [end_time] DATETIME2 (0) NOT NULL, 23 | [tags] NVARCHAR (MAX) NULL, 24 | [recording_url] VARCHAR (1000) NULL, 25 | [require_embeddings_update] BIT DEFAULT ((0)) NOT NULL, 26 | [embeddings] VECTOR(1536) NULL, 27 | 28 | PRIMARY KEY CLUSTERED ([id] ASC), 29 | CHECK (isjson([tags])=(1)), 30 | UNIQUE NONCLUSTERED ([title] ASC) 31 | ); 32 | GO 33 | 34 | CREATE TABLE [web].[speakers] 35 | ( 36 | [id] INT DEFAULT (NEXT VALUE FOR [web].[global_id]) NOT NULL, 37 | [external_id] VARCHAR (100) COLLATE Latin1_General_100_BIN2 NULL, 38 | [full_name] NVARCHAR (100) NOT NULL, 39 | [require_embeddings_update] BIT DEFAULT ((0)) NOT NULL, 40 | [embeddings] VECTOR(1536) NULL, 41 | 42 | PRIMARY KEY CLUSTERED ([id] ASC), 43 | UNIQUE NONCLUSTERED ([full_name] ASC) 44 | ); 45 | GO 46 | 47 | CREATE TABLE [web].[sessions_speakers] ( 48 | [session_id] INT NOT NULL, 49 | [speaker_id] INT NOT NULL, 50 | 51 | PRIMARY KEY CLUSTERED ([session_id] ASC, [speaker_id] ASC), 52 | CONSTRAINT fk__sessions_speakers__sessions FOREIGN KEY ([session_id]) REFERENCES [web].[sessions] ([id]), 53 | CONSTRAINT fk__sessions_speakers__speakers FOREIGN KEY ([speaker_id]) REFERENCES [web].[speakers] ([id]) 54 | ); 55 | GO 56 | 57 | CREATE NONCLUSTERED INDEX [ix2] 58 | ON [web].[sessions_speakers]([speaker_id] ASC); 59 | GO 60 | 61 | ALTER TABLE [web].[sessions] ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF); 62 | GO 63 | 64 | ALTER TABLE [web].[speakers] ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF); 65 | GO 66 | 67 | -------------------------------------------------------------------------------- /database/sql/050-get_sessions_count.sql: -------------------------------------------------------------------------------- 1 | create or alter procedure [web].[get_sessions_count] 2 | as 3 | select count(*) as total_sessions from [web].[sessions]; 4 | GO 5 | 6 | -------------------------------------------------------------------------------- /database/sql/060-get_embedding.sql: -------------------------------------------------------------------------------- 1 | create or alter procedure [web].[get_embedding] 2 | @inputText nvarchar(max), 3 | @embedding vector(1536) output 4 | as 5 | begin try 6 | declare @retval int; 7 | declare @payload nvarchar(max) = json_object('input': @inputText); 8 | declare @response nvarchar(max) 9 | exec @retval = sp_invoke_external_rest_endpoint 10 | @url = '$OPENAI_URL$/openai/deployments/$OPENAI_MODEL$/embeddings?api-version=2023-03-15-preview', 11 | @method = 'POST', 12 | @credential = [$OPENAI_URL$], 13 | @payload = @payload, 14 | @response = @response output; 15 | end try 16 | begin catch 17 | select 18 | 'SQL' as error_source, 19 | error_number() as error_code, 20 | error_message() as error_message 21 | return; 22 | end catch 23 | 24 | if (@retval != 0) begin 25 | select 26 | 'OPENAI' as error_source, 27 | json_value(@response, '$.result.error.code') as error_code, 28 | json_value(@response, '$.result.error.message') as error_message, 29 | @response as error_response 30 | return; 31 | end; 32 | 33 | declare @re nvarchar(max) = json_query(@response, '$.result.data[0].embedding') 34 | set @embedding = cast(@re as vector(1536)); 35 | 36 | return @retval 37 | go -------------------------------------------------------------------------------- /database/sql/070-find_sessions.sql: -------------------------------------------------------------------------------- 1 | create or alter procedure [web].[find_sessions] 2 | @text nvarchar(max), 3 | @top int = 10, 4 | @min_similarity decimal(19,16) = 0.30 5 | as 6 | if (@text is null) return; 7 | 8 | insert into web.searched_text (searched_text) values (@text); 9 | declare @sid int = scope_identity(); 10 | 11 | declare @startTime as datetime2(7) = sysdatetime() 12 | 13 | declare @retval int, @qv vector(1536); 14 | 15 | exec @retval = web.get_embedding @text, @qv output; 16 | 17 | if (@retval != 0) return; 18 | 19 | declare @endTime1 as datetime2(7) = sysdatetime(); 20 | update [web].[searched_text] set ms_rest_call = datediff(ms, @startTime, @endTime1) where id = @sid; 21 | 22 | with cteSimilarSpeakers as 23 | ( 24 | select top(@top) 25 | sp.id as speaker_id, 26 | vector_distance('cosine', sp.[embeddings], @qv) as distance 27 | from 28 | web.speakers sp 29 | order by 30 | distance 31 | ), 32 | cteSimilar as 33 | ( 34 | select top(@top) 35 | se.id as session_id, 36 | vector_distance('cosine', se.[embeddings], @qv) as distance 37 | from 38 | web.sessions se 39 | order by 40 | distance 41 | 42 | union all 43 | 44 | select top(@top) 45 | ss.session_id, 46 | sp.distance 47 | from 48 | web.sessions_speakers ss 49 | inner join 50 | cteSimilarSpeakers sp on sp.speaker_id = ss.speaker_id 51 | order by distance 52 | ), 53 | cteSimilar2 as ( 54 | select top(@top) 55 | *, 56 | rn = row_number() over (partition by session_id order by distance) 57 | from 58 | cteSimilar 59 | order by 60 | distance 61 | ), 62 | cteSpeakers as 63 | ( 64 | select 65 | session_id, 66 | json_query('["' + string_agg(string_escape(full_name, 'json'), '","') + '"]') as speakers 67 | from 68 | web.sessions_speakers ss 69 | inner join 70 | web.speakers sp on sp.id = ss.speaker_id 71 | group by 72 | session_id 73 | ) 74 | select top(@top) 75 | a.id, 76 | a.title, 77 | a.abstract, 78 | a.external_id, 79 | a.start_time, 80 | a.end_time, 81 | a.recording_url, 82 | isnull((select top (1) speakers from cteSpeakers where session_id = a.id), '[]') as speakers, 83 | 1-distance as cosine_similarity 84 | from 85 | cteSimilar2 r 86 | inner join 87 | web.sessions a on r.session_id = a.id 88 | where 89 | (1-distance) > @min_similarity 90 | and 91 | rn = 1 92 | order by 93 | distance asc, a.title asc; 94 | 95 | declare @rc int = @@rowcount; 96 | 97 | declare @endTime2 as datetime2(7) = sysdatetime() 98 | update 99 | [web].[searched_text] 100 | set 101 | ms_vector_search = datediff(ms, @endTime1, @endTime2), 102 | found_sessions = @rc 103 | where 104 | id = @sid 105 | GO 106 | 107 | -------------------------------------------------------------------------------- /database/sql/080-update_session_embeddings.sql: -------------------------------------------------------------------------------- 1 | create or alter procedure [web].[update_session_embeddings] 2 | @id int, 3 | @embeddings nvarchar(max) 4 | as 5 | 6 | update 7 | web.sessions 8 | set 9 | embeddings = cast(@embeddings as vector(1536)), 10 | require_embeddings_update = 0 11 | where 12 | id = @id 13 | 14 | GO 15 | 16 | -------------------------------------------------------------------------------- /database/sql/090-update_speaker_embeddings.sql: -------------------------------------------------------------------------------- 1 | create or alter procedure [web].[update_speaker_embeddings] 2 | @id int, 3 | @embeddings nvarchar(max) 4 | as 5 | 6 | update 7 | web.speakers 8 | set 9 | embeddings = cast(@embeddings as vector(1536)), 10 | require_embeddings_update = 0 11 | where 12 | id = @id 13 | 14 | GO 15 | 16 | -------------------------------------------------------------------------------- /func/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /func/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp" 5 | ] 6 | } -------------------------------------------------------------------------------- /func/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureFunctions.pickProcess}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /func/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "bin/Release/net8.0/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~4", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish (functions)" 7 | } -------------------------------------------------------------------------------- /func/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean (functions)", 6 | "command": "dotnet", 7 | "args": [ 8 | "clean", 9 | "/property:GenerateFullPaths=true", 10 | "/consoleloggerparameters:NoSummary" 11 | ], 12 | "type": "process", 13 | "problemMatcher": "$msCompile" 14 | }, 15 | { 16 | "label": "build (functions)", 17 | "command": "dotnet", 18 | "args": [ 19 | "build", 20 | "/property:GenerateFullPaths=true", 21 | "/consoleloggerparameters:NoSummary" 22 | ], 23 | "type": "process", 24 | "dependsOn": "clean (functions)", 25 | "group": { 26 | "kind": "build", 27 | "isDefault": true 28 | }, 29 | "problemMatcher": "$msCompile" 30 | }, 31 | { 32 | "label": "clean release (functions)", 33 | "command": "dotnet", 34 | "args": [ 35 | "clean", 36 | "--configuration", 37 | "Release", 38 | "/property:GenerateFullPaths=true", 39 | "/consoleloggerparameters:NoSummary" 40 | ], 41 | "type": "process", 42 | "problemMatcher": "$msCompile" 43 | }, 44 | { 45 | "label": "publish (functions)", 46 | "command": "dotnet", 47 | "args": [ 48 | "publish", 49 | "--configuration", 50 | "Release", 51 | "/property:GenerateFullPaths=true", 52 | "/consoleloggerparameters:NoSummary" 53 | ], 54 | "type": "process", 55 | "dependsOn": "clean release (functions)", 56 | "problemMatcher": "$msCompile" 57 | }, 58 | { 59 | "type": "func", 60 | "dependsOn": "build (functions)", 61 | "options": { 62 | "cwd": "${workspaceFolder}/bin/Debug/net8.0" 63 | }, 64 | "command": "host start", 65 | "isBackground": true, 66 | "problemMatcher": "$func-dotnet-watch" 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /func/ChatHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text.Json; 4 | using Azure; 5 | using Azure.AI.OpenAI; 6 | using Dapper; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Azure.Functions.Worker; 10 | using Microsoft.Data.SqlClient; 11 | using Microsoft.Extensions.Logging; 12 | using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute; 13 | 14 | namespace SessionRecommender.RequestHandler; 15 | 16 | public record ChatTurn(string userPrompt, string? responseMessage); 17 | 18 | public record FoundSession( 19 | int Id, 20 | string Title, 21 | string Abstract, 22 | double Similarity, 23 | //string RecordingUrl, 24 | string Speakers, 25 | string ExternalId, 26 | DateTimeOffset Start, 27 | DateTimeOffset End 28 | ); 29 | 30 | public class ChatHandler(OpenAIClient openAIClient, SqlConnection conn, ILogger logger) 31 | { 32 | private readonly string _openAIDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_GPT_DEPLOYMENT_NAME") ?? "gpt-4"; 33 | 34 | private const string SystemMessage = """ 35 | You are a system assistant who helps users find the right session to watch from the conference, based off the sessions that are provided to you. 36 | 37 | Sessions will be provided in an assistant message in the format of `title|abstract|speakers|start-time|end-time`. You can use only the provided session list to help you answer the user's question. 38 | 39 | If the user ask a question that is not related to the provided sessions, you can respond with a message that you can't help with that question. 40 | """; 41 | 42 | [Function("ChatHandler")] 43 | public async Task AskAsync( 44 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "ask")] HttpRequest req, 45 | [FromBody] ChatTurn[] history) 46 | { 47 | logger.LogInformation("Retrieving similar sessions..."); 48 | 49 | DynamicParameters p = new(); 50 | p.Add("@text", history.Last().userPrompt); 51 | p.Add("@top", 25); 52 | p.Add("@min_similarity", 0.30); 53 | 54 | using IDataReader foundSessions = await conn.ExecuteReaderAsync("[web].[find_sessions]", commandType: CommandType.StoredProcedure, param: p); 55 | 56 | List sessions = []; 57 | while (foundSessions.Read()) 58 | { 59 | sessions.Add(new( 60 | Id: foundSessions.GetInt32(0), 61 | Title: foundSessions.GetString(1), 62 | Abstract: foundSessions.GetString(2), 63 | ExternalId: foundSessions.GetString(3), 64 | Start: foundSessions.GetDateTime(4), 65 | End: foundSessions.GetDateTime(5), 66 | //RecordingUrl: foundSessions.GetString(6), 67 | Speakers: foundSessions.GetString(7), 68 | Similarity: foundSessions.GetDouble(8) 69 | )); 70 | } 71 | 72 | logger.LogInformation($"{sessions.Count} similar sessions found."); 73 | 74 | logger.LogInformation("Calling GPT..."); 75 | 76 | string sessionDescriptions = string.Join("\r", sessions.Select(s => $"{s.Title}|{s.Abstract}|{s.Speakers}|{s.Start}|{s.End}")); 77 | 78 | List messages = [new ChatRequestSystemMessage(SystemMessage)]; 79 | 80 | foreach (ChatTurn turn in history) 81 | { 82 | messages.Add(new ChatRequestUserMessage(turn.userPrompt)); 83 | if (turn.responseMessage is not null) 84 | { 85 | messages.Add(new ChatRequestAssistantMessage(turn.responseMessage)); 86 | } 87 | } 88 | 89 | messages.Add(new ChatRequestUserMessage($@"## Source ## 90 | {sessionDescriptions} 91 | ## End ## 92 | 93 | You answer needs to divided in two sections: in the first section you'll add the answer to the question. 94 | In the second section, that must be named exactly '###thoughts###', and you must use the section name as typed, without any changes, you'll write brief thoughts on how you came up with the answer, e.g. what sources you used, what you thought about, etc. 95 | }}")); 96 | 97 | ChatCompletionsOptions options = new(_openAIDeploymentName, messages); 98 | 99 | try 100 | { 101 | var answerPayload = await openAIClient.GetChatCompletionsAsync(options); 102 | var answerContent = answerPayload.Value.Choices[0].Message.Content; 103 | 104 | //logger.LogInformation(answerContent); 105 | 106 | var answerPieces = answerContent 107 | .Replace("###Thoughts###", "###thoughts###", StringComparison.InvariantCultureIgnoreCase) 108 | .Replace("### Thoughts ###", "###thoughts###", StringComparison.InvariantCultureIgnoreCase) 109 | .Split("###thoughts###", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); 110 | var answer = answerPieces[0]; 111 | var thoughts = answerPieces.Length == 2 ? answerPieces[1] : "No thoughts provided."; 112 | 113 | logger.LogInformation("Done."); 114 | 115 | return new OkObjectResult(new 116 | { 117 | answer, 118 | thoughts 119 | }); 120 | } 121 | catch (Exception e) 122 | { 123 | logger.LogError(e, "Failed to get answer from OpenAI."); 124 | return new BadRequestObjectResult(e.Message); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /func/Program.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.AI.OpenAI; 3 | using Azure.Identity; 4 | using Azure.Security.KeyVault.Secrets; 5 | using Microsoft.Data.SqlClient; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | var host = new HostBuilder() 10 | 11 | .ConfigureServices(services => 12 | { 13 | Uri openaiEndPoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") is string value && 14 | Uri.TryCreate(value, UriKind.Absolute, out Uri? uri) && 15 | uri is not null 16 | ? uri 17 | : throw new ArgumentException( 18 | $"Unable to parse endpoint URI"); 19 | 20 | string? apiKey; 21 | var keyVaultEndpoint = Environment.GetEnvironmentVariable("AZURE_KEY_VAULT_ENDPOINT"); 22 | if (!string.IsNullOrEmpty(keyVaultEndpoint)) 23 | { 24 | var openAIKeyName = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY"); 25 | var keyVaultClient = new SecretClient(vaultUri: new Uri(keyVaultEndpoint), credential: new DefaultAzureCredential()); 26 | apiKey = keyVaultClient.GetSecret(openAIKeyName).Value.Value; 27 | } 28 | else 29 | { 30 | apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY"); 31 | } 32 | 33 | OpenAIClient openAIClient = apiKey != null ? 34 | new(openaiEndPoint, new AzureKeyCredential(apiKey)) : 35 | new(openaiEndPoint, new DefaultAzureCredential()); 36 | 37 | services.AddSingleton(openAIClient); 38 | 39 | services.AddTransient((_) => new SqlConnection(Environment.GetEnvironmentVariable("AZURE_SQL_CONNECTION_STRING"))); 40 | 41 | }) 42 | .ConfigureFunctionsWebApplication() 43 | .Build(); 44 | 45 | host.Run(); -------------------------------------------------------------------------------- /func/RequestHandler.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | preview 9 | f9d76b6e-3000-45fa-8f99-dec6e7819a55 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | PreserveNewest 26 | Never 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /func/SessionProcessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Data.SqlClient; 3 | using System.Data; 4 | using Dapper; 5 | using Microsoft.Azure.Functions.Worker; 6 | using Microsoft.Azure.Functions.Worker.Extensions.Sql; 7 | using Azure.AI.OpenAI; 8 | using System.Text.Json; 9 | using System.Text.Json.Serialization; 10 | 11 | namespace SessionRecommender.RequestHandler; 12 | 13 | public class Item 14 | { 15 | public required int Id { get; set; } 16 | 17 | [JsonPropertyName("require_embeddings_update")] 18 | public bool RequireEmbeddingsUpdate { get; set; } 19 | 20 | public override bool Equals(object? obj) 21 | { 22 | if (obj is null) return false; 23 | if (obj is not Item that) return false; 24 | return Id == that.Id; 25 | } 26 | 27 | public override int GetHashCode() 28 | { 29 | return Id.GetHashCode(); 30 | } 31 | 32 | public override string ToString() 33 | { 34 | return Id.ToString(); 35 | } 36 | } 37 | 38 | public class Session: Item 39 | { 40 | public string? Title { get; set; } 41 | 42 | public string? Abstract { get; set; } 43 | } 44 | 45 | public class Speaker: Item 46 | { 47 | [JsonPropertyName("full_name")] 48 | public string? FullName { get; set; } 49 | } 50 | 51 | public class ChangedItem: Item 52 | { 53 | public SqlChangeOperation Operation { get; set; } 54 | public required string Payload { get; set; } 55 | } 56 | 57 | public class SessionProcessor(OpenAIClient openAIClient, SqlConnection conn, ILogger logger) 58 | { 59 | private readonly string _openAIDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME") ?? "embeddings"; 60 | 61 | [Function(nameof(SessionTrigger))] 62 | public async Task SessionTrigger( 63 | [SqlTrigger("[web].[sessions]", "AZURE_SQL_CONNECTION_STRING")] 64 | IReadOnlyList> changes 65 | ) 66 | { 67 | var ci = from c in changes 68 | where c.Operation != SqlChangeOperation.Delete 69 | where c.Item.RequireEmbeddingsUpdate == true 70 | select new ChangedItem() { 71 | Id = c.Item.Id, 72 | Operation = c.Operation, 73 | Payload = c.Item.Title + ':' + c.Item.Abstract 74 | }; 75 | 76 | await ProcessChanges(ci, "web.sessions", "web.update_session_embeddings", logger); 77 | } 78 | 79 | [Function(nameof(SpeakerTrigger))] 80 | public async Task SpeakerTrigger( 81 | [SqlTrigger("[web].[speakers]", "AZURE_SQL_CONNECTION_STRING")] 82 | IReadOnlyList> changes 83 | ) 84 | { 85 | var ci = from c in changes 86 | where c.Operation != SqlChangeOperation.Delete 87 | where c.Item.RequireEmbeddingsUpdate == true 88 | select new ChangedItem() { 89 | Id = c.Item.Id, 90 | Operation = c.Operation, 91 | Payload = c.Item.FullName ?? "", 92 | RequireEmbeddingsUpdate = c.Item.RequireEmbeddingsUpdate 93 | }; 94 | 95 | await ProcessChanges(ci, "web.speakers", "web.update_speaker_embeddings", logger); 96 | } 97 | 98 | private async Task ProcessChanges(IEnumerable changes, string referenceTable, string upsertStoredProcedure, ILogger logger) 99 | { 100 | var ct = changes.Count(); 101 | if (ct == 0) { 102 | logger.LogInformation($"No useful changes detected on {referenceTable} table."); 103 | return; 104 | } 105 | 106 | logger.LogInformation($"There are {ct} changes that requires processing on table {referenceTable}."); 107 | 108 | foreach (var change in changes) 109 | { 110 | logger.LogInformation($"[{referenceTable}:{change.Id}] Processing change for operation: " + change.Operation.ToString()); 111 | 112 | var attempts = 0; 113 | var embeddingsReceived = false; 114 | while (attempts < 3) 115 | { 116 | attempts++; 117 | 118 | logger.LogInformation($"[{referenceTable}:{change.Id}] Attempt {attempts}/3 to get embeddings."); 119 | 120 | var response = await openAIClient.GetEmbeddingsAsync( 121 | new EmbeddingsOptions(_openAIDeploymentName, [change.Payload]) 122 | ); 123 | 124 | var e = response.Value.Data[0].Embedding; 125 | await conn.ExecuteAsync( 126 | upsertStoredProcedure, 127 | commandType: CommandType.StoredProcedure, 128 | param: new 129 | { 130 | @id = change.Id, 131 | @embeddings = JsonSerializer.Serialize(e) 132 | }); 133 | embeddingsReceived = true; 134 | 135 | logger.LogInformation($"[{referenceTable}:{change.Id}] Done."); 136 | 137 | break; 138 | } 139 | if (!embeddingsReceived) 140 | { 141 | logger.LogInformation($"[{referenceTable}:{change.Id}] Failed to get embeddings."); 142 | } 143 | } 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /func/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | }, 9 | "enableLiveMetricsFilters": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /func/local.settings.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", 6 | "AZURE_SQL_CONNECTION_STRING": "Server=.database.windows.net;Initial Catalog=;Persist Security Info=False;User ID=session_recommender_app;Password=unEno!h5!&*KP420xds&@P901afb$^M;MultipleActiveResultSets=False;Encrypt=True;Connection Timeout=30;", 7 | "AZURE_OPENAI_ENDPOINT": "https://.openai.azure.com/", 8 | "AZURE_OPENAI_KEY": "", 9 | "AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME": "embeddings", 10 | "AZURE_OPENAI_GPT_DEPLOYMENT_NAME": "gpt" 11 | } 12 | } -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des", 20 | "computeDisks": "disk", 21 | "computeDisksOs": "osdisk", 22 | "computeGalleries": "gal", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci", 27 | "containerRegistryRegistries": "cr", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla", 32 | "dataLakeStoreAccounts": "dls", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopics": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck", 56 | "kustoClusters": "dec", 57 | "kustoClustersDatabases": "dedb", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf", 73 | "networkFirewallPoliciesRuleGroups": "wafrg", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "recoveryServicesVaults": "rsv-", 108 | "resourcesResourceGroups": "rg-", 109 | "searchSearchServices": "srch-", 110 | "serviceBusNamespaces": "sb-", 111 | "serviceBusNamespacesQueues": "sbq-", 112 | "serviceBusNamespacesTopics": "sbt-", 113 | "serviceEndPointPolicies": "se-", 114 | "serviceFabricClusters": "sf-", 115 | "signalRServiceSignalR": "sigr", 116 | "sqlManagedInstances": "sqlmi-", 117 | "sqlServers": "sql-", 118 | "sqlServersDataWarehouse": "sqldw-", 119 | "sqlServersDatabases": "sqldb-", 120 | "sqlServersDatabasesStretch": "sqlstrdb-", 121 | "storageStorageAccounts": "st", 122 | "storageStorageAccountsVm": "stvm", 123 | "storSimpleManagers": "ssimp", 124 | "streamAnalyticsCluster": "asa-", 125 | "synapseWorkspaces": "syn", 126 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 127 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 128 | "synapseWorkspacesSqlPoolsSpark": "synsp", 129 | "timeSeriesInsightsEnvironments": "tsi-", 130 | "webServerFarms": "plan-", 131 | "webSitesAppService": "app-", 132 | "webSitesAppServiceEnvironment": "ase-", 133 | "webSitesFunctions": "func-", 134 | "webStaticSites": "stapp-" 135 | } 136 | -------------------------------------------------------------------------------- /infra/app/functions.bicep: -------------------------------------------------------------------------------- 1 | param functionAppName string 2 | param location string = resourceGroup().location 3 | param hostingPlanId string 4 | param storageAccountName string 5 | @secure() 6 | param sqlConnectionString string 7 | param keyVaultName string 8 | param tags object = {} 9 | param applicationInsightsConnectionString string 10 | param useKeyVault bool 11 | param keyVaultEndpoint string = '' 12 | @secure() 13 | param openAIEndpoint string 14 | param openAIKeyName string 15 | param openAIName string 16 | param openAIEmebddingDeploymentName string = 'embeddings' 17 | param openAIGPTDeploymentName string = 'gpt' 18 | 19 | module functionApp '../core/host/functions.bicep' = { 20 | name: 'function1' 21 | params: { 22 | location: location 23 | alwaysOn: false 24 | tags: union(tags, { 'azd-service-name': 'functionapp' }) 25 | kind: 'functionapp' 26 | keyVaultName: keyVaultName 27 | appServicePlanId: hostingPlanId 28 | name: functionAppName 29 | runtimeName: 'dotnet-isolated' 30 | runtimeVersion: '8.0' 31 | storageAccountName: storageAccountName 32 | appSettings: { 33 | WEBSITE_CONTENTSHARE: toLower(functionAppName) 34 | WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', storageAccountName), '2022-05-01').keys[0].value}' 35 | APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsightsConnectionString 36 | AZURE_SQL_CONNECTION_STRING: sqlConnectionString 37 | AZURE_OPENAI_ENDPOINT: openAIEndpoint 38 | AZURE_OPENAI_KEY: useKeyVault ? openAIKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', openAIName), '2023-05-01').key1 39 | AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: openAIEmebddingDeploymentName 40 | AZURE_OPENAI_GPT_DEPLOYMENT_NAME: openAIGPTDeploymentName 41 | AZURE_KEY_VAULT_ENDPOINT: useKeyVault ? keyVaultEndpoint : '' 42 | } 43 | } 44 | } 45 | 46 | output functionAppResourceId string = functionApp.outputs.functionAppResourceId 47 | output name string = functionApp.outputs.name 48 | output uri string = functionApp.outputs.uri 49 | output identityPrincipalId string = functionApp.outputs.identityPrincipalId 50 | -------------------------------------------------------------------------------- /infra/app/openai.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cognitive Services instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | @description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') 6 | param customSubDomainName string = name 7 | param deployments array = [] 8 | param kind string = 'OpenAI' 9 | param publicNetworkAccess string = 'Enabled' 10 | param sku object = { 11 | name: 'S0' 12 | } 13 | param keyVaultName string 14 | param useKeyVault bool 15 | 16 | resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 17 | name: name 18 | location: location 19 | tags: tags 20 | kind: kind 21 | properties: { 22 | customSubDomainName: customSubDomainName 23 | publicNetworkAccess: publicNetworkAccess 24 | } 25 | sku: sku 26 | } 27 | 28 | @batchSize(1) 29 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 30 | parent: account 31 | name: deployment.name 32 | properties: { 33 | model: deployment.model 34 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 35 | } 36 | sku: contains(deployment, 'sku') ? deployment.sku : { 37 | name: 'Standard' 38 | capacity: 20 39 | } 40 | }] 41 | 42 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (useKeyVault) { 43 | name: keyVaultName 44 | } 45 | 46 | resource openAIKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = if (useKeyVault) { 47 | parent: keyVault 48 | name: 'openAIKey' 49 | properties: { 50 | value: account.listKeys().key1 51 | } 52 | } 53 | 54 | output endpoint string = account.properties.endpoint 55 | output id string = account.id 56 | output name string = account.name 57 | output openAIKeyName string = openAIKey.name 58 | -------------------------------------------------------------------------------- /infra/app/sqlserver.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure SQL Server instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | param databaseName string 6 | param principalId string 7 | param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' 8 | 9 | param sqlAdmin string = 'sqlAdmin' 10 | @secure() 11 | param sqlAdminPassword string 12 | 13 | param appUser string = 'session_recommender_app' 14 | @secure() 15 | param appUserPassword string 16 | 17 | @secure() 18 | param openAIEndpoint string 19 | param openAIDeploymentName string 20 | param openAIServiceName string 21 | 22 | 23 | resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { 24 | name: name 25 | location: location 26 | tags: tags 27 | properties: { 28 | version: '12.0' 29 | minimalTlsVersion: '1.2' 30 | publicNetworkAccess: 'Enabled' 31 | administratorLogin: sqlAdmin 32 | administratorLoginPassword: sqlAdminPassword 33 | } 34 | 35 | resource database 'databases' = { 36 | name: databaseName 37 | location: location 38 | } 39 | 40 | resource firewall 'firewallRules' = { 41 | name: 'Azure Services' 42 | properties: { 43 | // Allow all clients 44 | // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". 45 | // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. 46 | startIpAddress: '0.0.0.0' 47 | endIpAddress: '0.0.0.0' 48 | } 49 | } 50 | 51 | resource symbolicname 'administrators@2022-05-01-preview' = { 52 | name: 'ActiveDirectory' 53 | properties: { 54 | administratorType: 'ActiveDirectory' 55 | login: 'EntraAdmin' 56 | sid: principalId 57 | tenantId: tenant().tenantId 58 | } 59 | } 60 | } 61 | 62 | resource createDBScript2 'Microsoft.Resources/deploymentScripts@2023-08-01' = { 63 | name: '${name}-createDB-script' 64 | location: location 65 | kind: 'AzurePowerShell' 66 | properties: { 67 | azPowerShellVersion: '10.0' 68 | retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running 69 | timeout: 'PT5M' // Five minutes 70 | cleanupPreference: 'OnSuccess' 71 | environmentVariables: [ 72 | { 73 | name: 'DBNAME' 74 | value: databaseName 75 | } 76 | { 77 | name: 'DBSERVER' 78 | value: sqlServer.properties.fullyQualifiedDomainName 79 | } 80 | { 81 | name: 'SQLCMDPASSWORD' 82 | secureValue: sqlAdminPassword 83 | } 84 | { 85 | name: 'SQLADMIN' 86 | value: sqlAdmin 87 | } 88 | { 89 | name: 'OPEN_AI_ENDPOINT' 90 | value: openAIEndpoint 91 | } 92 | { 93 | name: 'OPEN_AI_DEPLOYMENT' 94 | value: openAIDeploymentName 95 | } 96 | { 97 | name: 'OPEN_AI_KEY' 98 | secureValue: listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', openAIServiceName), '2023-05-01').key1 99 | } 100 | { 101 | name: 'APP_USER_PASSWORD' 102 | secureValue: appUserPassword 103 | } 104 | ] 105 | } 106 | } 107 | 108 | var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' 109 | output connectionStringKey string = connectionStringKey 110 | output connectionString string = connectionString 111 | output databaseName string = sqlServer::database.name 112 | output name string = sqlServer.name 113 | output id string = sqlServer.id 114 | -------------------------------------------------------------------------------- /infra/app/staticwebapp.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Static Web Apps instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | param sku object = { 6 | name: 'Standard' 7 | tier: 'Standard' 8 | } 9 | param sqlServerLocation string 10 | param sqlServerId string 11 | @secure() 12 | param sqlConnectionString string 13 | param apiResourceId string 14 | 15 | resource web 'Microsoft.Web/staticSites@2022-09-01' = { 16 | name: name 17 | location: location 18 | tags: tags 19 | properties: {} 20 | sku: sku 21 | resource apifunc 'linkedBackends@2022-09-01' = { 22 | name: 'default' 23 | properties: { 24 | backendResourceId: apiResourceId 25 | region: location 26 | } 27 | } 28 | resource dbconn 'databaseConnections@2022-09-01' = { 29 | name: 'default' 30 | properties: { 31 | connectionString: sqlConnectionString 32 | region: sqlServerLocation 33 | resourceId: sqlServerId 34 | } 35 | } 36 | } 37 | 38 | output name string = web.name 39 | output uri string = 'https://${web.properties.defaultHostname}' 40 | -------------------------------------------------------------------------------- /infra/core/host/appservice-appsettings.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Updates app settings for an Azure App Service.' 2 | @description('The name of the app service resource within the current resource group scope') 3 | param name string 4 | 5 | @description('The app settings to be applied to the app service') 6 | @secure() 7 | param appSettings object 8 | 9 | resource appService 'Microsoft.Web/sites@2022-03-01' existing = { 10 | name: name 11 | } 12 | 13 | resource settings 'Microsoft.Web/sites/config@2022-03-01' = { 14 | name: 'appsettings' 15 | parent: appService 16 | properties: appSettings 17 | } 18 | -------------------------------------------------------------------------------- /infra/core/host/appservice.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | // Reference Properties 7 | param applicationInsightsName string = '' 8 | param appServicePlanId string 9 | param keyVaultName string = '' 10 | param managedIdentity bool = !empty(keyVaultName) 11 | 12 | // Runtime Properties 13 | @allowed([ 14 | 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' 15 | ]) 16 | param runtimeName string 17 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' 18 | param runtimeVersion string 19 | 20 | // Microsoft.Web/sites Properties 21 | param kind string = 'app,linux' 22 | 23 | // Microsoft.Web/sites/config 24 | param allowedOrigins array = [] 25 | param alwaysOn bool = true 26 | param appCommandLine string = '' 27 | @secure() 28 | param appSettings object = {} 29 | param clientAffinityEnabled bool = false 30 | param enableOryxBuild bool = contains(kind, 'linux') 31 | param functionAppScaleLimit int = -1 32 | param linuxFxVersion string = runtimeNameAndVersion 33 | param minimumElasticInstanceCount int = -1 34 | param numberOfWorkers int = -1 35 | param scmDoBuildDuringDeployment bool = false 36 | param use32BitWorkerProcess bool = false 37 | param ftpsState string = 'FtpsOnly' 38 | param healthCheckPath string = '' 39 | 40 | resource appService 'Microsoft.Web/sites@2022-03-01' = { 41 | name: name 42 | location: location 43 | tags: tags 44 | kind: kind 45 | properties: { 46 | serverFarmId: appServicePlanId 47 | siteConfig: { 48 | linuxFxVersion: linuxFxVersion 49 | alwaysOn: alwaysOn 50 | ftpsState: ftpsState 51 | minTlsVersion: '1.2' 52 | appCommandLine: appCommandLine 53 | numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null 54 | minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null 55 | use32BitWorkerProcess: use32BitWorkerProcess 56 | functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null 57 | healthCheckPath: healthCheckPath 58 | cors: { 59 | allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) 60 | } 61 | } 62 | clientAffinityEnabled: clientAffinityEnabled 63 | httpsOnly: true 64 | } 65 | 66 | identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } 67 | 68 | resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { 69 | name: 'ftp' 70 | properties: { 71 | allow: false 72 | } 73 | } 74 | 75 | resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { 76 | name: 'scm' 77 | properties: { 78 | allow: false 79 | } 80 | } 81 | } 82 | 83 | // Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially 84 | // sites/web/config 'appsettings' 85 | module configAppSettings 'appservice-appsettings.bicep' = { 86 | name: '${name}-appSettings' 87 | params: { 88 | name: appService.name 89 | appSettings: union(appSettings, 90 | { 91 | SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) 92 | ENABLE_ORYX_BUILD: string(enableOryxBuild) 93 | }, 94 | runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {}, 95 | !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, 96 | !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) 97 | } 98 | } 99 | 100 | // sites/web/config 'logs' 101 | resource configLogs 'Microsoft.Web/sites/config@2022-03-01' = { 102 | name: 'logs' 103 | parent: appService 104 | properties: { 105 | applicationLogs: { fileSystem: { level: 'Verbose' } } 106 | detailedErrorMessages: { enabled: true } 107 | failedRequestsTracing: { enabled: true } 108 | httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } 109 | } 110 | dependsOn: [configAppSettings] 111 | } 112 | 113 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { 114 | name: keyVaultName 115 | } 116 | 117 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 118 | name: applicationInsightsName 119 | } 120 | 121 | output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' 122 | output name string = appService.name 123 | output uri string = 'https://${appService.properties.defaultHostName}' 124 | output functionAppResourceId string = appService.id 125 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure App Service plan.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param kind string = '' 7 | param reserved bool = true 8 | param sku object 9 | 10 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 11 | name: name 12 | location: location 13 | tags: tags 14 | sku: sku 15 | kind: kind 16 | properties: { 17 | reserved: reserved 18 | } 19 | } 20 | 21 | output id string = appServicePlan.id 22 | output name string = appServicePlan.name 23 | -------------------------------------------------------------------------------- /infra/core/host/functions.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Function in an existing Azure App Service plan.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | // Reference Properties 7 | param applicationInsightsName string = '' 8 | param appServicePlanId string 9 | param keyVaultName string = '' 10 | param managedIdentity bool = !empty(keyVaultName) 11 | param storageAccountName string 12 | 13 | // Runtime Properties 14 | @allowed([ 15 | 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' 16 | ]) 17 | param runtimeName string 18 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' 19 | param runtimeVersion string 20 | 21 | // Function Settings 22 | @allowed([ 23 | '~4', '~3', '~2', '~1' 24 | ]) 25 | param extensionVersion string = '~4' 26 | 27 | // Microsoft.Web/sites Properties 28 | param kind string = 'functionapp,linux' 29 | 30 | // Microsoft.Web/sites/config 31 | param allowedOrigins array = [] 32 | param alwaysOn bool = true 33 | param appCommandLine string = '' 34 | @secure() 35 | param appSettings object = {} 36 | param clientAffinityEnabled bool = false 37 | param enableOryxBuild bool = contains(kind, 'linux') 38 | param functionAppScaleLimit int = -1 39 | param linuxFxVersion string = runtimeNameAndVersion 40 | param minimumElasticInstanceCount int = -1 41 | param numberOfWorkers int = -1 42 | param scmDoBuildDuringDeployment bool = true 43 | param use32BitWorkerProcess bool = false 44 | param healthCheckPath string = '' 45 | 46 | module functions 'appservice.bicep' = { 47 | name: '${name}-functions' 48 | params: { 49 | name: name 50 | location: location 51 | tags: tags 52 | allowedOrigins: allowedOrigins 53 | alwaysOn: alwaysOn 54 | appCommandLine: appCommandLine 55 | applicationInsightsName: applicationInsightsName 56 | appServicePlanId: appServicePlanId 57 | appSettings: union(appSettings, { 58 | AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' 59 | FUNCTIONS_EXTENSION_VERSION: extensionVersion 60 | FUNCTIONS_WORKER_RUNTIME: runtimeName 61 | }) 62 | clientAffinityEnabled: clientAffinityEnabled 63 | enableOryxBuild: enableOryxBuild 64 | functionAppScaleLimit: functionAppScaleLimit 65 | healthCheckPath: healthCheckPath 66 | keyVaultName: keyVaultName 67 | kind: kind 68 | linuxFxVersion: linuxFxVersion 69 | managedIdentity: managedIdentity 70 | minimumElasticInstanceCount: minimumElasticInstanceCount 71 | numberOfWorkers: numberOfWorkers 72 | runtimeName: runtimeName 73 | runtimeVersion: runtimeVersion 74 | runtimeNameAndVersion: runtimeNameAndVersion 75 | scmDoBuildDuringDeployment: scmDoBuildDuringDeployment 76 | use32BitWorkerProcess: use32BitWorkerProcess 77 | } 78 | } 79 | 80 | resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { 81 | name: storageAccountName 82 | } 83 | 84 | output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' 85 | output name string = functions.outputs.name 86 | output uri string = functions.outputs.uri 87 | output functionAppResourceId string = functions.outputs.functionAppResourceId 88 | -------------------------------------------------------------------------------- /infra/core/monitor/applicationinsights-dashboard.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a dashboard for an Application Insights instance.' 2 | param name string 3 | param applicationInsightsName string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | // 2020-09-01-preview because that is the latest valid version 8 | resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | properties: { 13 | lenses: [ 14 | { 15 | order: 0 16 | parts: [ 17 | { 18 | position: { 19 | x: 0 20 | y: 0 21 | colSpan: 2 22 | rowSpan: 1 23 | } 24 | metadata: { 25 | inputs: [ 26 | { 27 | name: 'id' 28 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 29 | } 30 | { 31 | name: 'Version' 32 | value: '1.0' 33 | } 34 | ] 35 | #disable-next-line BCP036 36 | type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' 37 | asset: { 38 | idInputName: 'id' 39 | type: 'ApplicationInsights' 40 | } 41 | defaultMenuItemId: 'overview' 42 | } 43 | } 44 | { 45 | position: { 46 | x: 2 47 | y: 0 48 | colSpan: 1 49 | rowSpan: 1 50 | } 51 | metadata: { 52 | inputs: [ 53 | { 54 | name: 'ComponentId' 55 | value: { 56 | Name: applicationInsights.name 57 | SubscriptionId: subscription().subscriptionId 58 | ResourceGroup: resourceGroup().name 59 | } 60 | } 61 | { 62 | name: 'Version' 63 | value: '1.0' 64 | } 65 | ] 66 | #disable-next-line BCP036 67 | type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' 68 | asset: { 69 | idInputName: 'ComponentId' 70 | type: 'ApplicationInsights' 71 | } 72 | defaultMenuItemId: 'ProactiveDetection' 73 | } 74 | } 75 | { 76 | position: { 77 | x: 3 78 | y: 0 79 | colSpan: 1 80 | rowSpan: 1 81 | } 82 | metadata: { 83 | inputs: [ 84 | { 85 | name: 'ComponentId' 86 | value: { 87 | Name: applicationInsights.name 88 | SubscriptionId: subscription().subscriptionId 89 | ResourceGroup: resourceGroup().name 90 | } 91 | } 92 | { 93 | name: 'ResourceId' 94 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 95 | } 96 | ] 97 | #disable-next-line BCP036 98 | type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' 99 | asset: { 100 | idInputName: 'ComponentId' 101 | type: 'ApplicationInsights' 102 | } 103 | } 104 | } 105 | { 106 | position: { 107 | x: 4 108 | y: 0 109 | colSpan: 1 110 | rowSpan: 1 111 | } 112 | metadata: { 113 | inputs: [ 114 | { 115 | name: 'ComponentId' 116 | value: { 117 | Name: applicationInsights.name 118 | SubscriptionId: subscription().subscriptionId 119 | ResourceGroup: resourceGroup().name 120 | } 121 | } 122 | { 123 | name: 'TimeContext' 124 | value: { 125 | durationMs: 86400000 126 | endTime: null 127 | createdTime: '2018-05-04T01:20:33.345Z' 128 | isInitialTime: true 129 | grain: 1 130 | useDashboardTimeRange: false 131 | } 132 | } 133 | { 134 | name: 'Version' 135 | value: '1.0' 136 | } 137 | ] 138 | #disable-next-line BCP036 139 | type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' 140 | asset: { 141 | idInputName: 'ComponentId' 142 | type: 'ApplicationInsights' 143 | } 144 | } 145 | } 146 | { 147 | position: { 148 | x: 5 149 | y: 0 150 | colSpan: 1 151 | rowSpan: 1 152 | } 153 | metadata: { 154 | inputs: [ 155 | { 156 | name: 'ComponentId' 157 | value: { 158 | Name: applicationInsights.name 159 | SubscriptionId: subscription().subscriptionId 160 | ResourceGroup: resourceGroup().name 161 | } 162 | } 163 | { 164 | name: 'TimeContext' 165 | value: { 166 | durationMs: 86400000 167 | endTime: null 168 | createdTime: '2018-05-08T18:47:35.237Z' 169 | isInitialTime: true 170 | grain: 1 171 | useDashboardTimeRange: false 172 | } 173 | } 174 | { 175 | name: 'ConfigurationId' 176 | value: '78ce933e-e864-4b05-a27b-71fd55a6afad' 177 | } 178 | ] 179 | #disable-next-line BCP036 180 | type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' 181 | asset: { 182 | idInputName: 'ComponentId' 183 | type: 'ApplicationInsights' 184 | } 185 | } 186 | } 187 | { 188 | position: { 189 | x: 0 190 | y: 1 191 | colSpan: 3 192 | rowSpan: 1 193 | } 194 | metadata: { 195 | inputs: [] 196 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 197 | settings: { 198 | content: { 199 | settings: { 200 | content: '# Usage' 201 | title: '' 202 | subtitle: '' 203 | } 204 | } 205 | } 206 | } 207 | } 208 | { 209 | position: { 210 | x: 3 211 | y: 1 212 | colSpan: 1 213 | rowSpan: 1 214 | } 215 | metadata: { 216 | inputs: [ 217 | { 218 | name: 'ComponentId' 219 | value: { 220 | Name: applicationInsights.name 221 | SubscriptionId: subscription().subscriptionId 222 | ResourceGroup: resourceGroup().name 223 | } 224 | } 225 | { 226 | name: 'TimeContext' 227 | value: { 228 | durationMs: 86400000 229 | endTime: null 230 | createdTime: '2018-05-04T01:22:35.782Z' 231 | isInitialTime: true 232 | grain: 1 233 | useDashboardTimeRange: false 234 | } 235 | } 236 | ] 237 | #disable-next-line BCP036 238 | type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' 239 | asset: { 240 | idInputName: 'ComponentId' 241 | type: 'ApplicationInsights' 242 | } 243 | } 244 | } 245 | { 246 | position: { 247 | x: 4 248 | y: 1 249 | colSpan: 3 250 | rowSpan: 1 251 | } 252 | metadata: { 253 | inputs: [] 254 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 255 | settings: { 256 | content: { 257 | settings: { 258 | content: '# Reliability' 259 | title: '' 260 | subtitle: '' 261 | } 262 | } 263 | } 264 | } 265 | } 266 | { 267 | position: { 268 | x: 7 269 | y: 1 270 | colSpan: 1 271 | rowSpan: 1 272 | } 273 | metadata: { 274 | inputs: [ 275 | { 276 | name: 'ResourceId' 277 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 278 | } 279 | { 280 | name: 'DataModel' 281 | value: { 282 | version: '1.0.0' 283 | timeContext: { 284 | durationMs: 86400000 285 | createdTime: '2018-05-04T23:42:40.072Z' 286 | isInitialTime: false 287 | grain: 1 288 | useDashboardTimeRange: false 289 | } 290 | } 291 | isOptional: true 292 | } 293 | { 294 | name: 'ConfigurationId' 295 | value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' 296 | isOptional: true 297 | } 298 | ] 299 | #disable-next-line BCP036 300 | type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' 301 | isAdapter: true 302 | asset: { 303 | idInputName: 'ResourceId' 304 | type: 'ApplicationInsights' 305 | } 306 | defaultMenuItemId: 'failures' 307 | } 308 | } 309 | { 310 | position: { 311 | x: 8 312 | y: 1 313 | colSpan: 3 314 | rowSpan: 1 315 | } 316 | metadata: { 317 | inputs: [] 318 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 319 | settings: { 320 | content: { 321 | settings: { 322 | content: '# Responsiveness\r\n' 323 | title: '' 324 | subtitle: '' 325 | } 326 | } 327 | } 328 | } 329 | } 330 | { 331 | position: { 332 | x: 11 333 | y: 1 334 | colSpan: 1 335 | rowSpan: 1 336 | } 337 | metadata: { 338 | inputs: [ 339 | { 340 | name: 'ResourceId' 341 | value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 342 | } 343 | { 344 | name: 'DataModel' 345 | value: { 346 | version: '1.0.0' 347 | timeContext: { 348 | durationMs: 86400000 349 | createdTime: '2018-05-04T23:43:37.804Z' 350 | isInitialTime: false 351 | grain: 1 352 | useDashboardTimeRange: false 353 | } 354 | } 355 | isOptional: true 356 | } 357 | { 358 | name: 'ConfigurationId' 359 | value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' 360 | isOptional: true 361 | } 362 | ] 363 | #disable-next-line BCP036 364 | type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' 365 | isAdapter: true 366 | asset: { 367 | idInputName: 'ResourceId' 368 | type: 'ApplicationInsights' 369 | } 370 | defaultMenuItemId: 'performance' 371 | } 372 | } 373 | { 374 | position: { 375 | x: 12 376 | y: 1 377 | colSpan: 3 378 | rowSpan: 1 379 | } 380 | metadata: { 381 | inputs: [] 382 | type: 'Extension/HubsExtension/PartType/MarkdownPart' 383 | settings: { 384 | content: { 385 | settings: { 386 | content: '# Browser' 387 | title: '' 388 | subtitle: '' 389 | } 390 | } 391 | } 392 | } 393 | } 394 | { 395 | position: { 396 | x: 15 397 | y: 1 398 | colSpan: 1 399 | rowSpan: 1 400 | } 401 | metadata: { 402 | inputs: [ 403 | { 404 | name: 'ComponentId' 405 | value: { 406 | Name: applicationInsights.name 407 | SubscriptionId: subscription().subscriptionId 408 | ResourceGroup: resourceGroup().name 409 | } 410 | } 411 | { 412 | name: 'MetricsExplorerJsonDefinitionId' 413 | value: 'BrowserPerformanceTimelineMetrics' 414 | } 415 | { 416 | name: 'TimeContext' 417 | value: { 418 | durationMs: 86400000 419 | createdTime: '2018-05-08T12:16:27.534Z' 420 | isInitialTime: false 421 | grain: 1 422 | useDashboardTimeRange: false 423 | } 424 | } 425 | { 426 | name: 'CurrentFilter' 427 | value: { 428 | eventTypes: [ 429 | 4 430 | 1 431 | 3 432 | 5 433 | 2 434 | 6 435 | 13 436 | ] 437 | typeFacets: {} 438 | isPermissive: false 439 | } 440 | } 441 | { 442 | name: 'id' 443 | value: { 444 | Name: applicationInsights.name 445 | SubscriptionId: subscription().subscriptionId 446 | ResourceGroup: resourceGroup().name 447 | } 448 | } 449 | { 450 | name: 'Version' 451 | value: '1.0' 452 | } 453 | ] 454 | #disable-next-line BCP036 455 | type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' 456 | asset: { 457 | idInputName: 'ComponentId' 458 | type: 'ApplicationInsights' 459 | } 460 | defaultMenuItemId: 'browser' 461 | } 462 | } 463 | { 464 | position: { 465 | x: 0 466 | y: 2 467 | colSpan: 4 468 | rowSpan: 3 469 | } 470 | metadata: { 471 | inputs: [ 472 | { 473 | name: 'options' 474 | value: { 475 | chart: { 476 | metrics: [ 477 | { 478 | resourceMetadata: { 479 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 480 | } 481 | name: 'sessions/count' 482 | aggregationType: 5 483 | namespace: 'microsoft.insights/components/kusto' 484 | metricVisualization: { 485 | displayName: 'Sessions' 486 | color: '#47BDF5' 487 | } 488 | } 489 | { 490 | resourceMetadata: { 491 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 492 | } 493 | name: 'users/count' 494 | aggregationType: 5 495 | namespace: 'microsoft.insights/components/kusto' 496 | metricVisualization: { 497 | displayName: 'Users' 498 | color: '#7E58FF' 499 | } 500 | } 501 | ] 502 | title: 'Unique sessions and users' 503 | visualization: { 504 | chartType: 2 505 | legendVisualization: { 506 | isVisible: true 507 | position: 2 508 | hideSubtitle: false 509 | } 510 | axisVisualization: { 511 | x: { 512 | isVisible: true 513 | axisType: 2 514 | } 515 | y: { 516 | isVisible: true 517 | axisType: 1 518 | } 519 | } 520 | } 521 | openBladeOnClick: { 522 | openBlade: true 523 | destinationBlade: { 524 | extensionName: 'HubsExtension' 525 | bladeName: 'ResourceMenuBlade' 526 | parameters: { 527 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 528 | menuid: 'segmentationUsers' 529 | } 530 | } 531 | } 532 | } 533 | } 534 | } 535 | { 536 | name: 'sharedTimeRange' 537 | isOptional: true 538 | } 539 | ] 540 | #disable-next-line BCP036 541 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 542 | settings: {} 543 | } 544 | } 545 | { 546 | position: { 547 | x: 4 548 | y: 2 549 | colSpan: 4 550 | rowSpan: 3 551 | } 552 | metadata: { 553 | inputs: [ 554 | { 555 | name: 'options' 556 | value: { 557 | chart: { 558 | metrics: [ 559 | { 560 | resourceMetadata: { 561 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 562 | } 563 | name: 'requests/failed' 564 | aggregationType: 7 565 | namespace: 'microsoft.insights/components' 566 | metricVisualization: { 567 | displayName: 'Failed requests' 568 | color: '#EC008C' 569 | } 570 | } 571 | ] 572 | title: 'Failed requests' 573 | visualization: { 574 | chartType: 3 575 | legendVisualization: { 576 | isVisible: true 577 | position: 2 578 | hideSubtitle: false 579 | } 580 | axisVisualization: { 581 | x: { 582 | isVisible: true 583 | axisType: 2 584 | } 585 | y: { 586 | isVisible: true 587 | axisType: 1 588 | } 589 | } 590 | } 591 | openBladeOnClick: { 592 | openBlade: true 593 | destinationBlade: { 594 | extensionName: 'HubsExtension' 595 | bladeName: 'ResourceMenuBlade' 596 | parameters: { 597 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 598 | menuid: 'failures' 599 | } 600 | } 601 | } 602 | } 603 | } 604 | } 605 | { 606 | name: 'sharedTimeRange' 607 | isOptional: true 608 | } 609 | ] 610 | #disable-next-line BCP036 611 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 612 | settings: {} 613 | } 614 | } 615 | { 616 | position: { 617 | x: 8 618 | y: 2 619 | colSpan: 4 620 | rowSpan: 3 621 | } 622 | metadata: { 623 | inputs: [ 624 | { 625 | name: 'options' 626 | value: { 627 | chart: { 628 | metrics: [ 629 | { 630 | resourceMetadata: { 631 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 632 | } 633 | name: 'requests/duration' 634 | aggregationType: 4 635 | namespace: 'microsoft.insights/components' 636 | metricVisualization: { 637 | displayName: 'Server response time' 638 | color: '#00BCF2' 639 | } 640 | } 641 | ] 642 | title: 'Server response time' 643 | visualization: { 644 | chartType: 2 645 | legendVisualization: { 646 | isVisible: true 647 | position: 2 648 | hideSubtitle: false 649 | } 650 | axisVisualization: { 651 | x: { 652 | isVisible: true 653 | axisType: 2 654 | } 655 | y: { 656 | isVisible: true 657 | axisType: 1 658 | } 659 | } 660 | } 661 | openBladeOnClick: { 662 | openBlade: true 663 | destinationBlade: { 664 | extensionName: 'HubsExtension' 665 | bladeName: 'ResourceMenuBlade' 666 | parameters: { 667 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 668 | menuid: 'performance' 669 | } 670 | } 671 | } 672 | } 673 | } 674 | } 675 | { 676 | name: 'sharedTimeRange' 677 | isOptional: true 678 | } 679 | ] 680 | #disable-next-line BCP036 681 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 682 | settings: {} 683 | } 684 | } 685 | { 686 | position: { 687 | x: 12 688 | y: 2 689 | colSpan: 4 690 | rowSpan: 3 691 | } 692 | metadata: { 693 | inputs: [ 694 | { 695 | name: 'options' 696 | value: { 697 | chart: { 698 | metrics: [ 699 | { 700 | resourceMetadata: { 701 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 702 | } 703 | name: 'browserTimings/networkDuration' 704 | aggregationType: 4 705 | namespace: 'microsoft.insights/components' 706 | metricVisualization: { 707 | displayName: 'Page load network connect time' 708 | color: '#7E58FF' 709 | } 710 | } 711 | { 712 | resourceMetadata: { 713 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 714 | } 715 | name: 'browserTimings/processingDuration' 716 | aggregationType: 4 717 | namespace: 'microsoft.insights/components' 718 | metricVisualization: { 719 | displayName: 'Client processing time' 720 | color: '#44F1C8' 721 | } 722 | } 723 | { 724 | resourceMetadata: { 725 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 726 | } 727 | name: 'browserTimings/sendDuration' 728 | aggregationType: 4 729 | namespace: 'microsoft.insights/components' 730 | metricVisualization: { 731 | displayName: 'Send request time' 732 | color: '#EB9371' 733 | } 734 | } 735 | { 736 | resourceMetadata: { 737 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 738 | } 739 | name: 'browserTimings/receiveDuration' 740 | aggregationType: 4 741 | namespace: 'microsoft.insights/components' 742 | metricVisualization: { 743 | displayName: 'Receiving response time' 744 | color: '#0672F1' 745 | } 746 | } 747 | ] 748 | title: 'Average page load time breakdown' 749 | visualization: { 750 | chartType: 3 751 | legendVisualization: { 752 | isVisible: true 753 | position: 2 754 | hideSubtitle: false 755 | } 756 | axisVisualization: { 757 | x: { 758 | isVisible: true 759 | axisType: 2 760 | } 761 | y: { 762 | isVisible: true 763 | axisType: 1 764 | } 765 | } 766 | } 767 | } 768 | } 769 | } 770 | { 771 | name: 'sharedTimeRange' 772 | isOptional: true 773 | } 774 | ] 775 | #disable-next-line BCP036 776 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 777 | settings: {} 778 | } 779 | } 780 | { 781 | position: { 782 | x: 0 783 | y: 5 784 | colSpan: 4 785 | rowSpan: 3 786 | } 787 | metadata: { 788 | inputs: [ 789 | { 790 | name: 'options' 791 | value: { 792 | chart: { 793 | metrics: [ 794 | { 795 | resourceMetadata: { 796 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 797 | } 798 | name: 'availabilityResults/availabilityPercentage' 799 | aggregationType: 4 800 | namespace: 'microsoft.insights/components' 801 | metricVisualization: { 802 | displayName: 'Availability' 803 | color: '#47BDF5' 804 | } 805 | } 806 | ] 807 | title: 'Average availability' 808 | visualization: { 809 | chartType: 3 810 | legendVisualization: { 811 | isVisible: true 812 | position: 2 813 | hideSubtitle: false 814 | } 815 | axisVisualization: { 816 | x: { 817 | isVisible: true 818 | axisType: 2 819 | } 820 | y: { 821 | isVisible: true 822 | axisType: 1 823 | } 824 | } 825 | } 826 | openBladeOnClick: { 827 | openBlade: true 828 | destinationBlade: { 829 | extensionName: 'HubsExtension' 830 | bladeName: 'ResourceMenuBlade' 831 | parameters: { 832 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 833 | menuid: 'availability' 834 | } 835 | } 836 | } 837 | } 838 | } 839 | } 840 | { 841 | name: 'sharedTimeRange' 842 | isOptional: true 843 | } 844 | ] 845 | #disable-next-line BCP036 846 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 847 | settings: {} 848 | } 849 | } 850 | { 851 | position: { 852 | x: 4 853 | y: 5 854 | colSpan: 4 855 | rowSpan: 3 856 | } 857 | metadata: { 858 | inputs: [ 859 | { 860 | name: 'options' 861 | value: { 862 | chart: { 863 | metrics: [ 864 | { 865 | resourceMetadata: { 866 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 867 | } 868 | name: 'exceptions/server' 869 | aggregationType: 7 870 | namespace: 'microsoft.insights/components' 871 | metricVisualization: { 872 | displayName: 'Server exceptions' 873 | color: '#47BDF5' 874 | } 875 | } 876 | { 877 | resourceMetadata: { 878 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 879 | } 880 | name: 'dependencies/failed' 881 | aggregationType: 7 882 | namespace: 'microsoft.insights/components' 883 | metricVisualization: { 884 | displayName: 'Dependency failures' 885 | color: '#7E58FF' 886 | } 887 | } 888 | ] 889 | title: 'Server exceptions and Dependency failures' 890 | visualization: { 891 | chartType: 2 892 | legendVisualization: { 893 | isVisible: true 894 | position: 2 895 | hideSubtitle: false 896 | } 897 | axisVisualization: { 898 | x: { 899 | isVisible: true 900 | axisType: 2 901 | } 902 | y: { 903 | isVisible: true 904 | axisType: 1 905 | } 906 | } 907 | } 908 | } 909 | } 910 | } 911 | { 912 | name: 'sharedTimeRange' 913 | isOptional: true 914 | } 915 | ] 916 | #disable-next-line BCP036 917 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 918 | settings: {} 919 | } 920 | } 921 | { 922 | position: { 923 | x: 8 924 | y: 5 925 | colSpan: 4 926 | rowSpan: 3 927 | } 928 | metadata: { 929 | inputs: [ 930 | { 931 | name: 'options' 932 | value: { 933 | chart: { 934 | metrics: [ 935 | { 936 | resourceMetadata: { 937 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 938 | } 939 | name: 'performanceCounters/processorCpuPercentage' 940 | aggregationType: 4 941 | namespace: 'microsoft.insights/components' 942 | metricVisualization: { 943 | displayName: 'Processor time' 944 | color: '#47BDF5' 945 | } 946 | } 947 | { 948 | resourceMetadata: { 949 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 950 | } 951 | name: 'performanceCounters/processCpuPercentage' 952 | aggregationType: 4 953 | namespace: 'microsoft.insights/components' 954 | metricVisualization: { 955 | displayName: 'Process CPU' 956 | color: '#7E58FF' 957 | } 958 | } 959 | ] 960 | title: 'Average processor and process CPU utilization' 961 | visualization: { 962 | chartType: 2 963 | legendVisualization: { 964 | isVisible: true 965 | position: 2 966 | hideSubtitle: false 967 | } 968 | axisVisualization: { 969 | x: { 970 | isVisible: true 971 | axisType: 2 972 | } 973 | y: { 974 | isVisible: true 975 | axisType: 1 976 | } 977 | } 978 | } 979 | } 980 | } 981 | } 982 | { 983 | name: 'sharedTimeRange' 984 | isOptional: true 985 | } 986 | ] 987 | #disable-next-line BCP036 988 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 989 | settings: {} 990 | } 991 | } 992 | { 993 | position: { 994 | x: 12 995 | y: 5 996 | colSpan: 4 997 | rowSpan: 3 998 | } 999 | metadata: { 1000 | inputs: [ 1001 | { 1002 | name: 'options' 1003 | value: { 1004 | chart: { 1005 | metrics: [ 1006 | { 1007 | resourceMetadata: { 1008 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1009 | } 1010 | name: 'exceptions/browser' 1011 | aggregationType: 7 1012 | namespace: 'microsoft.insights/components' 1013 | metricVisualization: { 1014 | displayName: 'Browser exceptions' 1015 | color: '#47BDF5' 1016 | } 1017 | } 1018 | ] 1019 | title: 'Browser exceptions' 1020 | visualization: { 1021 | chartType: 2 1022 | legendVisualization: { 1023 | isVisible: true 1024 | position: 2 1025 | hideSubtitle: false 1026 | } 1027 | axisVisualization: { 1028 | x: { 1029 | isVisible: true 1030 | axisType: 2 1031 | } 1032 | y: { 1033 | isVisible: true 1034 | axisType: 1 1035 | } 1036 | } 1037 | } 1038 | } 1039 | } 1040 | } 1041 | { 1042 | name: 'sharedTimeRange' 1043 | isOptional: true 1044 | } 1045 | ] 1046 | #disable-next-line BCP036 1047 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1048 | settings: {} 1049 | } 1050 | } 1051 | { 1052 | position: { 1053 | x: 0 1054 | y: 8 1055 | colSpan: 4 1056 | rowSpan: 3 1057 | } 1058 | metadata: { 1059 | inputs: [ 1060 | { 1061 | name: 'options' 1062 | value: { 1063 | chart: { 1064 | metrics: [ 1065 | { 1066 | resourceMetadata: { 1067 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1068 | } 1069 | name: 'availabilityResults/count' 1070 | aggregationType: 7 1071 | namespace: 'microsoft.insights/components' 1072 | metricVisualization: { 1073 | displayName: 'Availability test results count' 1074 | color: '#47BDF5' 1075 | } 1076 | } 1077 | ] 1078 | title: 'Availability test results count' 1079 | visualization: { 1080 | chartType: 2 1081 | legendVisualization: { 1082 | isVisible: true 1083 | position: 2 1084 | hideSubtitle: false 1085 | } 1086 | axisVisualization: { 1087 | x: { 1088 | isVisible: true 1089 | axisType: 2 1090 | } 1091 | y: { 1092 | isVisible: true 1093 | axisType: 1 1094 | } 1095 | } 1096 | } 1097 | } 1098 | } 1099 | } 1100 | { 1101 | name: 'sharedTimeRange' 1102 | isOptional: true 1103 | } 1104 | ] 1105 | #disable-next-line BCP036 1106 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1107 | settings: {} 1108 | } 1109 | } 1110 | { 1111 | position: { 1112 | x: 4 1113 | y: 8 1114 | colSpan: 4 1115 | rowSpan: 3 1116 | } 1117 | metadata: { 1118 | inputs: [ 1119 | { 1120 | name: 'options' 1121 | value: { 1122 | chart: { 1123 | metrics: [ 1124 | { 1125 | resourceMetadata: { 1126 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1127 | } 1128 | name: 'performanceCounters/processIOBytesPerSecond' 1129 | aggregationType: 4 1130 | namespace: 'microsoft.insights/components' 1131 | metricVisualization: { 1132 | displayName: 'Process IO rate' 1133 | color: '#47BDF5' 1134 | } 1135 | } 1136 | ] 1137 | title: 'Average process I/O rate' 1138 | visualization: { 1139 | chartType: 2 1140 | legendVisualization: { 1141 | isVisible: true 1142 | position: 2 1143 | hideSubtitle: false 1144 | } 1145 | axisVisualization: { 1146 | x: { 1147 | isVisible: true 1148 | axisType: 2 1149 | } 1150 | y: { 1151 | isVisible: true 1152 | axisType: 1 1153 | } 1154 | } 1155 | } 1156 | } 1157 | } 1158 | } 1159 | { 1160 | name: 'sharedTimeRange' 1161 | isOptional: true 1162 | } 1163 | ] 1164 | #disable-next-line BCP036 1165 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1166 | settings: {} 1167 | } 1168 | } 1169 | { 1170 | position: { 1171 | x: 8 1172 | y: 8 1173 | colSpan: 4 1174 | rowSpan: 3 1175 | } 1176 | metadata: { 1177 | inputs: [ 1178 | { 1179 | name: 'options' 1180 | value: { 1181 | chart: { 1182 | metrics: [ 1183 | { 1184 | resourceMetadata: { 1185 | id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' 1186 | } 1187 | name: 'performanceCounters/memoryAvailableBytes' 1188 | aggregationType: 4 1189 | namespace: 'microsoft.insights/components' 1190 | metricVisualization: { 1191 | displayName: 'Available memory' 1192 | color: '#47BDF5' 1193 | } 1194 | } 1195 | ] 1196 | title: 'Average available memory' 1197 | visualization: { 1198 | chartType: 2 1199 | legendVisualization: { 1200 | isVisible: true 1201 | position: 2 1202 | hideSubtitle: false 1203 | } 1204 | axisVisualization: { 1205 | x: { 1206 | isVisible: true 1207 | axisType: 2 1208 | } 1209 | y: { 1210 | isVisible: true 1211 | axisType: 1 1212 | } 1213 | } 1214 | } 1215 | } 1216 | } 1217 | } 1218 | { 1219 | name: 'sharedTimeRange' 1220 | isOptional: true 1221 | } 1222 | ] 1223 | #disable-next-line BCP036 1224 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 1225 | settings: {} 1226 | } 1227 | } 1228 | ] 1229 | } 1230 | ] 1231 | } 1232 | } 1233 | 1234 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 1235 | name: applicationInsightsName 1236 | } 1237 | -------------------------------------------------------------------------------- /infra/core/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' 2 | param name string 3 | param dashboardName string = '' 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | param logAnalyticsWorkspaceId string 7 | 8 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | kind: 'web' 13 | properties: { 14 | Application_Type: 'web' 15 | WorkspaceResourceId: logAnalyticsWorkspaceId 16 | } 17 | } 18 | 19 | module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { 20 | name: 'application-insights-dashboard' 21 | params: { 22 | name: dashboardName 23 | location: location 24 | applicationInsightsName: applicationInsights.name 25 | } 26 | } 27 | 28 | output connectionString string = applicationInsights.properties.ConnectionString 29 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 30 | output name string = applicationInsights.name 31 | -------------------------------------------------------------------------------- /infra/core/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a Log Analytics workspace.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 7 | name: name 8 | location: location 9 | tags: tags 10 | properties: any({ 11 | retentionInDays: 30 12 | features: { 13 | searchVersion: 1 14 | } 15 | sku: { 16 | name: 'PerGB2018' 17 | } 18 | }) 19 | } 20 | 21 | output id string = logAnalytics.id 22 | output name string = logAnalytics.name 23 | -------------------------------------------------------------------------------- /infra/core/security/keyvault-access.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Assigns an Azure Key Vault access policy.' 2 | param name string = 'add' 3 | 4 | param keyVaultName string 5 | param permissions object = { secrets: [ 'get', 'list' ] } 6 | param principalId string 7 | 8 | resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { 9 | parent: keyVault 10 | name: name 11 | properties: { 12 | accessPolicies: [ { 13 | objectId: principalId 14 | tenantId: subscription().tenantId 15 | permissions: permissions 16 | } ] 17 | } 18 | } 19 | 20 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 21 | name: keyVaultName 22 | } 23 | -------------------------------------------------------------------------------- /infra/core/security/keyvault.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Key Vault.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param principalId string = '' 7 | 8 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | properties: { 13 | tenantId: subscription().tenantId 14 | sku: { family: 'A', name: 'standard' } 15 | accessPolicies: !empty(principalId) ? [ 16 | { 17 | objectId: principalId 18 | permissions: { secrets: [ 'get', 'list' ] } 19 | tenantId: subscription().tenantId 20 | } 21 | ] : [] 22 | } 23 | } 24 | 25 | output endpoint string = keyVault.properties.vaultUri 26 | output name string = keyVault.name 27 | -------------------------------------------------------------------------------- /infra/core/security/role.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a role assignment for a service principal.' 2 | param principalId string 3 | 4 | @allowed([ 5 | 'Device' 6 | 'ForeignGroup' 7 | 'Group' 8 | 'ServicePrincipal' 9 | 'User' 10 | ]) 11 | param principalType string = 'ServicePrincipal' 12 | param roleDefinitionId string 13 | 14 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 15 | name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) 16 | properties: { 17 | principalId: principalId 18 | principalType: principalType 19 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /infra/core/storage/storage-account.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure storage account.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @allowed([ 7 | 'Cool' 8 | 'Hot' 9 | 'Premium' ]) 10 | param accessTier string = 'Hot' 11 | param allowBlobPublicAccess bool = true 12 | param allowCrossTenantReplication bool = true 13 | param allowSharedKeyAccess bool = true 14 | param containers array = [] 15 | param defaultToOAuthAuthentication bool = false 16 | param deleteRetentionPolicy object = {} 17 | @allowed([ 'AzureDnsZone', 'Standard' ]) 18 | param dnsEndpointType string = 'Standard' 19 | param kind string = 'StorageV2' 20 | param minimumTlsVersion string = 'TLS1_2' 21 | param supportsHttpsTrafficOnly bool = true 22 | param networkAcls object = { 23 | bypass: 'AzureServices' 24 | defaultAction: 'Allow' 25 | } 26 | @allowed([ 'Enabled', 'Disabled' ]) 27 | param publicNetworkAccess string = 'Enabled' 28 | param sku object = { name: 'Standard_LRS' } 29 | 30 | resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { 31 | name: name 32 | location: location 33 | tags: tags 34 | kind: kind 35 | sku: sku 36 | properties: { 37 | accessTier: accessTier 38 | allowBlobPublicAccess: allowBlobPublicAccess 39 | allowCrossTenantReplication: allowCrossTenantReplication 40 | allowSharedKeyAccess: allowSharedKeyAccess 41 | defaultToOAuthAuthentication: defaultToOAuthAuthentication 42 | dnsEndpointType: dnsEndpointType 43 | minimumTlsVersion: minimumTlsVersion 44 | networkAcls: networkAcls 45 | publicNetworkAccess: publicNetworkAccess 46 | supportsHttpsTrafficOnly: supportsHttpsTrafficOnly 47 | } 48 | 49 | resource blobServices 'blobServices' = if (!empty(containers)) { 50 | name: 'default' 51 | properties: { 52 | deleteRetentionPolicy: deleteRetentionPolicy 53 | } 54 | resource container 'containers' = [for container in containers: { 55 | name: container.name 56 | properties: { 57 | publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' 58 | } 59 | }] 60 | } 61 | } 62 | 63 | output name string = storage.name 64 | output primaryEndpoints object = storage.properties.primaryEndpoints 65 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | param location string 11 | 12 | // Identity 13 | @description('Id of the user or app to assign application roles') 14 | param principalId string 15 | 16 | // OpenAI 17 | param openAIServiceName string = '' 18 | param openAISkuName string = 'S0' 19 | param embeddingDeploymentName string = 'embeddings' 20 | param gptDeploymentName string = 'gpt' 21 | 22 | // Azure SQL 23 | @secure() 24 | @description('SQL Server administrator password') 25 | param sqlAdminPassword string 26 | @secure() 27 | @description('Application user password') 28 | param appUserPassword string 29 | param dbServiceName string = '' 30 | param dbName string = 'session_recommender_v2' 31 | 32 | param keyVaultName string = '' 33 | 34 | param storageAccountName string = '' 35 | 36 | param functionAppName string = '' 37 | 38 | param hostingPlanName string = '' 39 | param staticWebAppName string = '' 40 | 41 | param applicationInsightsName string = '' 42 | 43 | param logAnalyticsName string = '' 44 | 45 | @description('Flag to Use keyvault to store and use keys') 46 | param useKeyVault bool = true 47 | 48 | param myTags object = {} 49 | 50 | var abbrs = loadJsonContent('./abbreviations.json') 51 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 52 | var tags = union({ 'azd-env-name': environmentName }, myTags) 53 | var rgName = 'rg-${environmentName}' 54 | 55 | // Organize resources in a resource group 56 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 57 | name: rgName 58 | location: location 59 | tags: tags 60 | } 61 | 62 | module openAI 'app/openai.bicep' = { 63 | name: 'openai' 64 | scope: rg 65 | params: { 66 | name: !empty(openAIServiceName) ? openAIServiceName : '${abbrs.cognitiveServicesAccounts}${resourceToken}' 67 | location: location 68 | tags: tags 69 | sku: { 70 | name: openAISkuName 71 | } 72 | deployments: [ 73 | { 74 | name: embeddingDeploymentName 75 | model: { 76 | format: 'OpenAI' 77 | name: 'text-embedding-ada-002' 78 | } 79 | capacity: 30 80 | } 81 | { 82 | name: gptDeploymentName 83 | model: { 84 | format: 'OpenAI' 85 | name: 'gpt-35-turbo' 86 | } 87 | capacity: 120 88 | } 89 | ] 90 | keyVaultName: keyVault.outputs.name 91 | useKeyVault: useKeyVault 92 | } 93 | } 94 | 95 | module database 'app/sqlserver.bicep' = { 96 | name: 'database' 97 | scope: rg 98 | params: { 99 | tags: tags 100 | location: location 101 | appUserPassword: appUserPassword 102 | sqlAdminPassword: sqlAdminPassword 103 | databaseName: dbName 104 | name: !empty(dbServiceName) ? dbServiceName : '${abbrs.sqlServers}catalog-${resourceToken}' 105 | openAIEndpoint: openAI.outputs.endpoint 106 | openAIServiceName: openAI.outputs.name 107 | openAIDeploymentName: embeddingDeploymentName 108 | principalId: principalId 109 | } 110 | } 111 | 112 | module keyVault 'core/security/keyvault.bicep' = { 113 | name: 'keyvault' 114 | scope: rg 115 | params: { 116 | name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' 117 | location: location 118 | tags: tags 119 | principalId: principalId 120 | } 121 | } 122 | 123 | module hostingPlan 'core/host/appserviceplan.bicep' = { 124 | name: 'hostingPlan' 125 | scope: rg 126 | params: { 127 | tags: tags 128 | location: location 129 | name: !empty(hostingPlanName) ? hostingPlanName : '${abbrs.webServerFarms}${resourceToken}' 130 | sku: { 131 | name: 'B1' 132 | } 133 | kind: 'linux' 134 | } 135 | } 136 | 137 | module logAnalytics 'core/monitor/loganalytics.bicep' ={ 138 | name: 'logAnalytics' 139 | scope: rg 140 | params: { 141 | name: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.insightsComponents}${resourceToken}' 142 | location: location 143 | } 144 | } 145 | 146 | module applicationInsights 'core/monitor/applicationinsights.bicep' = { 147 | name: 'monitoring' 148 | scope: rg 149 | params: { 150 | location: location 151 | tags: tags 152 | name: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' 153 | logAnalyticsWorkspaceId: logAnalytics.outputs.id 154 | } 155 | } 156 | 157 | module functionApp 'app/functions.bicep' = { 158 | name: 'function' 159 | scope: rg 160 | params: { 161 | tags: union(tags, { 'azd-service-name': 'functionapp' }) 162 | location: location 163 | storageAccountName: storageAccount.outputs.name 164 | openAIKeyName: useKeyVault ? openAI.outputs.openAIKeyName : '' 165 | functionAppName: !empty(functionAppName) ? functionAppName : '${abbrs.webSitesFunctions}${resourceToken}' 166 | hostingPlanId: hostingPlan.outputs.id 167 | sqlConnectionString: '${database.outputs.connectionString}; Password=${appUserPassword}' 168 | openAIEmebddingDeploymentName: embeddingDeploymentName 169 | openAIGPTDeploymentName: gptDeploymentName 170 | openAIEndpoint: openAI.outputs.endpoint 171 | keyVaultName: keyVault.outputs.name 172 | applicationInsightsConnectionString: applicationInsights.outputs.connectionString 173 | useKeyVault: useKeyVault 174 | openAIName: openAI.outputs.name 175 | keyVaultEndpoint: keyVault.outputs.endpoint 176 | } 177 | } 178 | 179 | module storageAccount 'core/storage/storage-account.bicep' = { 180 | name: 'storage' 181 | scope: rg 182 | params: { 183 | tags: tags 184 | name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' 185 | location: location 186 | } 187 | } 188 | 189 | module funcaccess './core/security/keyvault-access.bicep' = if (useKeyVault) { 190 | name: 'web-keyvault-access' 191 | scope: rg 192 | params: { 193 | keyVaultName: keyVault.outputs.name 194 | principalId: functionApp.outputs.identityPrincipalId 195 | } 196 | } 197 | 198 | module web 'app/staticwebapp.bicep' = { 199 | name: 'web' 200 | scope: rg 201 | params: { 202 | name: !empty(staticWebAppName) ? staticWebAppName : '${abbrs.webStaticSites}${resourceToken}' 203 | location: location 204 | tags: union(tags, { 'azd-service-name': 'web' }) 205 | sqlConnectionString: '${database.outputs.connectionString}; Password=${appUserPassword}' 206 | sqlServerId: database.outputs.id 207 | sqlServerLocation: location 208 | apiResourceId: functionApp.outputs.functionAppResourceId 209 | } 210 | } 211 | 212 | output AZURE_SQL_SQLSERVICE_CONNECTION_STRING_KEY string = database.outputs.connectionStringKey 213 | output AZURE_FUNCTIONAPP_NAME string = functionApp.outputs.name 214 | output AZURE_FUNCTIONAPP_ID string = functionApp.outputs.functionAppResourceId 215 | output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint 216 | output AZURE_KEY_VALUT_NAME string = keyVault.outputs.name 217 | output AZURE_LOCATION string = location 218 | output AZURE_TENANT_ID string = tenant().tenantId 219 | output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString 220 | output AZURE_STORAGE_NAME string = storageAccount.outputs.name 221 | output AZURE_STATIC_WEB_URL string = web.outputs.uri 222 | output LOG_ANALYTICS_ID string = logAnalytics.outputs.id 223 | output USE_KEY_VAULT bool = useKeyVault 224 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "${AZURE_LOCATION}" 7 | }, 8 | "environmentName": { 9 | "value": "${AZURE_ENV_NAME}" 10 | }, 11 | "principalId": { 12 | "value": "${AZURE_PRINCIPAL_ID}" 13 | }, 14 | "sqlAdminPassword": { 15 | "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} sqlAdminPassword)" 16 | }, 17 | "appUserPassword": { 18 | "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} appUserPassword)" 19 | }, 20 | "useKeyVault": { 21 | "value": "${USE_KEY_VAULT=false}" 22 | }, 23 | "myTags": { 24 | "value": {} 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /scripts/install-dev-tools.sh: -------------------------------------------------------------------------------- 1 | sudo cp ./scripts/ms-repo.pref /etc/apt/preferences.d/ 2 | 3 | export dotnet_version="8.0" 4 | export dab_version="1.1.7" 5 | export sqlcmd_version="1.6.0" 6 | export func_version="4" 7 | export sqlprj_version="0.1.19-preview" 8 | 9 | export debian_version=$(if command -v lsb_release &> /dev/null; then lsb_release -r -s; else grep -oP '(?<=^VERSION_ID=).+' /etc/os-release | tr -d '"'; fi) 10 | 11 | wget https://packages.microsoft.com/config/debian/$debian_version/packages-microsoft-prod.deb -O packages-microsoft-prod.deb 12 | sudo dpkg -i packages-microsoft-prod.deb 13 | rm packages-microsoft-prod.deb 14 | sudo apt update 15 | 16 | sudo apt install dotnet-sdk-$dotnet_version -y 17 | 18 | npm install -g azure-functions-core-tools@$func_version --unsafe-perm true 19 | 20 | npm install -g @azure/static-web-apps-cli 21 | 22 | dotnet tool install -g microsoft.sqlpackage 23 | dotnet new install Microsoft.Build.Sql.Templates::$sqlprj_version 24 | 25 | dotnet tool install -g Microsoft.DataApiBuilder --version $dab_version 26 | 27 | sudo apt-get install sqlcmd 28 | sudo wget https://github.com/microsoft/go-sqlcmd/releases/download/v$sqlcmd_version/sqlcmd-v$sqlcmd_version-linux-amd64.tar.bz2 29 | sudo bunzip2 sqlcmd-v$sqlcmd_version-linux-amd64.tar.bz2 30 | sudo tar xvf sqlcmd-v$sqlcmd_version-linux-amd64.tar 31 | sudo mv sqlcmd /usr/bin/sqlcmd 32 | sudo rm sqlcmd-v$sqlcmd_version-linux-amd64.tar 33 | sudo rm sqlcmd_debug 34 | sudo rm NOTICE.md 35 | 36 | if [[ ":$PATH:" == *":$HOME/.dotnet/tools:"* ]]; then 37 | echo "Path already includes ~/.dotnet/tools, skipping." 38 | else 39 | echo "Adding ~/.dotnet/tools to path." 40 | echo 'PATH=$PATH:$HOME/.dotnet/tools' >> ~/.bashrc 41 | fi -------------------------------------------------------------------------------- /scripts/ms-repo.pref: -------------------------------------------------------------------------------- 1 | Package: dotnet* aspnet* netstandard* 2 | Pin: origin "archive.ubuntu.com" 3 | Pin-Priority: -10 -------------------------------------------------------------------------------- /swa-cli.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", 3 | "configurations": { 4 | "client": { 5 | "appLocation": "client", 6 | "outputLocation": "dist", 7 | "apiLocation": "func", 8 | "dataApiLocation": "swa-db-connections", 9 | "appBuildCommand": "npm run build", 10 | "run": "npm run dev" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /swa-db-connections/staticwebapp.database.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/Azure/data-api-builder/releases/download/v0.9.7/dab.draft.schema.json", 3 | "data-source": { 4 | "database-type": "mssql", 5 | "connection-string": "@env('MSSQL')", 6 | "options": { 7 | "set-session-context": false 8 | } 9 | }, 10 | "runtime": { 11 | "rest": { 12 | "enabled": true, 13 | "path": "/rest", 14 | "request-body-strict": true 15 | }, 16 | "graphql": { 17 | "enabled": true, 18 | "path": "/graphql", 19 | "allow-introspection": true 20 | }, 21 | "host": { 22 | "cors": { 23 | "origins": [ 24 | "*" 25 | ], 26 | "allow-credentials": false 27 | }, 28 | "authentication": { 29 | "provider": "StaticWebApps" 30 | }, 31 | "mode": "development" 32 | } 33 | }, 34 | "entities": { 35 | "FindRelatedSessions": { 36 | "source": { 37 | "object": "web.find_sessions", 38 | "type": "stored-procedure", 39 | "parameters": { 40 | "text": "", 41 | "top": 10, 42 | "min_similarity": 0.30 43 | } 44 | }, 45 | "graphql": { 46 | "enabled": false, 47 | "operation": "query" 48 | }, 49 | "rest": { 50 | "enabled": true, 51 | "path": "/find", 52 | "methods": [ 53 | "post" 54 | ] 55 | }, 56 | "permissions": [ 57 | { 58 | "role": "anonymous", 59 | "actions": [ 60 | { 61 | "action": "execute" 62 | } 63 | ] 64 | } 65 | ] 66 | }, 67 | "GetSessionsCount": { 68 | "source": { 69 | "object": "web.get_sessions_count", 70 | "type": "stored-procedure" 71 | }, 72 | "graphql": { 73 | "enabled": false, 74 | "operation": "query" 75 | }, 76 | "rest": { 77 | "enabled": true, 78 | "path": "/sessions-count", 79 | "methods": [ 80 | "get" 81 | ] 82 | }, 83 | "permissions": [ 84 | { 85 | "role": "anonymous", 86 | "actions": [ 87 | { 88 | "action": "execute" 89 | } 90 | ] 91 | } 92 | ] 93 | }, 94 | "Session": { 95 | "source": { 96 | "object": "web.sessions", 97 | "type": "table" 98 | }, 99 | "graphql": { 100 | "enabled": true, 101 | "type": { 102 | "singular": "Session", 103 | "plural": "Sessions" 104 | } 105 | }, 106 | "rest": { 107 | "enabled": true, 108 | "path": "/sessions" 109 | }, 110 | "permissions": [ 111 | { 112 | "role": "anonymous", 113 | "actions": [ 114 | { 115 | "action": "read" 116 | } 117 | ] 118 | }, 119 | { 120 | "role": "authenticated", 121 | "actions": [ 122 | { 123 | "action": "*" 124 | } 125 | ] 126 | } 127 | 128 | ] 129 | } 130 | } 131 | } --------------------------------------------------------------------------------