From 6f5923878d74326b8d9e079382e58cfc621f6a86 Mon Sep 17 00:00:00 2001 From: Tim Schaeps Date: Sat, 29 Apr 2023 21:22:05 +0200 Subject: [PATCH] Initial commit --- .devcontainer/Dockerfile | 4 + .devcontainer/devcontainer.json | 49 +++++ .github/CODE_OF_CONDUCT.md | 9 + .github/ISSUE_TEMPLATE.md | 33 +++ .github/PULL_REQUEST_TEMPLATE.md | 45 ++++ .gitignore | 359 +++++++++++++++++++++++++++++++ App.razor | 12 ++ CHANGELOG.md | 13 ++ CONTRIBUTING.md | 76 +++++++ Components/Confirmation.razor | 64 ++++++ Components/Input.razor | 48 +++++ Constants/Interface.cs | 6 + Constants/Participants.cs | 7 + LICENSE.md | 21 ++ Models/Message.cs | 35 +++ Models/Session.cs | 47 ++++ Options/CosmosDb.cs | 12 ++ Options/OpenAi.cs | 12 ++ Pages/ChatPane.razor | 260 ++++++++++++++++++++++ Pages/Error.cshtml | 42 ++++ Pages/Error.cshtml.cs | 27 +++ Pages/Index.razor | 55 +++++ Pages/NavMenu.razor | 333 ++++++++++++++++++++++++++++ Pages/_Host.cshtml | 8 + Pages/_Layout.cshtml | 36 ++++ Program.cs | 78 +++++++ Properties/launchSettings.json | 14 ++ Services/ChatService.cs | 209 ++++++++++++++++++ Services/CosmosDbService.cs | 184 ++++++++++++++++ Services/OpenAiService.cs | 134 ++++++++++++ Shared/MainLayout.razor | 4 + _Imports.razor | 12 ++ appsettings.json | 22 ++ azuredeploy.bicep | 217 +++++++++++++++++++ azuredeploy.json | 285 ++++++++++++++++++++++++ cosmoschatgpt.csproj | 14 ++ cosmoschatgpt.sln | 25 +++ readme.md | 85 ++++++++ screenshot.png | Bin 0 -> 85985 bytes wwwroot/favicon.ico | Bin 0 -> 15406 bytes wwwroot/js/site.js | 9 + 41 files changed, 2905 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 App.razor create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Components/Confirmation.razor create mode 100644 Components/Input.razor create mode 100644 Constants/Interface.cs create mode 100644 Constants/Participants.cs create mode 100644 LICENSE.md create mode 100644 Models/Message.cs create mode 100644 Models/Session.cs create mode 100644 Options/CosmosDb.cs create mode 100644 Options/OpenAi.cs create mode 100644 Pages/ChatPane.razor create mode 100644 Pages/Error.cshtml create mode 100644 Pages/Error.cshtml.cs create mode 100644 Pages/Index.razor create mode 100644 Pages/NavMenu.razor create mode 100644 Pages/_Host.cshtml create mode 100644 Pages/_Layout.cshtml create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 Services/ChatService.cs create mode 100644 Services/CosmosDbService.cs create mode 100644 Services/OpenAiService.cs create mode 100644 Shared/MainLayout.razor create mode 100644 _Imports.razor create mode 100644 appsettings.json create mode 100644 azuredeploy.bicep create mode 100644 azuredeploy.json create mode 100644 cosmoschatgpt.csproj create mode 100644 cosmoschatgpt.sln create mode 100644 readme.md create mode 100644 screenshot.png create mode 100644 wwwroot/favicon.ico create mode 100644 wwwroot/js/site.js diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..bf776ec --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,4 @@ +ARG VARIANT=latest +FROM mcr.microsoft.com/dotnet/sdk:${VARIANT} + +RUN dotnet dev-certs https --trust \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ee17db8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,49 @@ +{ + "name": "Cosmos Chat GPT Blazor Server", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "7.0.203" + } + }, + "portsAttributes": { + "8100": { + "label": "Blazor web application", + "onAutoForward": "openPreview" + } + }, + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": { + "installBicep": true, + "version": "latest" + } + }, + "updateContentCommand": "dotnet build cosmoschatgpt.sln", + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "vsls-contrib.codetour", + "VisualStudioExptTeam.vscodeintellicode", + "ms-azuretools.vscode-cosmosdb", + "ms-azuretools.vscode-bicep" + ], + "settings": { + "omnisharp.defaultLaunchSolution": "cosmoschatgpt.sln", + "csharp.suppressDotnetRestoreNotification": true, + "csharp.suppressDotnetInstallWarning": true, + "csharp.suppressBuildAssetsNotification": true, + "codetour.promptForWorkspaceTours": false, + "codetour.recordMode": "pattern", + "codetour.showMarkers": false, + "explorer.sortOrder": "type", + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.js": "${capture}.js.map", + "*.razor": "${capture}.razor.cs,${capture}.razor.css" + }, + "git.autofetch": true + } + } + } +} diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f9ba8cf --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..15c7f60 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,33 @@ + +> Please provide us with the following information: +> --------------------------------------------------------------- + +### This issue is for a: (mark with an `x`) +``` +- [ ] bug report -> please search issues before submitting +- [ ] feature request +- [ ] documentation issue or request +- [ ] regression (a behavior that used to work and stopped in a new release) +``` + +### Minimal steps to reproduce +> + +### Any log messages given by the failure +> + +### Expected/desired behavior +> + +### OS and Version? +> Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) + +### Versions +> + +### Mention any other details that might be useful + +> --------------------------------------------------------------- +> Thanks! We'll be in touch soon. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ab05e29 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ +## Purpose + +* ... + +## Does this introduce a breaking change? + +``` +[ ] Yes +[ ] No +``` + +## Pull Request Type +What kind of change does this Pull Request introduce? + + +``` +[ ] Bugfix +[ ] Feature +[ ] Code style update (formatting, local variables) +[ ] Refactoring (no functional changes, no api changes) +[ ] Documentation content changes +[ ] Other... Please describe: +``` + +## How to Test +* Get the code + +``` +git clone [repo-address] +cd [repo-name] +git checkout [branch-name] +npm install +``` + +* Test the code + +``` +``` + +## What to Check +Verify that the following are valid +* ... + +## Other Information + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..311e4d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,359 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# Environment-specific app settings +appsettings.development.json +appsettings.production.json +appsettings.*.json + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ +/appsettings.Development.json +/appsettings.json +/appsettings.Development.json +/appsettings.Development.json diff --git a/App.razor b/App.razor new file mode 100644 index 0000000..6fd3ed1 --- /dev/null +++ b/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9824752 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## [project-title] Changelog + + +# x.y.z (yyyy-mm-dd) + +*Features* +* ... + +*Bug Fixes* +* ... + +*Breaking Changes* +* ... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a9115cf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributing to [project-title] + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + + - [Code of Conduct](#coc) + - [Issues and Bugs](#issue) + - [Feature Requests](#feature) + - [Submission Guidelines](#submit) + +## Code of Conduct +Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +## Found an Issue? +If you find a bug in the source code or a mistake in the documentation, you can help us by +[submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can +[submit a Pull Request](#submit-pr) with a fix. + +## Want a Feature? +You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub +Repository. If you would like to *implement* a new feature, please submit an issue with +a proposal for your work first, to be sure that we can use it. + +* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). + +## Submission Guidelines + +### Submitting an Issue +Before you submit an issue, search the archive, maybe your question was already answered. + +If your issue appears to be a bug, and hasn't been reported, open a new issue. +Help us to maximize the effort we can spend fixing issues and adding new +features, by not reporting duplicate issues. Providing the following information will increase the +chances of your issue being dealt with quickly: + +* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps +* **Version** - what version is affected (e.g. 0.1.2) +* **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you +* **Browsers and Operating System** - is this a problem with all browsers? +* **Reproduce the Error** - provide a live example or a unambiguous set of steps +* **Related Issues** - has a similar issue been reported before? +* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be + causing the problem (line of code or commit) + +You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. + +### Submitting a Pull Request (PR) +Before you submit your Pull Request (PR) consider the following guidelines: + +* Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR + that relates to your submission. You don't want to duplicate effort. + +* Make your changes in a new git fork: + +* Commit your changes using a descriptive commit message +* Push your fork to GitHub: +* In GitHub, create a pull request +* If we suggest changes then: + * Make the required updates. + * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): + + ```shell + git rebase master -i + git push -f + ``` + +That's it! Thank you for your contribution! diff --git a/Components/Confirmation.razor b/Components/Confirmation.razor new file mode 100644 index 0000000..3a42450 --- /dev/null +++ b/Components/Confirmation.razor @@ -0,0 +1,64 @@ + +@code { + [Parameter] public string? Caption { get; set; } + [Parameter] public string? Message { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + [Parameter] public Category Type { get; set; } + private Task Cancel() + { + return OnClose.InvokeAsync(false); + } + private Task Ok() + { + return OnClose.InvokeAsync(true); + } + public enum Category + { + Okay, + SaveNot, + DeleteNot + } +} \ No newline at end of file diff --git a/Components/Input.razor b/Components/Input.razor new file mode 100644 index 0000000..e209bac --- /dev/null +++ b/Components/Input.razor @@ -0,0 +1,48 @@ + +@code { + [Parameter] public string? Caption { get; set; } + [Parameter] public string? Value { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + + public string? ReturnValue { get; set; } + + private Task Cancel() + { + return OnClose.InvokeAsync(""); + } + private Task Ok() + { + return OnClose.InvokeAsync(ReturnValue); + } + + public Task Enter(KeyboardEventArgs e) + { + if (e.Code == "Enter" || e.Code == "NumpadEnter") + { + return OnClose.InvokeAsync(ReturnValue); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Constants/Interface.cs b/Constants/Interface.cs new file mode 100644 index 0000000..7bf2e37 --- /dev/null +++ b/Constants/Interface.cs @@ -0,0 +1,6 @@ +namespace Cosmos.Chat.GPT.Constants; + +public static class Interface +{ + public static readonly string EMPTY_SESSION = "empty-session-404"; +} \ No newline at end of file diff --git a/Constants/Participants.cs b/Constants/Participants.cs new file mode 100644 index 0000000..c92c2f4 --- /dev/null +++ b/Constants/Participants.cs @@ -0,0 +1,7 @@ +namespace Cosmos.Chat.GPT.Constants; + +public enum Participants +{ + User = 0, + Assistant +} \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7965606 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE \ No newline at end of file diff --git a/Models/Message.cs b/Models/Message.cs new file mode 100644 index 0000000..b3d9987 --- /dev/null +++ b/Models/Message.cs @@ -0,0 +1,35 @@ +namespace Cosmos.Chat.GPT.Models; + +public record Message +{ + /// + /// Unique identifier + /// + public string Id { get; set; } + + public string Type { get; set; } + + /// + /// Partition key + /// + public string SessionId { get; set; } + + public DateTime TimeStamp { get; set; } + + public string Sender { get; set; } + + public int? Tokens { get; set; } + + public string Text { get; set; } + + public Message(string sessionId, string sender, int? tokens, string text) + { + Id = Guid.NewGuid().ToString(); + Type = nameof(Message); + SessionId = sessionId; + Sender = sender; + Tokens = tokens; + TimeStamp = DateTime.UtcNow; + Text = text; + } +} \ No newline at end of file diff --git a/Models/Session.cs b/Models/Session.cs new file mode 100644 index 0000000..07b5de4 --- /dev/null +++ b/Models/Session.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; + +namespace Cosmos.Chat.GPT.Models; + +public record Session +{ + /// + /// Unique identifier + /// + public string Id { get; set; } + + public string Type { get; set; } + + /// + /// Partition key + /// + public string SessionId { get; set; } + + public int? TokensUsed { get; set; } + + public string Name { get; set; } + + [JsonIgnore] + public List Messages { get; set; } + + public Session() + { + Id = Guid.NewGuid().ToString(); + Type = nameof(Session); + SessionId = this.Id; + TokensUsed = 0; + Name = "New Chat"; + Messages = new List(); + } + + public void AddMessage(Message message) + { + Messages.Add(message); + } + + public void UpdateMessage(Message message) + { + var match = Messages.Single(m => m.Id == message.Id); + var index = Messages.IndexOf(match); + Messages[index] = message; + } +} \ No newline at end of file diff --git a/Options/CosmosDb.cs b/Options/CosmosDb.cs new file mode 100644 index 0000000..c0e53a9 --- /dev/null +++ b/Options/CosmosDb.cs @@ -0,0 +1,12 @@ +namespace Cosmos.Chat.GPT.Options; + +public record CosmosDb +{ + public required string Endpoint { get; init; } + + public required string Key { get; init; } + + public required string Database { get; init; } + + public required string Container { get; init; } +}; \ No newline at end of file diff --git a/Options/OpenAi.cs b/Options/OpenAi.cs new file mode 100644 index 0000000..754793b --- /dev/null +++ b/Options/OpenAi.cs @@ -0,0 +1,12 @@ +namespace Cosmos.Chat.GPT.Options; + +public record OpenAi +{ + public required string Endpoint { get; init; } + + public required string Key { get; init; } + + public required string Deployment { get; init; } + + public string? MaxConversationTokens { get; init; } +} \ No newline at end of file diff --git a/Pages/ChatPane.razor b/Pages/ChatPane.razor new file mode 100644 index 0000000..a759e11 --- /dev/null +++ b/Pages/ChatPane.razor @@ -0,0 +1,260 @@ +@using Cosmos.Chat.GPT.Constants +@using Cosmos.Chat.GPT.Services +@using Humanizer +@inject ChatService chatService +@inject IJSRuntime JSRuntime + +
+ @if (ShowHeader) + { + + } +
@GetChatSessionName()
+
+ @if (CurrentSession is null) + { +
+
+
+ Loading... +
+ Loading... +
+

+ Please wait while your chat loads. +

+
+ } + else if (CurrentSession.SessionId == Interface.EMPTY_SESSION) + { +
+

+ + No Chats Available +

+

+ Use the New Chat option to start a new chat. +

+
+ } + else + { + if (_messagesInChat is null || _loadingComplete == false) + { +
+
+
+ Loading... +
+ Loading... +
+

+ Please wait while your chat loads. +

+
+ } + else + { + if (_messagesInChat.Count == 0) + { +
+

+ + Get Started +

+

+ Start chatting with your helpful AI assistant. +

+
+ } + else + { +
+ @foreach (var msg in _messagesInChat) + { +
+
+ + + @msg.Sender + + @if(msg.Tokens is not null) { + + Tokens: @msg.Tokens + + } + + @msg.TimeStamp.Humanize() + +
+
+ + @msg.Text +
+
+ } +
+ } + } + } +
+
+
+ + + +
+
+
+ +@code { + + [Parameter] + public EventCallback OnChatUpdated { get; set; } + + [Parameter] + public Session? CurrentSession { get; set; } + + [Parameter] + public bool ShowHeader { get; set; } + + [Parameter] + public EventCallback OnNavBarVisibilityUpdated { get; set; } + + private string? UserPrompt { get; set; } + + private string? UserPromptSet { get; set; } + + private List? _messagesInChat; + private static event EventHandler? _onMessagePosted; + private bool _loadingComplete; + + async private Task ToggleNavMenu() + { + await OnNavBarVisibilityUpdated.InvokeAsync(); + } + + public async Task ReloadChatMessagesAsync() + { + if (CurrentSession is not null) + { + _messagesInChat = await chatService.GetChatSessionMessagesAsync(CurrentSession.SessionId); + } + } + + protected override void OnInitialized() + { + _onMessagePosted += async (o, e) => + { + await this.InvokeAsync(async () => + { + if (e.SessionId == CurrentSession?.SessionId) + { + await this.ReloadChatMessagesAsync(); + this.StateHasChanged(); + } + }); + }; + } + + protected override async Task OnParametersSetAsync() + { + if (CurrentSession is null) + { + return; + } + + if (CurrentSession.SessionId != Interface.EMPTY_SESSION & CurrentSession.SessionId is not null) + { + _messagesInChat = await chatService.GetChatSessionMessagesAsync(CurrentSession?.SessionId); + } + + _loadingComplete = true; + } + + public void ChangeCurrentChatSession(Session session) + { + CurrentSession = session; + } + + public async Task Enter(KeyboardEventArgs e) + { + if (e.Code == "Enter" || e.Code == "NumpadEnter") + { + await SubmitPromptAsync(); + } + } + + private async Task SubmitPromptAsync() + { + if (CurrentSession?.SessionId == Interface.EMPTY_SESSION || UserPrompt == String.Empty || UserPrompt is null) + { + return; + } + + if (UserPrompt != String.Empty) + { + UserPromptSet = String.Empty; + } + + await chatService.GetChatCompletionAsync(CurrentSession?.SessionId, UserPrompt); + + if(_messagesInChat?.Count == 2) + { + string newSessionName; + newSessionName = await chatService.SummarizeChatSessionNameAsync(CurrentSession?.SessionId, String.Join(Environment.NewLine, _messagesInChat.Select(m => m.Text))); + + if (CurrentSession is not null) + { + CurrentSession.Name = newSessionName; + } + } + await OnChatUpdated.InvokeAsync(); + + if (_onMessagePosted is not null && CurrentSession is not null) + { + _onMessagePosted.Invoke(null, CurrentSession); + } + + await ScrollLastChatToView(); + } + + private string GetChatSessionName() => CurrentSession switch + { + null => String.Empty, + (Session s) when s.SessionId == Interface.EMPTY_SESSION => String.Empty, + _ => CurrentSession.Name + }; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await ScrollLastChatToView(); + } + + /// + /// This is a hack to get the scroll to work. Need to find a better way to do this. + /// + private async Task ScrollLastChatToView() + { + await JSRuntime.InvokeAsync("scrollToLastMessage"); + } +} \ No newline at end of file diff --git a/Pages/Error.cshtml b/Pages/Error.cshtml new file mode 100644 index 0000000..e12b520 --- /dev/null +++ b/Pages/Error.cshtml @@ -0,0 +1,42 @@ +@page +@model Cosmos.Chat.GPT.Pages.ErrorModel + + + + + + + + Error + + + + + +
+
+

Error.

+

An error occurred while processing your request.

+ + @if (Model.ShowRequestId) + { +

+ Request ID: @Model.RequestId +

+ } + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

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

+
+
+ + + diff --git a/Pages/Error.cshtml.cs b/Pages/Error.cshtml.cs new file mode 100644 index 0000000..4980e14 --- /dev/null +++ b/Pages/Error.cshtml.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Diagnostics; + +namespace Cosmos.Chat.GPT.Pages; + +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[IgnoreAntiforgeryToken] +public class ErrorModel : PageModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + _logger.LogError($"An error occurred while processing your request."); + } +} \ No newline at end of file diff --git a/Pages/Index.razor b/Pages/Index.razor new file mode 100644 index 0000000..e404187 --- /dev/null +++ b/Pages/Index.razor @@ -0,0 +1,55 @@ +@page "/" + +Azure Cosmos DB & Azure OpenAI +
+ @if (!IsNavMenuCollapsed) + { + + } +
+ +
+
+ +@code { + + [Parameter] + public EventCallback OnChatUpdated { get; set; } = default!; + + private Session? CurrentSession; + private ChatPane? ChatPane = default; + private NavMenu? NavMenu = default; + private bool IsNavMenuCollapsed { get; set; } + + private void UpdateNavBarVisibility() + { + IsNavMenuCollapsed = !IsNavMenuCollapsed; + } + + protected override void OnInitialized() + { + NavMenu = new NavMenu(); + ChatPane = new ChatPane(); + } + + public async void LoadChatEventHandlerAsync(Session session) + { + CurrentSession = session; + + if (ChatPane is not null) + { + ChatPane.ChangeCurrentChatSession(session); + } + + // Inform blazor the UI needs updating + await InvokeAsync(StateHasChanged); + } + + public async void ForceRefreshAsync() + { + // Inform blazor the UI needs updating + await InvokeAsync(StateHasChanged); + + NavMenu?.UpdateNavMenuDisplay("Rename by Open AI"); + } +} \ No newline at end of file diff --git a/Pages/NavMenu.razor b/Pages/NavMenu.razor new file mode 100644 index 0000000..212f0a5 --- /dev/null +++ b/Pages/NavMenu.razor @@ -0,0 +1,333 @@ +@using Cosmos.Chat.GPT.Constants +@using Cosmos.Chat.GPT.Services +@inject ChatService chatService + + +
+ + + +
+ @if (_loadingComplete == true) + { +
+ +
+ } +
+
+ + + +@if (_deletePopUpOpen) +{ + + +} + + +@if (_renamePopUpOpen) +{ + + +} + + +@code { + + [Parameter] + public EventCallback OnChatClicked { get; set; } = default!; + + [Parameter] + public static List ChatSessions { get; set; } = new(); + + [Parameter] + public EventCallback OnNavBarVisibilityUpdated { get; set; } + + [Parameter] + public EventCallback OnThemeUpdated { get; set; } + + private string? _sessionId; + private string? _popUpText; + private bool _deletePopUpOpen = false; + private bool _loadingComplete; + private bool _renamePopUpOpen = false; + + public Session? CurrentSession; + + private static event EventHandler? OnNavMenuChanged; + + async private Task ToggleNavMenu() + { + await OnNavBarVisibilityUpdated.InvokeAsync(); + } + + async private Task ChangeTheme() + { + await OnThemeUpdated.InvokeAsync(); + } + + protected override void OnInitialized() + { + OnNavMenuChanged += async (o, e) => + { + await this.InvokeAsync(async () => + { + this.StateHasChanged(); + await LoadCurrentChatAsync(); + }); + }; + } + + private void OpenConfirmation(string id, string title) + { + _deletePopUpOpen = true; + _sessionId = id; + _popUpText = $"Do you want to delete the chat \"{title}\"?"; + } + + public void UpdateNavMenuDisplay(string reason = "") + { + if (OnNavMenuChanged is not null) + { + OnNavMenuChanged.Invoke(null, reason); + } + } + + private async Task OnConfirmationClose(bool isOk) + { + bool updateCurrentChat=false; + + if (CurrentSession is not null & _sessionId == CurrentSession?.SessionId) + updateCurrentChat = true; + + if (isOk) + { + _deletePopUpOpen = false; + await chatService.DeleteChatSessionAsync(_sessionId); + + _deletePopUpOpen = false; + + UpdateNavMenuDisplay("Delete"); + + if (!updateCurrentChat) + return; + + CurrentSession = new Session(); + CurrentSession.SessionId = Interface.EMPTY_SESSION; + CurrentSession.Name = string.Empty; + + if (ChatSessions is not null & ChatSessions?.Count > 0) + { + var match = ChatSessions?.FirstOrDefault(); + if (match is not null) + { + CurrentSession.SessionId = match.SessionId; + CurrentSession.Name = match.Name; + CurrentSession.TokensUsed = match.TokensUsed; + } + } + + await LoadCurrentChatAsync(); + } + + _deletePopUpOpen = false; + } + + private void OpenInput(string id, string title) + { + _renamePopUpOpen = true; + _sessionId = id; + _popUpText = title; + } + + private async Task OnInputClose(string newName) + { + if (newName!="") + { + bool updateCurrentChat = false; + + if (_sessionId == CurrentSession?.SessionId) + { + updateCurrentChat = true; + } + + await chatService.RenameChatSessionAsync(_sessionId, newName); + + _renamePopUpOpen = false; + + UpdateNavMenuDisplay("Rename"); + + if (!updateCurrentChat) + { + return; + } + + if (CurrentSession is not null) + { + CurrentSession.Name = newName; + } + await LoadCurrentChatAsync(); + } + + _renamePopUpOpen = false; + } + + private async Task NewChat() + { + await chatService.CreateNewChatSessionAsync(); + + if (ChatSessions.Count == 1) + { + CurrentSession = ChatSessions[0] with { }; + await LoadCurrentChatAsync(); + } + + UpdateNavMenuDisplay("Add"); + } + + protected override async Task OnParametersSetAsync() + { + if (_loadingComplete == true) + return; + + _loadingComplete = false; + + ChatSessions = await chatService.GetAllChatSessionsAsync(); + if (CurrentSession is not null && ChatSessions is not null & ChatSessions?.Count > 0) + { + var match = ChatSessions?.FirstOrDefault(); + if (match is not null) + { + CurrentSession.SessionId = match.SessionId; + CurrentSession.Name = match.Name; + CurrentSession.TokensUsed = match.TokensUsed; + } + } + + _loadingComplete = true; + await LoadCurrentChatAsync(); + + } + + private async Task LoadCurrentChatAsync() + { + int index = 0; + if (CurrentSession is not null & ChatSessions is not null & ChatSessions?.Count > 0) + { + index = ChatSessions?.FindIndex(s => s.SessionId == CurrentSession?.SessionId) ?? 0; + } + if (CurrentSession is null || index < 0) + { + CurrentSession = new Session(); + CurrentSession.SessionId = Interface.EMPTY_SESSION; + CurrentSession.Name = string.Empty; + + if (ChatSessions is not null & ChatSessions?.Count > 0) + { + var match = ChatSessions?.FirstOrDefault(); + if (match is not null) + { + CurrentSession.SessionId = match.SessionId; + CurrentSession.Name = match.Name; + CurrentSession.TokensUsed = match.TokensUsed; + } + } + } + + await OnChatClicked.InvokeAsync(CurrentSession); + + return 0; + } + + async private Task LoadChat(string _sessionId, string sessionName, int? tokensUsed) + { + if (ChatSessions is null) return 0; + + if (CurrentSession is null) + CurrentSession = new Session(); + + CurrentSession.SessionId = _sessionId; + CurrentSession.Name = sessionName; + CurrentSession.TokensUsed = tokensUsed; + + await LoadCurrentChatAsync(); + + return 0; + } + + private bool IsActiveSession(string _sessionId) => CurrentSession switch + { + null => true, + (Session s) when s.SessionId == _sessionId => true, + _ => false + }; + + public string SafeSubstring(string text, int maxLength) => text switch + { + null => string.Empty, + _ => text.Length > maxLength ? text.Substring(0, maxLength) + "..." : text + }; +} \ No newline at end of file diff --git a/Pages/_Host.cshtml b/Pages/_Host.cshtml new file mode 100644 index 0000000..01bd1ca --- /dev/null +++ b/Pages/_Host.cshtml @@ -0,0 +1,8 @@ +@page "/" +@namespace Cosmos.Chat.GPT.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = "_Layout"; +} + + diff --git a/Pages/_Layout.cshtml b/Pages/_Layout.cshtml new file mode 100644 index 0000000..14035c1 --- /dev/null +++ b/Pages/_Layout.cshtml @@ -0,0 +1,36 @@ +@using Microsoft.AspNetCore.Components.Web +@namespace Cosmos.Chat.GPT.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + + + + @RenderBody() + + + + \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a433c49 --- /dev/null +++ b/Program.cs @@ -0,0 +1,78 @@ +using Cosmos.Chat.GPT.Options; +using Cosmos.Chat.GPT.Services; +using Microsoft.Extensions.Options; + +var builder = WebApplication.CreateBuilder(args); + +builder.RegisterConfiguration(); +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); +builder.Services.RegisterServices(); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +await app.RunAsync(); + +static class ProgramExtensions +{ + public static void RegisterConfiguration(this WebApplicationBuilder builder) + { + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(nameof(CosmosDb))); + + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(nameof(OpenAi))); + } + + public static void RegisterServices(this IServiceCollection services) + { + services.AddSingleton((provider) => + { + var cosmosDbOptions = provider.GetRequiredService>(); + if (cosmosDbOptions is null) + { + throw new ArgumentException($"{nameof(IOptions)} was not resolved through dependency injection."); + } + else + { + return new CosmosDbService( + endpoint: cosmosDbOptions.Value?.Endpoint ?? String.Empty, + key: cosmosDbOptions.Value?.Key ?? String.Empty, + databaseName: cosmosDbOptions.Value?.Database ?? String.Empty, + containerName: cosmosDbOptions.Value?.Container ?? String.Empty + ); + } + }); + services.AddSingleton((provider) => + { + var openAiOptions = provider.GetRequiredService>(); + if (openAiOptions is null) + { + throw new ArgumentException($"{nameof(IOptions)} was not resolved through dependency injection."); + } + else + { + return new OpenAiService( + endpoint: openAiOptions.Value?.Endpoint ?? String.Empty, + key: openAiOptions.Value?.Key ?? String.Empty, + deploymentName: openAiOptions.Value?.Deployment ?? String.Empty, + maxConversationTokens: openAiOptions.Value?.MaxConversationTokens ?? String.Empty + ); + } + }); + services.AddSingleton(); + } +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..898b332 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "cosmosdbchat": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:8100", + "hotReloadProfile": "blazorwasm" + } + } +} \ No newline at end of file diff --git a/Services/ChatService.cs b/Services/ChatService.cs new file mode 100644 index 0000000..4099b66 --- /dev/null +++ b/Services/ChatService.cs @@ -0,0 +1,209 @@ +using Azure.AI.OpenAI; +using Cosmos.Chat.GPT.Constants; +using Cosmos.Chat.GPT.Models; + +namespace Cosmos.Chat.GPT.Services; + +public class ChatService +{ + /// + /// All data is cached in the _sessions List object. + /// + private static List _sessions = new(); + + private readonly CosmosDbService _cosmosDbService; + private readonly OpenAiService _openAiService; + private readonly int _maxConversationTokens; + + public ChatService(CosmosDbService cosmosDbService, OpenAiService openAiService) + { + _cosmosDbService = cosmosDbService; + _openAiService = openAiService; + + _maxConversationTokens = openAiService.MaxConversationTokens; + } + + /// + /// Returns list of chat session ids and names for left-hand nav to bind to (display Name and ChatSessionId as hidden) + /// + public async Task> GetAllChatSessionsAsync() + { + return _sessions = await _cosmosDbService.GetSessionsAsync(); + } + + /// + /// Returns the chat messages to display on the main web page when the user selects a chat from the left-hand nav + /// + public async Task> GetChatSessionMessagesAsync(string? sessionId) + { + ArgumentNullException.ThrowIfNull(sessionId); + + List chatMessages = new(); + + if (_sessions.Count == 0) + { + return Enumerable.Empty().ToList(); + } + + int index = _sessions.FindIndex(s => s.SessionId == sessionId); + + if (_sessions[index].Messages.Count == 0) + { + // Messages are not cached, go read from database + chatMessages = await _cosmosDbService.GetSessionMessagesAsync(sessionId); + + // Cache results + _sessions[index].Messages = chatMessages; + } + else + { + // Load from cache + chatMessages = _sessions[index].Messages; + } + + return chatMessages; + } + + /// + /// User creates a new Chat Session. + /// + public async Task CreateNewChatSessionAsync() + { + Session session = new(); + + _sessions.Add(session); + + await _cosmosDbService.InsertSessionAsync(session); + + } + + /// + /// Rename the Chat Ssssion from "New Chat" to the summary provided by OpenAI + /// + public async Task RenameChatSessionAsync(string? sessionId, string newChatSessionName) + { + ArgumentNullException.ThrowIfNull(sessionId); + + int index = _sessions.FindIndex(s => s.SessionId == sessionId); + + _sessions[index].Name = newChatSessionName; + + await _cosmosDbService.UpdateSessionAsync(_sessions[index]); + } + + /// + /// User deletes a chat session + /// + public async Task DeleteChatSessionAsync(string? sessionId) + { + ArgumentNullException.ThrowIfNull(sessionId); + + int index = _sessions.FindIndex(s => s.SessionId == sessionId); + + _sessions.RemoveAt(index); + + await _cosmosDbService.DeleteSessionAndMessagesAsync(sessionId); + } + + /// + /// Get a completion from _openAiService + /// + public async Task GetChatCompletionAsync(string? sessionId, string prompt) + { + ArgumentNullException.ThrowIfNull(sessionId); + + Message promptMessage = await AddPromptMessageAsync(sessionId, prompt); + + string conversation = GetChatSessionConversation(sessionId); + + (string response, int promptTokens, int responseTokens) = await _openAiService.GetChatCompletionAsync(sessionId, conversation); + + await AddPromptCompletionMessagesAsync(sessionId, promptTokens, responseTokens, promptMessage, response); + + return response; + } + + /// + /// Get current conversation from newest to oldest up to max conversation tokens and add to the prompt + /// + private string GetChatSessionConversation(string sessionId) + { + + int? tokensUsed = 0; + + List conversationBuilder = new List(); + + int index = _sessions.FindIndex(s => s.SessionId == sessionId); + + List messages = _sessions[index].Messages; + + //Start at the end of the list and work backwards + for(int i = messages.Count - 1; i >= 0; i--) + { + tokensUsed += messages[i].Tokens is null ? 0 : messages[i].Tokens; + + if(tokensUsed > _maxConversationTokens) + break; + + conversationBuilder.Add(messages[i].Text); + } + + //Invert the chat messages to put back into chronological order and output as string. + string conversation = string.Join(Environment.NewLine, conversationBuilder.Reverse()); + + return conversation; + + } + + public async Task SummarizeChatSessionNameAsync(string? sessionId, string prompt) + { + ArgumentNullException.ThrowIfNull(sessionId); + + string response = await _openAiService.SummarizeAsync(sessionId, prompt); + + await RenameChatSessionAsync(sessionId, response); + + return response; + } + + /// + /// Add user prompt to the chat session message list object and insert into the data service. + /// + private async Task AddPromptMessageAsync(string sessionId, string promptText) + { + Message promptMessage = new(sessionId, nameof(Participants.User), default, promptText); + + int index = _sessions.FindIndex(s => s.SessionId == sessionId); + + _sessions[index].AddMessage(promptMessage); + + return await _cosmosDbService.InsertMessageAsync(promptMessage); + } + + /// + /// Add user prompt and AI assistance response to the chat session message list object and insert into the data service as a transaction. + /// + private async Task AddPromptCompletionMessagesAsync(string sessionId, int promptTokens, int completionTokens, Message promptMessage, string completionText) + { + + int index = _sessions.FindIndex(s => s.SessionId == sessionId); + + //Create completion message, add to the cache + Message completionMessage = new(sessionId, nameof(Participants.Assistant), completionTokens, completionText); + _sessions[index].AddMessage(completionMessage); + + + //Update prompt message with tokens used and insert into the cache + Message updatedPromptMessage = promptMessage with { Tokens = promptTokens }; + _sessions[index].UpdateMessage(updatedPromptMessage); + + + //Update session with tokens users and udate the cache + _sessions[index].TokensUsed += updatedPromptMessage.Tokens; + _sessions[index].TokensUsed += completionMessage.Tokens; + + + await _cosmosDbService.UpsertSessionBatchAsync(updatedPromptMessage, completionMessage, _sessions[index]); + + } +} \ No newline at end of file diff --git a/Services/CosmosDbService.cs b/Services/CosmosDbService.cs new file mode 100644 index 0000000..7282d3f --- /dev/null +++ b/Services/CosmosDbService.cs @@ -0,0 +1,184 @@ +using Cosmos.Chat.GPT.Models; +using Microsoft.Azure.Cosmos; +using Microsoft.Azure.Cosmos.Fluent; + +namespace Cosmos.Chat.GPT.Services; + +/// +/// Service to access Azure Cosmos DB for NoSQL. +/// +public class CosmosDbService +{ + private readonly Container _container; + + /// + /// Creates a new instance of the service. + /// + /// Endpoint URI. + /// Account key. + /// Name of the database to access. + /// Name of the container to access. + /// Thrown when endpoint, key, databaseName, or containerName is either null or empty. + /// + /// This constructor will validate credentials and create a service client instance. + /// + public CosmosDbService(string endpoint, string key, string databaseName, string containerName) + { + ArgumentNullException.ThrowIfNullOrEmpty(endpoint); + ArgumentNullException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNullOrEmpty(databaseName); + ArgumentNullException.ThrowIfNullOrEmpty(containerName); + + + CosmosSerializationOptions options = new() + { + PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase + }; + + CosmosClient client = new CosmosClientBuilder(endpoint, key) + .WithSerializerOptions(options) + .Build(); + + Database? database = client?.GetDatabase(databaseName); + Container? container = database?.GetContainer(containerName); + + _container = container ?? + throw new ArgumentException("Unable to connect to existing Azure Cosmos DB container or database."); + } + + /// + /// Gets a list of all current chat sessions. + /// + /// List of distinct chat session items. + public async Task> GetSessionsAsync() + { + QueryDefinition query = new QueryDefinition("SELECT DISTINCT * FROM c WHERE c.type = @type") + .WithParameter("@type", nameof(Session)); + + FeedIterator response = _container.GetItemQueryIterator(query); + + List output = new(); + while (response.HasMoreResults) + { + FeedResponse results = await response.ReadNextAsync(); + output.AddRange(results); + } + return output; + } + + /// + /// Gets a list of all current chat messages for a specified session identifier. + /// + /// Chat session identifier used to filter messsages. + /// List of chat message items for the specified session. + public async Task> GetSessionMessagesAsync(string sessionId) + { + QueryDefinition query = new QueryDefinition("SELECT * FROM c WHERE c.sessionId = @sessionId AND c.type = @type") + .WithParameter("@sessionId", sessionId) + .WithParameter("@type", nameof(Message)); + + FeedIterator results = _container.GetItemQueryIterator(query); + + List output = new(); + while (results.HasMoreResults) + { + FeedResponse response = await results.ReadNextAsync(); + output.AddRange(response); + } + return output; + } + + /// + /// Creates a new chat session. + /// + /// Chat session item to create. + /// Newly created chat session item. + public async Task InsertSessionAsync(Session session) + { + PartitionKey partitionKey = new(session.SessionId); + return await _container.CreateItemAsync( + item: session, + partitionKey: partitionKey + ); + } + + /// + /// Creates a new chat message. + /// + /// Chat message item to create. + /// Newly created chat message item. + public async Task InsertMessageAsync(Message message) + { + PartitionKey partitionKey = new(message.SessionId); + return await _container.CreateItemAsync( + item: message, + partitionKey: partitionKey + ); + } + + /// + /// Updates an existing chat session. + /// + /// Chat session item to update. + /// Revised created chat session item. + public async Task UpdateSessionAsync(Session session) + { + PartitionKey partitionKey = new(session.SessionId); + return await _container.ReplaceItemAsync( + item: session, + id: session.Id, + partitionKey: partitionKey + ); + } + + /// + /// Batch create or update chat messages and session. + /// + /// Chat message and session items to create or replace. + public async Task UpsertSessionBatchAsync(params dynamic[] messages) + { + if (messages.Select(m => m.SessionId).Distinct().Count() > 1) + { + throw new ArgumentException("All items must have the same partition key."); + } + + PartitionKey partitionKey = new(messages.First().SessionId); + TransactionalBatch batch = _container.CreateTransactionalBatch(partitionKey); + foreach (var message in messages) + { + batch.UpsertItem( + item: message + ); + } + await batch.ExecuteAsync(); + } + + /// + /// Batch deletes an existing chat session and all related messages. + /// + /// Chat session identifier used to flag messages and sessions for deletion. + public async Task DeleteSessionAndMessagesAsync(string sessionId) + { + PartitionKey partitionKey = new(sessionId); + + // TODO: await container.DeleteAllItemsByPartitionKeyStreamAsync(partitionKey); + + QueryDefinition query = new QueryDefinition("SELECT c.id FROM c WHERE c.sessionId = @sessionId") + .WithParameter("@sessionId", sessionId); + + FeedIterator response = _container.GetItemQueryIterator(query); + + TransactionalBatch batch = _container.CreateTransactionalBatch(partitionKey); + while (response.HasMoreResults) + { + FeedResponse results = await response.ReadNextAsync(); + foreach (var item in results) + { + batch.DeleteItem( + id: item.Id + ); + } + } + await batch.ExecuteAsync(); + } +} \ No newline at end of file diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs new file mode 100644 index 0000000..dd86e47 --- /dev/null +++ b/Services/OpenAiService.cs @@ -0,0 +1,134 @@ +using Azure; +using Azure.AI.OpenAI; +using Cosmos.Chat.GPT.Models; + +namespace Cosmos.Chat.GPT.Services; + +/// +/// Service to access Azure OpenAI. +/// +public class OpenAiService +{ + private readonly string _deploymentName = String.Empty; + private readonly int _maxConversationTokens = default; + private readonly OpenAIClient _client; + + + //System prompt to send with user prompts to instruct the model for chat session + private readonly string _systemPrompt = @" + You are an AI assistant that helps people find information. + Provide concise answers that are polite and professional." + Environment.NewLine; + + + //System prompt to send with user prompts to instruct the model for summarization + private readonly string _summarizePrompt = @" + Summarize this prompt in one or two words to use as a label in a button on a web page" + Environment.NewLine; + + + /// + /// Gets the maximum number of tokens to limit chat conversation length. + /// + public int MaxConversationTokens + { + get => _maxConversationTokens; + } + + /// + /// Creates a new instance of the service. + /// + /// Endpoint URI. + /// Account key. + /// Name of the deployment access. + /// Maximum number of tokens per request. + /// Thrown when endpoint, key, deploymentName, or maxTokens is either null or empty. + /// + /// This constructor will validate credentials and create a HTTP client instance. + /// + public OpenAiService(string endpoint, string key, string deploymentName, string maxConversationTokens) + { + ArgumentNullException.ThrowIfNullOrEmpty(endpoint); + ArgumentNullException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNullOrEmpty(deploymentName); + ArgumentNullException.ThrowIfNullOrEmpty(maxConversationTokens); + + _deploymentName = deploymentName; + _maxConversationTokens = Int32.TryParse(maxConversationTokens, out _maxConversationTokens) ? _maxConversationTokens : 3000; + + _client = new(new Uri(endpoint), new AzureKeyCredential(key)); + } + + /// + /// Sends a prompt to the deployed OpenAI LLM model and returns the response. + /// + /// Chat session identifier for the current conversation. + /// Prompt message to send to the deployment. + /// Response from the OpenAI model along with tokens for the prompt and response. + public async Task<(string response, int promptTokens, int responseTokens)> GetChatCompletionAsync(string sessionId, string userPrompt) + { + + ChatMessage systemMessage = new(ChatRole.System, _systemPrompt); + ChatMessage userMessage = new(ChatRole.User, userPrompt); + + ChatCompletionsOptions options = new() + { + + Messages = + { + systemMessage, + userMessage + }, + User = sessionId, + MaxTokens = 256, + Temperature = 0.3f, + NucleusSamplingFactor = 0.5f, + FrequencyPenalty = 0, + PresencePenalty = 0 + }; + + Response completionsResponse = await _client.GetChatCompletionsAsync(_deploymentName, options); + + + ChatCompletions completions = completionsResponse.Value; + + return ( + response: completions.Choices[0].Message.Content, + promptTokens: completions.Usage.PromptTokens, + responseTokens: completions.Usage.CompletionTokens + ); + } + + /// + /// Sends the existing conversation to the OpenAI model and returns a two word summary. + /// + /// Chat session identifier for the current conversation. + /// Prompt conversation to send to the deployment. + /// Summarization response from the OpenAI model deployment. + public async Task SummarizeAsync(string sessionId, string userPrompt) + { + + ChatMessage systemMessage = new(ChatRole.System, _summarizePrompt); + ChatMessage userMessage = new(ChatRole.User, userPrompt); + + ChatCompletionsOptions options = new() + { + Messages = { + systemMessage, + userMessage + }, + User = sessionId, + MaxTokens = 200, + Temperature = 0.0f, + NucleusSamplingFactor = 1.0f, + FrequencyPenalty = 0, + PresencePenalty = 0 + }; + + Response completionsResponse = await _client.GetChatCompletionsAsync(_deploymentName, options); + + ChatCompletions completions = completionsResponse.Value; + + string summary = completions.Choices[0].Message.Content; + + return summary; + } +} \ No newline at end of file diff --git a/Shared/MainLayout.razor b/Shared/MainLayout.razor new file mode 100644 index 0000000..9633770 --- /dev/null +++ b/Shared/MainLayout.razor @@ -0,0 +1,4 @@ +@inherits LayoutComponentBase + +Azure Cosmos DB + ChatGPT +@Body \ No newline at end of file diff --git a/_Imports.razor b/_Imports.razor new file mode 100644 index 0000000..47b89c5 --- /dev/null +++ b/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Cosmos.Chat.GPT +@using Cosmos.Chat.GPT.Models +@using Cosmos.Chat.GPT.Shared +@using Cosmos.Chat.GPT.Components diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..6f7671e --- /dev/null +++ b/appsettings.json @@ -0,0 +1,22 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CosmosDb": { + "Endpoint": "", + "Key": "", + "Database": "chatdatabase", + "Container": "chatcontainer" + }, + "OpenAi": { + "Endpoint": "", + "Key": "", + "Deployment": "chatmodel", + "MaxConversationTokens": "2000" + } +} \ No newline at end of file diff --git a/azuredeploy.bicep b/azuredeploy.bicep new file mode 100644 index 0000000..679fff7 --- /dev/null +++ b/azuredeploy.bicep @@ -0,0 +1,217 @@ +@description('Location where all resources will be deployed. This value defaults to the **South Central US** region.') +@allowed([ + 'South Central US' + 'East US' +]) +param location string = 'South Central US' + +@description(''' +Unique name for the chat application. The name is required to be unique as it will be used as a prefix for the names of these resources: +- Azure Cosmos DB +- Azure App Service +- Azure OpenAI +The name defaults to a unique string generated from the resource group identifier. +''') +param name string = uniqueString(resourceGroup().id) + +@description('Boolean indicating whether Azure Cosmos DB free tier should be used for the account. This defaults to **true**.') +param cosmosDbEnableFreeTier bool = true + +@description('Specifies the SKU for the Azure App Service plan. Defaults to **F1**') +@allowed([ + 'F1' + 'D1' + 'B1' +]) +param appServiceSku string = 'F1' + +@description('Specifies the SKU for the Azure OpenAI resource. Defaults to **S0**') +@allowed([ + 'S0' +]) +param openAiSku string = 'S0' + +@description('Git repository URL for the chat application. This defaults to the [`azure-samples/cosmosdb-chatgpt`](https://github.com/azure-samples/cosmosdb-chatgpt) repository.') +param appGitRepository string = 'https://github.com/azure-samples/cosmosdb-chatgpt.git' + +@description('Git repository branch for the chat application. This defaults to the [**main** branch of the `azure-samples/cosmosdb-chatgpt`](https://github.com/azure-samples/cosmosdb-chatgpt/tree/main) repository.') +param appGetRepositoryBranch string = 'main' + +var openAiSettings = { + name: '${name}-openai' + sku: openAiSku + maxConversationTokens: '2000' + model: { + name: 'gpt-35-turbo' + version: '0301' + deployment: { + name: 'chatmodel' + } + } +} + +var cosmosDbSettings = { + name: '${name}-cosmos-nosql' + enableFreeTier: cosmosDbEnableFreeTier + database: { + name: 'chatdatabase' + } + container: { + name: 'chatcontainer' + throughput: 400 + } +} + +var appServiceSettings = { + plan: { + name: '${name}-web-plan' + } + web: { + name: '${name}-web' + git: { + repo: appGitRepository + branch: appGetRepositoryBranch + } + } + sku: appServiceSku +} + +resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = { + name: cosmosDbSettings.name + location: location + kind: 'GlobalDocumentDB' + properties: { + consistencyPolicy: { + defaultConsistencyLevel: 'Session' + } + databaseAccountOfferType: 'Standard' + enableFreeTier: cosmosDbSettings.enableFreeTier + locations: [ + { + failoverPriority: 0 + isZoneRedundant: false + locationName: location + } + ] + } +} + +resource cosmosDbDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-08-15' = { + parent: cosmosDbAccount + name: cosmosDbSettings.database.name + properties: { + resource: { + id: cosmosDbSettings.database.name + } + } +} + +resource cosmosDbContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2022-08-15' = { + parent: cosmosDbDatabase + name: cosmosDbSettings.container.name + properties: { + resource: { + id: cosmosDbSettings.container.name + partitionKey: { + paths: [ + '/sessionId' + ] + kind: 'Hash' + version: 2 + } + indexingPolicy: { + indexingMode: 'Consistent' + automatic: true + includedPaths: [ + { + path: '/sessionId/?' + } + { + path: '/type/?' + } + ] + excludedPaths: [ + { + path: '/*' + } + ] + } + } + options: { + throughput: cosmosDbSettings.container.throughput + } + } +} + +resource openAiAccount 'Microsoft.CognitiveServices/accounts@2022-12-01' = { + name: openAiSettings.name + location: location + sku: { + name: openAiSettings.sku + } + kind: 'OpenAI' + properties: { + customSubDomainName: openAiSettings.name + publicNetworkAccess: 'Enabled' + } +} + +resource openAiModelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = { + parent: openAiAccount + name: openAiSettings.model.deployment.name + properties: { + model: { + format: 'OpenAI' + name: openAiSettings.model.name + version: openAiSettings.model.version + } + scaleSettings: { + scaleType: 'Standard' + } + } +} + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { + name: appServiceSettings.plan.name + location: location + sku: { + name: appServiceSettings.sku + } +} + +resource appServiceWeb 'Microsoft.Web/sites@2022-03-01' = { + name: appServiceSettings.web.name + location: location + properties: { + serverFarmId: appServicePlan.id + httpsOnly: true + } +} + +resource appServiceWebSettings 'Microsoft.Web/sites/config@2022-03-01' = { + parent: appServiceWeb + name: 'appsettings' + kind: 'string' + properties: { + COSMOSDB__ENDPOINT: cosmosDbAccount.properties.documentEndpoint + COSMOSDB__KEY: cosmosDbAccount.listKeys().primaryMasterKey + COSMOSDB__DATABASE: cosmosDbDatabase.name + COSMOSDB__CONTAINER: cosmosDbContainer.name + OPENAI__ENDPOINT: openAiAccount.properties.endpoint + OPENAI__KEY: openAiAccount.listKeys().key1 + OPENAI__DEPLOYMENT: openAiModelDeployment.name + OPENAI__MAXCONVERSATIONTOKENS: openAiSettings.maxConversationTokens + } +} + +resource appServiceWebDeployment 'Microsoft.Web/sites/sourcecontrols@2021-03-01' = { + parent: appServiceWeb + name: 'web' + properties: { + repoUrl: appServiceSettings.web.git.repo + branch: appServiceSettings.web.git.branch + isManualIntegration: true + } +} + +output deployedUrl string = appServiceWeb.properties.defaultHostName diff --git a/azuredeploy.json b/azuredeploy.json new file mode 100644 index 0000000..5317c0c --- /dev/null +++ b/azuredeploy.json @@ -0,0 +1,285 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.16.2.56959", + "templateHash": "17621165066116285839" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "South Central US", + "allowedValues": [ + "South Central US", + "East US" + ], + "metadata": { + "description": "Location where all resources will be deployed. This value defaults to the **South Central US** region." + } + }, + "name": { + "type": "string", + "defaultValue": "[uniqueString(resourceGroup().id)]", + "metadata": { + "description": "Unique name for the chat application. The name is required to be unique as it will be used as a prefix for the names of these resources:\n- Azure Cosmos DB\n- Azure App Service\n- Azure OpenAI\nThe name defaults to a unique string generated from the resource group identifier.\n" + } + }, + "cosmosDbEnableFreeTier": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Boolean indicating whether Azure Cosmos DB free tier should be used for the account. This defaults to **true**." + } + }, + "appServiceSku": { + "type": "string", + "defaultValue": "F1", + "allowedValues": [ + "F1", + "D1", + "B1" + ], + "metadata": { + "description": "Specifies the SKU for the Azure App Service plan. Defaults to **F1**" + } + }, + "openAiSku": { + "type": "string", + "defaultValue": "S0", + "allowedValues": [ + "S0" + ], + "metadata": { + "description": "Specifies the SKU for the Azure OpenAI resource. Defaults to **S0**" + } + }, + "appGitRepository": { + "type": "string", + "defaultValue": "https://github.com/azure-samples/cosmosdb-chatgpt.git", + "metadata": { + "description": "Git repository URL for the chat application. This defaults to the [`azure-samples/cosmosdb-chatgpt`](https://github.com/azure-samples/cosmosdb-chatgpt) repository." + } + }, + "appGetRepositoryBranch": { + "type": "string", + "defaultValue": "main", + "metadata": { + "description": "Git repository branch for the chat application. This defaults to the [**main** branch of the `azure-samples/cosmosdb-chatgpt`](https://github.com/azure-samples/cosmosdb-chatgpt/tree/main) repository." + } + } + }, + "variables": { + "openAiSettings": { + "name": "[format('{0}-openai', parameters('name'))]", + "sku": "[parameters('openAiSku')]", + "maxConversationTokens": "2000", + "model": { + "name": "gpt-35-turbo", + "version": "0301", + "deployment": { + "name": "chatmodel" + } + } + }, + "cosmosDbSettings": { + "name": "[format('{0}-cosmos-nosql', parameters('name'))]", + "enableFreeTier": "[parameters('cosmosDbEnableFreeTier')]", + "database": { + "name": "chatdatabase" + }, + "container": { + "name": "chatcontainer", + "throughput": 400 + } + }, + "appServiceSettings": { + "plan": { + "name": "[format('{0}-web-plan', parameters('name'))]" + }, + "web": { + "name": "[format('{0}-web', parameters('name'))]", + "git": { + "repo": "[parameters('appGitRepository')]", + "branch": "[parameters('appGetRepositoryBranch')]" + } + }, + "sku": "[parameters('appServiceSku')]" + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2022-08-15", + "name": "[variables('cosmosDbSettings').name]", + "location": "[parameters('location')]", + "kind": "GlobalDocumentDB", + "properties": { + "consistencyPolicy": { + "defaultConsistencyLevel": "Session" + }, + "databaseAccountOfferType": "Standard", + "enableFreeTier": "[variables('cosmosDbSettings').enableFreeTier]", + "locations": [ + { + "failoverPriority": 0, + "isZoneRedundant": false, + "locationName": "[parameters('location')]" + } + ] + } + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2022-08-15", + "name": "[format('{0}/{1}', variables('cosmosDbSettings').name, variables('cosmosDbSettings').database.name)]", + "properties": { + "resource": { + "id": "[variables('cosmosDbSettings').database.name]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbSettings').name)]" + ] + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2022-08-15", + "name": "[format('{0}/{1}/{2}', variables('cosmosDbSettings').name, variables('cosmosDbSettings').database.name, variables('cosmosDbSettings').container.name)]", + "properties": { + "resource": { + "id": "[variables('cosmosDbSettings').container.name]", + "partitionKey": { + "paths": [ + "/sessionId" + ], + "kind": "Hash", + "version": 2 + }, + "indexingPolicy": { + "indexingMode": "Consistent", + "automatic": true, + "includedPaths": [ + { + "path": "/sessionId/?" + }, + { + "path": "/type/?" + } + ], + "excludedPaths": [ + { + "path": "/*" + } + ] + } + }, + "options": { + "throughput": "[variables('cosmosDbSettings').container.throughput]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', variables('cosmosDbSettings').name, variables('cosmosDbSettings').database.name)]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2022-12-01", + "name": "[variables('openAiSettings').name]", + "location": "[parameters('location')]", + "sku": { + "name": "[variables('openAiSettings').sku]" + }, + "kind": "OpenAI", + "properties": { + "customSubDomainName": "[variables('openAiSettings').name]", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2022-12-01", + "name": "[format('{0}/{1}', variables('openAiSettings').name, variables('openAiSettings').model.deployment.name)]", + "properties": { + "model": { + "format": "OpenAI", + "name": "[variables('openAiSettings').model.name]", + "version": "[variables('openAiSettings').model.version]" + }, + "scaleSettings": { + "scaleType": "Standard" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', variables('openAiSettings').name)]" + ] + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2022-03-01", + "name": "[variables('appServiceSettings').plan.name]", + "location": "[parameters('location')]", + "sku": { + "name": "[variables('appServiceSettings').sku]" + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2022-03-01", + "name": "[variables('appServiceSettings').web.name]", + "location": "[parameters('location')]", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServiceSettings').plan.name)]", + "httpsOnly": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('appServiceSettings').plan.name)]" + ] + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2022-03-01", + "name": "[format('{0}/{1}', variables('appServiceSettings').web.name, 'appsettings')]", + "kind": "string", + "properties": { + "COSMOSDB__ENDPOINT": "[reference(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbSettings').name), '2022-08-15').documentEndpoint]", + "COSMOSDB__KEY": "[listKeys(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbSettings').name), '2022-08-15').primaryMasterKey]", + "COSMOSDB__DATABASE": "[variables('cosmosDbSettings').database.name]", + "COSMOSDB__CONTAINER": "[variables('cosmosDbSettings').container.name]", + "OPENAI__ENDPOINT": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('openAiSettings').name), '2022-12-01').endpoint]", + "OPENAI__KEY": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('openAiSettings').name), '2022-12-01').key1]", + "OPENAI__DEPLOYMENT": "[variables('openAiSettings').model.deployment.name]", + "OPENAI__MAXCONVERSATIONTOKENS": "[variables('openAiSettings').maxConversationTokens]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('appServiceSettings').web.name)]", + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmosDbSettings').name)]", + "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', variables('cosmosDbSettings').name, variables('cosmosDbSettings').database.name, variables('cosmosDbSettings').container.name)]", + "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', variables('cosmosDbSettings').name, variables('cosmosDbSettings').database.name)]", + "[resourceId('Microsoft.CognitiveServices/accounts', variables('openAiSettings').name)]", + "[resourceId('Microsoft.CognitiveServices/accounts/deployments', variables('openAiSettings').name, variables('openAiSettings').model.deployment.name)]" + ] + }, + { + "type": "Microsoft.Web/sites/sourcecontrols", + "apiVersion": "2021-03-01", + "name": "[format('{0}/{1}', variables('appServiceSettings').web.name, 'web')]", + "properties": { + "repoUrl": "[variables('appServiceSettings').web.git.repo]", + "branch": "[variables('appServiceSettings').web.git.branch]", + "isManualIntegration": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('appServiceSettings').web.name)]" + ] + } + ], + "outputs": { + "deployedUrl": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Web/sites', variables('appServiceSettings').web.name), '2022-03-01').defaultHostName]" + } + } +} \ No newline at end of file diff --git a/cosmoschatgpt.csproj b/cosmoschatgpt.csproj new file mode 100644 index 0000000..4168240 --- /dev/null +++ b/cosmoschatgpt.csproj @@ -0,0 +1,14 @@ + + + net7.0 + enable + enable + Cosmos.Chat.GPT + + + + + + + + \ No newline at end of file diff --git a/cosmoschatgpt.sln b/cosmoschatgpt.sln new file mode 100644 index 0000000..09eeab0 --- /dev/null +++ b/cosmoschatgpt.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cosmoschatgpt", "cosmoschatgpt.csproj", "{AB2C218B-2B3F-46CA-A50F-EF5648363AE5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB2C218B-2B3F-46CA-A50F-EF5648363AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB2C218B-2B3F-46CA-A50F-EF5648363AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB2C218B-2B3F-46CA-A50F-EF5648363AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB2C218B-2B3F-46CA-A50F-EF5648363AE5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA6CDB32-FE1E-4637-AF09-B7646FDBD005} + EndGlobalSection +EndGlobal diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..586a213 --- /dev/null +++ b/readme.md @@ -0,0 +1,85 @@ +--- +page_type: sample +languages: +- csharp +products: +- azure-cosmos-db +- azure-openai +name: Sample chat app using Azure Cosmos DB for NoSQL and Azure OpenAI +urlFragment: chat-app +description: Sample application that implements multiple chat threads using the Azure OpenAI "text-davinci-003" model and Azure Cosmos DB for NoSQL for storage. +azureDeploy: https://raw.githubusercontent.com/azure-samples/cosmosdb-chatgpt/main/azuredeploy.json +--- + +# Azure Cosmos DB + OpenAI ChatGPT + +This sample application combines Azure Cosmos DB with OpenAI ChatGPT with a Blazor Server front-end for an intelligent chat bot application that shows off how you can build a +simple chat application with OpenAi ChatGPT and Azure Cosmos DB. + +![Cosmos DB + ChatGPT user interface](screenshot.png) + +## Features + +This application has individual chat sessions which are displayed and can be selected in the left-hand nav. Clicking on a session will show the messages that contain +human prompts and AI completions. + +When a new prompt is sent to the Azure OpenAI service, some of the conversation history is sent with it. This provides context allowing ChatGPT to respond +as though it is having a conversation. The length of this conversation history can be configured from appsettings.json +with the `OpenAiMaxTokens` value that is then translated to a maximum conversation string length that is 1/2 of this value. + +Please note that the "text-davinci-003" model used by this sample has a maximum of 4096 tokens. Token are used in both the request and reponse from the service. Overriding the maxConversationLength to values approaching maximum token value could result in completions that contain little to no text if all of it has been used in the request. + +The history for all prompts and completions for each chat session is stored in Azure Cosmos DB. Deleting a chat session in the UI will delete it's corresponding data as well. + +The application will also summarize the name of the chat session by asking ChatGPT to provide a one or two word summary of the first prompt. This allows you to easily +identity different chat sessions. + +Please note this is a sample application. It is intended to demonstrate how to use Azure Cosmos DB and Azure OpenAI ChatGPT together. It is not intended for production or other large scale use + + +## Getting Started + +### Prerequisites + +- Azure Subscription +- Subscription access to Azure OpenAI service. Start here to [Request Acces to Azure OpenAI Service](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUOFA5Qk1UWDRBMjg0WFhPMkIzTzhKQ1dWNyQlQCN0PWcu) +- Visual Studio, VS Code, or some editor if you want to edit or view the source for this sample. + + +### Installation + +1. Fork this repository to your own GitHub account. +1. Depending on whether you deploy using the ARM Template or Bicep, modify this variable in one of those files to point to your fork of this repository, "webSiteRepository": "https://github.com/Azure-Samples/cosmosdb-chatgpt.git" +1. If using the Deploy to Azure button below, also modify this README.md file to change the path for the Deploy To Azure button to your local repository. +1. If you deploy this application without making either of these changes, you can update the repository by disconnecting and connecting an external git repository pointing to your fork. + + +The provided ARM or Bicep Template will provision the following resources: +1. Azure Cosmos DB account with database and container at 400 RU/s. This can optionally be configured to run on the Cosmos DB free tier if available for your subscription. +1. Azure App service. This will be configured for CI/CD to your forked GitHub repository. This service can also be configured to run on App Service free tier. +1. Azure Open AI account. You must also specify a name for the deployment of the "text-davinci-003" model which is used by this application. + +Note: You must have access to Azure Open AI service from your subscription before attempting to deploy this application. + +All connection information for Azure Cosmos DB and Open AI is zero-touch and injected as environment variables in the Azure App Service instance at deployment time. + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fcosmosdb-chatgpt%2Fmain%2Fazuredeploy.json) + + +### Quickstart + +1. After deployment, go to the resource group for your deployment and open the Azure App Service in the Azure Portal. Click the web url to launch the website. +1. Click + New Chat to create a new chat session. +1. Type your question in the text box and press Enter. + + +## Clean up + +To remove all the resources used by this sample, you must first manually delete the deployed model within the Azure AI service. You can then delete the resource group for your deployment. This will delete all remaining resources. + +## Resources + +- [Azure Cosmos DB + Azure OpenAI ChatGPT Blog Post Announcement](https://devblogs.microsoft.com/cosmosdb/) +- [Azure Cosmos DB Free Trial](https://aka.ms/TryCosmos) +- [Open AI Platform documentation](https://platform.openai.com/docs/introduction/overview) +- [Azure Open AI Service documentation](https://learn.microsoft.com/azure/cognitive-services/openai/) diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d0e8bcc4bc7ba874819e7589cd1897af461b0228 GIT binary patch literal 85985 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 }U_bQqLWcx;%K;3>l| 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 NyZ+w_{7+ed{{#z2X4L=y literal 0 HcmV?d00001 diff --git a/wwwroot/js/site.js b/wwwroot/js/site.js new file mode 100644 index 0000000..f0259e0 --- /dev/null +++ b/wwwroot/js/site.js @@ -0,0 +1,9 @@ +function scrollToLastMessage() +{ + if (document.getElementById('MessagesInChatdiv')) { + var elem = document.getElementById('MessagesInChatdiv'); + elem.scrollTop = elem.scrollHeight; + return true; + } + return false; +} \ No newline at end of file