├── .devcontainer ├── devcontainer.json └── post-start.sh ├── .gitattributes ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── azure.yaml ├── imgs ├── 01SampleApp.png ├── 10azdinit.png ├── 15deployservicescomplete.png ├── 16AzureResourceDeployComplete.png ├── 16OllamaRAM.png ├── 17infrafiles.png ├── 20OpenAzureWebApp.png ├── 25ChatCheckLLMStatus.png ├── 30ModelWorkinginAzure.png ├── 60youtubeoverview.png └── 61youtubedeploy.png ├── infra ├── apiservice.tmpl.yaml ├── cache.tmpl.yaml ├── main.bicep ├── main.parameters.json ├── ollama.tmpl.yaml ├── resources.bicep ├── sample │ ├── apiservice.tmpl.yaml │ ├── cache.tmpl.yaml │ ├── main.bicep │ ├── main.parameters.json │ ├── ollama.tmpl.yaml │ ├── resources.bicep │ └── webfrontend.tmpl.yaml └── webfrontend.tmpl.yaml ├── scripts ├── listAIQuotas.ps1 ├── listAIQuotas.sh ├── listApiVersions.ps1 └── listApiVersions.sh └── src ├── AspireAIBlazorChatBot.ApiService ├── AspireAIBlazorChatBot.ApiService.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json ├── AspireAIBlazorChatBot.AppHost ├── .gitignore ├── AspireAIBlazorChatBot.AppHost.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json ├── azure.yaml ├── infra │ ├── apiservice.tmpl.yaml │ ├── cache.tmpl.yaml │ ├── main.bicep │ ├── main.parameters.json │ ├── ollama.tmpl.yaml │ ├── resources.bicep │ └── webfrontend.tmpl.yaml └── next-steps.md ├── AspireAIBlazorChatBot.ServiceDefaults ├── AspireAIBlazorChatBot.ServiceDefaults.csproj └── Extensions.cs ├── AspireAIBlazorChatBot.Web ├── AspireAIBlazorChatBot.Web.csproj ├── Components │ ├── App.razor │ ├── Chatbot │ │ ├── ChatState.cs │ │ ├── Chatbot.razor │ │ ├── Chatbot.razor.css │ │ ├── Chatbot.razor.js │ │ ├── MessageProcessor.cs │ │ ├── ShowChatbotButton.razor │ │ └── ShowChatbotButton.razor.css │ ├── Layout │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ └── NavMenu.razor.css │ ├── Pages │ │ ├── Counter.razor │ │ ├── Error.razor │ │ ├── Home.razor │ │ └── Weather.razor │ ├── Routes.razor │ └── _Imports.razor ├── OllamaApiService.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── WeatherApiClient.cs ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── app.css │ ├── bootstrap │ ├── bootstrap.min.css │ └── bootstrap.min.css.map │ ├── chat.png │ └── favicon.png └── AspireAIBlazorChatBot.sln /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-aspire-blazor-chat-quickstart-csharp", 3 | "image": "mcr.microsoft.com/dotnet/sdk:8.0", 4 | "features": { 5 | "ghcr.io/devcontainers/features/azure-cli:1": { 6 | "installBicep": true, 7 | "version": "latest" 8 | }, 9 | "ghcr.io/azure/azure-dev/azd:latest": { 10 | "version": "stable" 11 | }, 12 | "ghcr.io/devcontainers/features/docker-in-docker:2.12.0": {} 13 | }, 14 | "customizations": { 15 | "vscode": { 16 | "extensions": [ 17 | "ms-dotnettools.csdevkit", 18 | "ms-dotnettools.vscode-dotnet-runtime", 19 | "dbaeumer.vscode-eslint", 20 | "esbenp.prettier-vscode", 21 | "GitHub.vscode-github-actions", 22 | "ms-azuretools.azure-dev", 23 | "ms-azuretools.vscode-bicep", 24 | "ms-azuretools.vscode-docker", 25 | "ms-vscode.js-debug" ] 26 | } 27 | }, 28 | //"postCreateCommand": "sudo chown -R vscode:vscode /workspaces/ai-aspire-blazor-chat-quickstart-csharp | ./.devcontainer/post-start.sh", 29 | "postCreateCommand": "./.devcontainer/post-start.sh", 30 | "postAttachCommand": "", 31 | "forwardPorts": [3000, 3100], 32 | "hostRequirements": { 33 | "memory": "8gb" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.devcontainer/post-start.sh: -------------------------------------------------------------------------------- 1 | echo "==================================================" 2 | echo "Running post-start.sh" 3 | echo "==================================================" 4 | 5 | echo "Running apt-get update" 6 | apt-get update && \ 7 | apt upgrade -y && \ 8 | apt-get install -y dos2unix libsecret-1-0 xdg-utils && \ 9 | apt clean -y && \ 10 | rm -rf /var/lib/apt/lists/* 11 | echo "Installed dos2unix, libsecret-1-0, xdg-utils" 12 | 13 | ## Install .NET Aspire workload 14 | echo "Installing .NET Aspire workload" 15 | dotnet workload update 16 | dotnet workload install aspire 17 | echo "Installed .NET Aspire workload" 18 | 19 | # build the project 20 | echo "Building the project [./src/AspireBlazorAIChatBot.AppHost]" 21 | cd ./src/AspireBlazorAIChatBot.AppHost 22 | dotnet restore 23 | dotnet build 24 | echo "Project built [./src/AspireBlazorAIChatBot.AppHost]" 25 | 26 | echo "==================================================" 27 | echo "Devcontainer setup complete" 28 | echo "==================================================" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Azure Developer CLI generated folder 2 | # including environment variables 3 | .azure 4 | 5 | # User-specific configuration 6 | appsettings.Local.json 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | ## 11 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 12 | 13 | # User-specific files 14 | *.rsuser 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # User-specific files (MonoDevelop/Xamarin Studio) 21 | *.userprefs 22 | 23 | # Mono auto generated files 24 | mono_crash.* 25 | 26 | # Build results 27 | [Dd]ebug/ 28 | [Dd]ebugPublic/ 29 | [Rr]elease/ 30 | [Rr]eleases/ 31 | x64/ 32 | x86/ 33 | [Ww][Ii][Nn]32/ 34 | [Aa][Rr][Mm]/ 35 | [Aa][Rr][Mm]64/ 36 | bld/ 37 | [Bb]in/ 38 | [Oo]bj/ 39 | [Ll]og/ 40 | [Ll]ogs/ 41 | 42 | # Visual Studio 2015/2017 cache/options directory 43 | .vs/ 44 | # Uncomment if you have tasks that create the project's static files in wwwroot 45 | #wwwroot/ 46 | 47 | # Visual Studio 2017 auto generated files 48 | Generated\ Files/ 49 | 50 | # MSTest test Results 51 | [Tt]est[Rr]esult*/ 52 | [Bb]uild[Ll]og.* 53 | 54 | # NUnit 55 | *.VisualState.xml 56 | TestResult.xml 57 | nunit-*.xml 58 | 59 | # Build Results of an ATL Project 60 | [Dd]ebugPS/ 61 | [Rr]eleasePS/ 62 | dlldata.c 63 | 64 | # Benchmark Results 65 | BenchmarkDotNet.Artifacts/ 66 | 67 | # .NET Core 68 | project.lock.json 69 | project.fragment.lock.json 70 | artifacts/ 71 | 72 | # ASP.NET Scaffolding 73 | ScaffoldingReadMe.txt 74 | 75 | # StyleCop 76 | StyleCopReport.xml 77 | 78 | # Files built by Visual Studio 79 | *_i.c 80 | *_p.c 81 | *_h.h 82 | *.ilk 83 | *.meta 84 | *.obj 85 | *.iobj 86 | *.pch 87 | *.pdb 88 | *.ipdb 89 | *.pgc 90 | *.pgd 91 | *.rsp 92 | *.sbr 93 | *.tlb 94 | *.tli 95 | *.tlh 96 | *.tmp 97 | *.tmp_proj 98 | *_wpftmp.csproj 99 | *.log 100 | *.tlog 101 | *.vspscc 102 | *.vssscc 103 | .builds 104 | *.pidb 105 | *.svclog 106 | *.scc 107 | 108 | # Chutzpah Test files 109 | _Chutzpah* 110 | 111 | # Visual C++ cache files 112 | ipch/ 113 | *.aps 114 | *.ncb 115 | *.opendb 116 | *.opensdf 117 | *.sdf 118 | *.cachefile 119 | *.VC.db 120 | *.VC.VC.opendb 121 | 122 | # Visual Studio profiler 123 | *.psess 124 | *.vsp 125 | *.vspx 126 | *.sap 127 | 128 | # Visual Studio Trace Files 129 | *.e2e 130 | 131 | # TFS 2012 Local Workspace 132 | $tf/ 133 | 134 | # Guidance Automation Toolkit 135 | *.gpState 136 | 137 | # ReSharper is a .NET coding add-in 138 | _ReSharper*/ 139 | *.[Rr]e[Ss]harper 140 | *.DotSettings.user 141 | 142 | # TeamCity is a build add-in 143 | _TeamCity* 144 | 145 | # DotCover is a Code Coverage Tool 146 | *.dotCover 147 | 148 | # AxoCover is a Code Coverage Tool 149 | .axoCover/* 150 | !.axoCover/settings.json 151 | 152 | # Coverlet is a free, cross platform Code Coverage Tool 153 | coverage*.json 154 | coverage*.xml 155 | coverage*.info 156 | 157 | # Visual Studio code coverage results 158 | *.coverage 159 | *.coveragexml 160 | 161 | # NCrunch 162 | _NCrunch_* 163 | .*crunch*.local.xml 164 | nCrunchTemp_* 165 | 166 | # MightyMoose 167 | *.mm.* 168 | AutoTest.Net/ 169 | 170 | # Web workbench (sass) 171 | .sass-cache/ 172 | 173 | # Installshield output folder 174 | [Ee]xpress/ 175 | 176 | # DocProject is a documentation generator add-in 177 | DocProject/buildhelp/ 178 | DocProject/Help/*.HxT 179 | DocProject/Help/*.HxC 180 | DocProject/Help/*.hhc 181 | DocProject/Help/*.hhk 182 | DocProject/Help/*.hhp 183 | DocProject/Help/Html2 184 | DocProject/Help/html 185 | 186 | # Click-Once directory 187 | publish/ 188 | 189 | # Publish Web Output 190 | *.[Pp]ublish.xml 191 | *.azurePubxml 192 | # Note: Comment the next line if you want to checkin your web deploy settings, 193 | # but database connection strings (with potential passwords) will be unencrypted 194 | *.pubxml 195 | *.publishproj 196 | 197 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 198 | # checkin your Azure Web App publish settings, but sensitive information contained 199 | # in these scripts will be unencrypted 200 | PublishScripts/ 201 | 202 | # NuGet Packages 203 | *.nupkg 204 | # NuGet Symbol Packages 205 | *.snupkg 206 | # The packages folder can be ignored because of Package Restore 207 | **/[Pp]ackages/* 208 | # except build/, which is used as an MSBuild target. 209 | !**/[Pp]ackages/build/ 210 | # Uncomment if necessary however generally it will be regenerated when needed 211 | #!**/[Pp]ackages/repositories.config 212 | # NuGet v3's project.json files produces more ignorable files 213 | *.nuget.props 214 | *.nuget.targets 215 | 216 | # Microsoft Azure Build Output 217 | csx/ 218 | *.build.csdef 219 | 220 | # Microsoft Azure Emulator 221 | ecf/ 222 | rcf/ 223 | 224 | # Windows Store app package directories and files 225 | AppPackages/ 226 | BundleArtifacts/ 227 | Package.StoreAssociation.xml 228 | _pkginfo.txt 229 | *.appx 230 | *.appxbundle 231 | *.appxupload 232 | 233 | # Visual Studio cache files 234 | # files ending in .cache can be ignored 235 | *.[Cc]ache 236 | # but keep track of directories ending in .cache 237 | !?*.[Cc]ache/ 238 | 239 | # Others 240 | ClientBin/ 241 | ~$* 242 | *~ 243 | *.dbmdl 244 | *.dbproj.schemaview 245 | *.jfm 246 | *.pfx 247 | *.publishsettings 248 | orleans.codegen.cs 249 | 250 | # Including strong name files can present a security risk 251 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 252 | #*.snk 253 | 254 | # Since there are multiple workflows, uncomment next line to ignore bower_components 255 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 256 | #bower_components/ 257 | 258 | # RIA/Silverlight projects 259 | Generated_Code/ 260 | 261 | # Backup & report files from converting an old project file 262 | # to a newer Visual Studio version. Backup files are not needed, 263 | # because we have git ;-) 264 | _UpgradeReport_Files/ 265 | Backup*/ 266 | UpgradeLog*.XML 267 | UpgradeLog*.htm 268 | ServiceFabricBackup/ 269 | *.rptproj.bak 270 | 271 | # SQL Server files 272 | *.mdf 273 | *.ldf 274 | *.ndf 275 | 276 | # Business Intelligence projects 277 | *.rdl.data 278 | *.bim.layout 279 | *.bim_*.settings 280 | *.rptproj.rsuser 281 | *- [Bb]ackup.rdl 282 | *- [Bb]ackup ([0-9]).rdl 283 | *- [Bb]ackup ([0-9][0-9]).rdl 284 | 285 | # Microsoft Fakes 286 | FakesAssemblies/ 287 | 288 | # GhostDoc plugin setting file 289 | *.GhostDoc.xml 290 | 291 | # Node.js Tools for Visual Studio 292 | .ntvs_analysis.dat 293 | node_modules/ 294 | 295 | # Visual Studio 6 build log 296 | *.plg 297 | 298 | # Visual Studio 6 workspace options file 299 | *.opt 300 | 301 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 302 | *.vbw 303 | 304 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 305 | *.vbp 306 | 307 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 308 | *.dsw 309 | *.dsp 310 | 311 | # Visual Studio 6 technical files 312 | *.ncb 313 | *.aps 314 | 315 | # Visual Studio LightSwitch build output 316 | **/*.HTMLClient/GeneratedArtifacts 317 | **/*.DesktopClient/GeneratedArtifacts 318 | **/*.DesktopClient/ModelManifest.xml 319 | **/*.Server/GeneratedArtifacts 320 | **/*.Server/ModelManifest.xml 321 | _Pvt_Extensions 322 | 323 | # Paket dependency manager 324 | .paket/paket.exe 325 | paket-files/ 326 | 327 | # FAKE - F# Make 328 | .fake/ 329 | 330 | # CodeRush personal settings 331 | .cr/personal 332 | 333 | # Python Tools for Visual Studio (PTVS) 334 | __pycache__/ 335 | *.pyc 336 | 337 | # Cake - Uncomment if you are using it 338 | # tools/** 339 | # !tools/packages.config 340 | 341 | # Tabs Studio 342 | *.tss 343 | 344 | # Telerik's JustMock configuration file 345 | *.jmconfig 346 | 347 | # BizTalk build output 348 | *.btp.cs 349 | *.btm.cs 350 | *.odx.cs 351 | *.xsd.cs 352 | 353 | # OpenCover UI analysis results 354 | OpenCover/ 355 | 356 | # Azure Stream Analytics local run output 357 | ASALocalRun/ 358 | 359 | # MSBuild Binary and Structured Log 360 | *.binlog 361 | 362 | # NVidia Nsight GPU debugger configuration file 363 | *.nvuser 364 | 365 | # MFractors (Xamarin productivity tool) working folder 366 | .mfractor/ 367 | 368 | # Local History for Visual Studio 369 | .localhistory/ 370 | 371 | # Visual Studio History (VSHistory) files 372 | .vshistory/ 373 | 374 | # BeatPulse healthcheck temp database 375 | healthchecksdb 376 | 377 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 378 | MigrationBackup/ 379 | 380 | # Ionide (cross platform F# VS Code tools) working folder 381 | .ionide/ 382 | 383 | # Fody - auto-generated XML schema 384 | FodyWeavers.xsd 385 | 386 | # VS Code files for those working on multiple tools 387 | .vscode/* 388 | !.vscode/settings.json 389 | !.vscode/tasks.json 390 | !.vscode/launch.json 391 | !.vscode/extensions.json 392 | *.code-workspace 393 | 394 | # Local History for Visual Studio Code 395 | .history/ 396 | 397 | # Windows Installer files from build outputs 398 | *.cab 399 | *.msi 400 | *.msix 401 | *.msm 402 | *.msp 403 | 404 | # JetBrains Rider 405 | *.sln.iml 406 | 407 | # AZD 408 | .[aA]zure/ -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ai-chat-quickstart-csharp 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/Azure-Samples/ai-chat-quickstart-csharp/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: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2022 (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 | # Chat Application using Aspire and Ollama (C#/.NET) 2 | 3 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/elbruno/ai-aspire-blazor-chat-quickstart-csharp) 4 | [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp) 5 | 6 | This repository includes a .NET/C# app, created using the .NET Aspire Starter App sample demo, that uses redis. 7 | 8 | The repository includes all the infrastructure and configuration needed to provision the solution resources and deploy the app to [Azure Container Apps](https://learn.microsoft.com/azure/container-apps/overview) using the [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview). 9 | 10 | Check the resources section to find supporting videos with an overview of the project, and a step-by-step video to demo the delpoyt to Azure. 11 | 12 | ***Note:** The current solution uses [.NET Aspire Community Toolkit Ollama integration](https://learn.microsoft.com/en-us/dotnet/aspire/community-toolkit/ollama?tabs=dotnet-cli%2Cdocker) to include and deploy a Phi-3.5 model without the need to install ollama locally.* 13 | 14 | * [Features](#features) 15 | * [Architecture diagram](#architecture-diagram) 16 | * [Getting started](#getting-started) 17 | * [Local Environment - Visual Studio or VS Code](#local-environment) 18 | * [GitHub Codespaces](#github-codespaces) 19 | * [VS Code Dev Containers](#vs-code-dev-containers) 20 | * [Deploying](#deploying) 21 | * [Azure account setup](#azure-account-setup) 22 | * [Deploying with azd](#deploying-with-azd) 23 | * [Aspire deploy process overview](#aspire-deploy-process-overview) 24 | * [Next Steps](#next-steps) 25 | * [Deploy Phi 3.5 in Azure AI](#use-slm-deploy-in-azure-ai) 26 | * [Update to use Phi 3.5 in Azure](#use-slm-deploy-in-azure-ai) 27 | * [Guidance](#guidance) 28 | * [Costs](#costs) 29 | * [Security Guidelines](#security-guidelines) 30 | * [Resources](#resources) 31 | 32 | ## Features 33 | 34 | * A [Blazor Front End](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor) that uses [Microsoft.Extensions.AI](https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/) package to access language models to generate responses to user messages. 35 | * The Blazor Frontend app included with the .NET Aspire Starter App template, that display a chat panel to interact with the LLM. 36 | * [Bicep files](https://docs.microsoft.com/azure/azure-resource-manager/bicep/) for provisioning the necessary Azure resources, including Azure Container Apps, Azure Container Registry, Azure Log Analytics, and RBAC roles. 37 | * Using the [Phi-3.5](https://aka.ms/Phi-3CookBook) model through [Ollama](https://ollama.com/library) running in a container. 38 | 39 | ![Screenshot of the chat app](/imgs/01SampleApp.png) 40 | 41 | ## Architecture diagram 42 | 43 | ** Work In Progress ** 44 | 45 | ## Getting started 46 | 47 | You have a few options for getting started with this template. 48 | The quickest way to get started is GitHub Codespaces, since it will setup all the tools for you, but you can also [set it up locally](#local-environment). 49 | 50 | ### GitHub Codespaces 51 | 52 | You can run this template virtually by using GitHub Codespaces. The button will open a web-based VS Code instance in your browser: 53 | 54 | 1. Open the template (this may take several minutes): 55 | 56 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/elbruno/ai-aspire-blazor-chat-quickstart-csharp) 57 | 58 | 2. Open a terminal window 59 | 3. Continue with the [deploying steps](#deploying) 60 | 61 | ### Local Environment 62 | 63 | If you're not using one of the above options for opening the project, then you'll need to: 64 | 65 | 1. Make sure the following tools are installed: 66 | 67 | * [.NET 8](https://dotnet.microsoft.com/downloads/) 68 | * [Git](https://git-scm.com/downloads) 69 | * [Azure Developer CLI (azd)](https://aka.ms/install-azd) 70 | * [VS Code](https://code.visualstudio.com/Download) or [Visual Studio](https://visualstudio.microsoft.com/downloads/) 71 | * If using VS Code, install the [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) 72 | 73 | 2. Fork and Clone the current repository: 74 | 75 | 3. If you're using Visual Studio, open `the src/AspireAIBlazorChatBot.sln` solution file. If you're using VS Code, open the src folder. 76 | 77 | 4. Continue with the [deploying steps](#deploying). 78 | 79 | ### VS Code Dev Containers 80 | 81 | A related option is VS Code Dev Containers, which will open the project in your local VS Code using the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers): 82 | 83 | 1. Start Docker Desktop (install it if not already installed) 84 | 2. Open the project: 85 | 86 | [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp) 87 | 88 | 3. In the VS Code window that opens, once the project files show up (this may take several minutes), open a terminal window. 89 | 90 | 4. Continue with the [deploying steps](#deploying) 91 | 92 | ## Deploying 93 | 94 | Once you've opened the project in [Codespaces](#github-codespaces), in [Dev Containers](#vs-code-dev-containers), or [locally](#local-environment), you can deploy it to Azure. 95 | 96 | ### Azure account setup 97 | 98 | 1. Sign up for a [free Azure account](https://azure.microsoft.com/free/) and create an Azure Subscription. 99 | 2. Check that you have the necessary permissions: 100 | 101 | * Your Azure account must have `Microsoft.Authorization/roleAssignments/write` permissions, such as [Role Based Access Control Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview), [User Access Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#user-access-administrator), or [Owner](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner). If you don't have subscription-level permissions, you must be granted [RBAC](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview) for an existing resource group and deploy to that existing group. 102 | * Your Azure account also needs `Microsoft.Resources/deployments/write` permissions on the subscription level. 103 | 104 | ### Deploying with azd 105 | 106 | From a Terminal window, open the folder with the clone of this repo. Run the following commands. 107 | 108 | 1. Login to Azure: 109 | 110 | ```bash 111 | azd auth login 112 | ``` 113 | 114 | 1. Init the resources for the Azure Deploy, define the environmetn name. Run the command: 115 | 116 | ```bash 117 | azd init 118 | ``` 119 | 1. Start the Azure Deploy process. Select the Azure Subscription and the Region (ie: **East US 2**). Run the command: 120 | 121 | ```bash 122 | azd up 123 | ``` 124 | 125 | 1. When `azd up` has finished deploying, you'll see 5 deployed URIs: 126 | 127 | * service apiservice 128 | * service cache 129 | * service ollama service 130 | * service webfrontend 131 | * Aspire Dashboard 132 | 133 | ![Console deploy complete including the endpoint URIs for service apiservice, service cache, service ollama service, service webfrontendand Aspire Dashboard](./imgs/15deployservicescomplete.png) 134 | 135 | The Azure resource group now have the following services deployed: 136 | 137 | ![Azure resource Group with all the services deployed](./imgs/16AzureResourceDeployComplete.png) 138 | 139 | 1. Open the webfrontend service url, and click on the `Check Model Status` button. This will start the download of the Phi-3.5 model. After a couple of minutes, click again and once the model is downloaded, the chat will be ready to be used. 140 | 141 | ### Aspire Deploy process overview 142 | 143 | The Azure Developer CLI (azd) has been extended to support deploying .NET Aspire projects. The following steps will help you walk through the process of deploying the current .NET Aspire project to Azure Container Apps using the Azure Developer CLI. 144 | 145 | * Explore how azd integration works with .NET Aspire projects 146 | * Provision and deploy resources on Azure for a .NET Aspire project using azd 147 | * Generate Bicep infrastructure and other template files using azd 148 | 149 | From a Terminal window, open the folder with the clone of this repo. Run the following commands. 150 | 151 | 1. Navigate to the .NET Aspire AppHost project: 152 | 153 | In Windows: 154 | 155 | ```bash 156 | cd .\src\AspireAIBlazorChatBot.AppHost\ 157 | ``` 158 | 159 | In Mac / Linux: 160 | 161 | ```bash 162 | cd ./src/AspireAIBlazorChatBot.AppHost/ 163 | ``` 164 | 165 | 1. Login to Azure: 166 | 167 | ```bash 168 | azd auth login 169 | ``` 170 | 171 | 1. Init the resources for the Azure Deploy, and select the option to use the code in the current directory and define the name for the new environment. Run the command: 172 | 173 | ```bash 174 | azd init 175 | ``` 176 | 177 | This image shows an example creating an environment named `aspireollamachat`. 178 | 179 | ![Sample console running azd init, selecting the project option and defining a new environment name](./imgs/10azdinit.png) 180 | 181 | To describe the infrastructure and application, an `azure.yaml` was added with the AppHost directory. 182 | 183 | 1. Provision and deploy all the resources: 184 | 185 | ```shell 186 | azd up 187 | ``` 188 | 189 | It will prompt you to select a Subscription from your Azure account, and select a location (like "**East US 2**"). Then it will provision the resources in your account and deploy the latest code. If you get an error or timeout with deployment, changing the location can help. 190 | 191 | 1. When `azd up` has finished deploying, you'll see 5 deployed URIs: 192 | 193 | * service apiservice 194 | * service cache 195 | * service ollama service 196 | * service webfrontend 197 | * Aspire Dashboard 198 | 199 | ![Console deploy complete including the endpoint URIs for service apiservice, service cache, service ollama service, service webfrontendand Aspire Dashboard](./imgs/15deployservicescomplete.png) 200 | 201 | The Azure resource group now have the following services deployed: 202 | 203 | ![Azure resource Group with all the services deployed](./imgs/16AzureResourceDeployComplete.png) 204 | 205 | ***Important:** The **ollama service** is not ready yet! Follow the next steps to complete the ollama deployment.* 206 | 207 | ### Update the Ollama Service 208 | 209 | The ollama service is ready to be use, however, it does not have any model yet. When connected to the console and try to pull a model, the following error will be displayed. 210 | 211 | ```shell 212 | Error: model requires more system memory (5.6 GiB) than is available (1.5 GiB) 213 | ``` 214 | 215 | ![Downloading models in ollama container in Azure require more RAM](./imgs/16OllamaRAM.png) 216 | 217 | We need to redeploy the ollama service with more system memory assigned. To modify the infrastructure that `azd` uses, run `azd infra synth` to persist it to disk. 218 | 219 | 1. Run the command to generate the infrastructure files. 220 | 221 | ```bash 222 | azd infra synth 223 | ``` 224 | 225 | 1. After running the command some additional directories will be created in the AppHost project directory: 226 | 227 | ```yaml 228 | - infra/ # Infrastructure as Code (bicep) files 229 | - main.bicep # main deployment module 230 | - resources.bicep # resources shared across your application's services 231 | ``` 232 | 233 | In addition, for each project resource referenced by in the app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. Each file contains the infrastructure as code for running the project on Azure Container Apps. 234 | 235 | ***Note**: Once you have synthesized your infrastructure to disk, changes made to your App Host will not be reflected in the infrastructure. You can re-generate the infrastructure by running `azd infra synth` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra synth` to overwrite the files without prompting.* 236 | 237 | 1. Let's edit the the `ollama.tmpl.yaml` file: 238 | 239 | ![Generated infrastructure files, including the ollama.tmpl.yaml](./imgs/17infrafiles.png) 240 | 241 | 1. On the ollama container definition, add the necessary resources including `cpu: 3` and `memory: "6.0Gi"`. 242 | 243 | ```yml 244 | containers: 245 | - image: {{ .Image }} 246 | name: ollama 247 | env: 248 | - name: AZURE_CLIENT_ID 249 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 250 | volumeMounts: 251 | - volumeName: ollama-aspireaiblazorchatbotapphostollamaollama 252 | mountPath: /root/.ollama 253 | resources: 254 | cpu: 3 255 | memory: "6.0Gi" 256 | ``` 257 | 258 | ***Note:** You can see a sample of the file in `.\infra\sample\ollama.tmpl.yaml`.* 259 | 260 | 1. When you've made any changes to the ollama infrastructure code, let's redeploy the service: 261 | 262 | ```shell 263 | azd deploy ollama 264 | ``` 265 | 266 | 1. Now we can deploy the model from the chat app. Open the chat application from the published url. 267 | 268 | In the azure portal, you can check the url from the published resource. 269 | 270 | ![Url to open the chat sample app from the Azure Portal](./imgs/20OpenAzureWebApp.png) 271 | 272 | 1. In the home page, press the `Check Model Status` button. This will start the model download if the model is not downloaded yet. And show the progress. **This process may take a couple of minutes**. 273 | 274 | 1. Once the model is full downloaded, the home page should display this: 275 | 276 | ![Url to open the chat sample app from the Azure Portal](./imgs/25ChatCheckLLMStatus.png) 277 | 278 | 1. Now the chat panel should be ready to interact with the user. 279 | 280 | ![Chat sample working](./imgs/30ModelWorkinginAzure.png) 281 | 282 | ### Continuous deployment with GitHub Actions 283 | 284 | 1. Create a workflow pipeline file locally. The following starters are available: 285 | 286 | * [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml) 287 | * [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml) 288 | 289 | 2. In the AppHost project folder, run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. 290 | 291 | ## Next steps 292 | 293 | ### Use SLM deploy in Azure AI 294 | 295 | **Coming soon** 296 | 297 | ## Guidance 298 | 299 | ### Costs 300 | 301 | Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage. 302 | The majority of the Azure resources used in this infrastructure are on usage-based pricing tiers. 303 | However, Azure Container Registry has a fixed cost per registry per day. 304 | 305 | You can try the [Azure pricing calculator](https://azure.com/e/2176802ea14941e4959eae8ad335aeb5) for the resources: 306 | 307 | * Azure Container App: Consumption tier with 0.5 CPU, 1GiB memory/storage. Pricing is based on resource allocation, and each month allows for a certain amount of free usage. [Pricing](https://azure.microsoft.com/pricing/details/container-apps/) 308 | * Azure Container Registry: Basic tier. [Pricing](https://azure.microsoft.com/pricing/details/container-registry/) 309 | * Log analytics: Pay-as-you-go tier. Costs based on data ingested. [Pricing](https://azure.microsoft.com/pricing/details/monitor/) 310 | 311 | ⚠️ To avoid unnecessary costs, remember to take down your app if it's no longer in use, 312 | either by deleting the resource group in the Portal or running `azd down`. 313 | 314 | ### Security Guidelines 315 | 316 | This template is ready to use [Managed Identity](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview) for authenticating to services like Azure OpenAI service. 317 | 318 | Additionally, we have added a [GitHub Action](https://github.com/microsoft/security-devops-action) that scans the infrastructure-as-code files and generates a report containing any detected issues. To ensure continued best practices in your own repository, we recommend that anyone creating solutions based on our templates ensure that the [Github secret scanning](https://docs.github.com/code-security/secret-scanning/about-secret-scanning) setting is enabled. 319 | 320 | You may want to consider additional security measures, such as: 321 | 322 | * Protecting the Azure Container Apps instance with a [firewall](https://learn.microsoft.com/azure/container-apps/waf-app-gateway) and/or [Virtual Network](https://learn.microsoft.com/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli). 323 | 324 | ## Resources 325 | 326 | * [RAG chat with Azure AI Search + C#/.NET](https://aka.ms/ragchatnet): A more advanced chat app that uses Azure AI Search to ground responses in domain knowledge. Includes user authentication with Microsoft Entra as well as data access controls. 327 | 328 | * [Develop .NET Apps with AI Features](https://learn.microsoft.com/en-us/dotnet/ai/get-started/dotnet-ai-overview) 329 | 330 | * [Build an AI Chat in .NET Aspire with Ollama: Quickstart Guide!](https://www.youtube.com/watch?v=jo1uyq-j26Y) 331 | 332 | ![Build an AI Chat in .NET Aspire with Ollama: Quickstart Guide!](./imgs/60youtubeoverview.png) 333 | 334 | * [Build an AI Chat in .NET Aspire with Ollama: Deploy to Azure Guide!](https://www.youtube.com/watch?v=J-Vy3pKaXS0) 335 | 336 | ![Build an AI Chat in .NET Aspire with Ollama: Deploy to Azure Guide!](./imgs/61youtubedeploy.png) -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: AspireAIBlazorChatBot.AppHost 4 | services: 5 | app: 6 | language: dotnet 7 | project: ./src/AspireAIBlazorChatBot.AppHost/AspireAIBlazorChatBot.AppHost.csproj 8 | host: containerapp 9 | -------------------------------------------------------------------------------- /imgs/01SampleApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/01SampleApp.png -------------------------------------------------------------------------------- /imgs/10azdinit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/10azdinit.png -------------------------------------------------------------------------------- /imgs/15deployservicescomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/15deployservicescomplete.png -------------------------------------------------------------------------------- /imgs/16AzureResourceDeployComplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/16AzureResourceDeployComplete.png -------------------------------------------------------------------------------- /imgs/16OllamaRAM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/16OllamaRAM.png -------------------------------------------------------------------------------- /imgs/17infrafiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/17infrafiles.png -------------------------------------------------------------------------------- /imgs/20OpenAzureWebApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/20OpenAzureWebApp.png -------------------------------------------------------------------------------- /imgs/25ChatCheckLLMStatus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/25ChatCheckLLMStatus.png -------------------------------------------------------------------------------- /imgs/30ModelWorkinginAzure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/30ModelWorkinginAzure.png -------------------------------------------------------------------------------- /imgs/60youtubeoverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/60youtubeoverview.png -------------------------------------------------------------------------------- /imgs/61youtubedeploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/imgs/61youtubedeploy.png -------------------------------------------------------------------------------- /infra/apiservice.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: {{ targetPortOrDefault 8080 }} 18 | transport: http 19 | allowInsecure: true 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | containers: 25 | - image: {{ .Image }} 26 | name: apiservice 27 | env: 28 | - name: AZURE_CLIENT_ID 29 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 30 | - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED 31 | value: "true" 32 | - name: HTTP_PORTS 33 | value: '{{ targetPortOrDefault 0 }}' 34 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 35 | value: "true" 36 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 37 | value: "true" 38 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 39 | value: in_memory 40 | scale: 41 | minReplicas: 1 42 | tags: 43 | azd-service-name: apiservice 44 | aspire-resource-name: apiservice 45 | -------------------------------------------------------------------------------- /infra/cache.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: 6379 18 | transport: tcp 19 | allowInsecure: false 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | containers: 25 | - image: {{ .Image }} 26 | name: cache 27 | env: 28 | - name: AZURE_CLIENT_ID 29 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 30 | scale: 31 | minReplicas: 1 32 | tags: 33 | azd-service-name: cache 34 | aspire-resource-name: cache 35 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('The location used for all deployed resources') 10 | param location string 11 | 12 | @description('Id of the user or app to assign application roles') 13 | param principalId string = '' 14 | 15 | 16 | var tags = { 17 | 'azd-env-name': environmentName 18 | } 19 | 20 | resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { 21 | name: 'rg-${environmentName}' 22 | location: location 23 | tags: tags 24 | } 25 | 26 | module resources 'resources.bicep' = { 27 | scope: rg 28 | name: 'resources' 29 | params: { 30 | location: location 31 | tags: tags 32 | principalId: principalId 33 | } 34 | } 35 | 36 | output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID 37 | output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME 38 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME 39 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT 40 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = resources.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID 41 | output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME 42 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID 43 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN 44 | output SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME string = resources.outputs.SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME 45 | output AZURE_VOLUMES_STORAGE_ACCOUNT string = resources.outputs.AZURE_VOLUMES_STORAGE_ACCOUNT 46 | -------------------------------------------------------------------------------- /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 | "principalId": { 6 | "value": "${AZURE_PRINCIPAL_ID}" 7 | }, 8 | "environmentName": { 9 | "value": "${AZURE_ENV_NAME}" 10 | }, 11 | "location": { 12 | "value": "${AZURE_LOCATION}" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /infra/ollama.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: 11434 18 | transport: http 19 | allowInsecure: true 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | volumes: 25 | - name: ollama-aspireaiblazorchatbotapphostollamaollama 26 | storageType: AzureFile 27 | storageName: {{ .Env.SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME }} 28 | containers: 29 | - image: {{ .Image }} 30 | name: ollama 31 | env: 32 | - name: AZURE_CLIENT_ID 33 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 34 | volumeMounts: 35 | - volumeName: ollama-aspireaiblazorchatbotapphostollamaollama 36 | mountPath: /root/.ollama 37 | resources: 38 | cpu: 3 39 | memory: "6.0Gi" 40 | scale: 41 | minReplicas: 1 42 | tags: 43 | azd-service-name: ollama 44 | aspire-resource-name: ollama 45 | -------------------------------------------------------------------------------- /infra/resources.bicep: -------------------------------------------------------------------------------- 1 | @description('The location used for all deployed resources') 2 | param location string = resourceGroup().location 3 | @description('Id of the user or app to assign application roles') 4 | param principalId string = '' 5 | 6 | 7 | @description('Tags that will be applied to all resources') 8 | param tags object = {} 9 | 10 | var resourceToken = uniqueString(resourceGroup().id) 11 | 12 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 13 | name: 'mi-${resourceToken}' 14 | location: location 15 | tags: tags 16 | } 17 | 18 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { 19 | name: replace('acr-${resourceToken}', '-', '') 20 | location: location 21 | sku: { 22 | name: 'Basic' 23 | } 24 | tags: tags 25 | } 26 | 27 | resource caeMiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 28 | name: guid(containerRegistry.id, managedIdentity.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) 29 | scope: containerRegistry 30 | properties: { 31 | principalId: managedIdentity.properties.principalId 32 | principalType: 'ServicePrincipal' 33 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 34 | } 35 | } 36 | 37 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 38 | name: 'law-${resourceToken}' 39 | location: location 40 | properties: { 41 | sku: { 42 | name: 'PerGB2018' 43 | } 44 | } 45 | tags: tags 46 | } 47 | 48 | resource storageVolume 'Microsoft.Storage/storageAccounts@2022-05-01' = { 49 | name: 'vol${resourceToken}' 50 | location: location 51 | kind: 'StorageV2' 52 | sku: { 53 | name: 'Standard_LRS' 54 | } 55 | properties: { 56 | largeFileSharesState: 'Enabled' 57 | } 58 | } 59 | 60 | resource storageVolumeFileService 'Microsoft.Storage/storageAccounts/fileServices@2022-05-01' = { 61 | parent: storageVolume 62 | name: 'default' 63 | } 64 | 65 | resource ollamaAspireAIBlazorChatBotAppHostOllamaOllamaFileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-05-01' = { 66 | parent: storageVolumeFileService 67 | name: take('${toLower('ollama')}-${toLower('AspireAIBlazorChatBotAppHostollamaollama')}', 60) 68 | properties: { 69 | shareQuota: 1024 70 | enabledProtocols: 'SMB' 71 | } 72 | } 73 | 74 | resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { 75 | name: 'cae-${resourceToken}' 76 | location: location 77 | properties: { 78 | workloadProfiles: [{ 79 | workloadProfileType: 'Consumption' 80 | name: 'consumption' 81 | }] 82 | appLogsConfiguration: { 83 | destination: 'log-analytics' 84 | logAnalyticsConfiguration: { 85 | customerId: logAnalyticsWorkspace.properties.customerId 86 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 87 | } 88 | } 89 | } 90 | tags: tags 91 | 92 | resource aspireDashboard 'dotNetComponents' = { 93 | name: 'aspire-dashboard' 94 | properties: { 95 | componentType: 'AspireDashboard' 96 | } 97 | } 98 | 99 | } 100 | 101 | resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 102 | name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) 103 | scope: containerAppEnvironment 104 | properties: { 105 | principalId: principalId 106 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') 107 | } 108 | } 109 | 110 | resource ollamaAspireAIBlazorChatBotAppHostOllamaOllamaStore 'Microsoft.App/managedEnvironments/storages@2023-05-01' = { 111 | parent: containerAppEnvironment 112 | name: take('${toLower('ollama')}-${toLower('AspireAIBlazorChatBotAppHostollamaollama')}', 32) 113 | properties: { 114 | azureFile: { 115 | shareName: ollamaAspireAIBlazorChatBotAppHostOllamaOllamaFileShare.name 116 | accountName: storageVolume.name 117 | accountKey: storageVolume.listKeys().keys[0].value 118 | accessMode: 'ReadWrite' 119 | } 120 | } 121 | } 122 | 123 | output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId 124 | output MANAGED_IDENTITY_NAME string = managedIdentity.name 125 | output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId 126 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = logAnalyticsWorkspace.name 127 | output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = logAnalyticsWorkspace.id 128 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer 129 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = managedIdentity.id 130 | output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = containerAppEnvironment.name 131 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = containerAppEnvironment.id 132 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = containerAppEnvironment.properties.defaultDomain 133 | output SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME string = ollamaAspireAIBlazorChatBotAppHostOllamaOllamaStore.name 134 | output AZURE_VOLUMES_STORAGE_ACCOUNT string = storageVolume.name 135 | -------------------------------------------------------------------------------- /infra/sample/apiservice.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: {{ targetPortOrDefault 8080 }} 18 | transport: http 19 | allowInsecure: true 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | containers: 25 | - image: {{ .Image }} 26 | name: apiservice 27 | env: 28 | - name: AZURE_CLIENT_ID 29 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 30 | - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED 31 | value: "true" 32 | - name: HTTP_PORTS 33 | value: '{{ targetPortOrDefault 0 }}' 34 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 35 | value: "true" 36 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 37 | value: "true" 38 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 39 | value: in_memory 40 | scale: 41 | minReplicas: 1 42 | tags: 43 | azd-service-name: apiservice 44 | aspire-resource-name: apiservice 45 | -------------------------------------------------------------------------------- /infra/sample/cache.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: 6379 18 | transport: tcp 19 | allowInsecure: false 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | containers: 25 | - image: {{ .Image }} 26 | name: cache 27 | env: 28 | - name: AZURE_CLIENT_ID 29 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 30 | scale: 31 | minReplicas: 1 32 | tags: 33 | azd-service-name: cache 34 | aspire-resource-name: cache 35 | -------------------------------------------------------------------------------- /infra/sample/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('The location used for all deployed resources') 10 | param location string 11 | 12 | @description('Id of the user or app to assign application roles') 13 | param principalId string = '' 14 | 15 | 16 | var tags = { 17 | 'azd-env-name': environmentName 18 | } 19 | 20 | resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { 21 | name: 'rg-${environmentName}' 22 | location: location 23 | tags: tags 24 | } 25 | 26 | module resources 'resources.bicep' = { 27 | scope: rg 28 | name: 'resources' 29 | params: { 30 | location: location 31 | tags: tags 32 | principalId: principalId 33 | } 34 | } 35 | 36 | output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID 37 | output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME 38 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME 39 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT 40 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = resources.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID 41 | output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME 42 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID 43 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN 44 | output SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME string = resources.outputs.SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME 45 | output AZURE_VOLUMES_STORAGE_ACCOUNT string = resources.outputs.AZURE_VOLUMES_STORAGE_ACCOUNT 46 | -------------------------------------------------------------------------------- /infra/sample/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 | "principalId": { 6 | "value": "${AZURE_PRINCIPAL_ID}" 7 | }, 8 | "environmentName": { 9 | "value": "${AZURE_ENV_NAME}" 10 | }, 11 | "location": { 12 | "value": "${AZURE_LOCATION}" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /infra/sample/ollama.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: 11434 18 | transport: http 19 | allowInsecure: true 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | volumes: 25 | - name: ollama-aspireaiblazorchatbotapphostollamaollama 26 | storageType: AzureFile 27 | storageName: {{ .Env.SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME }} 28 | containers: 29 | - image: {{ .Image }} 30 | name: ollama 31 | env: 32 | - name: AZURE_CLIENT_ID 33 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 34 | volumeMounts: 35 | - volumeName: ollama-aspireaiblazorchatbotapphostollamaollama 36 | mountPath: /root/.ollama 37 | resources: 38 | cpu: 3 39 | memory: "6.0Gi" 40 | scale: 41 | minReplicas: 1 42 | tags: 43 | azd-service-name: ollama 44 | aspire-resource-name: ollama 45 | -------------------------------------------------------------------------------- /infra/sample/resources.bicep: -------------------------------------------------------------------------------- 1 | @description('The location used for all deployed resources') 2 | param location string = resourceGroup().location 3 | @description('Id of the user or app to assign application roles') 4 | param principalId string = '' 5 | 6 | 7 | @description('Tags that will be applied to all resources') 8 | param tags object = {} 9 | 10 | var resourceToken = uniqueString(resourceGroup().id) 11 | 12 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 13 | name: 'mi-${resourceToken}' 14 | location: location 15 | tags: tags 16 | } 17 | 18 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { 19 | name: replace('acr-${resourceToken}', '-', '') 20 | location: location 21 | sku: { 22 | name: 'Basic' 23 | } 24 | tags: tags 25 | } 26 | 27 | resource caeMiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 28 | name: guid(containerRegistry.id, managedIdentity.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) 29 | scope: containerRegistry 30 | properties: { 31 | principalId: managedIdentity.properties.principalId 32 | principalType: 'ServicePrincipal' 33 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 34 | } 35 | } 36 | 37 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 38 | name: 'law-${resourceToken}' 39 | location: location 40 | properties: { 41 | sku: { 42 | name: 'PerGB2018' 43 | } 44 | } 45 | tags: tags 46 | } 47 | 48 | resource storageVolume 'Microsoft.Storage/storageAccounts@2022-05-01' = { 49 | name: 'vol${resourceToken}' 50 | location: location 51 | kind: 'StorageV2' 52 | sku: { 53 | name: 'Standard_LRS' 54 | } 55 | properties: { 56 | largeFileSharesState: 'Enabled' 57 | } 58 | } 59 | 60 | resource storageVolumeFileService 'Microsoft.Storage/storageAccounts/fileServices@2022-05-01' = { 61 | parent: storageVolume 62 | name: 'default' 63 | } 64 | 65 | resource ollamaAspireAIBlazorChatBotAppHostOllamaOllamaFileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-05-01' = { 66 | parent: storageVolumeFileService 67 | name: take('${toLower('ollama')}-${toLower('AspireAIBlazorChatBotAppHostollamaollama')}', 60) 68 | properties: { 69 | shareQuota: 1024 70 | enabledProtocols: 'SMB' 71 | } 72 | } 73 | 74 | resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { 75 | name: 'cae-${resourceToken}' 76 | location: location 77 | properties: { 78 | workloadProfiles: [{ 79 | workloadProfileType: 'Consumption' 80 | name: 'consumption' 81 | }] 82 | appLogsConfiguration: { 83 | destination: 'log-analytics' 84 | logAnalyticsConfiguration: { 85 | customerId: logAnalyticsWorkspace.properties.customerId 86 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 87 | } 88 | } 89 | } 90 | tags: tags 91 | 92 | resource aspireDashboard 'dotNetComponents' = { 93 | name: 'aspire-dashboard' 94 | properties: { 95 | componentType: 'AspireDashboard' 96 | } 97 | } 98 | 99 | } 100 | 101 | resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 102 | name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) 103 | scope: containerAppEnvironment 104 | properties: { 105 | principalId: principalId 106 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') 107 | } 108 | } 109 | 110 | resource ollamaAspireAIBlazorChatBotAppHostOllamaOllamaStore 'Microsoft.App/managedEnvironments/storages@2023-05-01' = { 111 | parent: containerAppEnvironment 112 | name: take('${toLower('ollama')}-${toLower('AspireAIBlazorChatBotAppHostollamaollama')}', 32) 113 | properties: { 114 | azureFile: { 115 | shareName: ollamaAspireAIBlazorChatBotAppHostOllamaOllamaFileShare.name 116 | accountName: storageVolume.name 117 | accountKey: storageVolume.listKeys().keys[0].value 118 | accessMode: 'ReadWrite' 119 | } 120 | } 121 | } 122 | 123 | output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId 124 | output MANAGED_IDENTITY_NAME string = managedIdentity.name 125 | output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId 126 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = logAnalyticsWorkspace.name 127 | output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = logAnalyticsWorkspace.id 128 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer 129 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = managedIdentity.id 130 | output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = containerAppEnvironment.name 131 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = containerAppEnvironment.id 132 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = containerAppEnvironment.properties.defaultDomain 133 | output SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME string = ollamaAspireAIBlazorChatBotAppHostOllamaOllamaStore.name 134 | output AZURE_VOLUMES_STORAGE_ACCOUNT string = storageVolume.name 135 | -------------------------------------------------------------------------------- /infra/sample/webfrontend.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: true 17 | targetPort: {{ targetPortOrDefault 8080 }} 18 | transport: http 19 | allowInsecure: false 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | secrets: 24 | - name: connectionstrings--cache 25 | value: cache:6379 26 | - name: connectionstrings--ollama 27 | value: http://ollama.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}:80 28 | template: 29 | containers: 30 | - image: {{ .Image }} 31 | name: webfrontend 32 | env: 33 | - name: AZURE_CLIENT_ID 34 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 35 | - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED 36 | value: "true" 37 | - name: Aspire__OllamaSharp__ollama__Models__0 38 | value: phi3.5 39 | - name: HTTP_PORTS 40 | value: '{{ targetPortOrDefault 0 }}' 41 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 42 | value: "true" 43 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 44 | value: "true" 45 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 46 | value: in_memory 47 | - name: services__apiservice__http__0 48 | value: http://apiservice.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} 49 | - name: services__apiservice__https__0 50 | value: https://apiservice.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} 51 | - name: ConnectionStrings__cache 52 | secretRef: connectionstrings--cache 53 | - name: ConnectionStrings__ollama 54 | secretRef: connectionstrings--ollama 55 | scale: 56 | minReplicas: 1 57 | tags: 58 | azd-service-name: webfrontend 59 | aspire-resource-name: webfrontend 60 | -------------------------------------------------------------------------------- /infra/webfrontend.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: true 17 | targetPort: {{ targetPortOrDefault 8080 }} 18 | transport: http 19 | allowInsecure: false 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | secrets: 24 | - name: connectionstrings--cache 25 | value: cache:6379 26 | - name: connectionstrings--ollama 27 | value: http://ollama.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}:80 28 | template: 29 | containers: 30 | - image: {{ .Image }} 31 | name: webfrontend 32 | env: 33 | - name: AZURE_CLIENT_ID 34 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 35 | - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED 36 | value: "true" 37 | - name: Aspire__OllamaSharp__ollama__Models__0 38 | value: phi3.5 39 | - name: HTTP_PORTS 40 | value: '{{ targetPortOrDefault 0 }}' 41 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 42 | value: "true" 43 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 44 | value: "true" 45 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 46 | value: in_memory 47 | - name: services__apiservice__http__0 48 | value: http://apiservice.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} 49 | - name: services__apiservice__https__0 50 | value: https://apiservice.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} 51 | - name: ConnectionStrings__cache 52 | secretRef: connectionstrings--cache 53 | - name: ConnectionStrings__ollama 54 | secretRef: connectionstrings--ollama 55 | scale: 56 | minReplicas: 1 57 | tags: 58 | azd-service-name: webfrontend 59 | aspire-resource-name: webfrontend 60 | -------------------------------------------------------------------------------- /scripts/listAIQuotas.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script uses Azure CLI to list the remaining quotas for the specified models in the specified locations. 4 | .DESCRIPTION 5 | This script lists the remaining quotas for the specified models in the specified locations. 6 | The script requires Azure CLI to be installed and logged in. 7 | .EXAMPLE 8 | az login 9 | ./listAIQuotas.ps1 10 | .EXAMPLE 11 | ./listAIQuotas.ps1 -s 12 | .EXAMPLE 13 | ./listAIQuotas.ps1 -m "OpenAI.Standard.gpt-35-turbo:1106,0301,0613 OpenAI.Standard.gpt-4:1106-Preview" 14 | .EXAMPLE 15 | ./listAIQuotas.ps1 -m "OpenAI.Standard.gpt-35-turbo OpenAI.Standard.gpt-4:*" 16 | .PARAMETER subscription 17 | The subscription ID to fetch the quotas from. If not provided, the script will use the current subscription. 18 | .PARAMETER models 19 | The list of candidate models to fetch the quotas for. The format is "kind.sku.name:version1,version2 kind.sku.name:version1,version2". 20 | If not provided, the script will use the default candidate models. 21 | .PARAMETER help 22 | Show the help message. 23 | #> 24 | [CmdletBinding()] 25 | param ( 26 | [Parameter(Mandatory = $false)] 27 | [Alias("s")] 28 | [string] $subscription, 29 | [Parameter(Mandatory = $false)] 30 | [Alias("m")] 31 | [string] $models, 32 | [Parameter(Mandatory = $false)] 33 | [Alias("h")] 34 | [switch] $help 35 | ) 36 | 37 | if ($help) { 38 | Get-Help .\listAIQuotas.ps1 -Detailed 39 | exit 40 | } 41 | 42 | function Get-Quotas() { 43 | $fetched_quotas_table = @() 44 | 45 | foreach ($location in $locations) { 46 | Write-Host "Fetching quotas for location $location..." 47 | try { 48 | $usages = (az cognitiveservices usage list --location $location) | ConvertFrom-Json 49 | $models = (az cognitiveservices model list --location $location) | ConvertFrom-Json 50 | } 51 | catch { 52 | Write-Host "Failed to fetch quotas for location $location : $_" 53 | break 54 | } 55 | 56 | foreach ($usage in $usages) { 57 | $modelName = $usage.name.value 58 | # continue if kind.sku.name not in cadidate_models 59 | $candidate = $candidate_models | Where-Object { $modelName -eq $_.kind + "." + $_.sku + "." + $_.name } 60 | if (!$candidate) { 61 | continue 62 | } 63 | 64 | # Find the candidate model in the list of models and get the available versions 65 | $available_versions = @() 66 | $models | ForEach-Object { 67 | $model = $_ 68 | $skuMatch = $model.model.skus | Where-Object { $_.name -eq $candidate.sku } 69 | if ($model.model.name -eq $candidate.name -and $model.kind -eq $candidate.kind -and $skuMatch) { 70 | if ($candidate.versions -contains '*' -or $candidate.versions -contains $model.model.version) { 71 | $available_versions += $model.model.version 72 | } 73 | } 74 | } 75 | # Skip if no available versions 76 | if ($available_versions.Count -eq 0) { 77 | continue 78 | } 79 | 80 | $currentValue = $usage.currentValue 81 | $limit = $usage.limit 82 | $fetched_quotas_table += [PSCustomObject]@{ 83 | "Location" = $location 84 | "Model" = "$($candidate.kind).$($candidate.sku).$($candidate.name)" 85 | "Available Versions" = $available_versions -join ", " 86 | "Remaining Quotas" = $($limit - $currentValue).ToString() 87 | "Total Quotas" = $limit.ToString() 88 | } 89 | } 90 | } 91 | return $fetched_quotas_table 92 | } 93 | 94 | function Check_Az() { 95 | # Check if Azure CLI is installed. If not, prompt the user to install it 96 | $azCli = Get-Command az -ErrorAction SilentlyContinue 97 | if (-not $azCli) { 98 | $installCli = Read-Host "Azure CLI is not installed. Do you want to install it now? (Y/N)" 99 | if ($installCli -eq 'Y') { 100 | Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi 101 | Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi' 102 | Remove-Item .\AzureCLI.msi 103 | Write-Host "Azure CLI has been installed. Please refresh your terminal and login to your Azure account using 'az login' before running this script" 104 | exit 105 | } 106 | else { 107 | Write-Host "This script requires Azure CLI. Please install it and run the script again." 108 | exit 109 | } 110 | } 111 | } 112 | 113 | # list of locations in which to look for candidate models 114 | $locations = @( "australiaeast", "eastus", "eastus2", "francecentral", "norwayeast", "swedencentral", "uksouth") 115 | 116 | # list of candidate models we need 117 | $candidate_models = @() 118 | if ($models) { 119 | $modelList = $models -split ' ' 120 | foreach ($model in $modelList) { 121 | $modelParts = $model -split ':' 122 | $kindSkuName = $modelParts[0] -split '\.' 123 | $versions = if ($modelParts[1]) { $modelParts[1] -split ',' } else { @("*") } 124 | $candidate_models += @{ 125 | "name" = $kindSkuName[2] 126 | "versions" = $versions 127 | "sku" = $kindSkuName[1] 128 | "kind" = $kindSkuName[0] 129 | } 130 | } 131 | } 132 | else { 133 | # Default candidate models 134 | $candidate_models = @( 135 | @{ 136 | "name" = "gpt-35-turbo" 137 | "versions" = @("1106", "0301", "0613") 138 | "sku" = "Standard" 139 | "kind" = "OpenAI" 140 | }, 141 | @{ 142 | "name" = "gpt-4" 143 | "versions" = @("1106-Preview") 144 | "sku" = "Standard" 145 | "kind" = "OpenAI" 146 | } 147 | ) 148 | } 149 | 150 | Check_Az 151 | 152 | try { 153 | $account = az account show | ConvertFrom-Json 154 | } 155 | catch { 156 | Write-Host "Failed to get account: $_" 157 | exit 158 | } 159 | # exit if not logged in 160 | if (!$account -or !$account.id) { 161 | Write-Host "Please login to your Azure account using 'az login' before running this script" 162 | exit 163 | } 164 | 165 | $originSubscription = $account.id 166 | 167 | # Set the subscription if provided 168 | $switchSubscription = $false 169 | if ($subscription -and $subscription -ne $originSubscription) { 170 | az account set -s $subscription 171 | Write-Host "Fetching quotas for the candidate models in the candidate locations for subscription $($subscription)" 172 | $switchSubscription = $true 173 | } 174 | else { 175 | Write-Host "Fetching quotas for the candidate models in the candidate locations for subscription $($originSubscription)" 176 | } 177 | 178 | $quotas = Get-Quotas 179 | Write-Output $quotas | Format-Table -AutoSize -Wrap 180 | 181 | # Switch back to the original subscription if we switched 182 | if ($switchSubscription) { 183 | az account set -s $originSubscription 184 | } 185 | -------------------------------------------------------------------------------- /scripts/listAIQuotas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script lists the remaining quotas for the specified models in the specified locations. 4 | # The script requires Azure CLI to be installed and logged in. 5 | # 6 | # How to run: 7 | # az login 8 | # az account set --subscription "" 9 | # bash ./listAIQuotas.sh 10 | # bash ./listAIQuotas.sh -s "" 11 | # bash ./listAIQuotas.sh -m "OpenAI.Standard.gpt-35-turbo:1106,0301,0613" -s "" 12 | # bash ./listAIQuotas.sh -m "OpenAI.Standard.gpt-35-turbo OpenAi.Standard.gpt-4:1106-Preview" -s "" 13 | 14 | set -e 15 | set -u 16 | 17 | get_quotas() { 18 | local fetched_quotas_table=("location,Model,Available Version,Remaining Quotas,Total Quotas") 19 | 20 | for location in "${locations[@]}"; do 21 | echo "Fetching quotas for location $location..." >&2 22 | 23 | local usages=$(az cognitiveservices usage list --location $location --query "[].{name: name.value, currentValue: currentValue, limit: limit}" -o tsv) 24 | local models=$(az cognitiveservices model list --location $location --query "[].{name: model.name, sku: model.skus[0].name, kind: kind, version: model.version}" -o tsv) 25 | 26 | IFS=$'\n' 27 | for usage in $usages; do 28 | local model_fullname=$(echo $usage | cut -f1) 29 | local current_value=$(echo $usage | cut -f2) 30 | local limit=$(echo $usage | cut -f3) 31 | 32 | for candidate in "${candidate_models[@]}"; do 33 | if [[ "$candidate" == *":"* ]]; then 34 | # If candidate contains ":", split it into candidate_model and versions 35 | candidate_model_name="${candidate%%:*}" 36 | versions="${candidate#*:}" 37 | else 38 | # If candidate does not contain ":", set candidate_model to candidate and versions to "*" 39 | candidate_model_name="$candidate" 40 | versions="*" 41 | fi 42 | 43 | if [[ $candidate_model_name != $model_fullname ]]; then 44 | continue 45 | fi 46 | 47 | # Find the candidate model in the list of models and get the available versions 48 | available_versions=() 49 | for model in $models; do 50 | local model_name=$(echo $model | cut -f1) 51 | local sku=$(echo $model | cut -f2) 52 | local kind=$(echo $model | cut -f3) 53 | local version=$(echo $model | cut -f4) 54 | if [[ $model_fullname == "$kind.$sku.$model_name" ]]; then 55 | if [[ "$versions" == *"*"* ]] || echo "$versions" | grep -q -w "$version"; then 56 | available_versions+=("$version") 57 | fi 58 | fi 59 | done 60 | 61 | # Skip if no available versions 62 | if [ ${#available_versions[@]} -eq 0 ]; then 63 | continue 64 | fi 65 | 66 | available_versions_str=$(printf "; %s" "${available_versions[@]}") 67 | available_versions_str=${available_versions_str:1} 68 | fetched_quotas_table+=("$location,$model_fullname,$available_versions_str,$(echo "$limit - $current_value" | bc),$limit") 69 | 70 | # skip the rest of the candidate_models 71 | break 72 | done 73 | done 74 | unset IFS 75 | done 76 | 77 | if [ ${#fetched_quotas_table[@]} -eq 1 ]; then 78 | echo "No quotas found for the candidate models" >&2 79 | return 80 | fi 81 | printf "%s\n" "${fetched_quotas_table[@]}" 82 | } 83 | 84 | check_az() { 85 | # Check if Azure CLI is installed. If not, prompt the user to install it 86 | if ! command -v az &> /dev/null; then 87 | echo "Azure CLI is not installed. Would you like to install it now? (Y/N)" >&2 88 | read answer 89 | if [ "$answer" == "Y" ]; then 90 | curl -L https://aka.ms/InstallAzureCli | bash 91 | echo "Azure CLI has been installed. Please login using 'az login' and restart the script." >&2 92 | exit 0 93 | else 94 | echo "Azure CLI is required for this script to run. Exiting." >&2 95 | exit 1 96 | fi 97 | fi 98 | 99 | } 100 | 101 | 102 | # list of locations in which to look for candidate models 103 | locations=("australiaeast" "eastus" "eastus2" "francecentral" "norwayeast" "swedencentral" "uksouth") 104 | 105 | # list of candidate models we need 106 | candidate_models=( 107 | "OpenAI.Standard.gpt-35-turbo:*" 108 | "OpenAI.Standard.gpt-4:1106-Preview" 109 | ) 110 | 111 | subscription="" 112 | 113 | while (( "$#" )); do 114 | case "$1" in 115 | -m|--models) 116 | candidate_models=($2) 117 | shift 2 118 | ;; 119 | -s|--subscription) 120 | subscription="$2" 121 | shift 2 122 | ;; 123 | -h|--help) 124 | echo "Usage: $0 [-m|--models ] [-s|--subscription ]" 125 | echo "Options:" 126 | echo " -m|--models List of candidate models to check. Default: OpenAI.Standard.gpt-35-turbo:* OpenAI.Standard.gpt-4:1106-Preview" 127 | echo " -s|--subscription Azure subscription ID to use. Default: Current subscription" 128 | echo " -h|--help Show help" 129 | exit 0 130 | ;; 131 | *) 132 | echo "Invalid option: $1" 1>&2 133 | exit 1 134 | ;; 135 | esac 136 | done 137 | 138 | check_az 139 | 140 | original_subscription=$(az account show --query id -o tsv 2>/dev/null) 141 | 142 | # Exit if not logged in 143 | if [ -z "$original_subscription" ]; then 144 | echo "Please login to your Azure account using 'az login' before running this script" >&2 145 | exit 1 146 | fi 147 | 148 | # Set the subscription if provided 149 | switched_subscription=false 150 | if [ -n "$subscription" ] && [ "$subscription" != "$original_subscription" ]; then 151 | az account set --subscription "$subscription" 152 | switched_subscription=true 153 | echo "Fetching quotas for the candidate models in the candidate locations for subscription $subscription" 154 | else 155 | echo "Fetching quotas for the candidate models in the candidate locations for subscription $original_subscription" 156 | fi 157 | 158 | quotas=$(get_quotas) 159 | printf "%s\n" "${quotas[@]}" | column -s, -t 160 | 161 | # Switch back to the original subscription if we switched 162 | if [ -n "$switched_subscription" ]; then 163 | az account set --subscription "$original_subscription" 164 | fi 165 | -------------------------------------------------------------------------------- /scripts/listApiVersions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script uses Azure CLI to list the available API versions for the specified resource type and namespace. 4 | .DESCRIPTION 5 | This script lists the available API versions for the specified resource type and namespace. 6 | The script requires Azure CLI to be installed and logged in. 7 | .EXAMPLE 8 | ./listApiVersions.ps1 9 | .EXAMPLE 10 | ./listApiVersions.ps1 -r "accounts" -n "Microsoft.CognitiveServices" 11 | .PARAMETER resourceType 12 | The resource type to fetch the API versions for. If not provided, the script will use the default value "accounts". 13 | .PARAMETER namespace 14 | The namespace to fetch the API versions for. If not provided, the script will use the default value "Microsoft.CognitiveServices". 15 | .PARAMETER help 16 | Show the help message. 17 | #> 18 | param( 19 | [Alias("r")] 20 | [string]$resourceType = "accounts", 21 | [Alias("n")] 22 | [string]$namespace = "Microsoft.CognitiveServices", 23 | [Alias("h")] 24 | [switch]$help 25 | ) 26 | 27 | if ($help) { 28 | Get-Help .\listApiVersion.ps1 -Detailed 29 | exit 30 | } 31 | 32 | # Fetch API versions 33 | $apiVersions = az provider show --namespace ${namespace} --query "resourceTypes[?resourceType=='${resourceType}'].apiVersions[]" | ConvertFrom-Json 34 | 35 | # Get the latest stable version 36 | $latestStableVersion = $apiVersions | Where-Object { $_ -notlike "*-preview" } | Select-Object -First 1 37 | 38 | # Print versions 39 | Write-Host "API Versions for ${namespace}/${resourceType}:" 40 | foreach ($version in $apiVersions) { 41 | if ($version -eq $latestStableVersion) { 42 | # Print latest stable version in bold green 43 | Write-Host ([char]27 + "[1m$version [Latest stable]" + [char]27 + "[0m") -ForegroundColor Green 44 | } else { 45 | Write-Host $version 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scripts/listApiVersions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script lists the API versions for the specified resource type in the specified namespace. 4 | # The script requires Azure CLI to be installed and logged in. 5 | # 6 | # How to run: 7 | # az login 8 | # bash ./listApiVersions.sh 9 | # bash ./listApiVersions.sh -r "" -n "" 10 | 11 | # Default values 12 | resourceType="accounts" 13 | namespace="Microsoft.CognitiveServices" 14 | 15 | # Parse command line arguments 16 | while (( "$#" )); do 17 | case "$1" in 18 | -r|--resourceType) 19 | resourceType="$2" 20 | shift 2 21 | ;; 22 | -n|--namespace) 23 | namespace="$2" 24 | shift 2 25 | ;; 26 | -h|--help) 27 | echo "Usage: $0 [-r|--resourceType ] [-n|--namespace ]" 28 | echo "Options:" 29 | echo " -r|--resourceType The resource type to fetch the API versions for. Default: accounts" 30 | echo " -n|--namespace The namespace to fetch the API versions for. Default: Microsoft.CognitiveServices" 31 | echo " -h|--help Show help" 32 | exit 0 33 | ;; 34 | *) 35 | echo "Invalid option: $1" 1>&2 36 | exit 1 37 | ;; 38 | esac 39 | done 40 | 41 | # Fetch API versions 42 | apiVersions=$(az provider show --namespace ${namespace} --query "resourceTypes[?resourceType=='${resourceType}'].apiVersions[]" -o tsv) 43 | 44 | # Find the latest stable version 45 | stableVersions=($(for version in $apiVersions; do [[ $version != *"-preview"* ]] && echo $version; done)) 46 | latestStableVersion=${stableVersions[0]} 47 | 48 | # Print versions 49 | echo "API Versions for ${namespace}/${resourceType}:" 50 | for version in $apiVersions; do 51 | if [ "$version" == "$latestStableVersion" ]; then 52 | version=$(echo $version | tr -d ' ') 53 | # Print latest stable version in bold green 54 | echo -e "\033[1;32m${version} [Latest stable]\033[0m" 55 | else 56 | echo "$version" 57 | fi 58 | done 59 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.ApiService/AspireAIBlazorChatBot.ApiService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.ApiService/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | // Add service defaults & Aspire components. 4 | builder.AddServiceDefaults(); 5 | 6 | // Add services to the container. 7 | builder.Services.AddProblemDetails(); 8 | 9 | var app = builder.Build(); 10 | 11 | // Configure the HTTP request pipeline. 12 | app.UseExceptionHandler(); 13 | 14 | var summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | app.MapGet("/weatherforecast", () => 20 | { 21 | var forecast = Enumerable.Range(1, 5).Select(index => 22 | new WeatherForecast 23 | ( 24 | DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 25 | Random.Shared.Next(-20, 55), 26 | summaries[Random.Shared.Next(summaries.Length)] 27 | )) 28 | .ToArray(); 29 | return forecast; 30 | }); 31 | 32 | app.MapDefaultEndpoints(); 33 | 34 | app.Run(); 35 | 36 | record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) 37 | { 38 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 39 | } 40 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.ApiService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "weatherforecast", 9 | "applicationUrl": "http://localhost:5402", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "https": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "weatherforecast", 19 | "applicationUrl": "https://localhost:7464;http://localhost:5402", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.ApiService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.ApiService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/.gitignore: -------------------------------------------------------------------------------- 1 | .azure 2 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/AspireAIBlazorChatBot.AppHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | 19551b62-8fe8-4a98-8752-000c226c24c8 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = DistributedApplication.CreateBuilder(args); 2 | 3 | var cache = builder.AddRedis("cache"); 4 | 5 | var ollama = builder.AddOllama(name: "ollama", port: null) 6 | .AddModel("phi3.5") 7 | .WithOpenWebUI() 8 | .WithDataVolume() 9 | .PublishAsContainer(); 10 | 11 | var apiService = builder.AddProject("apiservice"); 12 | 13 | builder.AddProject("webfrontend") 14 | .WithExternalHttpEndpoints() 15 | .WithReference(cache) 16 | .WithReference(ollama) 17 | .WithReference(apiService); 18 | 19 | builder.Build().Run(); 20 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://localhost:17192;http://localhost:15251", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "DOTNET_ENVIRONMENT": "Development", 12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21186", 13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22274" 14 | } 15 | }, 16 | "http": { 17 | "commandName": "Project", 18 | "dotnetRunMessages": true, 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:15251", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development", 23 | "DOTNET_ENVIRONMENT": "Development", 24 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19181", 25 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20298" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Aspire.Hosting.Dcp": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: AspireAIBlazorChatBot.AppHost 4 | services: 5 | app: 6 | language: dotnet 7 | project: ./AspireAIBlazorChatBot.AppHost.csproj 8 | host: containerapp 9 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/infra/apiservice.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: {{ targetPortOrDefault 8080 }} 18 | transport: http 19 | allowInsecure: true 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | containers: 25 | - image: {{ .Image }} 26 | name: apiservice 27 | env: 28 | - name: AZURE_CLIENT_ID 29 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 30 | - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED 31 | value: "true" 32 | - name: HTTP_PORTS 33 | value: '{{ targetPortOrDefault 0 }}' 34 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 35 | value: "true" 36 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 37 | value: "true" 38 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 39 | value: in_memory 40 | scale: 41 | minReplicas: 1 42 | tags: 43 | azd-service-name: apiservice 44 | aspire-resource-name: apiservice 45 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/infra/cache.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: 6379 18 | transport: tcp 19 | allowInsecure: false 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | containers: 25 | - image: {{ .Image }} 26 | name: cache 27 | env: 28 | - name: AZURE_CLIENT_ID 29 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 30 | scale: 31 | minReplicas: 1 32 | tags: 33 | azd-service-name: cache 34 | aspire-resource-name: cache 35 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('The location used for all deployed resources') 10 | param location string 11 | 12 | @description('Id of the user or app to assign application roles') 13 | param principalId string = '' 14 | 15 | 16 | var tags = { 17 | 'azd-env-name': environmentName 18 | } 19 | 20 | resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { 21 | name: 'rg-${environmentName}' 22 | location: location 23 | tags: tags 24 | } 25 | 26 | module resources 'resources.bicep' = { 27 | scope: rg 28 | name: 'resources' 29 | params: { 30 | location: location 31 | tags: tags 32 | principalId: principalId 33 | } 34 | } 35 | 36 | output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID 37 | output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME 38 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME 39 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT 40 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = resources.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID 41 | output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME 42 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID 43 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN 44 | output SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME string = resources.outputs.SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME 45 | output AZURE_VOLUMES_STORAGE_ACCOUNT string = resources.outputs.AZURE_VOLUMES_STORAGE_ACCOUNT 46 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/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 | "principalId": { 6 | "value": "${AZURE_PRINCIPAL_ID}" 7 | }, 8 | "environmentName": { 9 | "value": "${AZURE_ENV_NAME}" 10 | }, 11 | "location": { 12 | "value": "${AZURE_LOCATION}" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/infra/ollama.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: false 17 | targetPort: 11434 18 | transport: http 19 | allowInsecure: true 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | template: 24 | volumes: 25 | - name: ollama-aspireaiblazorchatbotapphostollamaollama 26 | storageType: AzureFile 27 | storageName: {{ .Env.SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME }} 28 | containers: 29 | - image: {{ .Image }} 30 | name: ollama 31 | env: 32 | - name: AZURE_CLIENT_ID 33 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 34 | volumeMounts: 35 | - volumeName: ollama-aspireaiblazorchatbotapphostollamaollama 36 | mountPath: /root/.ollama 37 | resources: 38 | cpu: 3 39 | memory: "6.0Gi" 40 | scale: 41 | minReplicas: 1 42 | tags: 43 | azd-service-name: ollama 44 | aspire-resource-name: ollama 45 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/infra/resources.bicep: -------------------------------------------------------------------------------- 1 | @description('The location used for all deployed resources') 2 | param location string = resourceGroup().location 3 | @description('Id of the user or app to assign application roles') 4 | param principalId string = '' 5 | 6 | 7 | @description('Tags that will be applied to all resources') 8 | param tags object = {} 9 | 10 | var resourceToken = uniqueString(resourceGroup().id) 11 | 12 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 13 | name: 'mi-${resourceToken}' 14 | location: location 15 | tags: tags 16 | } 17 | 18 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { 19 | name: replace('acr-${resourceToken}', '-', '') 20 | location: location 21 | sku: { 22 | name: 'Basic' 23 | } 24 | tags: tags 25 | } 26 | 27 | resource caeMiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 28 | name: guid(containerRegistry.id, managedIdentity.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) 29 | scope: containerRegistry 30 | properties: { 31 | principalId: managedIdentity.properties.principalId 32 | principalType: 'ServicePrincipal' 33 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 34 | } 35 | } 36 | 37 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 38 | name: 'law-${resourceToken}' 39 | location: location 40 | properties: { 41 | sku: { 42 | name: 'PerGB2018' 43 | } 44 | } 45 | tags: tags 46 | } 47 | 48 | resource storageVolume 'Microsoft.Storage/storageAccounts@2022-05-01' = { 49 | name: 'vol${resourceToken}' 50 | location: location 51 | kind: 'StorageV2' 52 | sku: { 53 | name: 'Standard_LRS' 54 | } 55 | properties: { 56 | largeFileSharesState: 'Enabled' 57 | } 58 | } 59 | 60 | resource storageVolumeFileService 'Microsoft.Storage/storageAccounts/fileServices@2022-05-01' = { 61 | parent: storageVolume 62 | name: 'default' 63 | } 64 | 65 | resource ollamaAspireAIBlazorChatBotAppHostOllamaOllamaFileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-05-01' = { 66 | parent: storageVolumeFileService 67 | name: take('${toLower('ollama')}-${toLower('AspireAIBlazorChatBotAppHostollamaollama')}', 60) 68 | properties: { 69 | shareQuota: 1024 70 | enabledProtocols: 'SMB' 71 | } 72 | } 73 | 74 | resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { 75 | name: 'cae-${resourceToken}' 76 | location: location 77 | properties: { 78 | workloadProfiles: [{ 79 | workloadProfileType: 'Consumption' 80 | name: 'consumption' 81 | }] 82 | appLogsConfiguration: { 83 | destination: 'log-analytics' 84 | logAnalyticsConfiguration: { 85 | customerId: logAnalyticsWorkspace.properties.customerId 86 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 87 | } 88 | } 89 | } 90 | tags: tags 91 | 92 | resource aspireDashboard 'dotNetComponents' = { 93 | name: 'aspire-dashboard' 94 | properties: { 95 | componentType: 'AspireDashboard' 96 | } 97 | } 98 | 99 | } 100 | 101 | resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 102 | name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) 103 | scope: containerAppEnvironment 104 | properties: { 105 | principalId: principalId 106 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') 107 | } 108 | } 109 | 110 | resource ollamaAspireAIBlazorChatBotAppHostOllamaOllamaStore 'Microsoft.App/managedEnvironments/storages@2023-05-01' = { 111 | parent: containerAppEnvironment 112 | name: take('${toLower('ollama')}-${toLower('AspireAIBlazorChatBotAppHostollamaollama')}', 32) 113 | properties: { 114 | azureFile: { 115 | shareName: ollamaAspireAIBlazorChatBotAppHostOllamaOllamaFileShare.name 116 | accountName: storageVolume.name 117 | accountKey: storageVolume.listKeys().keys[0].value 118 | accessMode: 'ReadWrite' 119 | } 120 | } 121 | } 122 | 123 | output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId 124 | output MANAGED_IDENTITY_NAME string = managedIdentity.name 125 | output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId 126 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = logAnalyticsWorkspace.name 127 | output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = logAnalyticsWorkspace.id 128 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer 129 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = managedIdentity.id 130 | output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = containerAppEnvironment.name 131 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = containerAppEnvironment.id 132 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = containerAppEnvironment.properties.defaultDomain 133 | output SERVICE_OLLAMA_VOLUME_ASPIREAIBLAZORCHATBOTAPPHOSTOLLAMAOLLAMA_NAME string = ollamaAspireAIBlazorChatBotAppHostOllamaOllamaStore.name 134 | output AZURE_VOLUMES_STORAGE_ACCOUNT string = storageVolume.name 135 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/infra/webfrontend.tmpl.yaml: -------------------------------------------------------------------------------- 1 | api-version: 2024-02-02-preview 2 | location: {{ .Env.AZURE_LOCATION }} 3 | identity: 4 | type: UserAssigned 5 | userAssignedIdentities: 6 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 7 | : {} 8 | properties: 9 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 10 | configuration: 11 | activeRevisionsMode: single 12 | runtime: 13 | dotnet: 14 | autoConfigureDataProtection: true 15 | ingress: 16 | external: true 17 | targetPort: {{ targetPortOrDefault 8080 }} 18 | transport: http 19 | allowInsecure: false 20 | registries: 21 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 22 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 23 | secrets: 24 | - name: connectionstrings--cache 25 | value: cache:6379 26 | - name: connectionstrings--ollama 27 | value: http://ollama.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }}:80 28 | template: 29 | containers: 30 | - image: {{ .Image }} 31 | name: webfrontend 32 | env: 33 | - name: AZURE_CLIENT_ID 34 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 35 | - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED 36 | value: "true" 37 | - name: Aspire__OllamaSharp__ollama__Models__0 38 | value: phi3.5 39 | - name: HTTP_PORTS 40 | value: '{{ targetPortOrDefault 0 }}' 41 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 42 | value: "true" 43 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 44 | value: "true" 45 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 46 | value: in_memory 47 | - name: services__apiservice__http__0 48 | value: http://apiservice.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} 49 | - name: services__apiservice__https__0 50 | value: https://apiservice.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} 51 | - name: ConnectionStrings__cache 52 | secretRef: connectionstrings--cache 53 | - name: ConnectionStrings__ollama 54 | secretRef: connectionstrings--ollama 55 | scale: 56 | minReplicas: 1 57 | tags: 58 | azd-service-name: webfrontend 59 | aspire-resource-name: webfrontend 60 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.AppHost/next-steps.md: -------------------------------------------------------------------------------- 1 | # Next Steps after `azd init` 2 | 3 | ## Table of Contents 4 | 5 | 1. [Next Steps](#next-steps) 6 | 2. [What was added](#what-was-added) 7 | 3. [Billing](#billing) 8 | 4. [Troubleshooting](#troubleshooting) 9 | 10 | ## Next Steps 11 | 12 | ### Provision infrastructure and deploy application code 13 | 14 | Run `azd up` to provision your infrastructure and deploy to Azure in one step (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! 15 | 16 | To troubleshoot any issues, see [troubleshooting](#troubleshooting). 17 | 18 | ### Configure CI/CD pipeline 19 | 20 | 1. Create a workflow pipeline file locally. The following starters are available: 21 | - [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml) 22 | - [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml) 23 | 2. Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. 24 | 25 | ## What was added 26 | 27 | ### Infrastructure configuration 28 | 29 | To describe the infrastructure and application, an `azure.yaml` was added with the following directory structure: 30 | 31 | ```yaml 32 | - azure.yaml # azd project configuration 33 | ``` 34 | 35 | This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it. 36 | 37 | If you would like to see or modify the infrastructure that `azd` uses, run `azd infra synth` to persist it to disk. 38 | 39 | If you do this, some additional directories will be created: 40 | 41 | ```yaml 42 | - infra/ # Infrastructure as Code (bicep) files 43 | - main.bicep # main deployment module 44 | - resources.bicep # resources shared across your application's services 45 | ``` 46 | 47 | In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps. 48 | 49 | *Note*: Once you have synthesized your infrastructure to disk, changes made to your App Host will not be reflected in the infrastructure. You can re-generate the infrastructure by running `azd infra synth` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra synth` to overwrite the files without prompting. 50 | 51 | *Note*: `azd infra synth` is currently an alpha feature and must be explicitly enabled by running `azd config set alpha.infraSynth on`. You only need to do this once. 52 | 53 | ## Billing 54 | 55 | Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). 56 | 57 | ## Troubleshooting 58 | 59 | Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic welcome page, or an error page. 60 | 61 | A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: 62 | 63 | 1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. 64 | 2. Navigate to the specific Container App service that is failing to deploy. 65 | 3. Click on the failing revision under "Revisions with Issues". 66 | 4. Review "Status details" for more information about the type of failure. 67 | 5. Observe the log outputs from Console log stream and System log stream to identify any errors. 68 | 6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. 69 | 70 | For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). 71 | 72 | ### Additional information 73 | 74 | For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). 75 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.ServiceDefaults/AspireAIBlazorChatBot.ServiceDefaults.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.ServiceDefaults/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Diagnostics.HealthChecks; 5 | using Microsoft.Extensions.Logging; 6 | using OpenTelemetry; 7 | using OpenTelemetry.Metrics; 8 | using OpenTelemetry.Trace; 9 | 10 | namespace Microsoft.Extensions.Hosting; 11 | 12 | // Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. 13 | // This project should be referenced by each service project in your solution. 14 | // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults 15 | public static class Extensions 16 | { 17 | public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) 18 | { 19 | builder.ConfigureOpenTelemetry(); 20 | 21 | builder.AddDefaultHealthChecks(); 22 | 23 | builder.Services.AddServiceDiscovery(); 24 | 25 | builder.Services.ConfigureHttpClientDefaults(http => 26 | { 27 | // Turn on resilience by default 28 | http.AddStandardResilienceHandler(); 29 | 30 | // Turn on service discovery by default 31 | http.AddServiceDiscovery(); 32 | }); 33 | 34 | return builder; 35 | } 36 | 37 | public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) 38 | { 39 | builder.Logging.AddOpenTelemetry(logging => 40 | { 41 | logging.IncludeFormattedMessage = true; 42 | logging.IncludeScopes = true; 43 | }); 44 | 45 | builder.Services.AddOpenTelemetry() 46 | .WithMetrics(metrics => 47 | { 48 | metrics.AddAspNetCoreInstrumentation() 49 | .AddHttpClientInstrumentation() 50 | .AddRuntimeInstrumentation(); 51 | }) 52 | .WithTracing(tracing => 53 | { 54 | tracing.AddAspNetCoreInstrumentation() 55 | // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) 56 | //.AddGrpcClientInstrumentation() 57 | .AddHttpClientInstrumentation(); 58 | }); 59 | 60 | builder.AddOpenTelemetryExporters(); 61 | 62 | return builder; 63 | } 64 | 65 | private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) 66 | { 67 | var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); 68 | 69 | if (useOtlpExporter) 70 | { 71 | builder.Services.AddOpenTelemetry().UseOtlpExporter(); 72 | } 73 | 74 | // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) 75 | //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) 76 | //{ 77 | // builder.Services.AddOpenTelemetry() 78 | // .UseAzureMonitor(); 79 | //} 80 | 81 | return builder; 82 | } 83 | 84 | public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) 85 | { 86 | builder.Services.AddHealthChecks() 87 | // Add a default liveness check to ensure app is responsive 88 | .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); 89 | 90 | return builder; 91 | } 92 | 93 | public static WebApplication MapDefaultEndpoints(this WebApplication app) 94 | { 95 | // Adding health checks endpoints to applications in non-development environments has security implications. 96 | // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. 97 | if (app.Environment.IsDevelopment()) 98 | { 99 | // All health checks must pass for app to be considered ready to accept traffic after starting 100 | app.MapHealthChecks("/health"); 101 | 102 | // Only health checks tagged with the "live" tag must pass for app to be considered alive 103 | app.MapHealthChecks("/alive", new HealthCheckOptions 104 | { 105 | Predicate = r => r.Tags.Contains("live") 106 | }); 107 | } 108 | 109 | return app; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/AspireAIBlazorChatBot.Web.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Chatbot/ChatState.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | using System.Security.Claims; 3 | using System.Text; 4 | 5 | namespace AspireApp.WebApp.Chatbot; 6 | 7 | public class ChatState 8 | { 9 | private readonly ILogger _logger; 10 | private readonly IChatClient _chatClient; 11 | private List _chatMessages; 12 | 13 | public List ChatMessages { get => _chatMessages; set => _chatMessages = value; } 14 | 15 | public ChatState(ClaimsPrincipal user, IChatClient chatClient, List chatMessages, ILogger logger) 16 | { 17 | _logger = logger; 18 | _chatClient = chatClient; 19 | ChatMessages = chatMessages; 20 | } 21 | 22 | public async Task AddUserMessageAsync(string userText, Action onMessageAdded) 23 | { 24 | ChatMessages.Add(new ChatMessage(ChatRole.User, userText)); 25 | onMessageAdded(); 26 | 27 | try 28 | { 29 | _logger.LogInformation("Sending message to chat client."); 30 | _logger.LogInformation($"user Text: {userText}"); 31 | 32 | var result = await _chatClient.CompleteAsync(ChatMessages); 33 | ChatMessages.Add(new ChatMessage(ChatRole.Assistant, result.Message.Text)); 34 | 35 | _logger.LogInformation($"Assistant Response: {result.Message.Text}"); 36 | } 37 | catch (Exception e) 38 | { 39 | if (_logger.IsEnabled(LogLevel.Error)) 40 | { 41 | _logger.LogError(e, "Error getting chat completions."); 42 | } 43 | 44 | // format the exception using HTML to show the exception details in a chat panel as response 45 | ChatMessages.Add(new ChatMessage(ChatRole.Assistant, $"My apologies, but I encountered an unexpected error.\n\n

{e}

")); 46 | } 47 | onMessageAdded(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Chatbot/Chatbot.razor: -------------------------------------------------------------------------------- 1 | @rendermode @(new InteractiveServerRenderMode(prerender: false)) 2 | @using Microsoft.AspNetCore.Components.Authorization 3 | @using AspireApp.WebApp.Chatbot 4 | @using Microsoft.Extensions.AI 5 | @inject IJSRuntime JS 6 | @inject NavigationManager Nav 7 | 8 | @inject AuthenticationStateProvider AuthenticationStateProvider 9 | @inject ILogger Logger 10 | @inject IConfiguration Configuration 11 | @inject IServiceProvider ServiceProvider 12 | 13 |
14 | 15 | 16 |
17 | @if (chatState is not null) 18 | { 19 | foreach (var message in chatState.ChatMessages.Where(m => m.Role == ChatRole.Assistant || m.Role == ChatRole.User)) 20 | { 21 | if (!string.IsNullOrEmpty(message.Contents[0].ToString())) 22 | { 23 |

@MessageProcessor.AllowImages(message.Contents[0].ToString()!)

24 | } 25 | } 26 | } 27 | else if (missingConfiguration) 28 | { 29 |

The chatbot is missing required configuration. Please review your app settings.

30 | } 31 | 32 | @if (thinking) 33 | { 34 |

[@Configuration["Aspire:OllamaSharp:ollama:Models:0"]] is Thinking...

35 | } 36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 | @code { 46 | bool missingConfiguration; 47 | ChatState? chatState; 48 | ElementReference textbox; 49 | ElementReference chat; 50 | string? messageToSend; 51 | bool thinking; 52 | IJSObjectReference? jsModule; 53 | 54 | protected override async Task OnInitializedAsync() 55 | { 56 | IChatClient chatClient = ServiceProvider.GetService(); 57 | List chatMessages = ServiceProvider.GetService>(); 58 | if (chatClient is not null) 59 | { 60 | AuthenticationState auth = await AuthenticationStateProvider.GetAuthenticationStateAsync(); 61 | chatState = new ChatState(auth.User, chatClient, chatMessages, Logger); 62 | } 63 | else 64 | { 65 | missingConfiguration = true; 66 | } 67 | } 68 | 69 | private async Task SendMessageAsync() 70 | { 71 | var messageCopy = messageToSend?.Trim(); 72 | messageToSend = null; 73 | 74 | if (chatState is not null && !string.IsNullOrEmpty(messageCopy)) 75 | { 76 | thinking = true; 77 | await chatState.AddUserMessageAsync(messageCopy, onMessageAdded: StateHasChanged); 78 | thinking = false; 79 | } 80 | } 81 | 82 | protected override async Task OnAfterRenderAsync(bool firstRender) 83 | { 84 | jsModule ??= await JS.InvokeAsync("import", "./Components/Chatbot/Chatbot.razor.js"); 85 | await jsModule.InvokeVoidAsync("scrollToEnd", chat); 86 | 87 | if (firstRender) 88 | { 89 | await textbox.FocusAsync(); 90 | await jsModule.InvokeVoidAsync("submitOnEnter", textbox); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Chatbot/Chatbot.razor.css: -------------------------------------------------------------------------------- 1 | .floating-pane { 2 | position: fixed; 3 | padding-top: 1em; 4 | width: 25rem; 5 | height: 35rem; 6 | right: 3rem; 7 | bottom: 3rem; 8 | border: 0.0625rem solid silver; 9 | border-radius: 0.5rem; 10 | background-color: white; 11 | display: flex; 12 | flex-direction: column; 13 | font-weight: 400; 14 | font-family: "Segoe UI", arial, helvetica; 15 | animation: slide-in-from-right 0.3s ease-out; 16 | z-index: 2; 17 | } 18 | 19 | @keyframes slide-in-from-right { 20 | 0% { 21 | transform: translateX(30rem); 22 | } 23 | 100% { 24 | transform: translateX(0); 25 | } 26 | } 27 | 28 | .hide-chatbot { 29 | border: none; 30 | background-color: #B4B4B8; 31 | color: white; 32 | position: absolute; 33 | top: 0.25rem; 34 | right: 0.18rem; 35 | border-radius: 0.55rem; 36 | width: 2rem; 37 | height: 2rem; 38 | z-index: 10; 39 | text-decoration: none; 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | } 44 | 45 | .chatbot-input { 46 | margin-top: auto; 47 | display: flex; 48 | position: relative; 49 | border: 0.125rem solid #f4f0f4; 50 | border-radius: 0.5rem; 51 | padding: 0.5rem; 52 | margin: 0.5rem 0.75rem; 53 | gap: 0.3rem; 54 | height: 3.5rem; 55 | align-items: stretch; 56 | flex-shrink: 0; 57 | } 58 | 59 | .chatbot-input textarea { 60 | width: 100%; 61 | background: none; 62 | border: none; 63 | outline: none; 64 | resize: none; 65 | font-weight: 400; 66 | font-family: "Segoe UI", arial, helvetica; 67 | font-size: 16px; 68 | } 69 | 70 | .chatbot-input button { 71 | width: 6.25rem; 72 | height: 3.125rem; 73 | border-radius: 0.5rem; 74 | border-width: 0; 75 | cursor: pointer; 76 | font-size: 0.875rem; 77 | font-weight: 500; 78 | padding: 0.6rem 0.8rem; 79 | text-align: center; 80 | margin-right: -7px; 81 | margin-top: -7px; 82 | } 83 | 84 | .chatbot-chat { 85 | overflow-y: scroll; 86 | height: 100%; 87 | padding: 0.5rem 0.75rem; 88 | display: flex; 89 | flex-direction: column; 90 | } 91 | 92 | .chatbot-chat .message { 93 | padding: 0.5rem 1rem; 94 | border-radius: 1.25rem; 95 | max-width: 85%; 96 | display: inline-block; 97 | white-space: break-spaces; 98 | overflow-x: clip; 99 | margin-bottom: 0.75rem; 100 | margin-top: 0.25rem; 101 | } 102 | 103 | .chatbot-chat .message-assistant { 104 | background-color: #f4f0f4; 105 | margin-right: auto; 106 | } 107 | 108 | .chatbot-chat .message-user { 109 | background-color: #102c57; 110 | margin-left: auto; 111 | color: white; 112 | } 113 | 114 | .chatbot-chat .message-error { 115 | background-color: #102c57; 116 | margin-left: auto; 117 | color: red; 118 | } 119 | 120 | .chatbot-chat ::deep img { 121 | max-height: 10rem; 122 | } 123 | 124 | .thinking { 125 | color: gray; 126 | font-style: italic; 127 | animation: fade-in-and-out 1s infinite; 128 | padding: 0; 129 | margin: 0; 130 | padding-left: 0.6rem; 131 | font-size: 90%; 132 | } 133 | 134 | @keyframes fade-in-and-out { 135 | 0% { 136 | opacity: 0.2; 137 | } 138 | 139 | 50% { 140 | opacity: 0.9; 141 | } 142 | 143 | 100% { 144 | opacity: 0.2; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Chatbot/Chatbot.razor.js: -------------------------------------------------------------------------------- 1 | export function scrollToEnd(element) { 2 | element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' }); 3 | } 4 | 5 | export function submitOnEnter(element) { 6 | element.addEventListener('keydown', event => { 7 | if (event.key === 'Enter') { 8 | event.target.dispatchEvent(new Event('change')); 9 | event.target.closest('form').dispatchEvent(new Event('submit')); 10 | } 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Chatbot/MessageProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Encodings.Web; 3 | using System.Text.RegularExpressions; 4 | using Microsoft.AspNetCore.Components; 5 | 6 | namespace AspireApp.WebApp.Chatbot; 7 | 8 | public static partial class MessageProcessor 9 | { 10 | public static MarkupString AllowImages(string message) 11 | { 12 | // Having to process markdown and deal with HTML encoding isn't ideal. If the language model could return 13 | // search results in some defined format like JSON we could simply loop over it in .razor code. This is 14 | // fine for now though. 15 | 16 | var result = new StringBuilder(); 17 | var prevEnd = 0; 18 | message = message.Replace("<", "<").Replace(">", ">"); 19 | 20 | foreach (Match match in FindMarkdownImages().Matches(message)) 21 | { 22 | var contentToHere = message.Substring(prevEnd, match.Index - prevEnd); 23 | result.Append(HtmlEncoder.Default.Encode(contentToHere)); 24 | result.Append($""); 25 | 26 | prevEnd = match.Index + match.Length; 27 | } 28 | result.Append(HtmlEncoder.Default.Encode(message.Substring(prevEnd))); 29 | 30 | return new MarkupString(result.ToString()); 31 | } 32 | 33 | public static MarkupString ProcessMessageToHTML(string message) 34 | { 35 | return new MarkupString(message); 36 | } 37 | 38 | [GeneratedRegex(@"\!?\[([^\]]+)\]\s*\(([^\)]+)\)")] 39 | private static partial Regex FindMarkdownImages(); 40 | } 41 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Chatbot/ShowChatbotButton.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager Nav 2 | 3 | 4 | 5 | @if (ShowChat) 6 | { 7 | 8 | } 9 | 10 | @code { 11 | [SupplyParameterFromQuery(Name = "chat")] 12 | public bool ShowChat { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Chatbot/ShowChatbotButton.razor.css: -------------------------------------------------------------------------------- 1 | .show-chatbot { 2 | position: fixed; 3 | bottom: 5rem; 4 | right: 3rem; 5 | z-index: 1; 6 | box-shadow: 0 2px 7px 2px rgba(0,0,0,0.2); 7 | border-radius: 10px; 8 | background-image: url('chat.png'); 9 | background-size: contain; 10 | width: 4.5rem; 11 | height: 3.5rem; 12 | background-repeat: no-repeat; 13 | background-position: center; 14 | background-color: #f3f4f3; 15 | transition: transform ease-out 0.2s; 16 | border: 2px solid transparent; 17 | display: block; 18 | } 19 | 20 | .show-chatbot:hover { 21 | border-color: #49b4fe; 22 | cursor: pointer; 23 | transform: scale(1.2); 24 | box-shadow: 0 2px 7px 2px rgba(0,0,0,0.4); 25 | } 26 | 27 | .show-chatbot:active { 28 | transform: scale(1.1); 29 | transition: transform ease-out 0.01s; 30 | } 31 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @using AspireAIBlazorChatBot.Web.Components.Chatbot 2 | @inherits LayoutComponentBase 3 | 4 |
5 | 8 | 9 |
10 |
11 | About 12 |
13 | 14 |
15 | @Body 16 |
17 | 18 |
19 |
20 | 21 |
22 | An unhandled error has occurred. 23 | Reload 24 | 🗙 25 |
26 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .nav-item { 50 | font-size: 0.9rem; 51 | padding-bottom: 0.5rem; 52 | } 53 | 54 | .nav-item:first-of-type { 55 | padding-top: 1rem; 56 | } 57 | 58 | .nav-item:last-of-type { 59 | padding-bottom: 1rem; 60 | } 61 | 62 | .nav-item ::deep a { 63 | color: #d7d7d7; 64 | border-radius: 4px; 65 | height: 3rem; 66 | display: flex; 67 | align-items: center; 68 | line-height: 3rem; 69 | } 70 | 71 | .nav-item ::deep a.active { 72 | background-color: rgba(255,255,255,0.37); 73 | color: white; 74 | } 75 | 76 | .nav-item ::deep a:hover { 77 | background-color: rgba(255,255,255,0.1); 78 | color: white; 79 | } 80 | 81 | .nav-scrollable { 82 | display: none; 83 | } 84 | 85 | .navbar-toggler:checked ~ .nav-scrollable { 86 | display: block; 87 | } 88 | 89 | @media (min-width: 641px) { 90 | .navbar-toggler { 91 | display: none; 92 | } 93 | 94 | .nav-scrollable { 95 | /* Never collapse the sidebar for wide screens */ 96 | display: block; 97 | 98 | /* Allow sidebar to scroll for tall menus */ 99 | height: calc(100vh - 3.5rem); 100 | overflow-y: auto; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | @rendermode InteractiveServer 3 | 4 | Counter 5 | 6 |

Counter

7 | 8 |

Current count: @currentCount

9 | 10 | 11 | 12 | @code { 13 | private int currentCount = 0; 14 | 15 | private void IncrementCount() 16 | { 17 | currentCount++; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @requestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | public HttpContext? HttpContext { get; set; } 30 | 31 | private string? requestId; 32 | private bool ShowRequestId => !string.IsNullOrEmpty(requestId); 33 | 34 | protected override void OnInitialized() 35 | { 36 | requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @rendermode InteractiveServer 2 | @page "/" 3 | @using AspireApp.WebApp.Chatbot 4 | 5 | @inject IConfiguration Configuration 6 | 7 | Home 8 | 9 |

Hello, world!

10 | 11 | Let's test local models with Ollama! 12 | 13 |
14 | 15 |
    16 |
  • 17 | Model Name: 18 | @ollamaSLMName 19 |
  • 20 |
  • 21 | Ollama Url: 22 | @ollamaUrl 23 |
  • 24 |
  • 25 | Ollama Model List: 26 | @ollamaModelUrlModelList 27 |
  • 28 |
  • 29 | 30 |
  • 31 |
  • 32 |

    @MessageProcessor.ProcessMessageToHTML(modelStatus)

    33 |
  • 34 |
35 | 36 | @code { 37 | 38 | string ollamaSLMName = ""; 39 | string ollamaModelUrl = ""; 40 | string ollamaModelUrlModelList = ""; 41 | string ollamaUrl = ""; 42 | string modelStatus = ""; 43 | 44 | protected override void OnInitialized() 45 | { 46 | ollamaSLMName = Configuration["Aspire:OllamaSharp:ollama:Models:0"]!; 47 | ollamaModelUrl = $@"https://ollama.com/library/{ollamaSLMName}"; 48 | ollamaUrl = Configuration.GetConnectionString("ollama")!; 49 | ollamaModelUrlModelList = $@"{ollamaUrl}/api/tags"; 50 | } 51 | 52 | async void PullModel() 53 | { 54 | var os = new OllamaApiService(); 55 | modelStatus = await os.PullModelAsync(ollamaUrl, ollamaSLMName); 56 | 57 | // refresh the page 58 | StateHasChanged(); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Pages/Weather.razor: -------------------------------------------------------------------------------- 1 | @page "/weather" 2 | @attribute [StreamRendering(true)] 3 | @attribute [OutputCache(Duration = 5)] 4 | 5 | @inject WeatherApiClient WeatherApi 6 | 7 | Weather 8 | 9 |

Weather

10 | 11 |

This component demonstrates showing data loaded from a backend API service.

12 | 13 | @if (forecasts == null) 14 | { 15 |

Loading...

16 | } 17 | else 18 | { 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | @foreach (var forecast in forecasts) 30 | { 31 | 32 | 33 | 34 | 35 | 36 | 37 | } 38 | 39 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
40 | } 41 | 42 | @code { 43 | private WeatherForecast[]? forecasts; 44 | 45 | protected override async Task OnInitializedAsync() 46 | { 47 | forecasts = await WeatherApi.GetWeatherAsync(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.AspNetCore.OutputCaching 9 | @using Microsoft.JSInterop 10 | @using AspireAIBlazorChatBot.Web 11 | @using AspireAIBlazorChatBot.Web.Components 12 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/OllamaApiService.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | 4 | public class OllamaApiService 5 | { 6 | private readonly HttpClient _httpClient; 7 | 8 | public OllamaApiService() 9 | { 10 | _httpClient = new HttpClient(); 11 | } 12 | 13 | public async Task PullModelAsync(string ollamaUrl, string modelName) 14 | { 15 | var requestUri = $"{ollamaUrl}/api/pull"; 16 | var jsonContent = $"{{ \"name\": \"{modelName}\" }}"; 17 | var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); 18 | 19 | var response = await _httpClient.PostAsync(requestUri, content); 20 | response.EnsureSuccessStatusCode(); 21 | 22 | var responseString = await response.Content.ReadAsStringAsync(); 23 | // split the responseString into an array of string with a new element for each appearance of the string '{"status":' 24 | var responseArray = responseString.Split("{\"status\":"); 25 | 26 | // remote the char '}' from the each element of the array 27 | var responseHtml = ""; 28 | for (int i = 0; i < responseArray.Length; i++) 29 | { 30 | responseArray[i] = responseArray[i].Replace("}", ""); 31 | responseHtml += $"

{responseArray[i]}

"; 32 | } 33 | 34 | 35 | return responseHtml; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using AspireAIBlazorChatBot.Web; 2 | using AspireAIBlazorChatBot.Web.Components; 3 | using Microsoft.Extensions.AI; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // Add service defaults & Aspire components. 8 | builder.AddServiceDefaults(); 9 | builder.AddRedisOutputCache("cache"); 10 | 11 | // Add services to the container. 12 | builder.Services.AddRazorComponents() 13 | .AddInteractiveServerComponents(); 14 | 15 | builder.Services.AddHttpClient(client => 16 | { 17 | // This URL uses "https+http://" to indicate HTTPS is preferred over HTTP. 18 | // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes. 19 | client.BaseAddress = new("https+http://apiservice"); 20 | }); 21 | 22 | // Add logging with detailed information 23 | builder.Services.AddLogging(); 24 | builder.Logging.ClearProviders(); 25 | builder.Logging.AddConsole(); 26 | builder.Logging.AddDebug(); 27 | builder.Logging.AddEventSourceLogger(); 28 | builder.Logging.AddFilter("Microsoft", LogLevel.Information); 29 | builder.Logging.AddFilter("System", LogLevel.Information); 30 | builder.Logging.AddFilter("AspireAIBlazorChatBot", LogLevel.Debug); 31 | 32 | builder.Services.AddSingleton(static serviceProvider => 33 | { 34 | var lf = serviceProvider.GetRequiredService(); 35 | return lf.CreateLogger(typeof(Program)); 36 | }); 37 | 38 | // register chat client 39 | builder.Services.AddSingleton(static serviceProvider => 40 | { 41 | var logger = serviceProvider.GetRequiredService(); 42 | var config = serviceProvider.GetRequiredService(); 43 | var ollamaCnnString = config.GetConnectionString("ollama"); 44 | var defaultLLM = config["Aspire:OllamaSharp:ollama:Models:0"]; 45 | 46 | logger.LogInformation("Ollama connection string: {0}", ollamaCnnString); 47 | logger.LogInformation("Default LLM: {0}", defaultLLM); 48 | 49 | IChatClient chatClient = new OllamaChatClient(new Uri(ollamaCnnString), defaultLLM); 50 | 51 | return chatClient; 52 | }); 53 | 54 | // register chat nessages 55 | builder.Services.AddSingleton>(static serviceProvider => 56 | { 57 | return new List() 58 | { new ChatMessage(ChatRole.System, "You are a useful assistant that replies using short and precise sentences.")}; 59 | }); 60 | 61 | var app = builder.Build(); 62 | 63 | if (!app.Environment.IsDevelopment()) 64 | { 65 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 66 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 67 | app.UseHsts(); 68 | } 69 | 70 | app.UseHttpsRedirection(); 71 | 72 | app.UseStaticFiles(); 73 | app.UseAntiforgery(); 74 | 75 | app.UseOutputCache(); 76 | 77 | app.MapRazorComponents() 78 | .AddInteractiveServerRenderMode(); 79 | 80 | app.MapDefaultEndpoints(); 81 | 82 | app.Run(); 83 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "http://localhost:5015", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "applicationUrl": "https://localhost:7170;http://localhost:5015", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/WeatherApiClient.cs: -------------------------------------------------------------------------------- 1 | namespace AspireAIBlazorChatBot.Web; 2 | 3 | public class WeatherApiClient(HttpClient httpClient) 4 | { 5 | public async Task GetWeatherAsync(int maxItems = 10, CancellationToken cancellationToken = default) 6 | { 7 | List? forecasts = null; 8 | 9 | await foreach (var forecast in httpClient.GetFromJsonAsAsyncEnumerable("/weatherforecast", cancellationToken)) 10 | { 11 | if (forecasts?.Count >= maxItems) 12 | { 13 | break; 14 | } 15 | if (forecast is not null) 16 | { 17 | forecasts ??= []; 18 | forecasts.Add(forecast); 19 | } 20 | } 21 | 22 | return forecasts?.ToArray() ?? []; 23 | } 24 | } 25 | 26 | public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) 27 | { 28 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 29 | } 30 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50150; 33 | } 34 | 35 | .validation-message { 36 | color: #e50150; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/wwwroot/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/src/AspireAIBlazorChatBot.Web/wwwroot/chat.png -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.Web/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbruno/ai-aspire-blazor-chat-quickstart-csharp/e57a7f2621afd5c22d798e721bb453e78610b3c9/src/AspireAIBlazorChatBot.Web/wwwroot/favicon.png -------------------------------------------------------------------------------- /src/AspireAIBlazorChatBot.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.8.0.0 4 | MinimumVisualStudioVersion = 17.8.0.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireAIBlazorChatBot.AppHost", "AspireAIBlazorChatBot.AppHost\AspireAIBlazorChatBot.AppHost.csproj", "{F09A6073-0584-44AC-807B-23BD0DD5C17B}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireAIBlazorChatBot.ServiceDefaults", "AspireAIBlazorChatBot.ServiceDefaults\AspireAIBlazorChatBot.ServiceDefaults.csproj", "{5B9D3843-C3CE-4471-84C6-CB361847FB92}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireAIBlazorChatBot.ApiService", "AspireAIBlazorChatBot.ApiService\AspireAIBlazorChatBot.ApiService.csproj", "{6746F8F7-2640-4860-B086-6F4670CC0EAE}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireAIBlazorChatBot.Web", "AspireAIBlazorChatBot.Web\AspireAIBlazorChatBot.Web.csproj", "{DEB97888-0E5D-459C-BCEA-AC4C02D643D8}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {F09A6073-0584-44AC-807B-23BD0DD5C17B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {F09A6073-0584-44AC-807B-23BD0DD5C17B}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {F09A6073-0584-44AC-807B-23BD0DD5C17B}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {F09A6073-0584-44AC-807B-23BD0DD5C17B}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {5B9D3843-C3CE-4471-84C6-CB361847FB92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {5B9D3843-C3CE-4471-84C6-CB361847FB92}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {5B9D3843-C3CE-4471-84C6-CB361847FB92}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {5B9D3843-C3CE-4471-84C6-CB361847FB92}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {6746F8F7-2640-4860-B086-6F4670CC0EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {6746F8F7-2640-4860-B086-6F4670CC0EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {6746F8F7-2640-4860-B086-6F4670CC0EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {6746F8F7-2640-4860-B086-6F4670CC0EAE}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {DEB97888-0E5D-459C-BCEA-AC4C02D643D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {DEB97888-0E5D-459C-BCEA-AC4C02D643D8}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {DEB97888-0E5D-459C-BCEA-AC4C02D643D8}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {DEB97888-0E5D-459C-BCEA-AC4C02D643D8}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {0C245082-B48B-48E8-BF3B-F6669D642DCE} 41 | EndGlobalSection 42 | EndGlobal 43 | --------------------------------------------------------------------------------