├── .gitattributes ├── .gitignore ├── BlazorAIChat.sln ├── BlazorAIChat ├── .config │ └── dotnet-tools.json ├── AlertTypeEnum.cs ├── Authentication │ └── EasyAuthMiddleware.cs ├── BlazorAIChat.csproj ├── Components │ ├── App.razor │ ├── Confirmation.razor │ ├── Input.razor │ ├── Layout │ │ ├── MainLayout.razor │ │ └── MainLayout.razor.css │ ├── Pages │ │ ├── Admin │ │ │ ├── Admin.razor │ │ │ ├── Config.razor │ │ │ └── Users.razor │ │ ├── Error.razor │ │ ├── Home.razor │ │ ├── Home.razor.css │ │ ├── NavMenu.razor │ │ └── UserProfile.razor │ ├── Routes.razor │ ├── Shared │ │ ├── Alert.razor │ │ ├── Alert.razor.css │ │ ├── ChatCitation.razor │ │ ├── ChatCitation.razor.css │ │ ├── ChatInput.razor │ │ ├── ChatInput.razor.css │ │ ├── ChatMessages.Razor.css │ │ ├── ChatMessages.razor │ │ └── CustomModal.razor │ └── _Imports.razor ├── DBContext.cs ├── Migrations │ ├── 20240808174120_initial.Designer.cs │ ├── 20240808174120_initial.cs │ ├── 20240813233512_updated config.Designer.cs │ ├── 20240813233512_updated config.cs │ ├── 20240815143950_Added Chat History.Designer.cs │ ├── 20240815143950_Added Chat History.cs │ ├── 20240819192044_Added Citation.Designer.cs │ ├── 20240819192044_Added Citation.cs │ ├── 20240820173447_Add Session Document Tracking.Designer.cs │ ├── 20240820173447_Add Session Document Tracking.cs │ ├── 20250529151134_Added Competion Timestamp.Designer.cs │ ├── 20250529151134_Added Competion Timestamp.cs │ └── AIChatDBContextModelSnapshot.cs ├── Models │ ├── Config.cs │ ├── Constants.cs │ ├── Message.cs │ ├── Session.cs │ ├── SessionDocuments.cs │ ├── Settings.cs │ ├── User.cs │ └── UserRole.cs ├── Program.cs ├── Properties │ ├── ServiceDependencies │ │ ├── blazoraichat-muux6rot5kooq - Web Deploy │ │ │ └── profile.arm.json │ │ └── blazoraichat-wepqpxfmxukjo - Web Deploy │ │ │ └── profile.arm.json │ └── launchSettings.json ├── Services │ ├── AISearchService.cs │ ├── AIService.cs │ ├── ChatHistoryService.cs │ ├── McpPluginProvider.cs │ └── UserService.cs ├── Utils │ ├── AIUtils.cs │ ├── FileUtils.cs │ ├── Retry.cs │ ├── StringUtils.cs │ └── UserUtils.cs ├── appsettings.json └── wwwroot │ ├── Images │ ├── Excel_256x256.png │ ├── PowerPoint_256x256.png │ ├── Word_256x256.png │ ├── pdf_256x256.png │ └── txt_256x256.png │ ├── Lib │ ├── dompurify │ │ ├── README.md │ │ └── dist │ │ │ └── purify.es.mjs │ ├── marked │ │ ├── README.md │ │ └── dist │ │ │ └── marked.esm.js │ ├── pdf_viewer │ │ ├── viewer.html │ │ └── viewer.mjs │ └── pdfjs-dist │ │ ├── README.md │ │ └── dist │ │ ├── build │ │ ├── pdf.min.mjs │ │ └── pdf.worker.min.mjs │ │ └── web │ │ ├── images │ │ └── loading-icon.gif │ │ ├── pdf_viewer.css │ │ └── pdf_viewer.mjs │ ├── app.css │ ├── app.js │ └── favicon.ico ├── Infra ├── azuredeploy.json └── main.bicep └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | /BlazorAIChat/appsettings.Development.json 365 | /BlazorAIChat/memory.sqlite 366 | /BlazorAIChat/ConfigDatabase.db 367 | /BlazorAIChat/ConfigDatabase.db-shm 368 | /BlazorAIChat/ConfigDatabase.db-wal 369 | /BlazorAIChat/KNN/ 370 | /BlazorAIChat/SFS/ 371 | -------------------------------------------------------------------------------- /BlazorAIChat.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.11.35111.106 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorAIChat", "BlazorAIChat\BlazorAIChat.csproj", "{4E9638D7-0937-4D2D-90C9-A609FAE0C957}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {4E9638D7-0937-4D2D-90C9-A609FAE0C957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {4E9638D7-0937-4D2D-90C9-A609FAE0C957}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {4E9638D7-0937-4D2D-90C9-A609FAE0C957}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {4E9638D7-0937-4D2D-90C9-A609FAE0C957}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {410FDBC2-8D8D-41B1-A6C5-96DDEAF04741} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /BlazorAIChat/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "8.0.7", 7 | "commands": [ 8 | "dotnet-ef" 9 | ], 10 | "rollForward": false 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /BlazorAIChat/AlertTypeEnum.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAIChat 2 | { 3 | public enum AlertTypeEnum 4 | { 5 | primary, 6 | secondary, 7 | success, 8 | danger, 9 | warning, 10 | info, 11 | light, 12 | dark 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BlazorAIChat/Authentication/EasyAuthMiddleware.cs: -------------------------------------------------------------------------------- 1 | using BlazorAIChat.Models; 2 | using System.Security.Claims; 3 | 4 | namespace BlazorAIChat.Authentication 5 | { 6 | public class EasyAuthMiddleware 7 | { 8 | private readonly RequestDelegate _next; 9 | 10 | public EasyAuthMiddleware(RequestDelegate next) 11 | { 12 | _next = next; 13 | } 14 | 15 | public async Task InvokeAsync(HttpContext context) 16 | { 17 | //Only process if the user is authenticated with Easy Auth via App Service. 18 | if (context.Request.Headers.TryGetValue("X-MS-CLIENT-PRINCIPAL-ID", out var userId) && 19 | !string.IsNullOrEmpty(userId)) 20 | { 21 | var claims = new List 22 | { 23 | new Claim(ClaimTypes.NameIdentifier, userId!) 24 | }; 25 | 26 | //We set the name claim to the Principal Name if it exists, otherwise we use the userId. 27 | if (context.Request.Headers.TryGetValue("X-MS-CLIENT-PRINCIPAL-NAME", out var userName)) 28 | { 29 | claims.Add(new Claim(ClaimTypes.Name, userName!)); 30 | } 31 | else 32 | { 33 | claims.Add(new Claim(ClaimTypes.Name, userId!)); 34 | } 35 | 36 | if (context.Request.Headers.TryGetValue("X-MS-CLIENT-PRINCIPAL-IDP", out var idp)) 37 | { 38 | claims.Add(new Claim("idp", idp!)); 39 | } 40 | 41 | //Get the database context from the DI container 42 | using (var scope = context.RequestServices.CreateScope()) 43 | { 44 | var dbContext = scope.ServiceProvider.GetRequiredService(); 45 | 46 | //get the user details from the database 47 | var dbUser = await dbContext.Users.FindAsync(userId); 48 | if (dbUser != null) 49 | { 50 | //if the dbUser.Name has a value, replace the name claim with the value from the database 51 | if (!string.IsNullOrEmpty(dbUser.Name)) 52 | { 53 | claims.Remove(claims.First(c => c.Type == ClaimTypes.Name)); 54 | claims.Add(new Claim(ClaimTypes.Name, dbUser.Name)); 55 | } 56 | else 57 | { 58 | //if the dbUser.Name is empty, set the dbUser.Name to the value from the claim 59 | dbUser.Name = claims.First(c => c.Type == ClaimTypes.Name).Value; 60 | await dbContext.SaveChangesAsync(); 61 | } 62 | 63 | claims.Add(new Claim(ClaimTypes.Role, Enum.GetName(dbUser.Role)!)); 64 | claims.Add(new Claim(ClaimTypes.Email, dbUser.Email)); 65 | claims.Add(new Claim("dateRequested", dbUser.DateRequested.ToString())); 66 | if (dbUser.DateApproved != null) 67 | { 68 | claims.Add(new Claim("dateApproved", dbUser.DateApproved.Value.ToString())); 69 | } 70 | if (dbUser.ApprovedBy != null) 71 | { 72 | claims.Add(new Claim("approvedBy", dbUser.ApprovedBy)); 73 | } 74 | } 75 | else 76 | { 77 | claims.Add(new Claim(ClaimTypes.Role, Enum.GetName(UserRoles.Guest)!)); 78 | } 79 | 80 | var identity = new ClaimsIdentity(claims, "EasyAuth"); 81 | context.User = new ClaimsPrincipal(identity); 82 | 83 | } 84 | } 85 | 86 | await _next(context); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /BlazorAIChat/BlazorAIChat.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 16 | 17 | 18 | 19 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Confirmation.razor: -------------------------------------------------------------------------------- 1 |  45 | @code { 46 | [Parameter] public string? Caption { get; set; } 47 | [Parameter] public string? Message { get; set; } 48 | [Parameter] public EventCallback OnClose { get; set; } 49 | [Parameter] public Category Type { get; set; } 50 | private Task Cancel() 51 | { 52 | return OnClose.InvokeAsync(false); 53 | } 54 | private Task Ok() 55 | { 56 | return OnClose.InvokeAsync(true); 57 | } 58 | public enum Category 59 | { 60 | Okay, 61 | SaveNot, 62 | DeleteNot 63 | } 64 | } -------------------------------------------------------------------------------- /BlazorAIChat/Components/Input.razor: -------------------------------------------------------------------------------- 1 |  24 | @code { 25 | [Parameter] public string? Caption { get; set; } 26 | [Parameter] public string? Value { get; set; } 27 | [Parameter] public EventCallback OnClose { get; set; } 28 | 29 | public string? ReturnValue { get; set; } 30 | 31 | private Task Cancel() 32 | { 33 | return OnClose.InvokeAsync(""); 34 | } 35 | private Task Ok() 36 | { 37 | return OnClose.InvokeAsync(ReturnValue); 38 | } 39 | 40 | public Task Enter(KeyboardEventArgs e) 41 | { 42 | if (e.Code == "Enter" || e.Code == "NumpadEnter") 43 | { 44 | return OnClose.InvokeAsync(ReturnValue); 45 | } 46 | return Task.CompletedTask; 47 | } 48 | } -------------------------------------------------------------------------------- /BlazorAIChat/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | @Body 4 | 5 |
6 | An unhandled error has occurred. 7 | Reload 8 | 🗙 9 |
10 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | #blazor-error-ui { 2 | background: lightyellow; 3 | bottom: 0; 4 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 5 | display: none; 6 | left: 0; 7 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 8 | position: fixed; 9 | width: 100%; 10 | z-index: 1000; 11 | } 12 | 13 | #blazor-error-ui .dismiss { 14 | cursor: pointer; 15 | position: absolute; 16 | right: 0.75rem; 17 | top: 0.5rem; 18 | } 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Pages/Admin/Admin.razor: -------------------------------------------------------------------------------- 1 | @page "/admin" 2 | @using BlazorAIChat.Models 3 | @using Microsoft.AspNetCore.Authorization 4 | @attribute [Authorize(Roles = nameof(UserRoles.Admin))] 5 | 6 |
7 |

Admin

8 | 9 | 20 |
21 | 22 | 23 | @code { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Pages/Admin/Config.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/config" 2 | @attribute [Authorize(Roles = nameof(UserRoles.Admin))] 3 | @rendermode InteractiveServer 4 | @inject AIChatDBContext DbContext 5 | @inject NavigationManager NavigationManager 6 | @using BlazorAIChat.Models 7 | @using Microsoft.AspNetCore.Authorization 8 | @using Microsoft.EntityFrameworkCore 9 | 10 |
11 |

Config

12 | 13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 | 47 | @code { 48 | Models.Config config = new Models.Config() { Id=Guid.Empty}; 49 | 50 | protected override async Task OnInitializedAsync() 51 | { 52 | var result = await DbContext.Config.FirstOrDefaultAsync(); 53 | if (result != null) 54 | config = result; 55 | } 56 | 57 | private async void Save() 58 | { 59 | if (config.Id == Guid.Empty) 60 | { 61 | config.Id = Guid.NewGuid(); 62 | await DbContext.Config.AddAsync(config); 63 | } 64 | else 65 | { 66 | DbContext.Config.Update(config); 67 | } 68 | await DbContext.SaveChangesAsync(); 69 | } 70 | 71 | private void NavigateToAdmin() 72 | { 73 | NavigationManager.NavigateTo("/admin"); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Pages/Admin/Users.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/users" 2 | @attribute [Authorize(Roles = nameof(UserRoles.Admin))] 3 | @rendermode InteractiveServer 4 | @using BlazorAIChat.Models 5 | @using BlazorAIChat.Utils 6 | @using Microsoft.AspNetCore.Authorization 7 | @using Microsoft.AspNetCore.Components.Authorization 8 | @using Microsoft.EntityFrameworkCore 9 | @inject AIChatDBContext DbContext 10 | @inject AuthenticationStateProvider AuthenticationStateProvider 11 | @inject NavigationManager NavigationManager 12 | 13 |
14 |

Users

15 | 16 | @if (string.IsNullOrEmpty(selectedUser.Id)) 17 | { 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | @foreach (var user in users) 32 | { 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 47 | 48 | } 49 | 50 |
NameEmailRoleRequestedApprovedApproved byActions
@user.Name@user.Email@Enum.GetName(user.Role)@user.DateRequested@user.DateApproved@users.Where(x => x.Id == user.ApprovedBy).FirstOrDefault()?.Name 41 | @if (user.DateApproved == null) 42 | { 43 | 44 | } 45 | 46 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | } 58 |
59 | @if (!string.IsNullOrEmpty(selectedUser.Id)) 60 | { 61 |
62 |
63 |

Edit User

64 |
65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 | 73 |
74 |
75 | 76 | 77 | @foreach (var role in Enum.GetValues(typeof(UserRoles))) 78 | { 79 | 80 | } 81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 | 89 | 90 |
91 |
92 | 93 | 94 |
95 |
96 | 97 | } 98 | 99 | @code { 100 | private List users = new List(); 101 | private User selectedUser = new User() { Id=string.Empty }; 102 | private User? currentUser; 103 | 104 | protected override async Task OnInitializedAsync() 105 | { 106 | try 107 | { 108 | var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); 109 | var userPrincipal = authState.User; 110 | if (userPrincipal.Identity?.IsAuthenticated == true) 111 | { 112 | currentUser = UserUtils.ConvertPrincipalToUser(userPrincipal); 113 | } 114 | else 115 | { 116 | NavigationManager.NavigateTo("/"); 117 | } 118 | } 119 | catch (Exception ex) 120 | { 121 | Console.WriteLine($"Initialization error: {ex.Message}"); 122 | } 123 | 124 | users = await DbContext.Users.OrderBy(x => x.DateApproved).ThenBy(x => x.Name).ToListAsync(); 125 | } 126 | 127 | private void EditUser(User user) 128 | { 129 | selectedUser = user; 130 | StateHasChanged(); 131 | } 132 | 133 | private void CancelEdit() 134 | { 135 | selectedUser = new User() {Id=string.Empty }; 136 | StateHasChanged(); 137 | } 138 | 139 | private async Task SaveChanges() 140 | { 141 | if (!string.IsNullOrEmpty(selectedUser.Id) && currentUser != null) 142 | { 143 | if (selectedUser.Role == UserRoles.Guest) 144 | { 145 | selectedUser.DateApproved = null; 146 | selectedUser.ApprovedBy = null; 147 | } 148 | 149 | DbContext.Update(selectedUser); 150 | await DbContext.SaveChangesAsync(); 151 | selectedUser = new User() { Id=string.Empty }; 152 | users = await DbContext.Users.ToListAsync(); 153 | StateHasChanged(); 154 | } 155 | } 156 | 157 | private async void ApproveUser(User user) 158 | { 159 | if (currentUser != null) 160 | { 161 | user.DateApproved = DateTime.Now; 162 | user.ApprovedBy = currentUser.Id; 163 | user.Role = UserRoles.User; 164 | DbContext.Update(user); 165 | await DbContext.SaveChangesAsync(); 166 | users = await DbContext.Users.ToListAsync(); 167 | StateHasChanged(); 168 | } 169 | } 170 | 171 | private void NavigateToAdmin() 172 | { 173 | NavigationManager.NavigateTo("/admin"); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /BlazorAIChat/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 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Pages/Home.razor.css: -------------------------------------------------------------------------------- 1 | .ai-service-badge { 2 | background-color: #D3D3D3; 3 | color:#000000; 4 | } 5 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Pages/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @using BlazorAIChat.Models 2 | @using BlazorAIChat.Services 3 | @using BlazorAIChat.Components 4 | @inject ChatHistoryService chatHistoryService 5 | @inject NavigationManager NavigationManager 6 | 7 |
8 | 9 | 26 | 27 |
28 | @if (_loadingComplete == true) 29 | { 30 |
31 | 77 |
78 | } 79 |
80 |
81 | 82 | @if (_deletePopUpOpen) 83 | { 84 | 88 | 89 | } 90 | 91 | 92 | @if (_renamePopUpOpen) 93 | { 94 | 95 | } 96 | 97 | @code { 98 | [Parameter] 99 | public User? User { get; set; } = null; 100 | 101 | [Parameter] 102 | public EventCallback OnChatClicked { get; set; } 103 | 104 | [Parameter] 105 | public EventCallback OnNavBarVisibilityUpdated { get; set; } 106 | 107 | [Parameter] 108 | public EventCallback OnDeleteUploadedDocs { get; set; } 109 | 110 | private List _chatSessions { get; set; } = new(); 111 | private string? _sessionId; 112 | private string? _popUpText; 113 | private bool _deletePopUpOpen = false; 114 | private bool _loadingComplete = false; 115 | private bool _renamePopUpOpen = false; 116 | 117 | private Session? currentSession; 118 | 119 | 120 | // This method is called when the component is ready to start, having received its initial parameters. 121 | protected override async Task OnParametersSetAsync() 122 | { 123 | if (_loadingComplete == true) 124 | return; 125 | 126 | await SetMostRecentSession(); 127 | 128 | _loadingComplete = true; 129 | await LoadCurrentChatAsync(); 130 | 131 | } 132 | 133 | // Retrieve the most recent chat session if one exists for the user. 134 | private async Task SetMostRecentSession() 135 | { 136 | _chatSessions = await chatHistoryService.GetSessionsAsync(User?.Id ?? Models.Constants.NEW_CHAT); 137 | 138 | if (_chatSessions.Count > 0) 139 | currentSession = _chatSessions.OrderByDescending(x => x.SessionCreatedAt).FirstOrDefault(); 140 | else 141 | currentSession = null; 142 | } 143 | 144 | // Load the current chat session. 145 | private async Task LoadCurrentChatAsync() 146 | { 147 | int index = 0; 148 | if (currentSession is not null & _chatSessions is not null & _chatSessions?.Count > 0) 149 | { 150 | index = _chatSessions?.FindIndex(s => s.SessionId == currentSession?.SessionId) ?? 0; 151 | } 152 | if (currentSession is null || index < 0) 153 | { 154 | currentSession = new Session(); 155 | currentSession.SessionId = Constants.EMPTY_SESSION; 156 | currentSession.Name = Constants.NEW_CHAT; 157 | 158 | if (_chatSessions is not null & _chatSessions?.Count > 0) 159 | { 160 | var match = _chatSessions?.FirstOrDefault(); 161 | if (match is not null) 162 | { 163 | currentSession.Id = match.SessionId; 164 | currentSession.SessionId = match.SessionId; 165 | currentSession.Name = match.Name; 166 | } 167 | } 168 | } 169 | 170 | await OnChatClicked.InvokeAsync(currentSession); 171 | 172 | return 0; 173 | } 174 | 175 | // Create a new chat session. 176 | private async Task NewChat() 177 | { 178 | Session session = new(); 179 | session.UserId = User?.Id??string.Empty; 180 | await chatHistoryService.InsertSessionAsync(session); 181 | _chatSessions.Add(session); 182 | 183 | currentSession = session; 184 | 185 | UpdateNavMenuDisplay("Add"); 186 | await LoadChat(session.SessionId); 187 | StateHasChanged(); 188 | } 189 | 190 | // Update the navigation menu display. Used when the session name is updated 191 | public void UpdateNavMenuDisplay(string reason = "", Session? _session = null) 192 | { 193 | if (_session is not null) 194 | { 195 | int index = _chatSessions.FindIndex(s => s.SessionId == _session.SessionId); 196 | _chatSessions[index].Name = _session.Name; 197 | } 198 | 199 | } 200 | 201 | // Load the chat session based on the session id. 202 | async private Task LoadChat(string _sessionId) 203 | { 204 | if (_chatSessions is null) return 0; 205 | 206 | //get the session from _chatSessions 207 | currentSession = _chatSessions.Find(s => s.SessionId == _sessionId); 208 | 209 | await LoadCurrentChatAsync(); 210 | 211 | return 0; 212 | } 213 | 214 | // Open the confirmation dialog to delete a chat session. 215 | private void OpenConfirmation(string id, string title) 216 | { 217 | _deletePopUpOpen = true; 218 | _sessionId = id; 219 | _popUpText = $"Do you want to delete the chat \"{title}\"?"; 220 | } 221 | 222 | // Close the confirmation dialog to delete a chat session. 223 | // If the user agreed to delete the session, all uploaded documents are deleted 224 | // and the chat session is deleted. 225 | private async Task OnConfirmationClose(bool isOk) 226 | { 227 | 228 | if (isOk) 229 | { 230 | _deletePopUpOpen = false; 231 | 232 | if (_sessionId !=null) 233 | DeleteUploadedDocs(_sessionId); 234 | 235 | if (_sessionId!=null) 236 | await chatHistoryService.DeleteSessionAndMessagesAsync(_sessionId); 237 | 238 | int index = _chatSessions.FindIndex(s => s.SessionId == _sessionId); 239 | _chatSessions.RemoveAt(index); 240 | 241 | _deletePopUpOpen = false; 242 | 243 | UpdateNavMenuDisplay("Delete"); 244 | 245 | await SetMostRecentSession(); 246 | await LoadCurrentChatAsync(); 247 | } 248 | 249 | _deletePopUpOpen = false; 250 | } 251 | 252 | // Open the input dialog to rename a chat session. 253 | // Completes the rename process. 254 | private void OpenInput(string id, string title) 255 | { 256 | _renamePopUpOpen = true; 257 | _sessionId = id; 258 | _popUpText = title; 259 | } 260 | 261 | // Close the input dialog to rename a chat session. 262 | private async Task OnInputClose(string newName) 263 | { 264 | if (newName != "") 265 | { 266 | bool updateCurrentChat = false; 267 | 268 | if (_sessionId is null || currentSession is null) 269 | return; 270 | 271 | if (_sessionId == currentSession?.SessionId) 272 | { 273 | updateCurrentChat = true; 274 | } 275 | 276 | Session session = await chatHistoryService.GetSessionAsync(_sessionId); 277 | session.Name = newName; 278 | await chatHistoryService.UpdateSessionAsync(session); 279 | 280 | 281 | int index = _chatSessions.FindIndex(s => s.SessionId == _sessionId); 282 | _chatSessions[index].Name = newName; 283 | 284 | _renamePopUpOpen = false; 285 | 286 | UpdateNavMenuDisplay("Rename"); 287 | 288 | if (!updateCurrentChat) 289 | { 290 | return; 291 | } 292 | 293 | if (currentSession is not null) 294 | { 295 | currentSession.Name = newName; 296 | } 297 | await LoadCurrentChatAsync(); 298 | } 299 | 300 | _renamePopUpOpen = false; 301 | } 302 | 303 | // Check if the session is active. 304 | private bool IsActiveSession(string _sessionId) => currentSession switch 305 | { 306 | null => true, 307 | (Session s) when s.SessionId == _sessionId => true, 308 | _ => false 309 | }; 310 | 311 | // Creates a substring of the text that is under the maximum length. 312 | public string SafeSubstring(string text, int maxLength) => text switch 313 | { 314 | null => string.Empty, 315 | _ => text.Length > maxLength ? text.Substring(0, maxLength) + "..." : text 316 | }; 317 | 318 | // Invokes the delete uploaded documents event. 319 | // This is captured by the chat component to delete all uploaded documents. 320 | private void DeleteUploadedDocs(string sessionId) 321 | { 322 | OnDeleteUploadedDocs.InvokeAsync(sessionId); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Pages/UserProfile.razor: -------------------------------------------------------------------------------- 1 | @page "/userProfile" 2 | @rendermode InteractiveServer 3 | @using BlazorAIChat.Components.Shared 4 | @using BlazorAIChat.Models 5 | @using BlazorAIChat.Utils 6 | @using Microsoft.AspNetCore.Authorization 7 | @using Microsoft.AspNetCore.Components.Authorization 8 | @using Microsoft.EntityFrameworkCore 9 | @attribute [Authorize] 10 | @inject NavigationManager NavigationManager 11 | @inject AuthenticationStateProvider AuthenticationStateProvider 12 | @inject AIChatDBContext dbContext 13 | 14 | @if (currentUser!=null) 15 | { 16 |
17 |
18 |
19 |

User Profile

20 |
21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 | 49 | 50 | } 51 | 52 | @code { 53 | 54 | 55 | User? currentUser =null; 56 | private string alertMessage { get; set; } = string.Empty; 57 | private string alertType { get; set; } = string.Empty; 58 | 59 | protected override async Task OnInitializedAsync() 60 | { 61 | await LoadCurrentUser(); 62 | 63 | } 64 | 65 | private async Task LoadCurrentUser() 66 | { 67 | try 68 | { 69 | var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); 70 | var userPrincipal = authState.User; 71 | if (userPrincipal.Identity?.IsAuthenticated == true) 72 | { 73 | currentUser = UserUtils.ConvertPrincipalToUser(userPrincipal); 74 | } 75 | 76 | if (currentUser == null) 77 | { 78 | NavigationManager.NavigateTo("/"); 79 | } 80 | } 81 | catch (Exception ex) 82 | { 83 | Console.WriteLine($"Initialization error: {ex.Message}"); 84 | } 85 | } 86 | 87 | private async Task SaveChanges() 88 | { 89 | if (currentUser != null && !string.IsNullOrEmpty(currentUser.Id)) 90 | { 91 | var trackedEntity = dbContext.ChangeTracker.Entries().FirstOrDefault(e => e.Entity.Id == currentUser.Id); 92 | if (trackedEntity == null) 93 | { 94 | dbContext.Attach(currentUser); 95 | dbContext.Entry(currentUser).State = EntityState.Modified; 96 | } 97 | else 98 | { 99 | dbContext.Entry(trackedEntity.Entity).CurrentValues.SetValues(currentUser); 100 | } 101 | 102 | await dbContext.SaveChangesAsync(); 103 | ShowAlert("User profile updated successfully", AlertTypeEnum.success); 104 | StateHasChanged(); 105 | } 106 | } 107 | 108 | private void Back() 109 | { 110 | NavigationManager.NavigateTo("/",true); 111 | } 112 | 113 | private void ShowAlert(string message, AlertTypeEnum alertType = AlertTypeEnum.info) 114 | { 115 | 116 | alertMessage += message + " "; 117 | this.alertType = "alert-" + (Enum.GetName(typeof(AlertTypeEnum), alertType)?.ToLower() ?? "warning"); 118 | StateHasChanged(); 119 | } 120 | 121 | private void CloseAlert() 122 | { 123 | alertMessage = string.Empty; 124 | alertType = string.Empty; 125 | StateHasChanged(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/Alert.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | [Parameter] 3 | public string AlertType { get; set; } = string.Empty; 4 | 5 | [Parameter] 6 | public string AlertMessage { get; set; } = string.Empty; 7 | 8 | [Parameter] 9 | public EventCallback OnClose { get; set; } 10 | } 11 | 12 | @if (!string.IsNullOrEmpty(AlertType) && !string.IsNullOrEmpty(AlertMessage)) 13 | { 14 |
15 | 19 |
20 | } 21 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/Alert.razor.css: -------------------------------------------------------------------------------- 1 | .alert-top-full-width { 2 | width: 100%; 3 | position: absolute !important; 4 | top: 0; 5 | left: 0; 6 | margin-bottom: 0; 7 | z-index: 1000; /* Ensure it overlays other content */ 8 | height: auto; /* Adjust height as needed */ 9 | line-height: normal; /* Adjust line height as needed */ 10 | } 11 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/ChatCitation.razor: -------------------------------------------------------------------------------- 1 | @using System.Web 2 | 3 | @if (!string.IsNullOrEmpty(viewerUrl)) 4 | { 5 | 6 | 7 |
8 |
@File
9 |
@Quote
10 |
11 |
12 | } 13 | else 14 | { 15 |
16 | 17 |
18 |
@File
19 |
@Quote
20 |
21 |
22 | } 23 | 24 | @code { 25 | [Parameter] 26 | public required string File { get; set; } 27 | 28 | [Parameter] 29 | public int? PageNumber { get; set; } 30 | 31 | [Parameter] 32 | public required string Quote { get; set; } 33 | 34 | private string? viewerUrl; 35 | 36 | protected override void OnParametersSet() 37 | { 38 | viewerUrl = null; 39 | 40 | // If you ingest other types of content besides PDF files, construct a URL to an appropriate viewer here 41 | if (File.EndsWith(".pdf")) 42 | { 43 | var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\''); 44 | viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#page={PageNumber}&search={HttpUtility.UrlEncode(search)}&phrase=true"; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/ChatCitation.razor.css: -------------------------------------------------------------------------------- 1 | .citation { 2 | display: inline-flex; 3 | padding-top: 0.5rem; 4 | padding-bottom: 0.5rem; 5 | padding-left: 0.75rem; 6 | padding-right: 0.75rem; 7 | margin-top: 1rem; 8 | margin-right: 1rem; 9 | border-bottom: 2px solid #404040; 10 | gap: 0.5rem; 11 | border-radius: 0.25rem; 12 | font-size: 0.875rem; 13 | line-height: 1.25rem; 14 | background-color: #ffffff; 15 | } 16 | 17 | .citation[href]:hover { 18 | outline: 1px solid #808080; 19 | } 20 | .citation:hover { 21 | outline: 1px solid #808080; 22 | } 23 | 24 | .citation svg { 25 | width: 1.5rem; 26 | height: 1.5rem; 27 | } 28 | 29 | .citation:active { 30 | background-color: rgba(0,0,0,0.05); 31 | } 32 | 33 | .citation-content { 34 | display: flex; 35 | flex-direction: column; 36 | } 37 | 38 | .citation-file { 39 | font-weight: 600; 40 | } 41 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/ChatInput.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Forms 2 | @using Microsoft.AspNetCore.Components.Web 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | @code { 13 | 14 | [Parameter] 15 | public string UserMessage { get; set; } = string.Empty; 16 | 17 | [Parameter] 18 | public bool IsResponding { get; set; } 19 | 20 | [Parameter] 21 | public EventCallback OnStop { get; set; } 22 | 23 | [Parameter] 24 | public EventCallback OnFileSelected { get; set; } 25 | 26 | [Parameter] 27 | public EventCallback UserMessageChanged { get; set; } 28 | 29 | [Parameter] 30 | public bool IsFileInputDisabled { get; set; } 31 | 32 | [Parameter] 33 | public InputFile? InputFileRef { get; set; } 34 | 35 | private async Task HandleKeyDown(KeyboardEventArgs e) 36 | { 37 | if (e.Key == "Enter") 38 | { 39 | await SendMessage(); 40 | } 41 | } 42 | 43 | private async Task SendMessage() 44 | { 45 | if (!string.IsNullOrWhiteSpace(UserMessage)) 46 | { 47 | await UserMessageChanged.InvokeAsync(UserMessage); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/ChatInput.razor.css: -------------------------------------------------------------------------------- 1 | .file-selector { 2 | background-color: #D3D3D3; 3 | color: #000000; 4 | padding: 0.375rem 0.75rem; 5 | border: 1px solid transparent; 6 | border-radius: 0.25rem; 7 | cursor: pointer; 8 | display: inline-block; 9 | text-align: center; 10 | vertical-align: middle; 11 | user-select: none; 12 | transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; 13 | } 14 | 15 | .file-selector:hover { 16 | background-color: #BFBFBF; 17 | border-color: #004085; 18 | color: white; 19 | } 20 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/ChatMessages.Razor.css: -------------------------------------------------------------------------------- 1 | .chat-bubble { 2 | background-color: #E6F8F1; 3 | padding: 16px 28px; 4 | -webkit-border-radius: 20px; 5 | -webkit-border-bottom-left-radius: 2px; 6 | -moz-border-radius: 20px; 7 | -moz-border-radius-bottomleft: 2px; 8 | border-radius: 20px; 9 | border-bottom-left-radius: 2px; 10 | display: inline-block; 11 | } 12 | 13 | .typing { 14 | align-items: center; 15 | display: flex; 16 | height: 17px; 17 | } 18 | 19 | .typing .dot { 20 | animation: mercuryTypingAnimation 1.8s infinite ease-in-out; 21 | background-color: #6CAD96; 22 | border-radius: 50%; 23 | height: 7px; 24 | margin-right: 4px; 25 | vertical-align: middle; 26 | width: 7px; 27 | display: inline-block; 28 | } 29 | 30 | .typing .dot:nth-child(1) { 31 | animation-delay: 200ms; 32 | } 33 | 34 | .typing .dot:nth-child(2) { 35 | animation-delay: 300ms; 36 | } 37 | 38 | .typing .dot:nth-child(3) { 39 | animation-delay: 400ms; 40 | } 41 | 42 | .typing .dot:last-child { 43 | margin-right: 0; 44 | } 45 | 46 | @keyframes mercuryTypingAnimation { 47 | 0% { 48 | transform: translateY(0px); 49 | background-color: #6CAD96; 50 | } 51 | 52 | 28% { 53 | transform: translateY(-7px); 54 | background-color: #9ECAB9; 55 | } 56 | 57 | 44% { 58 | transform: translateY(0px); 59 | background-color: #B5D9CB; 60 | } 61 | } 62 | 63 | .chat-user-message { 64 | background-color: #A0D8EF; 65 | } 66 | 67 | .chat-assistant-message { 68 | background-color: #D3D3D3 69 | } 70 | 71 | .chat-timestamp { 72 | margin-left: auto; 73 | font-size: 0.95em; 74 | color: #6c757d; 75 | font-weight: 400; 76 | padding-left: 12px; 77 | align-self: center; 78 | /* visually right-align in flex header */ 79 | } 80 | 81 | .toast-header { 82 | display: flex; 83 | align-items: center; 84 | justify-content: space-between; 85 | } -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/ChatMessages.razor: -------------------------------------------------------------------------------- 1 | @using BlazorAIChat.Models 2 | @using BlazorAIChat.Utils 3 | 4 |
5 | @foreach (var msg in Messages) 6 | { 7 |
8 |
9 | 10 | User 11 | @msg.TimeStamp.ToLocalTime().ToString("MM/dd/yyyy HH:mm:ss") 12 |
13 |
14 | @if (msg.Prompt.StartsWith("data:image")) 15 | { 16 | 17 | } 18 | else if (msg.Prompt.StartsWith("data:doc")) 19 | { 20 | var fileName = msg.Prompt.Substring(9); 21 | @fileName 22 | } 23 | else 24 | @(new MarkupString(msg.Prompt)) 25 |
26 |
27 |
28 |
29 | 30 | Assistant 31 | @msg.CompletionTimeStamp?.ToLocalTime().ToString("MM/dd/yyyy HH:mm:ss") 32 |
33 |
34 | @if(string.IsNullOrEmpty(msg.Completion.Trim())) 35 | { 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | } 44 | else 45 | { 46 | 47 | 48 | 49 | 50 | @if (msg.Citations.Count > 0) 51 | { 52 |
53 | 54 | @foreach (var citation in msg.Citations) 55 | { 56 | 59 | } 60 | } 61 | } 62 |
63 |
64 | } 65 |
66 | 67 | @code { 68 | [Parameter] 69 | public List Messages { get; set; } = new List(); 70 | } 71 | -------------------------------------------------------------------------------- /BlazorAIChat/Components/Shared/CustomModal.razor: -------------------------------------------------------------------------------- 1 | @if (IsVisible) 2 | { 3 | 4 | 16 | } 17 | 18 | @code { 19 | [Parameter] public bool IsVisible { get; set; } 20 | [Parameter] public required string Title { get; set; } 21 | [Parameter] public required string Message { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /BlazorAIChat/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.JSInterop 9 | @using BlazorAIChat 10 | @using BlazorAIChat.Components 11 | -------------------------------------------------------------------------------- /BlazorAIChat/DBContext.cs: -------------------------------------------------------------------------------- 1 | using BlazorAIChat.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Options; 4 | using System.Configuration; 5 | 6 | namespace BlazorAIChat 7 | { 8 | public class AIChatDBContext : DbContext 9 | { 10 | 11 | protected readonly AppSettings _appSettings; 12 | 13 | public AIChatDBContext(IOptions appSettings) 14 | { 15 | _appSettings = appSettings.Value; 16 | } 17 | 18 | protected override void OnConfiguring(DbContextOptionsBuilder options) 19 | { 20 | // connect to SQLite database 21 | options.UseSqlite(_appSettings.ConnectionStrings.ConfigDatabase); 22 | } 23 | 24 | public DbSet Users { get; set; } 25 | public DbSet Config { get; set; } 26 | 27 | public DbSet Sessions { get; set; } 28 | 29 | public DbSet SessionDocuments { get; set; } 30 | public DbSet Messages { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240808174120_initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorAIChat; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorAIChat.Migrations 12 | { 13 | [DbContext(typeof(AIChatDBContext))] 14 | [Migration("20240808174120_initial")] 15 | partial class InitialMigration 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); 22 | 23 | modelBuilder.Entity("BlazorAIChat.Models.Config", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("ExpirationDays") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("RequireAccountApprovals") 33 | .HasColumnType("INTEGER"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("Config"); 38 | }); 39 | 40 | modelBuilder.Entity("BlazorAIChat.Models.User", b => 41 | { 42 | b.Property("Id") 43 | .HasColumnType("TEXT"); 44 | 45 | b.Property("ApprovedBy") 46 | .HasColumnType("TEXT"); 47 | 48 | b.Property("DateApproved") 49 | .HasColumnType("TEXT"); 50 | 51 | b.Property("DateRequested") 52 | .HasColumnType("TEXT"); 53 | 54 | b.Property("Email") 55 | .IsRequired() 56 | .HasColumnType("TEXT"); 57 | 58 | b.Property("Name") 59 | .IsRequired() 60 | .HasColumnType("TEXT"); 61 | 62 | b.Property("Role") 63 | .HasColumnType("INTEGER"); 64 | 65 | b.HasKey("Id"); 66 | 67 | b.ToTable("Users"); 68 | }); 69 | #pragma warning restore 612, 618 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240808174120_initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace BlazorAIChat.Migrations 7 | { 8 | /// 9 | public partial class InitialMigration : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Config", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "TEXT", nullable: false), 19 | RequireAccountApprovals = table.Column(type: "INTEGER", nullable: false), 20 | ExpirationDays = table.Column(type: "INTEGER", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Config", x => x.Id); 25 | }); 26 | 27 | migrationBuilder.CreateTable( 28 | name: "Users", 29 | columns: table => new 30 | { 31 | Id = table.Column(type: "TEXT", nullable: false), 32 | Name = table.Column(type: "TEXT", nullable: false), 33 | Email = table.Column(type: "TEXT", nullable: false), 34 | Role = table.Column(type: "INTEGER", nullable: false), 35 | DateRequested = table.Column(type: "TEXT", nullable: false), 36 | DateApproved = table.Column(type: "TEXT", nullable: true), 37 | ApprovedBy = table.Column(type: "TEXT", nullable: true) 38 | }, 39 | constraints: table => 40 | { 41 | table.PrimaryKey("PK_Users", x => x.Id); 42 | }); 43 | } 44 | 45 | /// 46 | protected override void Down(MigrationBuilder migrationBuilder) 47 | { 48 | migrationBuilder.DropTable( 49 | name: "Config"); 50 | 51 | migrationBuilder.DropTable( 52 | name: "Users"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240813233512_updated config.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorAIChat; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorAIChat.Migrations 12 | { 13 | [DbContext(typeof(AIChatDBContext))] 14 | [Migration("20240813233512_updated config")] 15 | partial class UpdatedConfig 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); 22 | 23 | modelBuilder.Entity("BlazorAIChat.Models.Config", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("AutomaticAccountApproval") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("ExpirationDays") 33 | .HasColumnType("INTEGER"); 34 | 35 | b.Property("RequireAccountApprovals") 36 | .HasColumnType("INTEGER"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Config"); 41 | }); 42 | 43 | modelBuilder.Entity("BlazorAIChat.Models.User", b => 44 | { 45 | b.Property("Id") 46 | .HasColumnType("TEXT"); 47 | 48 | b.Property("ApprovedBy") 49 | .HasColumnType("TEXT"); 50 | 51 | b.Property("DateApproved") 52 | .HasColumnType("TEXT"); 53 | 54 | b.Property("DateRequested") 55 | .HasColumnType("TEXT"); 56 | 57 | b.Property("Email") 58 | .IsRequired() 59 | .HasColumnType("TEXT"); 60 | 61 | b.Property("Name") 62 | .IsRequired() 63 | .HasColumnType("TEXT"); 64 | 65 | b.Property("Role") 66 | .HasColumnType("INTEGER"); 67 | 68 | b.HasKey("Id"); 69 | 70 | b.ToTable("Users"); 71 | }); 72 | #pragma warning restore 612, 618 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240813233512_updated config.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorAIChat.Migrations 6 | { 7 | /// 8 | public partial class UpdatedConfig : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "AutomaticAccountApproval", 15 | table: "Config", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: false); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "AutomaticAccountApproval", 26 | table: "Config"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240815143950_Added Chat History.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorAIChat; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorAIChat.Migrations 12 | { 13 | [DbContext(typeof(AIChatDBContext))] 14 | [Migration("20240815143950_Added Chat History")] 15 | partial class AddedChatHistory 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); 22 | 23 | modelBuilder.Entity("BlazorAIChat.Models.Config", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("AutomaticAccountApproval") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("ExpirationDays") 33 | .HasColumnType("INTEGER"); 34 | 35 | b.Property("RequireAccountApprovals") 36 | .HasColumnType("INTEGER"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Config"); 41 | }); 42 | 43 | modelBuilder.Entity("BlazorAIChat.Models.Message", b => 44 | { 45 | b.Property("Id") 46 | .HasColumnType("TEXT"); 47 | 48 | b.Property("Completion") 49 | .IsRequired() 50 | .HasColumnType("TEXT"); 51 | 52 | b.Property("Prompt") 53 | .IsRequired() 54 | .HasColumnType("TEXT"); 55 | 56 | b.Property("SessionId") 57 | .IsRequired() 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("TimeStamp") 61 | .HasColumnType("TEXT"); 62 | 63 | b.Property("Type") 64 | .IsRequired() 65 | .HasColumnType("TEXT"); 66 | 67 | b.HasKey("Id"); 68 | 69 | b.ToTable("Messages"); 70 | }); 71 | 72 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 73 | { 74 | b.Property("Id") 75 | .HasColumnType("TEXT"); 76 | 77 | b.Property("Name") 78 | .IsRequired() 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("SessionCreatedAt") 82 | .HasColumnType("TEXT"); 83 | 84 | b.Property("SessionId") 85 | .IsRequired() 86 | .HasColumnType("TEXT"); 87 | 88 | b.Property("Type") 89 | .IsRequired() 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("UserId") 93 | .IsRequired() 94 | .HasColumnType("TEXT"); 95 | 96 | b.HasKey("Id"); 97 | 98 | b.ToTable("Sessions"); 99 | }); 100 | 101 | modelBuilder.Entity("BlazorAIChat.Models.User", b => 102 | { 103 | b.Property("Id") 104 | .HasColumnType("TEXT"); 105 | 106 | b.Property("ApprovedBy") 107 | .HasColumnType("TEXT"); 108 | 109 | b.Property("DateApproved") 110 | .HasColumnType("TEXT"); 111 | 112 | b.Property("DateRequested") 113 | .HasColumnType("TEXT"); 114 | 115 | b.Property("Email") 116 | .IsRequired() 117 | .HasColumnType("TEXT"); 118 | 119 | b.Property("Name") 120 | .IsRequired() 121 | .HasColumnType("TEXT"); 122 | 123 | b.Property("Role") 124 | .HasColumnType("INTEGER"); 125 | 126 | b.HasKey("Id"); 127 | 128 | b.ToTable("Users"); 129 | }); 130 | #pragma warning restore 612, 618 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240815143950_Added Chat History.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace BlazorAIChat.Migrations 7 | { 8 | /// 9 | public partial class AddedChatHistory : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Messages", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "TEXT", nullable: false), 19 | Type = table.Column(type: "TEXT", nullable: false), 20 | SessionId = table.Column(type: "TEXT", nullable: false), 21 | TimeStamp = table.Column(type: "TEXT", nullable: false), 22 | Prompt = table.Column(type: "TEXT", nullable: false), 23 | Completion = table.Column(type: "TEXT", nullable: false) 24 | }, 25 | constraints: table => 26 | { 27 | table.PrimaryKey("PK_Messages", x => x.Id); 28 | }); 29 | 30 | migrationBuilder.CreateTable( 31 | name: "Sessions", 32 | columns: table => new 33 | { 34 | Id = table.Column(type: "TEXT", nullable: false), 35 | Type = table.Column(type: "TEXT", nullable: false), 36 | SessionId = table.Column(type: "TEXT", nullable: false), 37 | UserId = table.Column(type: "TEXT", nullable: false), 38 | Name = table.Column(type: "TEXT", nullable: false), 39 | SessionCreatedAt = table.Column(type: "TEXT", nullable: false) 40 | }, 41 | constraints: table => 42 | { 43 | table.PrimaryKey("PK_Sessions", x => x.Id); 44 | }); 45 | } 46 | 47 | /// 48 | protected override void Down(MigrationBuilder migrationBuilder) 49 | { 50 | migrationBuilder.DropTable( 51 | name: "Messages"); 52 | 53 | migrationBuilder.DropTable( 54 | name: "Sessions"); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240819192044_Added Citation.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorAIChat; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorAIChat.Migrations 12 | { 13 | [DbContext(typeof(AIChatDBContext))] 14 | [Migration("20240819192044_Added Citation")] 15 | partial class AddedCitation 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); 22 | 23 | modelBuilder.Entity("BlazorAIChat.Models.Config", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("AutomaticAccountApproval") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("ExpirationDays") 33 | .HasColumnType("INTEGER"); 34 | 35 | b.Property("RequireAccountApprovals") 36 | .HasColumnType("INTEGER"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Config"); 41 | }); 42 | 43 | modelBuilder.Entity("BlazorAIChat.Models.Message", b => 44 | { 45 | b.Property("Id") 46 | .HasColumnType("TEXT"); 47 | 48 | b.Property("Citations") 49 | .IsRequired() 50 | .HasColumnType("TEXT"); 51 | 52 | b.Property("Completion") 53 | .IsRequired() 54 | .HasColumnType("TEXT"); 55 | 56 | b.Property("Prompt") 57 | .IsRequired() 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("SessionId") 61 | .IsRequired() 62 | .HasColumnType("TEXT"); 63 | 64 | b.Property("TimeStamp") 65 | .HasColumnType("TEXT"); 66 | 67 | b.Property("Type") 68 | .IsRequired() 69 | .HasColumnType("TEXT"); 70 | 71 | b.HasKey("Id"); 72 | 73 | b.ToTable("Messages"); 74 | }); 75 | 76 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 77 | { 78 | b.Property("Id") 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("Name") 82 | .IsRequired() 83 | .HasColumnType("TEXT"); 84 | 85 | b.Property("SessionCreatedAt") 86 | .HasColumnType("TEXT"); 87 | 88 | b.Property("SessionId") 89 | .IsRequired() 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("Type") 93 | .IsRequired() 94 | .HasColumnType("TEXT"); 95 | 96 | b.Property("UserId") 97 | .IsRequired() 98 | .HasColumnType("TEXT"); 99 | 100 | b.HasKey("Id"); 101 | 102 | b.ToTable("Sessions"); 103 | }); 104 | 105 | modelBuilder.Entity("BlazorAIChat.Models.User", b => 106 | { 107 | b.Property("Id") 108 | .HasColumnType("TEXT"); 109 | 110 | b.Property("ApprovedBy") 111 | .HasColumnType("TEXT"); 112 | 113 | b.Property("DateApproved") 114 | .HasColumnType("TEXT"); 115 | 116 | b.Property("DateRequested") 117 | .HasColumnType("TEXT"); 118 | 119 | b.Property("Email") 120 | .IsRequired() 121 | .HasColumnType("TEXT"); 122 | 123 | b.Property("Name") 124 | .IsRequired() 125 | .HasColumnType("TEXT"); 126 | 127 | b.Property("Role") 128 | .HasColumnType("INTEGER"); 129 | 130 | b.HasKey("Id"); 131 | 132 | b.ToTable("Users"); 133 | }); 134 | #pragma warning restore 612, 618 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240819192044_Added Citation.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorAIChat.Migrations 6 | { 7 | /// 8 | public partial class AddedCitation : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "Citations", 15 | table: "Messages", 16 | type: "TEXT", 17 | nullable: false, 18 | defaultValue: "[]"); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "Citations", 26 | table: "Messages"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240820173447_Add Session Document Tracking.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorAIChat; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorAIChat.Migrations 12 | { 13 | [DbContext(typeof(AIChatDBContext))] 14 | [Migration("20240820173447_Add Session Document Tracking")] 15 | partial class AddSessionDocumentTracking 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); 22 | 23 | modelBuilder.Entity("BlazorAIChat.Models.Config", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("AutomaticAccountApproval") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("ExpirationDays") 33 | .HasColumnType("INTEGER"); 34 | 35 | b.Property("RequireAccountApprovals") 36 | .HasColumnType("INTEGER"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Config"); 41 | }); 42 | 43 | modelBuilder.Entity("BlazorAIChat.Models.Message", b => 44 | { 45 | b.Property("Id") 46 | .HasColumnType("TEXT"); 47 | 48 | b.Property("Citations") 49 | .IsRequired() 50 | .HasColumnType("TEXT"); 51 | 52 | b.Property("Completion") 53 | .IsRequired() 54 | .HasColumnType("TEXT"); 55 | 56 | b.Property("Prompt") 57 | .IsRequired() 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("SessionId") 61 | .IsRequired() 62 | .HasColumnType("TEXT"); 63 | 64 | b.Property("TimeStamp") 65 | .HasColumnType("TEXT"); 66 | 67 | b.Property("Type") 68 | .IsRequired() 69 | .HasColumnType("TEXT"); 70 | 71 | b.HasKey("Id"); 72 | 73 | b.ToTable("Messages"); 74 | }); 75 | 76 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 77 | { 78 | b.Property("Id") 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("Name") 82 | .IsRequired() 83 | .HasColumnType("TEXT"); 84 | 85 | b.Property("SessionCreatedAt") 86 | .HasColumnType("TEXT"); 87 | 88 | b.Property("SessionId") 89 | .IsRequired() 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("Type") 93 | .IsRequired() 94 | .HasColumnType("TEXT"); 95 | 96 | b.Property("UserId") 97 | .IsRequired() 98 | .HasColumnType("TEXT"); 99 | 100 | b.HasKey("Id"); 101 | 102 | b.ToTable("Sessions"); 103 | }); 104 | 105 | modelBuilder.Entity("BlazorAIChat.Models.SessionDocument", b => 106 | { 107 | b.Property("Id") 108 | .ValueGeneratedOnAdd() 109 | .HasColumnType("TEXT"); 110 | 111 | b.Property("DocId") 112 | .IsRequired() 113 | .HasColumnType("TEXT"); 114 | 115 | b.Property("FileNameOrUrl") 116 | .IsRequired() 117 | .HasColumnType("TEXT"); 118 | 119 | b.Property("SessionId") 120 | .IsRequired() 121 | .HasColumnType("TEXT"); 122 | 123 | b.HasKey("Id"); 124 | 125 | b.HasIndex("SessionId"); 126 | 127 | b.ToTable("SessionDocuments"); 128 | }); 129 | 130 | modelBuilder.Entity("BlazorAIChat.Models.User", b => 131 | { 132 | b.Property("Id") 133 | .HasColumnType("TEXT"); 134 | 135 | b.Property("ApprovedBy") 136 | .HasColumnType("TEXT"); 137 | 138 | b.Property("DateApproved") 139 | .HasColumnType("TEXT"); 140 | 141 | b.Property("DateRequested") 142 | .HasColumnType("TEXT"); 143 | 144 | b.Property("Email") 145 | .IsRequired() 146 | .HasColumnType("TEXT"); 147 | 148 | b.Property("Name") 149 | .IsRequired() 150 | .HasColumnType("TEXT"); 151 | 152 | b.Property("Role") 153 | .HasColumnType("INTEGER"); 154 | 155 | b.HasKey("Id"); 156 | 157 | b.ToTable("Users"); 158 | }); 159 | 160 | modelBuilder.Entity("BlazorAIChat.Models.SessionDocument", b => 161 | { 162 | b.HasOne("BlazorAIChat.Models.Session", null) 163 | .WithMany("Documents") 164 | .HasForeignKey("SessionId") 165 | .OnDelete(DeleteBehavior.Cascade) 166 | .IsRequired(); 167 | }); 168 | 169 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 170 | { 171 | b.Navigation("Documents"); 172 | }); 173 | #pragma warning restore 612, 618 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20240820173447_Add Session Document Tracking.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace BlazorAIChat.Migrations 7 | { 8 | /// 9 | public partial class AddSessionDocumentTracking : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "SessionDocuments", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "TEXT", nullable: false), 19 | SessionId = table.Column(type: "TEXT", nullable: false), 20 | DocId = table.Column(type: "TEXT", nullable: false), 21 | FileNameOrUrl = table.Column(type: "TEXT", nullable: false) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_SessionDocuments", x => x.Id); 26 | table.ForeignKey( 27 | name: "FK_SessionDocuments_Sessions_SessionId", 28 | column: x => x.SessionId, 29 | principalTable: "Sessions", 30 | principalColumn: "Id", 31 | onDelete: ReferentialAction.Cascade); 32 | }); 33 | 34 | migrationBuilder.CreateIndex( 35 | name: "IX_SessionDocuments_SessionId", 36 | table: "SessionDocuments", 37 | column: "SessionId"); 38 | } 39 | 40 | /// 41 | protected override void Down(MigrationBuilder migrationBuilder) 42 | { 43 | migrationBuilder.DropTable( 44 | name: "SessionDocuments"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20250529151134_Added Competion Timestamp.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorAIChat; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorAIChat.Migrations 12 | { 13 | [DbContext(typeof(AIChatDBContext))] 14 | [Migration("20250529151134_Added Competion Timestamp")] 15 | partial class AddedCompletionTimestamp 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "9.0.5"); 22 | 23 | modelBuilder.Entity("BlazorAIChat.Models.Config", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("AutomaticAccountApproval") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("ExpirationDays") 33 | .HasColumnType("INTEGER"); 34 | 35 | b.Property("RequireAccountApprovals") 36 | .HasColumnType("INTEGER"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Config"); 41 | }); 42 | 43 | modelBuilder.Entity("BlazorAIChat.Models.Message", b => 44 | { 45 | b.Property("Id") 46 | .HasColumnType("TEXT"); 47 | 48 | b.PrimitiveCollection("Citations") 49 | .IsRequired() 50 | .HasColumnType("TEXT"); 51 | 52 | b.Property("Completion") 53 | .IsRequired() 54 | .HasColumnType("TEXT"); 55 | 56 | b.Property("CompletionTimeStamp") 57 | .HasColumnType("TEXT"); 58 | 59 | b.Property("Prompt") 60 | .IsRequired() 61 | .HasColumnType("TEXT"); 62 | 63 | b.Property("SessionId") 64 | .IsRequired() 65 | .HasColumnType("TEXT"); 66 | 67 | b.Property("TimeStamp") 68 | .HasColumnType("TEXT"); 69 | 70 | b.Property("Type") 71 | .IsRequired() 72 | .HasColumnType("TEXT"); 73 | 74 | b.HasKey("Id"); 75 | 76 | b.ToTable("Messages"); 77 | }); 78 | 79 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 80 | { 81 | b.Property("Id") 82 | .HasColumnType("TEXT"); 83 | 84 | b.Property("Name") 85 | .IsRequired() 86 | .HasColumnType("TEXT"); 87 | 88 | b.Property("SessionCreatedAt") 89 | .HasColumnType("TEXT"); 90 | 91 | b.Property("SessionId") 92 | .IsRequired() 93 | .HasColumnType("TEXT"); 94 | 95 | b.Property("Type") 96 | .IsRequired() 97 | .HasColumnType("TEXT"); 98 | 99 | b.Property("UserId") 100 | .IsRequired() 101 | .HasColumnType("TEXT"); 102 | 103 | b.HasKey("Id"); 104 | 105 | b.ToTable("Sessions"); 106 | }); 107 | 108 | modelBuilder.Entity("BlazorAIChat.Models.SessionDocument", b => 109 | { 110 | b.Property("Id") 111 | .ValueGeneratedOnAdd() 112 | .HasColumnType("TEXT"); 113 | 114 | b.Property("DocId") 115 | .IsRequired() 116 | .HasColumnType("TEXT"); 117 | 118 | b.Property("FileNameOrUrl") 119 | .IsRequired() 120 | .HasColumnType("TEXT"); 121 | 122 | b.Property("SessionId") 123 | .IsRequired() 124 | .HasColumnType("TEXT"); 125 | 126 | b.HasKey("Id"); 127 | 128 | b.HasIndex("SessionId"); 129 | 130 | b.ToTable("SessionDocuments"); 131 | }); 132 | 133 | modelBuilder.Entity("BlazorAIChat.Models.User", b => 134 | { 135 | b.Property("Id") 136 | .HasColumnType("TEXT"); 137 | 138 | b.Property("ApprovedBy") 139 | .HasColumnType("TEXT"); 140 | 141 | b.Property("DateApproved") 142 | .HasColumnType("TEXT"); 143 | 144 | b.Property("DateRequested") 145 | .HasColumnType("TEXT"); 146 | 147 | b.Property("Email") 148 | .IsRequired() 149 | .HasColumnType("TEXT"); 150 | 151 | b.Property("Name") 152 | .IsRequired() 153 | .HasColumnType("TEXT"); 154 | 155 | b.Property("Role") 156 | .HasColumnType("INTEGER"); 157 | 158 | b.HasKey("Id"); 159 | 160 | b.ToTable("Users"); 161 | }); 162 | 163 | modelBuilder.Entity("BlazorAIChat.Models.SessionDocument", b => 164 | { 165 | b.HasOne("BlazorAIChat.Models.Session", null) 166 | .WithMany("Documents") 167 | .HasForeignKey("SessionId") 168 | .OnDelete(DeleteBehavior.Cascade) 169 | .IsRequired(); 170 | }); 171 | 172 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 173 | { 174 | b.Navigation("Documents"); 175 | }); 176 | #pragma warning restore 612, 618 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/20250529151134_Added Competion Timestamp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace BlazorAIChat.Migrations 7 | { 8 | /// 9 | public partial class AddedCompletionTimestamp : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "CompletionTimeStamp", 16 | table: "Messages", 17 | type: "TEXT", 18 | nullable: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "CompletionTimeStamp", 26 | table: "Messages"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BlazorAIChat/Migrations/AIChatDBContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorAIChat; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace BlazorAIChat.Migrations 11 | { 12 | [DbContext(typeof(AIChatDBContext))] 13 | partial class AIChatDBContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "9.0.5"); 19 | 20 | modelBuilder.Entity("BlazorAIChat.Models.Config", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("AutomaticAccountApproval") 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("ExpirationDays") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("RequireAccountApprovals") 33 | .HasColumnType("INTEGER"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("Config"); 38 | }); 39 | 40 | modelBuilder.Entity("BlazorAIChat.Models.Message", b => 41 | { 42 | b.Property("Id") 43 | .HasColumnType("TEXT"); 44 | 45 | b.PrimitiveCollection("Citations") 46 | .IsRequired() 47 | .HasColumnType("TEXT"); 48 | 49 | b.Property("Completion") 50 | .IsRequired() 51 | .HasColumnType("TEXT"); 52 | 53 | b.Property("CompletionTimeStamp") 54 | .HasColumnType("TEXT"); 55 | 56 | b.Property("Prompt") 57 | .IsRequired() 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("SessionId") 61 | .IsRequired() 62 | .HasColumnType("TEXT"); 63 | 64 | b.Property("TimeStamp") 65 | .HasColumnType("TEXT"); 66 | 67 | b.Property("Type") 68 | .IsRequired() 69 | .HasColumnType("TEXT"); 70 | 71 | b.HasKey("Id"); 72 | 73 | b.ToTable("Messages"); 74 | }); 75 | 76 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 77 | { 78 | b.Property("Id") 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("Name") 82 | .IsRequired() 83 | .HasColumnType("TEXT"); 84 | 85 | b.Property("SessionCreatedAt") 86 | .HasColumnType("TEXT"); 87 | 88 | b.Property("SessionId") 89 | .IsRequired() 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("Type") 93 | .IsRequired() 94 | .HasColumnType("TEXT"); 95 | 96 | b.Property("UserId") 97 | .IsRequired() 98 | .HasColumnType("TEXT"); 99 | 100 | b.HasKey("Id"); 101 | 102 | b.ToTable("Sessions"); 103 | }); 104 | 105 | modelBuilder.Entity("BlazorAIChat.Models.SessionDocument", b => 106 | { 107 | b.Property("Id") 108 | .ValueGeneratedOnAdd() 109 | .HasColumnType("TEXT"); 110 | 111 | b.Property("DocId") 112 | .IsRequired() 113 | .HasColumnType("TEXT"); 114 | 115 | b.Property("FileNameOrUrl") 116 | .IsRequired() 117 | .HasColumnType("TEXT"); 118 | 119 | b.Property("SessionId") 120 | .IsRequired() 121 | .HasColumnType("TEXT"); 122 | 123 | b.HasKey("Id"); 124 | 125 | b.HasIndex("SessionId"); 126 | 127 | b.ToTable("SessionDocuments"); 128 | }); 129 | 130 | modelBuilder.Entity("BlazorAIChat.Models.User", b => 131 | { 132 | b.Property("Id") 133 | .HasColumnType("TEXT"); 134 | 135 | b.Property("ApprovedBy") 136 | .HasColumnType("TEXT"); 137 | 138 | b.Property("DateApproved") 139 | .HasColumnType("TEXT"); 140 | 141 | b.Property("DateRequested") 142 | .HasColumnType("TEXT"); 143 | 144 | b.Property("Email") 145 | .IsRequired() 146 | .HasColumnType("TEXT"); 147 | 148 | b.Property("Name") 149 | .IsRequired() 150 | .HasColumnType("TEXT"); 151 | 152 | b.Property("Role") 153 | .HasColumnType("INTEGER"); 154 | 155 | b.HasKey("Id"); 156 | 157 | b.ToTable("Users"); 158 | }); 159 | 160 | modelBuilder.Entity("BlazorAIChat.Models.SessionDocument", b => 161 | { 162 | b.HasOne("BlazorAIChat.Models.Session", null) 163 | .WithMany("Documents") 164 | .HasForeignKey("SessionId") 165 | .OnDelete(DeleteBehavior.Cascade) 166 | .IsRequired(); 167 | }); 168 | 169 | modelBuilder.Entity("BlazorAIChat.Models.Session", b => 170 | { 171 | b.Navigation("Documents"); 172 | }); 173 | #pragma warning restore 612, 618 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/Config.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAIChat.Models 2 | { 3 | public class Config 4 | { 5 | public required Guid Id { get; set; } 6 | public bool RequireAccountApprovals { get; set; } = true; 7 | 8 | public bool AutomaticAccountApproval { get; set; } = true; 9 | public int ExpirationDays { get; set; } = 60; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAIChat.Models 2 | { 3 | public static class Constants 4 | { 5 | public static readonly string EMPTY_SESSION= "empty-session"; 6 | public static readonly string NEW_CHAT = "New Chat"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/Message.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.KernelMemory; 2 | 3 | namespace BlazorAIChat.Models 4 | { 5 | public record Message 6 | { 7 | /// 8 | /// Unique identifier 9 | /// 10 | public string Id { get; set; } 11 | 12 | public string Type { get; set; } 13 | 14 | /// 15 | /// Partition key 16 | /// 17 | public string SessionId { get; set; } 18 | 19 | public DateTime TimeStamp { get; set; } 20 | 21 | public string Prompt { get; set; } 22 | 23 | public string Completion { get; set; } 24 | 25 | public DateTime? CompletionTimeStamp { get; set; } 26 | 27 | public List Citations { get; set; } 28 | 29 | public Message(string sessionId, string prompt, string completion = "") 30 | { 31 | Id = Guid.NewGuid().ToString(); 32 | Type = nameof(Message); 33 | SessionId = sessionId; 34 | TimeStamp = DateTime.UtcNow; 35 | CompletionTimeStamp = null; 36 | Prompt = prompt; 37 | Completion = completion; 38 | Citations = new List(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/Session.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace BlazorAIChat.Models 4 | { 5 | public record Session 6 | { 7 | /// 8 | /// Unique identifier 9 | /// 10 | public string Id { get; set; } 11 | 12 | public string Type { get; set; } 13 | 14 | /// 15 | /// Partition key 16 | /// 17 | public string SessionId { get; set; } 18 | 19 | public string UserId { get; set; } 20 | 21 | public string Name { get; set; } 22 | 23 | public DateTime SessionCreatedAt { get; set; } 24 | 25 | public List Documents { get; set; } 26 | 27 | public Session() 28 | { 29 | Id = Guid.NewGuid().ToString(); 30 | Type = nameof(Session); 31 | SessionId = this.Id; 32 | Name = Constants.NEW_CHAT; 33 | UserId = string.Empty; 34 | SessionCreatedAt = DateTime.UtcNow; 35 | Documents = new List(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/SessionDocuments.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAIChat.Models 2 | { 3 | public class SessionDocument 4 | { 5 | public Guid Id { get; set; } 6 | public string SessionId { get; set; } 7 | public string DocId { get; set; } 8 | public string FileNameOrUrl { get; set; } 9 | 10 | public SessionDocument() 11 | { 12 | Id = Guid.NewGuid(); 13 | SessionId = string.Empty; 14 | DocId = string.Empty; 15 | FileNameOrUrl = string.Empty; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAIChat.Models 2 | { 3 | 4 | public class AppSettings 5 | { 6 | public AzureOpenAIChatCompletionSettings AzureOpenAIChatCompletion { get; set; } = new AzureOpenAIChatCompletionSettings(); 7 | public AzureOpenAIEmbeddingSettings AzureOpenAIEmbedding { get; set; } = new AzureOpenAIEmbeddingSettings(); 8 | public bool RequireEasyAuth { get; set; } = true; 9 | public string SystemMessage { get; set; } = string.Empty; 10 | public ConnectionStringsSettings ConnectionStrings { get; set; } = new ConnectionStringsSettings(); 11 | public CosmosDbSettings CosmosDb { get; set; } = new CosmosDbSettings(); 12 | public DocumentIntelligenceSettings DocumentIntelligence { get; set; } = new DocumentIntelligenceSettings(); 13 | public AzureAISearchSettings AzureAISearch { get; set; } = new AzureAISearchSettings(); 14 | public List MCPServers { get; set; } = new List(); 15 | public bool AIDeterminesRagUsage { get; set; } = true; 16 | public bool UsesPostgreSQL => !string.IsNullOrEmpty(ConnectionStrings.PostgreSQL); 17 | public bool UsesCosmosDb => !string.IsNullOrEmpty(ConnectionStrings.CosmosDb); 18 | public bool UsesAzureAISearch => !string.IsNullOrEmpty(AzureAISearch.Endpoint) && !string.IsNullOrEmpty(AzureAISearch.ApiKey) && AzureAISearch.IndexPerChatSession; 19 | public bool UsesAzureDocIntelligence => !string.IsNullOrEmpty(DocumentIntelligence.Endpoint) && !string.IsNullOrEmpty(DocumentIntelligence.ApiKey); 20 | 21 | public bool UsesAzureAISearchSharedKnowledge => !string.IsNullOrEmpty(AzureAISearch.Endpoint) && 22 | !string.IsNullOrEmpty(AzureAISearch.ApiKey) && 23 | !string.IsNullOrEmpty(AzureAISearch.SharedIndex) && 24 | !string.IsNullOrEmpty(AzureAISearch.SharedIndexAzureBlobStorageConnection) && 25 | !string.IsNullOrEmpty(AzureAISearch.SharedIndexAzureBlobStorageContainer); 26 | } 27 | 28 | public class AzureOpenAIChatCompletionSettings 29 | { 30 | public string Endpoint { get; set; } = string.Empty; 31 | public string ApiKey { get; set; } = string.Empty; 32 | public string DeploymentName { get; set; } = string.Empty; 33 | public string Tokenizer { get; set; } = string.Empty; 34 | public int MaxInputTokens { get; set; } = 128000; 35 | public bool SupportsImages { get; set; } = false; 36 | public int ResponseChunkSize { get; set; } = 50; 37 | } 38 | 39 | public class AzureOpenAIEmbeddingSettings 40 | { 41 | public string DeploymentName { get; set; } = string.Empty; 42 | public string Tokenizer { get; set; } = string.Empty; 43 | public int MaxInputTokens { get; set; } = 8192; 44 | } 45 | 46 | public class ConnectionStringsSettings 47 | { 48 | public string PostgreSQL { get; set; } = string.Empty; 49 | public string CosmosDb { get; set; } = string.Empty; 50 | public string ConfigDatabase { get; set; } = string.Empty; 51 | } 52 | 53 | public class CosmosDbSettings 54 | { 55 | public string Database { get; set; } = string.Empty; 56 | public string Container { get; set; } = string.Empty; 57 | } 58 | 59 | public class DocumentIntelligenceSettings 60 | { 61 | public string Endpoint { get; set; } = string.Empty; 62 | public string ApiKey { get; set; } = string.Empty; 63 | } 64 | 65 | public class AzureAISearchSettings 66 | { 67 | public string Endpoint { get; set; } = string.Empty; 68 | public string ApiKey { get; set; } = string.Empty; 69 | public bool IndexPerChatSession { get; set; } = false; 70 | public string SharedIndex { get; set; } = string.Empty; 71 | public string SharedIndexAzureBlobStorageConnection { get; set; } = string.Empty; 72 | public string SharedIndexAzureBlobStorageContainer { get; set; } = string.Empty; 73 | } 74 | 75 | public class MCPServerConfig 76 | { 77 | public string Endpoint { get; set; } = string.Empty; 78 | public string Name { get; set; } = string.Empty; 79 | public string Version { get; set; } = string.Empty; 80 | public string Type { get; set; } = string.Empty; 81 | public List Args { get; set; } = new List(); 82 | public Dictionary Headers { get; set; } = new Dictionary(); 83 | public Dictionary Env { get; set; } = new Dictionary(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace BlazorAIChat.Models 4 | { 5 | public class User 6 | { 7 | 8 | [Key] 9 | public required string Id { get; set; } 10 | public string Name { get; set; } = string.Empty; 11 | public string Email { get; set; } = string.Empty; 12 | public UserRoles Role { get; set; } = UserRoles.Guest; 13 | public DateTime DateRequested { get; set; } = DateTime.Now; 14 | public DateTime? DateApproved { get; set; } 15 | public string? ApprovedBy { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BlazorAIChat/Models/UserRole.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAIChat.Models 2 | { 3 | public enum UserRoles 4 | { 5 | Guest = 0, 6 | User = 1, 7 | Admin = 2 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazorAIChat/Program.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable SKEXP0010, SKEXP0001, SKEXP0020, KMEXP00 2 | using BlazorAIChat; 3 | using BlazorAIChat.Authentication; 4 | using BlazorAIChat.Components; 5 | using BlazorAIChat.Models; 6 | using BlazorAIChat.Services; 7 | using BlazorAIChat.Utils; 8 | using Microsoft.AspNetCore.Authentication.Cookies; 9 | using Microsoft.AspNetCore.Http.Features; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Options; 12 | using Microsoft.SemanticKernel; 13 | using ModelContextProtocol.Client; 14 | 15 | var builder = WebApplication.CreateBuilder(args); 16 | 17 | // Configure logging to default to the console 18 | builder.Logging.ClearProviders(); 19 | builder.Logging.AddConsole(); 20 | builder.Logging.SetMinimumLevel(LogLevel.Information); 21 | 22 | builder.Services.Configure(builder.Configuration); 23 | 24 | // Register a default HttpClient for streaming/long-lived connections 25 | builder.Services.AddHttpClient("defaultHttpClient"); 26 | 27 | //Register an HttpClient that has a retry policy handler. Used for Azure OpenAI calls. 28 | builder.Services.AddHttpClient("retryHttpClient").AddPolicyHandler(RetryHelper.GetRetryPolicy()); 29 | 30 | builder.Services.AddDbContext(); 31 | builder.Services.AddScoped(); 32 | builder.Services.AddSingleton(); 33 | builder.Services.AddScoped(); 34 | builder.Services.AddSingleton(); 35 | 36 | builder.Services.AddSingleton(); 37 | 38 | // Register the Kernel using DI, injecting the plugin collection from the provider 39 | builder.Services.AddTransient(serviceProvider => 40 | { 41 | var appSettings = serviceProvider.GetRequiredService>().Value; 42 | var httpClientFactory = serviceProvider.GetRequiredService(); 43 | var httpClient = httpClientFactory.CreateClient("retryHttpClient"); 44 | var pluginProvider = serviceProvider.GetRequiredService(); 45 | 46 | var kernelBuilder = Kernel.CreateBuilder() 47 | .AddAzureOpenAIChatCompletion( 48 | appSettings.AzureOpenAIChatCompletion.DeploymentName, 49 | appSettings.AzureOpenAIChatCompletion.Endpoint, 50 | appSettings.AzureOpenAIChatCompletion.ApiKey, 51 | httpClient: httpClient) 52 | .AddAzureOpenAIEmbeddingGenerator( 53 | appSettings.AzureOpenAIEmbedding.DeploymentName, 54 | appSettings.AzureOpenAIChatCompletion.Endpoint, 55 | appSettings.AzureOpenAIChatCompletion.ApiKey, 56 | httpClient: httpClient); 57 | 58 | // Add each plugin individually using the correct method 59 | foreach (var plugin in pluginProvider.Plugins) 60 | { 61 | kernelBuilder.Plugins.Add(plugin); 62 | } 63 | kernelBuilder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Trace)); 64 | return kernelBuilder.Build(); 65 | }); 66 | 67 | // Add services to the container. 68 | builder.Services.AddRazorComponents() 69 | .AddInteractiveServerComponents() 70 | .AddCircuitOptions(options => options.DetailedErrors = true); 71 | 72 | builder.Services.AddCascadingAuthenticationState(); 73 | 74 | builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) 75 | .AddCookie(); 76 | 77 | builder.Services.AddAuthorizationCore(); 78 | 79 | builder.Services.Configure(options => 80 | { 81 | options.MultipartBodyLengthLimit = int.MaxValue; 82 | }); 83 | 84 | var app = builder.Build(); 85 | 86 | //setup EF database and migrate to latest version 87 | using (var scope = app.Services.CreateScope()) 88 | { 89 | var services = scope.ServiceProvider; 90 | var context = services.GetRequiredService(); 91 | context.Database.Migrate(); 92 | 93 | // Initialize the plugin provider before the app runs 94 | var appSettings = services.GetRequiredService>().Value; 95 | var httpClientFactory = services.GetRequiredService(); 96 | var logger = services.GetRequiredService>(); 97 | var pluginProvider = services.GetRequiredService(); 98 | await pluginProvider.InitializeAsync(appSettings, httpClientFactory, logger); 99 | } 100 | 101 | // Configure the HTTP request pipeline. 102 | if (!app.Environment.IsDevelopment()) 103 | { 104 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 105 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 106 | app.UseHsts(); 107 | } 108 | 109 | app.UseHttpsRedirection(); 110 | 111 | app.UseStaticFiles(); 112 | app.UseAntiforgery(); 113 | 114 | app.UseAuthentication(); 115 | 116 | //Add easy auth middleware 117 | app.UseMiddleware(); 118 | 119 | app.UseAuthorization(); 120 | 121 | app.MapRazorComponents() 122 | .AddInteractiveServerRenderMode(); 123 | 124 | //Before we start the app, ensure that the KNN folder exists on the filesystem 125 | if (!Directory.Exists("KNN")) 126 | { 127 | Directory.CreateDirectory("KNN"); 128 | } 129 | if (!Directory.Exists("SFS")) 130 | { 131 | Directory.CreateDirectory("SFS"); 132 | } 133 | 134 | app.Run(); 135 | -------------------------------------------------------------------------------- /BlazorAIChat/Properties/ServiceDependencies/blazoraichat-muux6rot5kooq - Web Deploy/profile.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_dependencyType": "compute.appService.windows" 6 | }, 7 | "parameters": { 8 | "resourceGroupName": { 9 | "type": "string", 10 | "defaultValue": "BlazorAI-Chat", 11 | "metadata": { 12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 13 | } 14 | }, 15 | "resourceGroupLocation": { 16 | "type": "string", 17 | "defaultValue": "eastus2", 18 | "metadata": { 19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." 20 | } 21 | }, 22 | "resourceName": { 23 | "type": "string", 24 | "defaultValue": "blazoraichat-muux6rot5kooq", 25 | "metadata": { 26 | "description": "Name of the main resource to be created by this template." 27 | } 28 | }, 29 | "resourceLocation": { 30 | "type": "string", 31 | "defaultValue": "[parameters('resourceGroupLocation')]", 32 | "metadata": { 33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 34 | } 35 | } 36 | }, 37 | "variables": { 38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" 40 | }, 41 | "resources": [ 42 | { 43 | "type": "Microsoft.Resources/resourceGroups", 44 | "name": "[parameters('resourceGroupName')]", 45 | "location": "[parameters('resourceGroupLocation')]", 46 | "apiVersion": "2019-10-01" 47 | }, 48 | { 49 | "type": "Microsoft.Resources/deployments", 50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 51 | "resourceGroup": "[parameters('resourceGroupName')]", 52 | "apiVersion": "2019-10-01", 53 | "dependsOn": [ 54 | "[parameters('resourceGroupName')]" 55 | ], 56 | "properties": { 57 | "mode": "Incremental", 58 | "template": { 59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 60 | "contentVersion": "1.0.0.0", 61 | "resources": [ 62 | { 63 | "location": "[parameters('resourceLocation')]", 64 | "name": "[parameters('resourceName')]", 65 | "type": "Microsoft.Web/sites", 66 | "apiVersion": "2015-08-01", 67 | "tags": { 68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" 69 | }, 70 | "dependsOn": [ 71 | "[variables('appServicePlan_ResourceId')]" 72 | ], 73 | "kind": "app", 74 | "properties": { 75 | "name": "[parameters('resourceName')]", 76 | "kind": "app", 77 | "httpsOnly": true, 78 | "reserved": false, 79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]", 80 | "siteConfig": { 81 | "metadata": [ 82 | { 83 | "name": "CURRENT_STACK", 84 | "value": "dotnetcore" 85 | } 86 | ] 87 | } 88 | }, 89 | "identity": { 90 | "type": "SystemAssigned" 91 | } 92 | }, 93 | { 94 | "location": "[parameters('resourceLocation')]", 95 | "name": "[variables('appServicePlan_name')]", 96 | "type": "Microsoft.Web/serverFarms", 97 | "apiVersion": "2015-08-01", 98 | "sku": { 99 | "name": "S1", 100 | "tier": "Standard", 101 | "family": "S", 102 | "size": "S1" 103 | }, 104 | "properties": { 105 | "name": "[variables('appServicePlan_name')]" 106 | } 107 | } 108 | ] 109 | } 110 | } 111 | } 112 | ] 113 | } -------------------------------------------------------------------------------- /BlazorAIChat/Properties/ServiceDependencies/blazoraichat-wepqpxfmxukjo - Web Deploy/profile.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_dependencyType": "compute.appService.windows" 6 | }, 7 | "parameters": { 8 | "resourceGroupName": { 9 | "type": "string", 10 | "defaultValue": "BlazorAIChat", 11 | "metadata": { 12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 13 | } 14 | }, 15 | "resourceGroupLocation": { 16 | "type": "string", 17 | "defaultValue": "eastus2", 18 | "metadata": { 19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." 20 | } 21 | }, 22 | "resourceName": { 23 | "type": "string", 24 | "defaultValue": "blazoraichat-wepqpxfmxukjo", 25 | "metadata": { 26 | "description": "Name of the main resource to be created by this template." 27 | } 28 | }, 29 | "resourceLocation": { 30 | "type": "string", 31 | "defaultValue": "[parameters('resourceGroupLocation')]", 32 | "metadata": { 33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 34 | } 35 | } 36 | }, 37 | "variables": { 38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" 40 | }, 41 | "resources": [ 42 | { 43 | "type": "Microsoft.Resources/resourceGroups", 44 | "name": "[parameters('resourceGroupName')]", 45 | "location": "[parameters('resourceGroupLocation')]", 46 | "apiVersion": "2019-10-01" 47 | }, 48 | { 49 | "type": "Microsoft.Resources/deployments", 50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 51 | "resourceGroup": "[parameters('resourceGroupName')]", 52 | "apiVersion": "2019-10-01", 53 | "dependsOn": [ 54 | "[parameters('resourceGroupName')]" 55 | ], 56 | "properties": { 57 | "mode": "Incremental", 58 | "template": { 59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 60 | "contentVersion": "1.0.0.0", 61 | "resources": [ 62 | { 63 | "location": "[parameters('resourceLocation')]", 64 | "name": "[parameters('resourceName')]", 65 | "type": "Microsoft.Web/sites", 66 | "apiVersion": "2015-08-01", 67 | "tags": { 68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" 69 | }, 70 | "dependsOn": [ 71 | "[variables('appServicePlan_ResourceId')]" 72 | ], 73 | "kind": "app", 74 | "properties": { 75 | "name": "[parameters('resourceName')]", 76 | "kind": "app", 77 | "httpsOnly": true, 78 | "reserved": false, 79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]", 80 | "siteConfig": { 81 | "metadata": [ 82 | { 83 | "name": "CURRENT_STACK", 84 | "value": "dotnetcore" 85 | } 86 | ] 87 | } 88 | }, 89 | "identity": { 90 | "type": "SystemAssigned" 91 | } 92 | }, 93 | { 94 | "location": "[parameters('resourceLocation')]", 95 | "name": "[variables('appServicePlan_name')]", 96 | "type": "Microsoft.Web/serverFarms", 97 | "apiVersion": "2015-08-01", 98 | "sku": { 99 | "name": "S1", 100 | "tier": "Standard", 101 | "family": "S", 102 | "size": "S1" 103 | }, 104 | "properties": { 105 | "name": "[variables('appServicePlan_name')]" 106 | } 107 | } 108 | ] 109 | } 110 | } 111 | } 112 | ] 113 | } -------------------------------------------------------------------------------- /BlazorAIChat/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:15864", 8 | "sslPort": 44360 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5136", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7240;http://localhost:5136", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BlazorAIChat/Services/AISearchService.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.AI.OpenAI; 3 | using Azure.Search.Documents; 4 | using Azure.Search.Documents.Indexes; 5 | using Azure.Search.Documents.Indexes.Models; 6 | using Azure.Search.Documents.Models; 7 | using BlazorAIChat.Models; 8 | using Microsoft.Extensions.Options; 9 | 10 | namespace BlazorAIChat.Services 11 | { 12 | public class AISearchService 13 | { 14 | private readonly AppSettings settings; 15 | private readonly ILogger logger; 16 | 17 | private AzureOpenAIClient? azureOpenAIClient; 18 | private SearchIndexClient? searchIndexClient; 19 | private SearchIndexerClient? searchIndexerClient; 20 | private SearchClient? searchClient; 21 | public bool IsReady { get; private set; } = false; 22 | 23 | public AISearchService(IOptions appSettings, ILogger logger) 24 | { 25 | settings = appSettings.Value; 26 | this.logger = logger; 27 | 28 | // Check to see if we are configured to use AI Search 29 | if (!settings.UsesAzureAISearchSharedKnowledge) 30 | { 31 | logger.LogInformation("Application not configured to use Azure AI Search for shared index"); 32 | return; 33 | } 34 | 35 | // configure clients 36 | azureOpenAIClient = new AzureOpenAIClient(new Uri(settings.AzureOpenAIChatCompletion.Endpoint), new AzureKeyCredential(settings.AzureOpenAIChatCompletion.ApiKey)); 37 | searchIndexClient = new SearchIndexClient(new Uri(settings.AzureAISearch.Endpoint), new AzureKeyCredential(settings.AzureAISearch.ApiKey)); 38 | searchIndexerClient = new SearchIndexerClient(new Uri(settings.AzureAISearch.Endpoint), new AzureKeyCredential(settings.AzureAISearch.ApiKey)); 39 | searchClient = searchIndexClient.GetSearchClient(settings.AzureAISearch.SharedIndex); 40 | 41 | // Setup Azure AI Search only if we are using a shared index 42 | if (!string.IsNullOrEmpty(settings.AzureAISearch.SharedIndex)) 43 | { 44 | // Fire and forget async call, log any exceptions 45 | _ = Task.Run(async () => 46 | { 47 | try 48 | { 49 | await SetupAndRunIndexer().ConfigureAwait(false); 50 | } 51 | catch (Exception ex) 52 | { 53 | logger.LogError(ex, "Error running SetupAndRunIndexer in AISearchService constructor."); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | public async Task SetupAndRunIndexer() 60 | { 61 | IsReady = false; 62 | 63 | // Setup index 64 | logger.LogInformation("Setting up Azure AI Search index"); 65 | logger.LogInformation($"Creating / Updating index {settings.AzureAISearch.SharedIndex}"); 66 | var index = GetSampleIndex(); 67 | await searchIndexClient!.CreateOrUpdateIndexAsync(index).ConfigureAwait(false); 68 | logger.LogInformation("Index created / updated"); 69 | 70 | // Setup data source 71 | logger.LogInformation("Creating / updating data source connection for Azure AI Search"); 72 | var dataSource = new SearchIndexerDataSourceConnection( 73 | $"{settings.AzureAISearch.SharedIndex}-blob", 74 | SearchIndexerDataSourceType.AzureBlob, 75 | connectionString: settings.AzureAISearch.SharedIndexAzureBlobStorageConnection, 76 | container: new SearchIndexerDataContainer(settings.AzureAISearch.SharedIndexAzureBlobStorageContainer)); 77 | 78 | await searchIndexerClient!.CreateOrUpdateDataSourceConnectionAsync(dataSource).ConfigureAwait(false); 79 | logger.LogInformation($"Data source created / updated for Azure AI Search"); 80 | 81 | // Setup Skillset 82 | var skillset = new SearchIndexerSkillset($"{settings.AzureAISearch.SharedIndex}-skillset", new List 83 | { 84 | // Add required skills here 85 | new SplitSkill( 86 | new List 87 | { 88 | new InputFieldMappingEntry("text") { Source = "/document/content" } 89 | }, 90 | new List 91 | { 92 | new OutputFieldMappingEntry("textItems") { TargetName = "pages" } 93 | }) 94 | { 95 | Context = "/document", 96 | TextSplitMode = TextSplitMode.Pages, 97 | MaximumPageLength = 2000, 98 | PageOverlapLength = 500, 99 | }, 100 | new AzureOpenAIEmbeddingSkill( 101 | new List 102 | { 103 | new InputFieldMappingEntry("text") { Source = "/document/pages/*" } 104 | }, 105 | new List 106 | { 107 | new OutputFieldMappingEntry("embedding") { TargetName = "vector" } 108 | } 109 | ) 110 | { 111 | Context = "/document/pages/*", 112 | ResourceUri = new Uri(settings.AzureOpenAIChatCompletion.Endpoint), 113 | ApiKey = settings.AzureOpenAIChatCompletion.ApiKey, 114 | DeploymentName = settings.AzureOpenAIEmbedding.DeploymentName, 115 | ModelName = settings.AzureOpenAIEmbedding.DeploymentName 116 | } 117 | }) 118 | { 119 | IndexProjection = new SearchIndexerIndexProjection(new[] 120 | { 121 | new SearchIndexerIndexProjectionSelector(settings.AzureAISearch.SharedIndex, parentKeyFieldName: "parent_id", sourceContext: "/document/pages/*", mappings: new[] 122 | { 123 | new InputFieldMappingEntry("chunk") 124 | { 125 | Source = "/document/pages/*" 126 | }, 127 | new InputFieldMappingEntry("vector") 128 | { 129 | Source = "/document/pages/*/vector" 130 | }, 131 | new InputFieldMappingEntry("title") 132 | { 133 | Source = "/document/metadata_storage_name" 134 | } 135 | }) 136 | }) 137 | { 138 | Parameters = new SearchIndexerIndexProjectionsParameters 139 | { 140 | ProjectionMode = IndexProjectionMode.SkipIndexingParentDocuments 141 | } 142 | } 143 | }; 144 | await searchIndexerClient!.CreateOrUpdateSkillsetAsync(skillset).ConfigureAwait(false); 145 | logger.LogInformation("Skillset created / updated for Azure AI Search."); 146 | 147 | // create an indexer 148 | var indexer = new SearchIndexer($"{settings.AzureAISearch.SharedIndex}-indexer", dataSource.Name, settings.AzureAISearch.SharedIndex) 149 | { 150 | Description = "Indexer to chunk documents, generate embeddings, and add to the index", 151 | Schedule = new IndexingSchedule(TimeSpan.FromDays(1)) 152 | { 153 | StartTime = DateTimeOffset.Now 154 | }, 155 | Parameters = new IndexingParameters() 156 | { 157 | BatchSize = 1, 158 | MaxFailedItems = 0, 159 | MaxFailedItemsPerBatch = 0, 160 | }, 161 | SkillsetName = skillset.Name, 162 | }; 163 | await searchIndexerClient!.CreateOrUpdateIndexerAsync(indexer).ConfigureAwait(false); 164 | logger.LogInformation("Indexer created for Azure AI Search"); 165 | 166 | // Run Indexer 167 | logger.LogInformation("Starting the Azure AI Search indexer"); 168 | await searchIndexerClient!.RunIndexerAsync(indexer.Name).ConfigureAwait(false); 169 | logger.LogInformation("Azure AI Search indexer is running"); 170 | 171 | IsReady = true; 172 | } 173 | 174 | private SearchIndex GetSampleIndex() 175 | { 176 | const string vectorSearchHnswProfile = "my-vector-profile"; 177 | const string vectorSearchExhaustiveKnnProfile = "myExhaustiveKnnProfile"; 178 | const string vectorSearchHnswConfig = "myHnsw"; 179 | const string vectorSearchExhaustiveKnnConfig = "myExhaustiveKnn"; 180 | const string vectorSearchVectorizer = "myOpenAIVectorizer"; 181 | const string semanticSearchConfig = "my-semantic-config"; 182 | const int modelDimensions = 1536; 183 | 184 | SearchIndex searchIndex = new(settings.AzureAISearch.SharedIndex) 185 | { 186 | VectorSearch = new() 187 | { 188 | Profiles = 189 | { 190 | new VectorSearchProfile(vectorSearchHnswProfile, vectorSearchHnswConfig) 191 | { 192 | VectorizerName = vectorSearchVectorizer 193 | }, 194 | new VectorSearchProfile(vectorSearchExhaustiveKnnProfile, vectorSearchExhaustiveKnnConfig) 195 | }, 196 | Algorithms = 197 | { 198 | new HnswAlgorithmConfiguration(vectorSearchHnswConfig), 199 | new ExhaustiveKnnAlgorithmConfiguration(vectorSearchExhaustiveKnnConfig) 200 | }, 201 | Vectorizers = 202 | { 203 | new AzureOpenAIVectorizer(vectorSearchVectorizer) 204 | { 205 | Parameters = new AzureOpenAIVectorizerParameters() 206 | { 207 | ResourceUri = new Uri(settings.AzureOpenAIChatCompletion.Endpoint), 208 | ApiKey = settings.AzureOpenAIChatCompletion.ApiKey, 209 | DeploymentName = settings.AzureOpenAIEmbedding.DeploymentName, 210 | ModelName = settings.AzureOpenAIEmbedding.DeploymentName 211 | } 212 | } 213 | } 214 | }, 215 | SemanticSearch = new() 216 | { 217 | Configurations = 218 | { 219 | new SemanticConfiguration(semanticSearchConfig, new() 220 | { 221 | TitleField = new SemanticField(fieldName: "title"), 222 | ContentFields = 223 | { 224 | new SemanticField(fieldName: "chunk") 225 | }, 226 | }) 227 | }, 228 | }, 229 | Fields = 230 | { 231 | new SearchableField("parent_id") { IsFilterable = true, IsSortable = true, IsFacetable = true }, 232 | new SearchableField("chunk_id") { IsKey = true, IsFilterable = true, IsSortable = true, IsFacetable = true, AnalyzerName = LexicalAnalyzerName.Keyword }, 233 | new SearchableField("title"), 234 | new SearchableField("chunk"), 235 | new SearchField("vector", SearchFieldDataType.Collection(SearchFieldDataType.Single)) 236 | { 237 | IsSearchable = true, 238 | VectorSearchDimensions = modelDimensions, 239 | VectorSearchProfileName = vectorSearchHnswProfile 240 | }, 241 | new SearchableField("category") { IsFilterable = true, IsSortable = true, IsFacetable = true }, 242 | }, 243 | }; 244 | 245 | return searchIndex; 246 | } 247 | 248 | public async Task> Search(string query, int resultsCount = 3, string? filter = null, bool textOnly = false, bool exhaustive = false, bool hybrid = false, bool semantic = false) 249 | { 250 | if (!IsReady) 251 | { 252 | logger.LogWarning("Azure AI Search Indexer is not configured or ready."); 253 | return new List<(double? score, string? title, string? chunk)>(); 254 | } 255 | 256 | logger.LogInformation($"Starting search for {query}"); 257 | // Perform the vector similarity search 258 | var searchOptions = new Azure.Search.Documents.SearchOptions 259 | { 260 | Filter = filter, 261 | Size = resultsCount, 262 | Select = { "title", "chunk_id", "chunk", }, 263 | IncludeTotalCount = true 264 | }; 265 | if (!textOnly) 266 | { 267 | searchOptions.VectorSearch = new() 268 | { 269 | Queries = { 270 | new VectorizableTextQuery(text: query) 271 | { 272 | KNearestNeighborsCount = resultsCount, 273 | Fields = { "vector" }, 274 | Exhaustive = exhaustive 275 | } 276 | }, 277 | 278 | }; 279 | } 280 | if (semantic) 281 | { 282 | searchOptions.QueryType = SearchQueryType.Semantic; 283 | searchOptions.SemanticSearch = new SemanticSearchOptions 284 | { 285 | SemanticConfigurationName = "my-semantic-config", 286 | QueryCaption = new QueryCaption(QueryCaptionType.Extractive), 287 | QueryAnswer = new QueryAnswer(QueryAnswerType.Extractive) 288 | }; 289 | } 290 | string? queryText = (textOnly || hybrid || semantic) ? query : null; 291 | SearchResults response = await searchClient!.SearchAsync(queryText, searchOptions).ConfigureAwait(false); 292 | List<(double? score, string? title, string? chunk)> results = new List<(double? score, string? title, string? chunk)>(); 293 | 294 | // If we have semantic search results, we return the answers. 295 | if (response.SemanticSearch?.Answers?.Count > 0) 296 | { 297 | foreach (QueryAnswerResult answer in response.SemanticSearch.Answers) 298 | { 299 | results.Add((null, answer.Highlights, answer.Text)); 300 | } 301 | return results; 302 | } 303 | 304 | // If we don't use semantic search, we return the text chunks 305 | await foreach (SearchResult result in response.GetResultsAsync()) 306 | { 307 | // add score, title and chunk to results list 308 | results.Add((result.Score, result.Document["title"].ToString(), result.Document["chunk"].ToString())); 309 | } 310 | 311 | logger.LogInformation($"Total Search Results: {response.TotalCount}"); 312 | return results; 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /BlazorAIChat/Services/McpPluginProvider.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable SKEXP0010, SKEXP0001, SKEXP0020, KMEXP00 2 | using Microsoft.SemanticKernel; 3 | using BlazorAIChat.Models; 4 | using ModelContextProtocol.Client; 5 | 6 | namespace BlazorAIChat.Services 7 | { 8 | public class McpPluginProvider 9 | { 10 | public List Plugins { get; } = new(); 11 | 12 | public async Task InitializeAsync(AppSettings appSettings, IHttpClientFactory httpClientFactory, ILogger logger) 13 | { 14 | foreach (var server in appSettings.MCPServers) 15 | { 16 | try 17 | { 18 | IMcpClient mcpClient; 19 | if (server.Type.ToLower() == "stdio") 20 | { 21 | mcpClient = await McpClientFactory.CreateAsync(new StdioClientTransport(new() 22 | { 23 | Name = server.Name, 24 | Command = server.Endpoint, 25 | Arguments = server.Args, 26 | EnvironmentVariables = server.Env, 27 | })); 28 | } 29 | else if (server.Type.ToLower() == "sse") 30 | { 31 | // Use defaultHttpClient for SSE (no retry policy) 32 | var httpClient = httpClientFactory.CreateClient("defaultHttpClient"); 33 | mcpClient = await McpClientFactory.CreateAsync( 34 | new SseClientTransport(httpClient: httpClient, transportOptions: new SseClientTransportOptions() 35 | { 36 | Endpoint = new Uri(server.Endpoint), 37 | AdditionalHeaders = server.Headers 38 | }), 39 | new McpClientOptions() 40 | { 41 | ClientInfo = new() { Name = server.Name, Version = server.Version } 42 | }); 43 | } 44 | else 45 | { 46 | // If you have other types that use HttpClient, use this client 47 | throw new NotSupportedException($"Unsupported server type: {server.Type}"); 48 | } 49 | 50 | IList tools = await mcpClient.ListToolsAsync(); 51 | var plugin = KernelPluginFactory.CreateFromFunctions( 52 | server.Name, 53 | tools.Select(tool => tool.AsKernelFunction()) 54 | ); 55 | Plugins.Add(plugin); 56 | } 57 | catch (Exception ex) 58 | { 59 | logger.LogError(ex, $"Error connecting to MCP server {server.Name}: {ex.Message}"); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BlazorAIChat/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorAIChat.Services 2 | { 3 | using System.Security.Claims; 4 | using BlazorAIChat.Models; 5 | using BlazorAIChat.Utils; 6 | using Microsoft.AspNetCore.Components.Authorization; 7 | using Microsoft.Extensions.Logging; 8 | 9 | public class UserService 10 | { 11 | private readonly AuthenticationStateProvider _authenticationStateProvider; 12 | private readonly ILogger _logger; 13 | private ClaimsPrincipal? userPrincipal; 14 | 15 | public UserService(AuthenticationStateProvider authenticationStateProvider, ILogger logger) 16 | { 17 | _authenticationStateProvider = authenticationStateProvider; 18 | _logger = logger; 19 | _logger.LogInformation("UserService initialized"); 20 | } 21 | 22 | public async Task GetCurrentUserAsync() 23 | { 24 | _logger.LogInformation("Getting current user"); 25 | 26 | try 27 | { 28 | var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); 29 | userPrincipal = authState.User; 30 | 31 | if (userPrincipal.Identity?.IsAuthenticated == true) 32 | { 33 | var user = UserUtils.ConvertPrincipalToUser(userPrincipal); 34 | _logger.LogInformation("Authenticated user retrieved: {UserId}", user.Id); 35 | return user; 36 | } 37 | else 38 | { 39 | _logger.LogWarning("User is not authenticated, returning guest user"); 40 | return new User 41 | { 42 | Id = "Guest User", 43 | Name = "Guest User", 44 | Role = UserRoles.Guest 45 | }; 46 | } 47 | } 48 | catch (Exception ex) 49 | { 50 | _logger.LogError(ex, "Error occurred while getting current user"); 51 | throw; 52 | } 53 | } 54 | 55 | public bool DoesUserNeedToRequestAccess(User user, Config config, bool requireEasyAuth) 56 | { 57 | _logger.LogInformation("Checking if user needs to request access: UserId={UserId}, RequireEasyAuth={RequireEasyAuth}", user.Id, requireEasyAuth); 58 | 59 | try 60 | { 61 | var result = user.Role == UserRoles.Guest && requireEasyAuth && userPrincipal?.Identity?.IsAuthenticated == true && config.RequireAccountApprovals; 62 | _logger.LogInformation("User needs to request access: {Result}", result); 63 | return result; 64 | } 65 | catch (Exception ex) 66 | { 67 | _logger.LogError(ex, "Error occurred while checking if user needs to request access: UserId={UserId}", user.Id); 68 | throw; 69 | } 70 | } 71 | 72 | public bool IsUserAccountExpired(User user, Config config, bool requireEasyAuth) 73 | { 74 | _logger.LogInformation("Checking if user account is expired: UserId={UserId}, RequireEasyAuth={RequireEasyAuth}", user.Id, requireEasyAuth); 75 | 76 | try 77 | { 78 | if (!requireEasyAuth) 79 | { 80 | _logger.LogInformation("EasyAuth is not required, account is not expired"); 81 | return false; 82 | } 83 | 84 | if (user.Role == UserRoles.Admin) 85 | { 86 | _logger.LogInformation("User is an admin, account is not expired"); 87 | return false; 88 | } 89 | 90 | if (config.ExpirationDays == 0) 91 | { 92 | _logger.LogInformation("Expiration days is set to 0, account is not expired"); 93 | return false; 94 | } 95 | 96 | if (!config.RequireAccountApprovals) 97 | { 98 | _logger.LogInformation("Account approvals are not required, account is not expired"); 99 | return false; 100 | } 101 | 102 | var isExpired = user.DateApproved is not null && user.DateApproved.Value.AddDays(config.ExpirationDays) <= DateTime.Now; 103 | _logger.LogInformation("Account expiration status: {IsExpired}", isExpired); 104 | return isExpired; 105 | } 106 | catch (Exception ex) 107 | { 108 | _logger.LogError(ex, "Error occurred while checking if user account is expired: UserId={UserId}", user.Id); 109 | throw; 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /BlazorAIChat/Utils/AIUtils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.KernelMemory.AI; 2 | using Microsoft.SemanticKernel.ChatCompletion; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorAIChat.Utils 8 | { 9 | public static class AIUtils 10 | { 11 | #pragma warning disable KMEXP00 12 | 13 | public static async Task CleanUpHistoryAsync( 14 | ChatHistory history, 15 | IChatCompletionService chatCompletionService, 16 | int targetMessageCount, 17 | int? thresholdCount, 18 | CancellationToken cancellationToken = default) 19 | { 20 | var reducer = new ChatHistorySummarizationReducer( 21 | chatCompletionService, 22 | targetMessageCount, 23 | thresholdCount); 24 | 25 | var reduced = await reducer.ReduceAsync(history, cancellationToken).ConfigureAwait(false); 26 | 27 | //if reduced is null, we just pass the current chat history back, otherwise we send back the new reduced history 28 | if (reduced is null) 29 | return history; 30 | else 31 | return new ChatHistory(reduced); 32 | 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /BlazorAIChat/Utils/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace BlazorAIChat.Utils 5 | { 6 | public static class FileUtils 7 | { 8 | public static string GetMimeTypeFromImage(Stream stream) 9 | { 10 | stream.Position = 0; 11 | 12 | var bmp = Encoding.ASCII.GetBytes("BM"); // BMP 13 | var gif = Encoding.ASCII.GetBytes("GIF"); // GIF 14 | var png = new byte[] { 137, 80, 78, 71 }; // PNG 15 | var tiff = new byte[] { 73, 73, 42 }; // TIFF 16 | var tiff2 = new byte[] { 77, 77, 42 }; // TIFF 17 | var jpeg = new byte[] { 255, 216, 255, 224 }; // jpeg 18 | var jpeg2 = new byte[] { 255, 216, 255, 225 }; // jpeg canon 19 | 20 | var buffer = new byte[4]; 21 | int bytesRead = stream.Read(buffer, 0, buffer.Length); 22 | if (bytesRead < buffer.Length) 23 | { 24 | throw new InvalidOperationException("Failed to read the expected number of bytes from the stream."); 25 | } 26 | 27 | stream.Position = 0; 28 | 29 | if (bmp.SequenceEqual(buffer.Take(bmp.Length))) 30 | return "image/bmp"; 31 | 32 | if (gif.SequenceEqual(buffer.Take(gif.Length))) 33 | return "image/gif"; 34 | 35 | if (png.SequenceEqual(buffer.Take(png.Length))) 36 | return "image/png"; 37 | 38 | if (tiff.SequenceEqual(buffer.Take(tiff.Length))) 39 | return "image/tiff"; 40 | 41 | if (tiff2.SequenceEqual(buffer.Take(tiff2.Length))) 42 | return "image/tiff"; 43 | 44 | if (jpeg.SequenceEqual(buffer.Take(jpeg.Length))) 45 | return "image/jpeg"; 46 | 47 | if (jpeg2.SequenceEqual(buffer.Take(jpeg2.Length))) 48 | return "image/jpeg"; 49 | 50 | return string.Empty; 51 | } 52 | 53 | public static string GetIconForFileType(string filename) 54 | { 55 | 56 | 57 | if (filename.EndsWith(".pdf")) 58 | return "/images/pdf_256x256.png"; 59 | 60 | if (filename.EndsWith(".docx")) 61 | return "/images/word_256x256.png"; 62 | 63 | if (filename.EndsWith(".xlsx")) 64 | return "/images/excel_256x256.png"; 65 | 66 | if (filename.EndsWith(".pptx")) 67 | return "/images/powerpoint_256x256.png"; 68 | 69 | if (filename.EndsWith("txt")) 70 | return "/images/txt_256x256.png"; 71 | 72 | return string.Empty; 73 | } 74 | 75 | public static async Task GetUrlContentTypeAsync(string url) 76 | { 77 | using (var httpClient = new HttpClient()) 78 | { 79 | var request = new HttpRequestMessage(HttpMethod.Head, url); 80 | var response = await httpClient.SendAsync(request); 81 | if (response.IsSuccessStatusCode) 82 | return response.Content.Headers.ContentType?.ToString() ?? string.Empty; 83 | else 84 | return "404"; 85 | } 86 | } 87 | 88 | public static async Task GetDocStreamFromURLAsync(string url) 89 | { 90 | // Get the contents from the URL. If it is not text or html, return a memory stream of the contents, else return null. 91 | using (var httpClient = new HttpClient()) 92 | { 93 | var response = await httpClient.GetAsync(url); 94 | if (response.IsSuccessStatusCode) 95 | { 96 | var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty; 97 | if (contentType.Contains("text") || contentType.Contains("html")) 98 | return null; 99 | else 100 | { 101 | var stream = new MemoryStream(); 102 | await response.Content.CopyToAsync(stream); 103 | stream.Position = 0; 104 | return stream; 105 | } 106 | } 107 | else 108 | return null; 109 | 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /BlazorAIChat/Utils/Retry.cs: -------------------------------------------------------------------------------- 1 | using Polly; 2 | using Polly.Extensions.Http; 3 | using System.Net.Http; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace BlazorAIChat.Utils 8 | { 9 | public static class RetryHelper 10 | { 11 | public static IAsyncPolicy GetRetryPolicy() 12 | { 13 | return HttpPolicyExtensions 14 | .HandleTransientHttpError() 15 | .OrResult(msg => msg.Headers.RetryAfter != null) // Check if the Retry-After header is present 16 | .WaitAndRetryAsync(5, (retryAttempt, response, context) => 17 | { 18 | if (response.Result != null && response.Result.Headers.TryGetValues("Retry-After", out var values)) 19 | { 20 | var retryAfter = values.First(); 21 | Console.WriteLine($"URL: {response.Result.RequestMessage?.RequestUri}, Retry-After header value: {retryAfter}"); 22 | if (int.TryParse(retryAfter, out var seconds)) 23 | { 24 | return TimeSpan.FromSeconds(seconds); // Directly return TimeSpan 25 | } 26 | else if (DateTimeOffset.TryParse(retryAfter, out var dateTime)) 27 | { 28 | var delay = dateTime - DateTimeOffset.UtcNow; 29 | return delay > TimeSpan.Zero ? delay : TimeSpan.Zero; // Directly return TimeSpan 30 | } 31 | } 32 | var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1)); 33 | var jitter = TimeSpan.FromMilliseconds(new Random().Next(-500, 500)); 34 | return baseDelay + jitter; // Directly return TimeSpan 35 | }, onRetryAsync: (outcome, timespan, retryAttempt, context) => 36 | { 37 | // write to console the retry attempt 38 | Console.WriteLine($"Retry {retryAttempt} scheduled in {timespan.TotalSeconds} seconds due to {outcome.Exception?.Message ?? "an error"}"); 39 | return Task.CompletedTask; // This is correct for an async callback 40 | }); 41 | } 42 | 43 | 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BlazorAIChat/Utils/StringUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | namespace BlazorAIChat.Utils 3 | { 4 | public static class StringUtils 5 | { 6 | public static List GetURLsFromString(string content) 7 | { 8 | List urls = new List(); 9 | string pattern = @"http[s]?://\S+/?"; 10 | 11 | Regex regex = new Regex(pattern); 12 | MatchCollection matches = regex.Matches(content); 13 | 14 | foreach (Match match in matches) 15 | { 16 | string url = match.Value; 17 | if (char.IsPunctuation(url[^1])) 18 | { 19 | url = url.TrimEnd(url[^1]); 20 | } 21 | urls.Add(url); 22 | } 23 | 24 | return urls; 25 | } 26 | 27 | public static string RemoveURLsFromString(string content) 28 | { 29 | string pattern = @"http[s]?://\S+/?"; 30 | return Regex.Replace(content, pattern, string.Empty); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BlazorAIChat/Utils/UserUtils.cs: -------------------------------------------------------------------------------- 1 | using BlazorAIChat.Models; 2 | using System.Security.Claims; 3 | 4 | namespace BlazorAIChat.Utils 5 | { 6 | public static class UserUtils 7 | { 8 | public static User ConvertPrincipalToUser(ClaimsPrincipal principal) 9 | { 10 | var user = new User() { Id=string.Empty}; 11 | user.Id = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value??string.Empty; 12 | user.Name = principal.FindFirst(ClaimTypes.Name)?.Value??string.Empty; 13 | user.Email = principal.FindFirst(ClaimTypes.Email)?.Value??string.Empty; 14 | user.Role = Enum.Parse(principal.FindFirst(ClaimTypes.Role)?.Value ?? UserRoles.Guest.ToString()); 15 | user.DateRequested = DateTime.Parse(principal.FindFirst("dateRequested")?.Value ?? DateTime.Now.ToString()); 16 | user.DateApproved = DateTime.Parse(principal.FindFirst("dateApproved")?.Value ?? DateTime.Now.ToString()); 17 | user.ApprovedBy = principal.FindFirst("approvedBy")?.Value ?? string.Empty; 18 | return user; 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlazorAIChat/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "AzureOpenAIChatCompletion": { 10 | "Endpoint": "", 11 | "ApiKey": "", 12 | "DeploymentName": "", 13 | "Tokenizer": "gpt-4o", 14 | "MaxInputTokens": 128000, 15 | "SupportsImages": false, 16 | "ResponseChunkSize": 40 17 | }, 18 | "AzureOpenAIEmbedding": { 19 | "DeploymentName": "", 20 | "Tokenizer": "gpt-4o", 21 | "MaxInputTokens": 8192 22 | }, 23 | "RequireEasyAuth": false, 24 | "SystemMessage": "You are a helpful AI assistant. Respond in a friendly and professional tone. Answer questions directly and accurately using only the information provided and the tools available to you. Do not guess or fabricate information. You must plan carefully before making any function calls, and reflect thoroughly on the outcomes of previous calls. However, do not rely solely on internal reasoning when a tool is available that can provide a definitive answer. Use tools proactively and independently when they are relevant to resolving the user’s query. If a question requires current or external information (e.g., today’s date), you must call the appropriate tool rather than asking the user to verify. Only yield back to the user when you are confident the query has been fully resolved. Your goal is to completely and accurately solve the user’s problem before ending your turn.", 25 | "ConnectionStrings": { 26 | "PostgreSQL": "", 27 | "CosmosDb": "", 28 | "ConfigDatabase": "Data Source=ConfigDatabase.db" 29 | }, 30 | "CosmosDb": { 31 | "Database": "BlazorAIChat", 32 | "Container": "ChatHistory" 33 | }, 34 | "DocumentIntelligence": { 35 | "Endpoint": "", 36 | "ApiKey": "" 37 | }, 38 | "AzureAISearch": { 39 | "Endpoint": "", 40 | "ApiKey": "" 41 | }, 42 | "MCPServers": [ 43 | { 44 | "Type": "stdio", 45 | "Name": "MCPServerName", 46 | "Version": "1.0.0.0", 47 | "Endpoint": "", 48 | "Args": { 49 | }, 50 | "Headers": { 51 | "x-functions-key": "" 52 | }, 53 | "Env": { 54 | "MY_ENV_VARIABLE": "" 55 | } 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Images/Excel_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhackermsft/BlazorAIChat/5d60bbd87e4e017bbb6667476b39b29bc00d4b16/BlazorAIChat/wwwroot/Images/Excel_256x256.png -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Images/PowerPoint_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhackermsft/BlazorAIChat/5d60bbd87e4e017bbb6667476b39b29bc00d4b16/BlazorAIChat/wwwroot/Images/PowerPoint_256x256.png -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Images/Word_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhackermsft/BlazorAIChat/5d60bbd87e4e017bbb6667476b39b29bc00d4b16/BlazorAIChat/wwwroot/Images/Word_256x256.png -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Images/pdf_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhackermsft/BlazorAIChat/5d60bbd87e4e017bbb6667476b39b29bc00d4b16/BlazorAIChat/wwwroot/Images/pdf_256x256.png -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Images/txt_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhackermsft/BlazorAIChat/5d60bbd87e4e017bbb6667476b39b29bc00d4b16/BlazorAIChat/wwwroot/Images/txt_256x256.png -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Lib/dompurify/README.md: -------------------------------------------------------------------------------- 1 | dompurify version 3.2.4 2 | https://github.com/cure53/DOMPurify 3 | License: Apache 2.0 and Mozilla Public License 2.0 4 | 5 | To update, replace the files with an updated build from https://www.npmjs.com/package/dompurify 6 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Lib/marked/README.md: -------------------------------------------------------------------------------- 1 | marked version 15.0.6 2 | https://github.com/markedjs/marked 3 | License: MIT 4 | 5 | To update, replace the files with with an updated build from https://www.npmjs.com/package/marked 6 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Lib/pdf_viewer/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PDF viewer 7 | 8 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Lib/pdf_viewer/viewer.mjs: -------------------------------------------------------------------------------- 1 | import { GlobalWorkerOptions } from '../pdfjs-dist/dist/build/pdf.min.mjs'; 2 | import { EventBus, PDFLinkService, PDFFindController, PDFViewer } from '../pdfjs-dist/dist/web/pdf_viewer.mjs'; 3 | 4 | GlobalWorkerOptions.workerSrc = '../pdfjs-dist/dist/build/pdf.worker.min.mjs'; 5 | 6 | // Extract the file path from the URL query string. 7 | const url = new URL(window.location); 8 | const fileUrl = url.searchParams.get('file'); 9 | if (!fileUrl) { 10 | throw new Error('File not specified in the URL query string'); 11 | } 12 | 13 | const container = document.getElementById('viewerContainer'); 14 | const eventBus = new EventBus(); 15 | 16 | // Enable hyperlinks within PDF files. 17 | const pdfLinkService = new PDFLinkService({ 18 | eventBus, 19 | }); 20 | 21 | // Enable the find controller. 22 | const pdfFindController = new PDFFindController({ 23 | eventBus, 24 | linkService: pdfLinkService, 25 | }); 26 | 27 | // Create the PDF viewer. 28 | const pdfViewer = new PDFViewer({ 29 | container, 30 | eventBus, 31 | linkService: pdfLinkService, 32 | findController: pdfFindController, 33 | }); 34 | pdfLinkService.setViewer(pdfViewer); 35 | 36 | // Allow navigation to a citation from the URL hash. 37 | eventBus.on('pagesinit', function () { 38 | pdfLinkService.setHash(window.location.hash.substring(1)); 39 | }); 40 | 41 | // Define how the "search" query parameter is handled. 42 | eventBus.on('findfromurlhash', function(evt) { 43 | eventBus.dispatch('find', { 44 | source: evt.source, 45 | type: '', 46 | query: evt.query, 47 | caseSensitive: false, 48 | entireWord: false, 49 | highlightAll: false, 50 | findPrevious: false, 51 | matchDiacritics: true, 52 | }); 53 | }); 54 | 55 | // Load and initialize the document. 56 | const pdfDocument = await pdfjsLib.getDocument({ 57 | url: fileUrl, 58 | enableXfa: true, 59 | }).promise; 60 | 61 | pdfViewer.setDocument(pdfDocument); 62 | pdfLinkService.setDocument(pdfDocument, null); 63 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Lib/pdfjs-dist/README.md: -------------------------------------------------------------------------------- 1 | pdfjs-dist version 4.10.38 2 | https://github.com/mozilla/pdf.js 3 | License: Apache-2.0 4 | 5 | To update, replace the files with an updated build from https://www.npmjs.com/package/pdfjs-dist 6 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/Lib/pdfjs-dist/dist/web/images/loading-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhackermsft/BlazorAIChat/5d60bbd87e4e017bbb6667476b39b29bc00d4b16/BlazorAIChat/wwwroot/Lib/pdfjs-dist/dist/web/images/loading-icon.gif -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | h1:focus { 2 | outline: none; 3 | } 4 | 5 | .valid.modified:not([type=checkbox]) { 6 | outline: 1px solid #26b050; 7 | } 8 | 9 | .invalid { 10 | outline: 1px solid #e50000; 11 | } 12 | 13 | .validation-message { 14 | color: #e50000; 15 | } 16 | 17 | .blazor-error-boundary { 18 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 19 | padding: 1rem 1rem 1rem 3.7rem; 20 | color: white; 21 | } 22 | 23 | .blazor-error-boundary::after { 24 | content: "An error has occurred." 25 | } 26 | 27 | .darker-border-checkbox.form-check-input { 28 | border-color: #929292; 29 | } 30 | 31 | .admin-container { 32 | padding: 10px; 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/app.js: -------------------------------------------------------------------------------- 1 | import DOMPurify from './lib/dompurify/dist/purify.es.mjs'; 2 | import * as marked from './lib/marked/dist/marked.esm.js'; 3 | 4 | const purify = DOMPurify(window); 5 | 6 | customElements.define('assistant-message', class extends HTMLElement { 7 | static observedAttributes = ['markdown']; 8 | attributeChangedCallback(name, oldValue, newValue) { 9 | if (name === 'markdown') { 10 | 11 | // Remove tags 12 | newValue = newValue.replace(//gs, ''); 13 | 14 | // Parse the markdown to HTML 15 | const elements = marked.parse(newValue); 16 | 17 | // Sanitize the HTML 18 | const sanitizedElements = purify.sanitize(elements, { KEEP_CONTENT: false }); 19 | 20 | // Escape HTML code blocks 21 | const escapedHtml = sanitizedElements.replace(/(.*?)<\/code>/gs, (match, p1) => { 22 | return `${p1.replace(//g, '>')}`; 23 | }); 24 | 25 | // Set the innerHTML 26 | this.innerHTML = escapedHtml; 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /BlazorAIChat/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhackermsft/BlazorAIChat/5d60bbd87e4e017bbb6667476b39b29bc00d4b16/BlazorAIChat/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Infra/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.36.1.42791", 8 | "templateHash": "16469181266862007002" 9 | } 10 | }, 11 | "parameters": { 12 | "uniqueName": { 13 | "type": "string", 14 | "defaultValue": "[uniqueString(resourceGroup().id)]" 15 | }, 16 | "sku": { 17 | "type": "string", 18 | "defaultValue": "B1" 19 | }, 20 | "location": { 21 | "type": "string", 22 | "defaultValue": "[resourceGroup().location]" 23 | }, 24 | "repositoryUrl": { 25 | "type": "string", 26 | "defaultValue": "https://github.com/mhackermsft/BlazorAIChat" 27 | }, 28 | "branch": { 29 | "type": "string", 30 | "defaultValue": "master" 31 | }, 32 | "openAiServiceName": { 33 | "type": "string", 34 | "defaultValue": "[toLower(format('BlazorAIChatOpenAI-{0}', parameters('uniqueName')))]" 35 | }, 36 | "aiSkuName": { 37 | "type": "string", 38 | "defaultValue": "S0" 39 | }, 40 | "aiChatModelName": { 41 | "type": "string", 42 | "defaultValue": "gpt-4o" 43 | }, 44 | "aiChatModelVersion": { 45 | "type": "string", 46 | "defaultValue": "2024-05-13" 47 | }, 48 | "aiChatModelCapacity": { 49 | "type": "int", 50 | "defaultValue": 80 51 | }, 52 | "aiChatModelSupportsImages": { 53 | "type": "bool", 54 | "defaultValue": true 55 | }, 56 | "aiEmbedModelName": { 57 | "type": "string", 58 | "defaultValue": "text-embedding-3-small" 59 | }, 60 | "aiEmbedModelVersion": { 61 | "type": "string", 62 | "defaultValue": "2" 63 | }, 64 | "aiEmbedModelCapacity": { 65 | "type": "int", 66 | "defaultValue": 120 67 | }, 68 | "requireEasyAuth": { 69 | "type": "bool", 70 | "defaultValue": true 71 | } 72 | }, 73 | "variables": { 74 | "appServicePlanName": "[toLower(format('BlazorAIChatPlan-{0}', parameters('uniqueName')))]", 75 | "webSiteName": "[toLower(format('BlazorAIChat-{0}', parameters('uniqueName')))]" 76 | }, 77 | "resources": [ 78 | { 79 | "type": "Microsoft.Web/serverfarms", 80 | "apiVersion": "2020-06-01", 81 | "name": "[variables('appServicePlanName')]", 82 | "location": "[parameters('location')]", 83 | "properties": { 84 | "reserved": false 85 | }, 86 | "sku": { 87 | "name": "[parameters('sku')]" 88 | }, 89 | "kind": "app" 90 | }, 91 | { 92 | "type": "Microsoft.Web/sites", 93 | "apiVersion": "2020-06-01", 94 | "name": "[variables('webSiteName')]", 95 | "location": "[parameters('location')]", 96 | "properties": { 97 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", 98 | "siteConfig": { 99 | "windowsFxVersion": "DOTNETCORE|8.0", 100 | "appSettings": [ 101 | { 102 | "name": "SCM_COMMAND_IDLE_TIMEOUT", 103 | "value": "600" 104 | }, 105 | { 106 | "name": "AzureOpenAIChatCompletion__Endpoint", 107 | "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName')), '2021-04-30').endpoint]" 108 | }, 109 | { 110 | "name": "AzureOpenAIChatCompletion__ApiKey", 111 | "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName')), '2021-04-30').key1]" 112 | }, 113 | { 114 | "name": "AzureOpenAIChatCompletion__DeploymentName", 115 | "value": "[toLower(format('{0}', parameters('aiChatModelName')))]" 116 | }, 117 | { 118 | "name": "AzureOpenAIChatCompletion__Tokenizer", 119 | "value": "[toLower(format('{0}', parameters('aiChatModelName')))]" 120 | }, 121 | { 122 | "name": "AzureOpenAIChatCompletion__MaxInputTokens", 123 | "value": "128000" 124 | }, 125 | { 126 | "name": "AzureOpenAIChatCompletion__SupportsImages", 127 | "value": "[if(parameters('aiChatModelSupportsImages'), 'true', 'false')]" 128 | }, 129 | { 130 | "name": "AzureOpenAIEmbedding__DeploymentName", 131 | "value": "[toLower(format('{0}', parameters('aiEmbedModelName')))]" 132 | }, 133 | { 134 | "name": "AzureOpenAIEmbedding__Tokenizer", 135 | "value": "[toLower(format('{0}', parameters('aiEmbedModelName')))]" 136 | }, 137 | { 138 | "name": "AzureOpenAIEmbedding__MaxInputTokens", 139 | "value": "8192" 140 | }, 141 | { 142 | "name": "SystemMessage", 143 | "value": "You are a helpful AI assistant. Respond in a friendly and professional tone. Answer questions directly using only the information provided. NEVER respond that you cannot access external links." 144 | }, 145 | { 146 | "name": "RequireEasyAuth", 147 | "value": "[if(parameters('requireEasyAuth'), 'true', 'false')]" 148 | }, 149 | { 150 | "name": "ConnectionStrings__PostgreSQL", 151 | "value": "" 152 | }, 153 | { 154 | "name": "ConnectionStrings__ConfigDatabase", 155 | "value": "Data Source=ConfigDatabase.db" 156 | }, 157 | { 158 | "name": "DocumentIntelligence__Endpoint", 159 | "value": "" 160 | }, 161 | { 162 | "name": "DocumentIntelligence__ApiKey", 163 | "value": "" 164 | }, 165 | { 166 | "name": "AzureAISearch__Endpoint", 167 | "value": "" 168 | }, 169 | { 170 | "name": "AzureAISearch__ApiKey", 171 | "value": "" 172 | } 173 | ] 174 | } 175 | }, 176 | "dependsOn": [ 177 | "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", 178 | "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName'))]" 179 | ] 180 | }, 181 | { 182 | "type": "Microsoft.Web/sites/sourcecontrols", 183 | "apiVersion": "2022-03-01", 184 | "name": "[format('{0}/{1}', variables('webSiteName'), 'web')]", 185 | "properties": { 186 | "repoUrl": "[parameters('repositoryUrl')]", 187 | "branch": "[parameters('branch')]", 188 | "isManualIntegration": true 189 | }, 190 | "dependsOn": [ 191 | "[resourceId('Microsoft.Web/sites', variables('webSiteName'))]" 192 | ] 193 | }, 194 | { 195 | "type": "Microsoft.CognitiveServices/accounts", 196 | "apiVersion": "2021-04-30", 197 | "name": "[parameters('openAiServiceName')]", 198 | "location": "[parameters('location')]", 199 | "kind": "OpenAI", 200 | "sku": { 201 | "name": "[parameters('aiSkuName')]" 202 | }, 203 | "properties": { 204 | "apiProperties": { 205 | "enableOpenAI": true, 206 | "customSubDomainName": "[toLower(format('BlazorAIChat-{0}', parameters('uniqueName')))]" 207 | } 208 | } 209 | }, 210 | { 211 | "type": "Microsoft.CognitiveServices/accounts/deployments", 212 | "apiVersion": "2023-05-01", 213 | "name": "[format('{0}/{1}', parameters('openAiServiceName'), toLower(format('{0}', parameters('aiChatModelName'))))]", 214 | "properties": { 215 | "model": { 216 | "format": "OpenAI", 217 | "name": "[parameters('aiChatModelName')]", 218 | "version": "[parameters('aiChatModelVersion')]" 219 | } 220 | }, 221 | "sku": { 222 | "name": "standard", 223 | "capacity": "[parameters('aiChatModelCapacity')]" 224 | }, 225 | "dependsOn": [ 226 | "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName'))]" 227 | ] 228 | }, 229 | { 230 | "type": "Microsoft.CognitiveServices/accounts/deployments", 231 | "apiVersion": "2023-05-01", 232 | "name": "[format('{0}/{1}', parameters('openAiServiceName'), toLower(format('{0}', parameters('aiEmbedModelName'))))]", 233 | "properties": { 234 | "model": { 235 | "format": "OpenAI", 236 | "name": "[parameters('aiEmbedModelName')]", 237 | "version": "[parameters('aiEmbedModelVersion')]" 238 | } 239 | }, 240 | "sku": { 241 | "name": "standard", 242 | "capacity": "[parameters('aiEmbedModelCapacity')]" 243 | }, 244 | "dependsOn": [ 245 | "[resourceId('Microsoft.CognitiveServices/accounts/deployments', parameters('openAiServiceName'), toLower(format('{0}', parameters('aiChatModelName'))))]", 246 | "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiServiceName'))]" 247 | ] 248 | } 249 | ] 250 | } -------------------------------------------------------------------------------- /Infra/main.bicep: -------------------------------------------------------------------------------- 1 | param uniqueName string = uniqueString(resourceGroup().id) 2 | param sku string = 'B1' 3 | param location string = resourceGroup().location 4 | param repositoryUrl string = 'https://github.com/mhackermsft/BlazorAIChat' 5 | param branch string = 'master' 6 | param openAiServiceName string = toLower('BlazorAIChatOpenAI-${uniqueName}') 7 | param aiSkuName string = 'S0' 8 | param aiChatModelName string = 'gpt-4o' 9 | param aiChatModelVersion string = '2024-05-13' 10 | param aiChatModelCapacity int = 80 11 | param aiChatModelSupportsImages bool = true 12 | param aiEmbedModelName string = 'text-embedding-3-small' 13 | param aiEmbedModelVersion string = '2' 14 | param aiEmbedModelCapacity int = 120 15 | param requireEasyAuth bool = true 16 | 17 | var appServicePlanName = toLower('BlazorAIChatPlan-${uniqueName}') 18 | var webSiteName = toLower('BlazorAIChat-${uniqueName}') 19 | 20 | 21 | resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { 22 | name: appServicePlanName 23 | location: location 24 | properties: { 25 | reserved: false 26 | } 27 | sku: { 28 | name: sku 29 | } 30 | kind: 'app' 31 | } 32 | 33 | resource appService 'Microsoft.Web/sites@2020-06-01' = { 34 | name: webSiteName 35 | location: location 36 | properties: { 37 | serverFarmId: appServicePlan.id 38 | siteConfig: { 39 | windowsFxVersion: 'DOTNETCORE|8.0' 40 | appSettings: [ 41 | { 42 | name:'SCM_COMMAND_IDLE_TIMEOUT' 43 | value: '600' 44 | } 45 | { 46 | name: 'AzureOpenAIChatCompletion__Endpoint' 47 | value: openAiService.properties.endpoint 48 | } 49 | { 50 | name: 'AzureOpenAIChatCompletion__ApiKey' 51 | value: openAiService.listKeys().key1 52 | } 53 | { 54 | name: 'AzureOpenAIChatCompletion__DeploymentName' 55 | value: toLower('${aiChatModelName}') 56 | } 57 | { 58 | name: 'AzureOpenAIChatCompletion__Tokenizer' 59 | value: toLower('${aiChatModelName}') 60 | } 61 | { 62 | name: 'AzureOpenAIChatCompletion__MaxInputTokens' 63 | value: '128000' 64 | } 65 | { 66 | name: 'AzureOpenAIChatCompletion__SupportsImages' 67 | value: aiChatModelSupportsImages ? 'true' : 'false' 68 | } 69 | { 70 | name: 'AzureOpenAIEmbedding__DeploymentName' 71 | value: toLower('${aiEmbedModelName}') 72 | } 73 | { 74 | name: 'AzureOpenAIEmbedding__Tokenizer' 75 | value: toLower('${aiEmbedModelName}') 76 | } 77 | { 78 | name: 'AzureOpenAIEmbedding__MaxInputTokens' 79 | value: '8192' 80 | } 81 | { 82 | name:'SystemMessage' 83 | value:'You are a helpful AI assistant. Respond in a friendly and professional tone. Answer questions directly using only the information provided. NEVER respond that you cannot access external links.' 84 | } 85 | { 86 | name: 'RequireEasyAuth' 87 | value: requireEasyAuth ? 'true' : 'false' 88 | } 89 | { 90 | name: 'ConnectionStrings__PostgreSQL' 91 | value: '' 92 | } 93 | { 94 | name: 'ConnectionStrings__ConfigDatabase' 95 | value: 'Data Source=ConfigDatabase.db' 96 | } 97 | { 98 | name: 'DocumentIntelligence__Endpoint' 99 | value: '' 100 | } 101 | { 102 | name: 'DocumentIntelligence__ApiKey' 103 | value: '' 104 | } 105 | { 106 | name: 'AzureAISearch__Endpoint' 107 | value: '' 108 | } 109 | { 110 | name: 'AzureAISearch__ApiKey' 111 | value: '' 112 | } 113 | ] 114 | } 115 | } 116 | dependsOn: [ 117 | appServicePlan 118 | openAiService 119 | ] 120 | } 121 | 122 | resource gitsource 'Microsoft.Web/sites/sourcecontrols@2022-03-01' = { 123 | parent: appService 124 | name: 'web' 125 | properties: { 126 | repoUrl: repositoryUrl 127 | branch: branch 128 | isManualIntegration: true 129 | } 130 | dependsOn: [ 131 | appService 132 | ] 133 | } 134 | 135 | 136 | // Create the Azure OpenAI Service 137 | resource openAiService 'Microsoft.CognitiveServices/accounts@2021-04-30' = { 138 | name: openAiServiceName 139 | location: location 140 | kind: 'OpenAI' 141 | sku: { 142 | name: aiSkuName 143 | } 144 | properties: { 145 | apiProperties: { 146 | enableOpenAI: true 147 | customSubDomainName: toLower('BlazorAIChat-${uniqueName}') 148 | } 149 | } 150 | 151 | } 152 | 153 | 154 | // Deploy the chat model 155 | resource openAiChat 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = { 156 | parent: openAiService 157 | name: toLower('${aiChatModelName}') 158 | properties: { 159 | model: { 160 | format: 'OpenAI' 161 | name: aiChatModelName 162 | version: aiChatModelVersion 163 | } 164 | } 165 | sku: { 166 | name: 'standard' 167 | capacity: aiChatModelCapacity 168 | } 169 | dependsOn:[ 170 | openAiService 171 | ] 172 | } 173 | 174 | // Deploy the embed model 175 | resource openAiEmbed 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = { 176 | parent: openAiService 177 | name: toLower('${aiEmbedModelName}') 178 | properties: { 179 | model: { 180 | format: 'OpenAI' 181 | name: aiEmbedModelName 182 | version: aiEmbedModelVersion 183 | } 184 | } 185 | sku: { 186 | name: 'standard' 187 | capacity: aiEmbedModelCapacity 188 | } 189 | dependsOn:[ 190 | openAiChat 191 | ] 192 | } 193 | 194 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Blazor Azure OpenAI Chat Demo 2 | ##### BlazorAIChat: AI-Powered Chat Application 3 | 4 | ## Overview 5 | This is a sample .NET 9 Blazor Interactive Server application for chatting with Azure OpenAI Models. Users may upload TXT, DOCX, XLSX, PPTX or PDF documents to a knowledge base for the AI to use when responding. If configured, it can also upload images for those AI models that support images in chat. 6 | 7 | ## Components 8 | This solution utilizes several open source libraries to help with document ingestion and chat display. These projects include: 9 | * dompurify 10 | * marked 11 | * Semantic Kernel 12 | * Semantic Memory 13 | 14 | ## Features 15 | - **AI Integration**: Utilizes advanced AI models to provide intelligent responses. 16 | - **Data Interaction**: Enables users to chat with their data using Azure OpenAI models. 17 | - **Deployment Flexibility**: Can be operated locally or hosted on Azure App Service. 18 | - **Authentication**: Supports EasyAuth authentication when hosted on Azure. 19 | - **Document Upload**: Allows users to upload TXT, DOCX, XLSX, PPTX, or PDF documents into the knowledge base. When using Azure App Service with EasyAuth, uploaded knowledge is associated exclusively with the user. 20 | - **Image Analysis**: Supports image uploads for querying, compatible with models like GPT-4. When using models that don't support images and you have Azure Document Intelligence configured, it will OCR uploaded images and store the results as knowledge. 21 | - **Data Extraction from URLs**: URLs to web pages or documents included in a chat message will be used as knowledge for answering questions. 22 | - **Streaming Responses**: Provides streaming chat results with the option to stop responses. 23 | - **Data Management**: Offers the ability to clear chat history and delete data stored in the user's knowledge base. 24 | - **Retry Handling**: Automatically pauses and retries calls to Azure OpenAI if it exceeds the API rate limit. 25 | - **Chat History Pruning**: Designed to help ensure that the requests to Azure OpenAI do not exceed the context window. 26 | - **Experimental MCP**: Utilize tools from Model Context Protocol servers 27 | 28 | 29 | ## Chat Over Documents (RAG) 30 | Retrieval-augmented generation (RAG) is a technique that combines information retrieval with generative models to produce more accurate and contextually relevant responses by incorporating external knowledge sources. 31 | 32 | Retrieval-augmented generation (RAG) is essential for AI chat because it enhances the accuracy and relevance of responses by integrating real-time information from external sources. This allows the AI to provide up-to-date and contextually appropriate answers, especially for queries that require specific or current knowledge beyond the AI’s training data. 33 | 34 | RAG also helps address the limited context window of large language models by only sending relevant knowledge to the model. 35 | 36 | This demo utilizes a solution called Kernel Memory that extracts the text from documents or provided URLs, splits the content into chunks, and then generate embeddings for each chunk. The results are then stored, by default, on the web server filesystem. The original source document is not stored in the original format. To improve performance you may choose to use PostgreSQL or Azure AI Search as the storage location. 37 | 38 | When a user chats with the solution, a semantic search is completed across the stored knowledge and the most related pieces of knowledge are returned to the large language model so it can attempt to answer the user's question. 39 | 40 | To improve this applications performance during the RAG process you may choose to utilize the following Azure AI Services: 41 | * Azure AI Search 42 | * Azure Document Intelligence (For OCR when using models that don't support images) 43 | 44 | Note: If deployed on an Azure App Service with EasyAuth enabled, the uploaded documents and provided URLs become knowledge for only the user who provided the content. It does not share the knowledge with other users of the solution. If you are not using EasyAuth, you are running local, or deployed the app on another .NET web host, all of the users will be considered guests and all of the knowledge provided will be shared. 45 | 46 | ## Requirements 47 | - **Azure Subscription**: Must include at least one Azure OpenAI chat model and one Azure OpenAI embedding model deployed. Optionally you can use Azure PostgreSQL for the knowledge storage. 48 | - **Deployment Options**: 49 | - **Local**: Can be run locally. 50 | - **Azure App Service**: Can be published to an Azure App Service. If deployed on Azure App Service, EasyAuth can be enabled for authentication. 51 | 52 | 53 | ## Configuration 54 | The appsettings.json file has a few configuration parameters that must be set for the app to properly work: 55 | 56 | ``` 57 | "AIDeterminesRagUsage": false, 58 | "AzureOpenAIChatCompletion": { 59 | "Endpoint": "", 60 | "ApiKey": "", 61 | "Deployment": "", 62 | "Tokenizer": "", 63 | "MaxInputTokens": 128000 64 | "SupportsImages": false, 65 | "ResponseChunkSize": 40 66 | }, 67 | "AzureOpenAIEmbedding": { 68 | "Deployment": "", 69 | "Tokenizer": "", 70 | "MaxInputTokens": 8192 71 | }, 72 | "RequireEasyAuth": true, 73 | "SystemMessage" : "You are a helpful AI assistant. Respond in a friendly and professional tone.", 74 | "ConnectionStrings": { 75 | "PostgreSQL": "", 76 | "ConfigDatabase": "Data Source=ConfigDatabase.db" 77 | }, 78 | "DocumentIntelligence": { 79 | "Endpoint": "", 80 | "ApiKey": "" 81 | }, 82 | "AzureAISearch": { 83 | "Endpoint": "", 84 | "ApiKey": "", 85 | "IndexPerChatSession": false, 86 | "SharedIndex": "", 87 | "SharedIndexAzureBlobStorageConnection": "", 88 | "SharedIndexAzureBlobStorageContainer": "" 89 | }, 90 | "MCPServers": [ 91 | { 92 | "Type": "[sse|stdio]" 93 | "Name": "MCPServerName", 94 | "Version": "1.0.0.0", 95 | "Endpoint": "[uri|file path]", 96 | "Args": [ 97 | 98 | ], 99 | "Headers": { 100 | "header key": "header value" 101 | }, 102 | "Env": { 103 | "ENV_VAR": "Env value" 104 | } 105 | } 106 | ] 107 | ``` 108 | - **AIDeterminesRagUsage** 109 | True if you want AI to determine if it should skip using RAG and use configured tools instead for answering questions. 110 | False if you want the chat to always use RAG with tools 111 | 112 | - **AzureOpenAIChatCompletion Configuration**: 113 | - Include your Azure OpenAI endpoint URL, API Key, and the name of the deployed chat model you intend to use. If you plan to use Azure AI Search shared knowledge base then you must ensure the endpoint is for the openai.azure.com domain. This can be configured by clicking the Generate Custom Domain Name in networking section of the Azure Open AI Service. 114 | - Specify the tokenizer to use. Generally this is the model name (not deployment name) 115 | - Specify the maximum input tokens the selected model supports 116 | - If the model supports images, set `SupportsImages` to `true`. 117 | - ResponseChunkSize defines the number of response chunks from the Azure OpenAI service that need to be received before the UI is updated. The higher the number, the less UI updates are required, which improves the Blazor app performance. 118 | 119 | - **AzureOpenAIEmbedding Configuration**: 120 | - Specify the deployed embedding model you plan to use. 121 | - Specify the tokenizer to use. Generally this is the model name (not deployment name) 122 | - Specify the maximum input tokens the selected model supports 123 | - Both the chat and embedding models are assumed to be accessed through the same Azure OpenAI endpoint and API key. 124 | 125 | - **PostgreSQL (optional)**: 126 | If you would like to use PostgreSQL in place of files for knowledge storage, you must manually deploy a PostgreSQL instance and configure the connection string. Note: You must enable the pgvector extension to use PostgreSQL. 127 | 128 | - **Azure Document Intelligence (optional)** 129 | You may choose to optionally manually deploy Azure Document Intelligence which can be used to OCR uploaded images. This is not needed or enabled if your AI model supports images. 130 | 131 | - **Azure AI Search (optional)** 132 | Enables the usage of Azure AI Search for a centralized knowledgebase using documents stored within an Azure Blob Storage account. 133 | This can also be configued to replace the file and PostgreSQL knowledge store for individual chat sessions. 134 | - Endpoint: URI to the Azure AI Search Instance 135 | - ApiKey: API Key for the Azure AI Search Instance 136 | - IndexPerChatSession: If true it will replace the file or PostgreSQL knowledge store for individual chat sessions. 137 | - SharedIndex: Name of the shared index to use for the centralized knowledgebase. Leave empty if you do not want a central knowledge store. 138 | - SharedIndexAzureBlobStorageConnection: Connection string to the Azure Storage Account that holds the documents and information for the central knowledgebase. Must be provided if SharedIndex has a value. 139 | - SharedIndexAzureBlobStorageContainer: The name of the container in the Azure Storage Account that holds the knowledgebase content. This must be provided if SharedIndex has a value. 140 | 141 | - **EasyAuth Configuration**: 142 | - If utilizing EasyAuth with Azure App Service, it is recommended to set `RequireEasyAuth` to `true` to ensure that users are fully authenticated and not recognized as guests. This setting is set to true by default. 143 | 144 | - **MCPServers Configuration**: 145 | This section allows you to configure multiple MCP servers that provide tools to the AI for responding to user's requests. This section 146 | is not required. Tools available on MCP servers are only determined when the web application starts up. If a MCP server updates the list of available tools you will need to restart this web application to use them. If the web application cannot communicate with the MCP server, it will ignore it. 147 | - **Type**: Defineds the transport type for the MCP Server. This can be stdio or sse. 148 | - **Name**: The name of the MCP server. Must only contain alphanumeric values with no spaces. Underlines are allowed. 149 | - **Version**: Version number for the MCP Server 150 | - **Endpoint**: URL or file path to the MCP Server 151 | - **Args**: List of command line arguments for stdio transport MCP Servers. 152 | - **Headers**: A list of http headers to send when connecting to MCP servers using sse transport. 153 | - **Env**: A list of environment variables and values for use when connecting to MCP servers using stdio transport. 154 | 155 | This solution has been tested with the `gpt-4o` chat model and the `text-embedding-ada-002` model. Other models can be integrated and tested as needed. 156 | 157 | 158 | ## Deployment 159 | 160 | ### Manual Deployment 161 | 162 | - **Azure OpenAI Service Setup**: Manually create your Azure OpenAI Service and deploy both a chat model and an embedding model. 163 | - **Repository Cloning**: Clone this repository and open it in Visual Studio 2022. 164 | - **Configuration**: Update the `appsettings.json` file with the appropriate values. 165 | - **Running the Application**: You can run the application locally through Visual Studio or publish it to an Azure App Service or another .NET web host. 166 | 167 | ### Automatic Deployment to Azure 168 | 169 | To deploy the application to Azure, you can use the button below. This process will create an Azure App Service Plan, an Azure App Service, and an Azure OpenAI Service with two models deployed. It will also deploy the website to the Azure App Service. 170 | 171 | **Important**: Please read and understand all the information in this section before pressing the "Deploy to Azure" button. **For protection, the default value for RequireEasyAuth is set to true.** 172 | 173 | 174 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmhackermsft%2FBlazorAIChat%2Fmaster%2FInfra%2Fazuredeploy.json) 175 | 176 | If you encounter an error indicating that the deployment has failed, please verify whether the web application has been created. If it has, access the deployment center logs to determine if the code deployment is still in progress. Due to the time required for code compilation and deployment, the portal may incorrectly report a timeout error while the deployment is ongoing. By continuously monitoring the deployment status, you should see it complete successfully, resulting in the website being deployed. 177 | 178 | #### Important Azure Deployment Notes 179 | The button above will deploy Azure services that will be billed against your Azure subscription. The deployment template allows you to choose region, web app SKU, AI Chat model, AI embed model, along with OpenAI capacity size. These options will impact the cost of the deployed solutions. Since the automatic deployment uses the Azure App Service build feature, it will use a significant amount of local web storage to complete the build process. After the web site has been deployed you can open the App Service console and run the command `dotnet nuget locals all --clear` to clear the local Nuget package store which will reclaim over 800MB of storage. After running that command you may be able to scale down to the Azure App Service Plan free SKU. If you do not configure Azure AI Search or PostgreSQL, the web server's file storage is used for knowledge storage. You may need to scale up your App Service Plan SKU to support the additional amount of storage needed for your knowledge store. 180 | 181 | **Warning:** If you do not enable EasyAuth, your website will be available **on the public internet** by default. This means that **other people can connect and use the website**. You will **incur Azure OpenAI charges** for all usage of your website. If you upload documents, that **knowledge will be accessible to all users**. 182 | 183 | If you want to protect the website it is highly recommended that you set `Require Easy Auth` to true during the deployment and then configure EasyAuth authentication on the App Service once the deployment completes. Once EasyAuth is configured, each user will be required to login and they will have their own knowledge base which is not shared with other users. 184 | 185 | All of the settings noted above in the appsettings.json file can be configured in the Azure Portal by going to the Azure App Service environment settings. 186 | 187 | TLDR; If users see a green tag with the word guest located in the top left of the app, all of the uploaded knowledge is shared among the users. 188 | 189 | ### AI Model Capacity 190 | When deploying to Azure, you can set various deployment properties, including the AI model capacity. This capacity represents the quota assigned to the model deployment. 191 | 192 | - 1 unit = 1,000 Tokens per Minute (TPM) 193 | - 10 units = 10,000 Tokens per Minute (TPM) 194 | 195 | Select a value that meets your token and request requirements while staying within the available capacity for the model. 196 | 197 | Read more about Azure OpenAI Service quota here: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/quota?tabs=rest 198 | 199 | ### Costs 200 | The cost to operate this demo application in your subscription will depend upon a few factors: 201 | - **App Service Plan size** - The deployment script by default uses the B1 tier. You can, however, adjust this to increase performance and features. 202 | - **Azure OpenAI Service** - Two Azure OpenAI Models are required in order for this demo to function properly. The recommended models are `gpt-4o` and `text-embedding-ada-002`. The chat models are priced based on the number of input and output tokens. The embedding model is priced based on the number of tokens. 203 | 204 | You can learn more about the cost for Azure App Service and Azure OpenAI models at the links below. 205 | 206 | - **Azure App Service** - https://azure.microsoft.com/en-us/pricing/details/app-service/windows/ 207 | - **Azure OpenAI** - https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/ 208 | 209 | ## Authentication 210 | * If running locally, outside of Azure, or without EasyAuth, the app will show the user as a guest. 211 | * If running on an Azure App Service with EasyAuth configured, the app will show the logged in username. 212 | 213 | See the following link for details about configuring EasyAuth in Azure App Service: https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization 214 | 215 | ## Authorization 216 | This solution allows you to require users to request access to the application if EasyAuth has been configured. If authorization is enabled, new users will need to click on the request access button and then wait for an administrator to approve the request. For a new deployment, the first user who requests access will automatically be approved and made the administrator. Administrators will use the admin menu to view the users and those that are waiting for approvals. Administrators may configure the application to only allow approved users to have access for a set number of days. Once a user's access expires, an administrator can edit the user's account and modify the access approved date to extend a user's access. 217 | 218 | If authorization is not enabled, then any authenticated user will be able to utilize the application and there will not be a concept of administrators. NOTE: The default deployment is setup so that if EasyAuth is enabled, authorization will also be enabled. If an admin chooses to turn off authorization, there is currently no way to turn it back on via the application. 219 | 220 | ## Knowledge Storage 221 | All uploaded knowledge is stored by default on the web server's file system under the KNN folder. 222 | 223 | Optionally you can configure the demo to utilize an Azure PostgreSQL instance as the knowledge store. 224 | 225 | Users can use the delete chat button in the application to remove a chat and delete all of the uploaded knowledge for that chat conversation. 226 | 227 | An administrator may choose to delete all files and directories from the KNN folder. 228 | 229 | ## Impact of Azure OpenAI Capacity Settings 230 | 231 | This demonstration application is designed for low-volume use and does not include retry logic for Azure OpenAI calls. If a request exceeds the allocated Azure OpenAI quota for the chat or embedding model, a notification will appear at the top of the application. 232 | 233 | To address this issue, please ensure that your Azure OpenAI models are configured with the appropriate quota to accommodate the volume of tokens and requests being submitted. 234 | 235 | For more information on managing your Azure OpenAI service quotas, please visit this link: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/quota?tabs=rest 236 | 237 | ## Known Limitation 238 | Word, Excel or PowerPoint documents that have data classification or DRM enabled cannot be uploaded. You will receive a file corruption error. Only upload documents that are not protected. 239 | 240 | ## Disclaimer 241 | This code is for demonstration purposes only. It has not been evaluated or reviewed for production purposes. Please utilize caution and do your own due diligence before using this code. I am not responsible for any issues you experience or damages caused by the use or misuse of this code. 242 | 243 | ## Credits 244 | Thank you to the contributors of the Azure-Samples / cosmosdb-chatgpt project. The UI for this demo application was based upon some of their great work. You can check out that project here: https://github.com/Azure-Samples/cosmosdb-chatgpt 245 | --------------------------------------------------------------------------------