From 1b6b395148f89e66ca84db47243146a89621fa94 Mon Sep 17 00:00:00 2001 From: Teresa Hoang <125500434+teresaqhoang@users.noreply.github.com> Date: Thu, 14 Sep 2023 13:19:41 -0400 Subject: [PATCH] Querying chat messages with partition key (#346) ### Motivation and Context Some changes to complement [partition key change](https://github.com/microsoft/chat-copilot/pull/240/files#diff-f9c37b8a51287ee8b7d42811b02d2ee33e4bcb565a155b358216519494eb38a9): - Documentation updates to make partition key requirements more clear - Using chatId as partition key when updating chat message state - Using partition key on upsert ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/chat-copilot/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/chat-copilot/blob/main/CONTRIBUTING.md#development-scripts) raises no violations ~- [ ] All unit tests pass, and I have added new tests where possible~ - [x] I didn't break anyone :smile: --- webapi/README.md | 23 +++++++++++++++++++++-- webapi/Skills/ChatSkills/ChatSkill.cs | 16 ++++++++++------ webapi/Storage/CosmosDbContext.cs | 4 ++-- webapi/Storage/IRepository.cs | 4 ++-- webapi/Storage/IStorageEntity.cs | 6 ++++++ webapi/appsettings.json | 2 ++ 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/webapi/README.md b/webapi/README.md index 392b17c6d..90e01607d 100644 --- a/webapi/README.md +++ b/webapi/README.md @@ -3,9 +3,11 @@ This directory contains the source code for Chat Copilot's backend web API service. The front end web application component can be found in the [webapp/](../webapp/) directory. ## Running the Chat Copilot sample + To configure and run either the full Chat Copilot application or only the backend API, please view the [main instructions](../README.md#instructions). # (Under Development) + The following material is under development and may not be complete or accurate. ## Visual Studio Code @@ -20,14 +22,13 @@ The following material is under development and may not be complete or accurate. 2. In Solution Explorer, right-click on `CopilotChatWebApi` and select `Set as Startup Project`. 3. Start debugging by pressing `F5` or selecting the menu item `Debug`->`Start Debugging`. -1. **(Optional)** To enable support for uploading image file formats such as png, jpg and tiff, there are two options within the `OcrSupport` section of `./appsettings.json`, the Tesseract open source library and Azure Form Recognizer. +4. **(Optional)** To enable support for uploading image file formats such as png, jpg and tiff, there are two options within the `OcrSupport` section of `./appsettings.json`, the Tesseract open source library and Azure Form Recognizer. - **Tesseract** we have included the [Tesseract](https://www.nuget.org/packages/Tesseract) nuget package. - You will need to obtain one or more [tessdata language data files](https://github.com/tesseract-ocr/tessdata) such as `eng.traineddata` and add them to your `./data` directory or the location specified in the `OcrSupport:Tesseract:FilePath` location in `./appsettings.json`. - Set the `Copy to Output Directory` value to `Copy if newer`. - **Azure Form Recognizer** we have included the [Azure.AI.FormRecognizer](https://www.nuget.org/packages/Azure.AI.FormRecognizer) nuget package. - You will need to obtain an [Azure Form Recognizer](https://azure.microsoft.com/en-us/services/form-recognizer/) resource and add the `OcrSupport:AzureFormRecognizer:Endpoint` and `OcrSupport:AzureFormRecognizer:Key` values to the `./appsettings.json` file. - ## Enabling Sequential Planner If you want to use SequentialPlanner (multi-step) instead ActionPlanner (single-step), we recommend using `gpt-4` or `gpt-3.5-turbo` as the planner model. **SequentialPlanner works best with `gpt-4`.** Using `gpt-3.5-turbo` will require using a relevancy filter. @@ -49,6 +50,24 @@ To enable sequential planner, \* The `RelevancyThreshold` is a number from 0 to 1 that represents how similar a goal is to a function's name/description/inputs. You want to tune that value when using SequentialPlanner to help keep things scoped while not missing on on things that are relevant or including too many things that really aren't. `0.75` is an arbitrary threshold and we recommend developers play around with this number to see what best fits their scenarios. 1. Restart the `webapi` - Copilot Chat should be now running locally with SequentialPlanner. +## (Optional) Enabling Cosmos Chat Store. + +[Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) can be used as a persistent chat store for Chat Copilot. Chat stores are used for storing chat sessions, participants, and messages. + +### Prerequisites + +#### 1. Containers and PartitionKeys + +In an effort to optimize performance, each container must be created with a specific partition key: +| Store | ContainerName | PartitionKey | +| ----- | ------------- | ------------ | +| Chat Sessions | chatsessions | /id (default) | +| Chat Messages | chatmessages | /chatId | +| Chat Memory Sources | chatmemorysources | /chatId | +| Chat Partipants | chatparticipants | /userId | + +> For existing customers using CosmosDB before [Release 0.3](https://github.com/microsoft/chat-copilot/releases/tag/0.3), our recommendation is to remove the existing Cosmos DB containers and redeploy to realize the performance update related to the partition schema. To preserve existing chats, containers can be migrated as described [here](https://learn.microsoft.com/en-us/azure/cosmos-db/intra-account-container-copy#copy-a-container). + ## (Optional) Enabling the Qdrant Memory Store By default, the service uses an in-memory volatile memory store that, when the service stops or restarts, forgets all memories. diff --git a/webapi/Skills/ChatSkills/ChatSkill.cs b/webapi/Skills/ChatSkills/ChatSkill.cs index a9c0a0e41..ce00eff7e 100644 --- a/webapi/Skills/ChatSkills/ChatSkill.cs +++ b/webapi/Skills/ChatSkills/ChatSkill.cs @@ -302,7 +302,7 @@ public async Task ChatAsync( if (!string.IsNullOrWhiteSpace(planJson) && !string.IsNullOrEmpty(messageId)) { - await this.UpdateChatMessageContentAsync(planJson, messageId, cancellationToken); + await this.UpdateChatMessageContentAsync(planJson, messageId, chatId, cancellationToken); } ChatMessage chatMessage; @@ -634,14 +634,18 @@ private async Task SaveNewResponseAsync(string response, string pro /// Updates previously saved response in the chat history. /// /// Updated response from the chat. - /// The chat message ID + /// The chat message ID. + /// The chat ID that's used as the partition Id. /// The cancellation token. - private async Task UpdateChatMessageContentAsync(string updatedResponse, string messageId, CancellationToken cancellationToken) + private async Task UpdateChatMessageContentAsync(string updatedResponse, string messageId, string chatId, CancellationToken cancellationToken) { - // Make sure the chat exists. - var chatMessage = await this._chatMessageRepository.FindByIdAsync(messageId); - chatMessage.Content = updatedResponse; + ChatMessage? chatMessage = null; + if (!await this._chatMessageRepository.TryFindByIdAsync(messageId, chatId, callback: v => chatMessage = v)) + { + throw new ArgumentException($"Chat message {messageId} does not exist."); + } + chatMessage!.Content = updatedResponse; await this._chatMessageRepository.UpsertAsync(chatMessage); } diff --git a/webapi/Storage/CosmosDbContext.cs b/webapi/Storage/CosmosDbContext.cs index f3ccb8bc5..2162b7c4e 100644 --- a/webapi/Storage/CosmosDbContext.cs +++ b/webapi/Storage/CosmosDbContext.cs @@ -59,7 +59,7 @@ public async Task CreateAsync(T entity) throw new ArgumentOutOfRangeException(nameof(entity.Id), "Entity Id cannot be null or empty."); } - await this._container.CreateItemAsync(entity); + await this._container.CreateItemAsync(entity, new PartitionKey(entity.Partition)); } /// @@ -100,7 +100,7 @@ public async Task UpsertAsync(T entity) throw new ArgumentOutOfRangeException(nameof(entity.Id), "Entity Id cannot be null or empty."); } - await this._container.UpsertItemAsync(entity); + await this._container.UpsertItemAsync(entity, new PartitionKey(entity.Partition)); } public void Dispose() diff --git a/webapi/Storage/IRepository.cs b/webapi/Storage/IRepository.cs index 7e564afc8..104f65bc9 100644 --- a/webapi/Storage/IRepository.cs +++ b/webapi/Storage/IRepository.cs @@ -32,7 +32,7 @@ public interface IRepository where T : IStorageEntity /// Finds an entity by its id. /// /// Id of the entity. - /// Partition of the entity. + /// Partition key value of the entity. /// An entity Task FindByIdAsync(string id, string partition); @@ -40,7 +40,7 @@ public interface IRepository where T : IStorageEntity /// Tries to find an entity by its id. /// /// Id of the entity. - /// Partition of the entity. + /// Partition key value of the entity. /// The entity delegate. Note async methods don't support ref or out parameters. /// True if the entity was found, false otherwise. Task TryFindByIdAsync(string id, string partition, Action callback); diff --git a/webapi/Storage/IStorageEntity.cs b/webapi/Storage/IStorageEntity.cs index 91d5880ef..33f60646f 100644 --- a/webapi/Storage/IStorageEntity.cs +++ b/webapi/Storage/IStorageEntity.cs @@ -4,7 +4,13 @@ namespace CopilotChat.WebApi.Storage; public interface IStorageEntity { + /// + /// Unique ID of the entity. + /// string Id { get; set; } + /// + /// Partition key value. + /// string Partition { get; } } diff --git a/webapi/appsettings.json b/webapi/appsettings.json index 349371576..271041a47 100644 --- a/webapi/appsettings.json +++ b/webapi/appsettings.json @@ -121,6 +121,8 @@ }, "Cosmos": { "Database": "CopilotChat", + // IMPORTANT: Each container requires a specific partition key. Ensure these are set correctly in your CosmosDB instance. + // See details at ./README.md#1-containers-and-partitionkeys "ChatSessionsContainer": "chatsessions", "ChatMessagesContainer": "chatmessages", "ChatMemorySourcesContainer": "chatmemorysources",