├── .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 | 
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 | 
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 | 
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 |
18 | {children}
19 |
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 |
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 | }
--------------------------------------------------------------------------------