From 036192830f5f4e58deba25a5c6d250ac9701f11d Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Thu, 28 Sep 2023 21:03:45 -0700 Subject: [PATCH 1/8] Update WebApi routes for security and RESTful best practices --- ...Controller.cs => ChatArchiveController.cs} | 66 ++++++++----------- webapi/Controllers/ChatController.cs | 58 ++++++++-------- webapi/Controllers/ChatHistoryController.cs | 59 ++++++----------- webapi/Controllers/ChatMemoryController.cs | 7 +- .../Controllers/ChatParticipantController.cs | 21 +++--- webapi/Controllers/DocumentController.cs | 35 ++++------ webapi/Controllers/MaintenanceController.cs | 5 +- webapi/Controllers/ServiceInfoController.cs | 12 ++-- .../Extensions/IAsyncEnumerableExtensions.cs | 2 +- webapi/Extensions/SemanticKernelExtensions.cs | 18 ++--- webapi/Extensions/ServiceExtensions.cs | 2 - webapi/Models/Request/EditChatParameters.cs | 5 -- .../Response/{Bot.cs => ChatArchive.cs} | 14 ++-- ...onfig.cs => ChatArchiveEmbeddingConfig.cs} | 4 +- .../Models/Response/ServiceOptionsResponse.cs | 9 +++ ...emaOptions.cs => ChatArchiveSchemaInfo.cs} | 10 ++- webapi/Services/AzureContentSafety.cs | 39 ++--------- webapi/Services/IContentSafetyService.cs | 18 +---- webapi/Services/MaintenanceMiddleware.cs | 4 +- .../ChatSkills/SemanticMemoryRetriever.cs | 5 +- webapi/appsettings.json | 7 +- webapp/src/components/views/BackendProbe.tsx | 2 +- webapp/src/libs/hooks/useChat.ts | 4 +- webapp/src/libs/models/ServiceOptions.ts | 1 + webapp/src/libs/services/BotService.ts | 4 +- webapp/src/libs/services/ChatService.ts | 40 +++++------ .../libs/services/DocumentImportService.ts | 5 +- webapp/src/redux/features/app/AppState.ts | 2 +- 28 files changed, 184 insertions(+), 274 deletions(-) rename webapi/Controllers/{BotController.cs => ChatArchiveController.cs} (75%) rename webapi/Models/Response/{Bot.cs => ChatArchive.cs} (78%) rename webapi/Models/Response/{BotEmbeddingConfig.cs => ChatArchiveEmbeddingConfig.cs} (89%) rename webapi/Options/{BotSchemaOptions.cs => ChatArchiveSchemaInfo.cs} (59%) diff --git a/webapi/Controllers/BotController.cs b/webapi/Controllers/ChatArchiveController.cs similarity index 75% rename from webapi/Controllers/BotController.cs rename to webapi/Controllers/ChatArchiveController.cs index 36493df80..87d83e8e8 100644 --- a/webapi/Controllers/BotController.cs +++ b/webapi/Controllers/ChatArchiveController.cs @@ -21,36 +21,33 @@ namespace CopilotChat.WebApi.Controllers; [ApiController] -public class BotController : ControllerBase +public class ChatArchiveController : ControllerBase { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ISemanticMemoryClient _memoryClient; private readonly ChatSessionRepository _chatRepository; private readonly ChatMessageRepository _chatMessageRepository; private readonly ChatParticipantRepository _chatParticipantRepository; - private readonly BotEmbeddingConfig _embeddingConfig; - private readonly BotSchemaOptions _botSchemaOptions; + private readonly ChatArchiveEmbeddingConfig _embeddingConfig; private readonly PromptsOptions _promptOptions; /// - /// The constructor of BotController. + /// Constructor. /// /// Memory client. /// The chat session repository. /// The chat message repository. /// The chat participant repository. - /// The bot schema options. /// The document memory options. /// The logger. - public BotController( + public ChatArchiveController( ISemanticMemoryClient memoryClient, ChatSessionRepository chatRepository, ChatMessageRepository chatMessageRepository, ChatParticipantRepository chatParticipantRepository, - BotEmbeddingConfig embeddingConfig, - IOptions botSchemaOptions, + ChatArchiveEmbeddingConfig embeddingConfig, IOptions promptOptions, - ILogger logger) + ILogger logger) { this._memoryClient = memoryClient; this._logger = logger; @@ -58,69 +55,64 @@ public BotController( this._chatMessageRepository = chatMessageRepository; this._chatParticipantRepository = chatParticipantRepository; this._embeddingConfig = embeddingConfig; - this._botSchemaOptions = botSchemaOptions.Value; this._promptOptions = promptOptions.Value; } /// - /// Download a bot. + /// Download a chat archive. /// - /// The Semantic Kernel instance. - /// The chat id to be downloaded. - /// The serialized Bot object of the chat id. + /// The ID of chat to be downloaded. + /// Cancellation token. + /// The serialized chat archive object of the chat id. [HttpGet] - [ActionName("DownloadAsync")] - [Route("bot/download/{chatId:guid}")] + [Route("chats/archives/{chatId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = AuthPolicyName.RequireChatParticipant)] - public async Task> DownloadAsync(Guid chatId, CancellationToken cancellationToken = default) + public async Task> DownloadAsync(Guid chatId, CancellationToken cancellationToken = default) { - this._logger.LogDebug("Received call to download a bot"); + this._logger.LogDebug("Received call to download a chat archive"); - var memory = await this.CreateBotAsync(chatId, cancellationToken); + var memory = await this.CreateChatArchiveAsync(chatId, cancellationToken); return this.Ok(memory); } /// - /// Prepare the bot information of a given chat. + /// Prepare a chat archive. /// - /// The semantic kernel object. - /// The chat id of the bot - /// A Bot object that represents the chat session. - private async Task CreateBotAsync(Guid chatId, CancellationToken cancellationToken) + /// The chat id of the chat archive + /// Cancellation token. + /// A ChatArchive object that represents the chat session. + private async Task CreateChatArchiveAsync(Guid chatId, CancellationToken cancellationToken) { var chatIdString = chatId.ToString(); - var bot = new Bot + var chatArchive = new ChatArchive { - // get the bot schema version - Schema = this._botSchemaOptions, - - // get the embedding configuration + // Get embedding configuration EmbeddingConfigurations = this._embeddingConfig, }; // get the chat title ChatSession chat = await this._chatRepository.FindByIdAsync(chatIdString); - bot.ChatTitle = chat.Title; + chatArchive.ChatTitle = chat.Title; // get the system description - bot.SystemDescription = chat.SystemDescription; + chatArchive.SystemDescription = chat.SystemDescription; // get the chat history - bot.ChatHistory = await this.GetAllChatMessagesAsync(chatIdString); + chatArchive.ChatHistory = await this.GetAllChatMessagesAsync(chatIdString); foreach (var memory in this._promptOptions.MemoryMap.Keys) { - bot.Embeddings.Add( + chatArchive.Embeddings.Add( memory, await this.GetMemoryRecordsAndAppendToEmbeddingsAsync(chatIdString, memory, cancellationToken)); } // get the document memory collection names (global scope) - bot.DocumentEmbeddings.Add( + chatArchive.DocumentEmbeddings.Add( "GlobalDocuments", await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( Guid.Empty.ToString(), @@ -128,14 +120,14 @@ await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( cancellationToken)); // get the document memory collection names (user scope) - bot.DocumentEmbeddings.Add( + chatArchive.DocumentEmbeddings.Add( "ChatDocuments", await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( chatIdString, this._promptOptions.DocumentMemoryName, cancellationToken)); - return bot; + return chatArchive; } /// diff --git a/webapi/Controllers/ChatController.cs b/webapi/Controllers/ChatController.cs index 2f55be449..cfbdeb147 100644 --- a/webapi/Controllers/ChatController.cs +++ b/webapi/Controllers/ChatController.cs @@ -74,8 +74,9 @@ public ChatController(ILogger logger, ITelemetryService telemetr /// Repository of chat participants. /// Auth info for the current request. /// Prompt along with its parameters. + /// Chat ID. /// Results containing the response from the model. - [Route("chat")] + [Route("chats/{chatId:guid}/messages")] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -90,10 +91,12 @@ public async Task ChatAsync( [FromServices] ChatSessionRepository chatSessionRepository, [FromServices] ChatParticipantRepository chatParticipantRepository, [FromServices] IAuthInfo authInfo, - [FromBody] Ask ask) + [FromBody] Ask ask, + [FromRoute] Guid chatId) { this._logger.LogDebug("/chat request received."); - return await this.HandleRequest(ChatFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask); + + return await this.HandleRequest(ChatFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); } /// @@ -107,8 +110,9 @@ public async Task ChatAsync( /// Repository of chat participants. /// Auth info for the current request. /// Prompt along with its parameters. + /// Chat ID. /// Results containing the response from the model. - [Route("processplan")] + [Route("chats/{chatId:guid}/processPlan")] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -123,10 +127,12 @@ public async Task ProcessPlanAsync( [FromServices] ChatSessionRepository chatSessionRepository, [FromServices] ChatParticipantRepository chatParticipantRepository, [FromServices] IAuthInfo authInfo, - [FromBody] ExecutePlanParameters ask) + [FromBody] ExecutePlanParameters ask, + [FromRoute] Guid chatId) { this._logger.LogDebug("/processplan request received."); - return await this.HandleRequest(ProcessPlanFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask); + + return await this.HandleRequest(ProcessPlanFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); } #region Private Methods @@ -143,6 +149,7 @@ public async Task ProcessPlanAsync( /// Repository of chat participants. /// Auth info for the current request. /// Prompt along with its parameters. + /// /// Results containing the response from the model. private async Task HandleRequest( string functionName, @@ -153,29 +160,20 @@ private async Task HandleRequest( ChatSessionRepository chatSessionRepository, ChatParticipantRepository chatParticipantRepository, IAuthInfo authInfo, - Ask ask) + Ask ask, + string chatId) { // Verify that the chat exists and that the user has access to it. - const string ChatIdKey = "chatId"; - var chatIdFromContext = ask.Variables.FirstOrDefault(x => x.Key == ChatIdKey); - if (chatIdFromContext.Key is ChatIdKey) + var chat = await chatSessionRepository.FindByIdAsync(chatId); + if (chat == null) { - var chatId = chatIdFromContext.Value; - var chat = await chatSessionRepository.FindByIdAsync(chatId); - if (chat == null) - { - return this.NotFound("Failed to find chat session for the chatId specified in variables."); - } - - bool isUserInChat = await chatParticipantRepository.IsUserInChatAsync(authInfo.UserId, chatId); - if (!isUserInChat) - { - return this.Forbid("User does not have access to the chatId specified in variables."); - } + return this.NotFound("Failed to find chat session for the specified chatId."); } - else + + bool isUserInChat = await chatParticipantRepository.IsUserInChatAsync(authInfo.UserId, chatId); + if (!isUserInChat) { - return this.BadRequest("ChatId not specified."); + return this.Forbid("User does not have access to the specified chatId specified in variables."); } // Put ask's variables in the context we will use. @@ -225,16 +223,11 @@ private async Task HandleRequest( AskResult chatSkillAskResult = new() { Value = result.Result, - Variables = result.Variables.Select( - v => new KeyValuePair(v.Key, v.Value)) + Variables = result.Variables.Select(v => new KeyValuePair(v.Key, v.Value)) }; // Broadcast AskResult to all users - if (ask.Variables.Where(v => v.Key == "chatId").Any()) - { - var chatId = ask.Variables.Where(v => v.Key == "chatId").First().Value; - await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, null); - } + await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, null); return this.Ok(chatSkillAskResult); } @@ -270,11 +263,12 @@ private Dictionary GetPluginAuthHeaders(IHeaderDictionary header /// The domain of the manifest. /// The plugin's manifest JSON. [HttpGet] - [Route("getPluginManifest")] + [Route("pluginManifest")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetPluginManifest([FromQuery] Uri manifestDomain) { using HttpClient client = new(); + return this.Ok(await client.GetStringAsync(this.GetPluginManifestUri(manifestDomain.ToString()))); } diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 01cfd1605..b6bb58d57 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -9,7 +9,6 @@ using CopilotChat.WebApi.Extensions; using CopilotChat.WebApi.Hubs; using CopilotChat.WebApi.Models.Request; -using CopilotChat.WebApi.Models.Response; using CopilotChat.WebApi.Models.Storage; using CopilotChat.WebApi.Options; using CopilotChat.WebApi.Skills.Utils; @@ -81,7 +80,7 @@ public ChatHistoryController( /// Contains the title of the chat. /// The HTTP action result. [HttpPost] - [Route("chatSession/create")] + [Route("chats")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task CreateChatSessionAsync( @@ -109,10 +108,8 @@ public async Task CreateChatSessionAsync( await this._participantRepository.CreateAsync(new ChatParticipant(this._authInfo.UserId, newChat.Id)); this._logger.LogDebug("Created chat session with id {0}.", newChat.Id); - return this.CreatedAtAction( - nameof(this.GetChatSessionByIdAsync), - new { chatId = newChat.Id }, - new CreateChatResponse(newChat, chatMessage)); + + return this.CreatedAtAction(nameof(GetChatSessionByIdAsync), newChat.Id, chatMessage); } /// @@ -120,8 +117,7 @@ public async Task CreateChatSessionAsync( /// /// The chat id. [HttpGet] - [ActionName("GetChatSessionByIdAsync")] - [Route("chatSession/getChat/{chatId:guid}")] + [Route("chats/{chatId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -143,11 +139,11 @@ public async Task GetChatSessionByIdAsync(Guid chatId) /// The user id. /// A list of chat sessions. An empty list if the user is not in any chat session. [HttpGet] - [Route("chatSession/getAllChats/{userId}")] + [Route("chats")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetAllChatSessionsAsync(string userId) + public async Task GetAllChatSessionsAsync() { // Get all participants that belong to the user. // Then get all the chats from the list of participants. @@ -163,8 +159,7 @@ public async Task GetAllChatSessionsAsync(string userId) } else { - this._logger.LogDebug( - "Failed to find chat session with id {0}", chatParticipant.ChatId); + this._logger.LogDebug("Failed to find chat session with id {0}", chatParticipant.ChatId); } } @@ -179,13 +174,13 @@ public async Task GetAllChatSessionsAsync(string userId) /// The start index at which the first message will be returned. /// The number of messages to return. -1 will return all messages starting from startIdx. [HttpGet] - [Route("chatSession/getChatMessages/{chatId:guid}")] + [Route("chats/{chatId:guid}/messages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = AuthPolicyName.RequireChatParticipant)] public async Task GetChatMessagesAsync( - Guid chatId, + [FromRoute] Guid chatId, [FromQuery] int startIdx = 0, [FromQuery] int count = -1) { @@ -206,39 +201,26 @@ public async Task GetChatMessagesAsync( /// Edit a chat session. /// /// Object that contains the parameters to edit the chat. - [HttpPost] - [Route("chatSession/edit")] + [HttpPatch] + [Route("chats/{chatId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = AuthPolicyName.RequireChatParticipant)] public async Task EditChatSessionAsync( [FromServices] IHubContext messageRelayHubContext, - [FromBody] EditChatParameters chatParameters) + [FromBody] EditChatParameters chatParameters, + [FromRoute] Guid chatId) { - string? chatId = chatParameters.Id; - - if (chatId == null) - { - return this.BadRequest("Chat id must be specified."); - } - - // Verify access to chat session - // TODO: [Issue #141] This can be removed when route is updated to include chatId, so that we can leverage RequireChatParticipant policy. - bool isUserInChat = await this._participantRepository.IsUserInChatAsync(this._authInfo.UserId, chatId); - if (!isUserInChat) - { - return this.Forbid("User does not have access to the specified chat."); - } - ChatSession? chat = null; - if (await this._sessionRepository.TryFindByIdAsync(chatId, callback: v => chat = v)) + if (await this._sessionRepository.TryFindByIdAsync(chatId.ToString(), callback: v => chat = v)) { chat!.Title = chatParameters.Title ?? chat!.Title; chat!.SystemDescription = chatParameters.SystemDescription ?? chat!.SystemDescription; chat!.MemoryBalance = chatParameters.MemoryBalance ?? chat!.MemoryBalance; await this._sessionRepository.UpsertAsync(chat); - await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatEditedClientCall, chat); + await messageRelayHubContext.Clients.Group(chatId.ToString()).SendAsync(ChatEditedClientCall, chat); + return this.Ok(chat); } @@ -246,9 +228,9 @@ public async Task EditChatSessionAsync( } /// - /// Service API to get a list of imported sources. + /// Gets list of imported documents for a given chat. /// - [Route("chatSession/{chatId:guid}/sources")] + [Route("chats/{chatId:guid}/documents")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -261,7 +243,8 @@ public async Task>> GetSourcesAsync(Guid if (await this._sessionRepository.TryFindByIdAsync(chatId.ToString())) { - var sources = await this._sourceRepository.FindByChatIdAsync(chatId.ToString()); + IEnumerable sources = await this._sourceRepository.FindByChatIdAsync(chatId.ToString()); + return this.Ok(sources); } @@ -273,7 +256,7 @@ public async Task>> GetSourcesAsync(Guid /// /// The chat id. [HttpDelete] - [Route("chatSession/{chatId:guid}")] + [Route("chats/{chatId:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/webapi/Controllers/ChatMemoryController.cs b/webapi/Controllers/ChatMemoryController.cs index 27b19fb5e..b83221ade 100644 --- a/webapi/Controllers/ChatMemoryController.cs +++ b/webapi/Controllers/ChatMemoryController.cs @@ -53,14 +53,14 @@ public ChatMemoryController( /// The chat id. /// Type of memory. Must map to a member of . [HttpGet] - [Route("chatMemory/{chatId:guid}/{memoryType}")] + [Route("chats/{chatId:guid}/memories")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [Authorize(Policy = AuthPolicyName.RequireChatParticipant)] public async Task GetSemanticMemoriesAsync( [FromServices] ISemanticMemoryClient memoryClient, [FromRoute] string chatId, - [FromRoute] string memoryType) + [FromQuery] string memoryType) { // Sanitize the log input by removing new line characters. // https://github.com/microsoft/chat-copilot/security/code-scanning/1 @@ -99,8 +99,7 @@ await memoryClient.SearchMemoryAsync( relevanceThreshold: 0, resultCount: 1, chatId, - memoryContainerName) - .ConfigureAwait(false); + memoryContainerName); foreach (var memory in searchResult.Results.SelectMany(c => c.Partitions)) { diff --git a/webapi/Controllers/ChatParticipantController.cs b/webapi/Controllers/ChatParticipantController.cs index 62b5b6ee8..cecc4ae3f 100644 --- a/webapi/Controllers/ChatParticipantController.cs +++ b/webapi/Controllers/ChatParticipantController.cs @@ -48,39 +48,39 @@ public ChatParticipantController( /// /// Join the logged in user to a chat session given a chat ID. /// - /// The ID of the chat to join. /// Message Hub that performs the real time relay service. /// The auth info for the current request. + /// The ID of the chat to join. [HttpPost] - [Route("chatParticipant/join")] + [Route("chats/{chatId:guid}/participants")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task JoinChatAsync( [FromServices] IHubContext messageRelayHubContext, [FromServices] IAuthInfo authInfo, - [FromBody] ChatParticipant chatParticipantParam) + [FromRoute] Guid chatId) { - string chatId = chatParticipantParam.ChatId; string userId = authInfo.UserId; // Make sure the chat session exists. - if (!await this._chatSessionRepository.TryFindByIdAsync(chatId)) + if (!await this._chatSessionRepository.TryFindByIdAsync(chatId.ToString())) { return this.BadRequest("Chat session does not exist."); } // Make sure the user is not already in the chat session. - if (await this._chatParticipantRepository.IsUserInChatAsync(userId, chatId)) + if (await this._chatParticipantRepository.IsUserInChatAsync(userId, chatId.ToString())) { - return this.BadRequest("User is already in the chat session."); + return this.Conflict("User is already in the chat session."); } - var chatParticipant = new ChatParticipant(userId, chatId); + var chatParticipant = new ChatParticipant(userId, chatId.ToString()); await this._chatParticipantRepository.CreateAsync(chatParticipant); // Broadcast the user joined event to all the connected clients. // Note that the client who initiated the request may not have joined the group. - await messageRelayHubContext.Clients.Group(chatId).SendAsync(UserJoinedClientCall, chatId, userId); + await messageRelayHubContext.Clients.Group(chatId.ToString()).SendAsync(UserJoinedClientCall, chatId, userId); return this.Ok(chatParticipant); } @@ -90,7 +90,7 @@ public async Task JoinChatAsync( /// /// The ID of the chat to get all the participants from. [HttpGet] - [Route("chatParticipant/getAllParticipants/{chatId:guid}")] + [Route("chats/{chatId:guid}/participants")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = AuthPolicyName.RequireChatParticipant)] @@ -103,6 +103,7 @@ public async Task GetAllParticipantsAsync(Guid chatId) } var chatParticipants = await this._chatParticipantRepository.FindByChatIdAsync(chatId.ToString()); + return this.Ok(chatParticipants); } } diff --git a/webapi/Controllers/DocumentController.cs b/webapi/Controllers/DocumentController.cs index b96d2d7a0..a23799b7d 100644 --- a/webapi/Controllers/DocumentController.cs +++ b/webapi/Controllers/DocumentController.cs @@ -39,13 +39,14 @@ public class DocumentController : ControllerBase private readonly ILogger _logger; private readonly PromptsOptions _promptOptions; private readonly DocumentMemoryOptions _options; + private readonly ContentSafetyOptions _contentSafetyOptions; private readonly ChatSessionRepository _sessionRepository; private readonly ChatMemorySourceRepository _sourceRepository; private readonly ChatMessageRepository _messageRepository; private readonly ChatParticipantRepository _participantRepository; private readonly DocumentTypeProvider _documentTypeProvider; private readonly IAuthInfo _authInfo; - private readonly IContentSafetyService? _contentSafetyService; + private readonly IContentSafetyService _contentSafetyService; /// /// Initializes a new instance of the class. @@ -55,35 +56,25 @@ public DocumentController( IAuthInfo authInfo, IOptions documentMemoryOptions, IOptions promptOptions, + IOptions contentSafetyOptions, ChatSessionRepository sessionRepository, ChatMemorySourceRepository sourceRepository, ChatMessageRepository messageRepository, ChatParticipantRepository participantRepository, DocumentTypeProvider documentTypeProvider, - IContentSafetyService? contentSafety = null) + IContentSafetyService contentSafetyService) { this._logger = logger; this._options = documentMemoryOptions.Value; this._promptOptions = promptOptions.Value; + this._contentSafetyOptions = contentSafetyOptions.Value; this._sessionRepository = sessionRepository; this._sourceRepository = sourceRepository; this._messageRepository = messageRepository; this._participantRepository = participantRepository; this._documentTypeProvider = documentTypeProvider; this._authInfo = authInfo; - this._contentSafetyService = contentSafety; - } - - /// - /// Gets the status of content safety. - /// - /// - [HttpGet] - [Route("contentSafety/status")] - [ProducesResponseType(StatusCodes.Status200OK)] - public bool ContentSafetyStatus() - { - return this._contentSafetyService?.ContentSafetyStatus(this._logger) ?? false; + this._contentSafetyService = contentSafetyService; } /// @@ -195,23 +186,23 @@ async Task ImportDocumentAsync(IFormFile formFile) // Create memory source MemorySource memorySource = - new( - chatId.ToString(), + new(chatId.ToString(), formFile.FileName, this._authInfo.UserId, MemorySourceType.File, formFile.Length, hyperlink: null); - if (!(await this.TryUpsertMemorySourceAsync(memorySource).ConfigureAwait(false))) + if (!(await this.TryUpsertMemorySourceAsync(memorySource))) { this._logger.LogDebug("Failed to upsert memory source for file {0}.", formFile.FileName); + return ImportResult.Fail; } - if (!(await TryStoreMemoryAsync().ConfigureAwait(false))) + if (!(await TryStoreMemoryAsync())) { - await this.TryRemoveMemoryAsync(memorySource).ConfigureAwait(false); + await this.TryRemoveMemoryAsync(memorySource); } return new ImportResult(memorySource.Id); @@ -319,7 +310,7 @@ private async Task ValidateDocumentImportFormAsync(Guid chatId, DocumentScopes s if (isSafetyTarget && documentImportForm.UseContentSafety) { - if (this._contentSafetyService == null || !this._contentSafetyService.ContentSafetyStatus(this._logger)) + if (!this._contentSafetyOptions.Enabled) { throw new ArgumentException("Unable to analyze image. Content Safety is currently disabled in the backend."); } @@ -329,7 +320,7 @@ private async Task ValidateDocumentImportFormAsync(Guid chatId, DocumentScopes s { // Call the content safety controller to analyze the image var imageAnalysisResponse = await this._contentSafetyService.ImageAnalysisAsync(formFile, default); - violations = this._contentSafetyService.ParseViolatedCategories(imageAnalysisResponse, this._contentSafetyService.Options.ViolationThreshold); + violations = this._contentSafetyService.ParseViolatedCategories(imageAnalysisResponse, this._contentSafetyOptions.ViolationThreshold); } catch (Exception ex) when (!ex.IsCriticalException()) { diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index f3e54dd41..5b213f5dc 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -3,13 +3,11 @@ using System.Threading; using System.Threading.Tasks; using CopilotChat.WebApi.Auth; -using CopilotChat.WebApi.Hubs; using CopilotChat.WebApi.Models.Response; using CopilotChat.WebApi.Options; using CopilotChat.WebApi.Services.MemoryMigration; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -43,13 +41,12 @@ public MaintenanceController( /// /// Route for reporting the status of site maintenance. /// - [Route("maintenancestatus/")] + [Route("maintenanceStatus")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> GetMaintenanceStatusAsync( [FromServices] IChatMigrationMonitor migrationMonitor, - [FromServices] IHubContext messageRelayHubContext, CancellationToken cancellationToken = default) { MaintenanceResult? result = null; diff --git a/webapi/Controllers/ServiceInfoController.cs b/webapi/Controllers/ServiceInfoController.cs index da1b06722..ee40ed846 100644 --- a/webapi/Controllers/ServiceInfoController.cs +++ b/webapi/Controllers/ServiceInfoController.cs @@ -28,25 +28,28 @@ public class ServiceInfoController : ControllerBase private readonly SemanticMemoryConfig memoryOptions; private readonly ChatAuthenticationOptions _chatAuthenticationOptions; private readonly FrontendOptions _frontendOptions; + private readonly ContentSafetyOptions _contentSafetyOptions; public ServiceInfoController( ILogger logger, IConfiguration configuration, IOptions memoryOptions, IOptions chatAuthenticationOptions, - IOptions frontendOptions) + IOptions frontendOptions, + IOptions contentSafetyOptions) { this._logger = logger; this.Configuration = configuration; this.memoryOptions = memoryOptions.Value; this._chatAuthenticationOptions = chatAuthenticationOptions.Value; this._frontendOptions = frontendOptions.Value; + this._contentSafetyOptions = contentSafetyOptions.Value; } /// - /// Return the memory store type that is configured. + /// Return information on running service. /// - [Route("serviceOptions")] + [Route("options")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public IActionResult GetServiceOptions() @@ -58,7 +61,8 @@ public IActionResult GetServiceOptions() Types = Enum.GetNames(typeof(MemoryStoreType)), SelectedType = this.memoryOptions.GetMemoryStoreType(this.Configuration).ToString(), }, - Version = GetAssemblyFileVersion() + Version = GetAssemblyFileVersion(), + IsContentSafetyEnabled = this._contentSafetyOptions.Enabled }; return this.Ok(response); diff --git a/webapi/Extensions/IAsyncEnumerableExtensions.cs b/webapi/Extensions/IAsyncEnumerableExtensions.cs index 7eba934d5..af09b1904 100644 --- a/webapi/Extensions/IAsyncEnumerableExtensions.cs +++ b/webapi/Extensions/IAsyncEnumerableExtensions.cs @@ -16,7 +16,7 @@ public static class IAsyncEnumerableExtensions internal static async Task> ToListAsync(this IAsyncEnumerable source) { var result = new List(); - await foreach (var item in source.ConfigureAwait(false)) + await foreach (var item in source) { result.Add(item); } diff --git a/webapi/Extensions/SemanticKernelExtensions.cs b/webapi/Extensions/SemanticKernelExtensions.cs index 4c87c018f..f5c54232e 100644 --- a/webapi/Extensions/SemanticKernelExtensions.cs +++ b/webapi/Extensions/SemanticKernelExtensions.cs @@ -253,18 +253,14 @@ private static Task RegisterPluginsAsync(IServiceProvider sp, IKernel kernel) internal static void AddContentSafety(this IServiceCollection services) { IConfiguration configuration = services.BuildServiceProvider().GetRequiredService(); - var options = configuration.GetSection(ContentSafetyOptions.PropertyName).Get(); - - if (options?.Enabled ?? false) - { - services.AddSingleton(sp => new AzureContentSafety(new Uri(options.Endpoint), options.Key, options)); - } + var options = configuration.GetSection(ContentSafetyOptions.PropertyName).Get() ?? new ContentSafetyOptions { Enabled = false }; + services.AddSingleton(sp => new AzureContentSafety(options.Endpoint, options.Key)); } /// /// Get the embedding model from the configuration. /// - private static BotEmbeddingConfig WithBotConfig(this IServiceProvider provider, IConfiguration configuration) + private static ChatArchiveEmbeddingConfig WithBotConfig(this IServiceProvider provider, IConfiguration configuration) { var memoryOptions = provider.GetRequiredService>().Value; @@ -274,18 +270,18 @@ private static BotEmbeddingConfig WithBotConfig(this IServiceProvider provider, case string y when y.Equals("AzureOpenAIEmbedding", StringComparison.OrdinalIgnoreCase): var azureAIOptions = memoryOptions.GetServiceConfig(configuration, "AzureOpenAIEmbedding"); return - new BotEmbeddingConfig + new ChatArchiveEmbeddingConfig { - AIService = BotEmbeddingConfig.AIServiceType.AzureOpenAIEmbedding, + AIService = ChatArchiveEmbeddingConfig.AIServiceType.AzureOpenAIEmbedding, DeploymentOrModelId = azureAIOptions.Deployment, }; case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): var openAIOptions = memoryOptions.GetServiceConfig(configuration, "OpenAI"); return - new BotEmbeddingConfig + new ChatArchiveEmbeddingConfig { - AIService = BotEmbeddingConfig.AIServiceType.OpenAI, + AIService = ChatArchiveEmbeddingConfig.AIServiceType.OpenAI, DeploymentOrModelId = openAIOptions.EmbeddingModel, }; diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index 8dc4e2b48..c6a48341a 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -47,8 +47,6 @@ public static IServiceCollection AddOptions(this IServiceCollection services, Co // Azure speech token configuration AddOptions(AzureSpeechOptions.PropertyName); - AddOptions(BotSchemaOptions.PropertyName); - AddOptions(DocumentMemoryOptions.PropertyName); // Chat prompt options diff --git a/webapi/Models/Request/EditChatParameters.cs b/webapi/Models/Request/EditChatParameters.cs index a5d902a77..97985c934 100644 --- a/webapi/Models/Request/EditChatParameters.cs +++ b/webapi/Models/Request/EditChatParameters.cs @@ -7,11 +7,6 @@ namespace CopilotChat.WebApi.Models.Request; /// public class EditChatParameters { - /// - /// Chat ID that is persistent and unique. - /// - public string? Id { get; set; } - /// /// Title of the chat. /// diff --git a/webapi/Models/Response/Bot.cs b/webapi/Models/Response/ChatArchive.cs similarity index 78% rename from webapi/Models/Response/Bot.cs rename to webapi/Models/Response/ChatArchive.cs index 7f148aa1f..36ba186b2 100644 --- a/webapi/Models/Response/Bot.cs +++ b/webapi/Models/Response/ChatArchive.cs @@ -8,22 +8,22 @@ namespace CopilotChat.WebApi.Models.Response; /// -/// The data model of a bot for portability. +/// The data model of a chat archive. /// -public class Bot +public class ChatArchive { /// - /// The schema information of the bot data model. + /// Schema information for the chat archive. /// - public BotSchemaOptions Schema { get; set; } = new BotSchemaOptions(); + public ChatArchiveSchemaInfo Schema { get; set; } = new ChatArchiveSchemaInfo(); /// /// The embedding configurations. /// - public BotEmbeddingConfig EmbeddingConfigurations { get; set; } = new BotEmbeddingConfig(); + public ChatArchiveEmbeddingConfig EmbeddingConfigurations { get; set; } = new ChatArchiveEmbeddingConfig(); /// - /// The title of the chat with the bot. + /// Chat title. /// public string ChatTitle { get; set; } = string.Empty; @@ -38,7 +38,7 @@ public class Bot public List ChatHistory { get; set; } = new List(); /// - /// The embeddings of the bot. + /// Chat archive's embeddings. /// public Dictionary> Embeddings { get; set; } = new Dictionary>(); diff --git a/webapi/Models/Response/BotEmbeddingConfig.cs b/webapi/Models/Response/ChatArchiveEmbeddingConfig.cs similarity index 89% rename from webapi/Models/Response/BotEmbeddingConfig.cs rename to webapi/Models/Response/ChatArchiveEmbeddingConfig.cs index e0599c4dc..e2aa01264 100644 --- a/webapi/Models/Response/BotEmbeddingConfig.cs +++ b/webapi/Models/Response/ChatArchiveEmbeddingConfig.cs @@ -6,9 +6,9 @@ namespace CopilotChat.WebApi.Models.Response; /// -/// The embedding configuration of a bot. Used in the Bot object for portability. +/// Chat archive embedding configuration. /// -public class BotEmbeddingConfig +public class ChatArchiveEmbeddingConfig { /// /// Supported types of AI services. diff --git a/webapi/Models/Response/ServiceOptionsResponse.cs b/webapi/Models/Response/ServiceOptionsResponse.cs index dad1cb091..1ac2de386 100644 --- a/webapi/Models/Response/ServiceOptionsResponse.cs +++ b/webapi/Models/Response/ServiceOptionsResponse.cs @@ -6,6 +6,9 @@ namespace CopilotChat.WebApi.Models.Response; +/// +/// Information on running service. +/// public class ServiceOptionsResponse { /// @@ -19,6 +22,12 @@ public class ServiceOptionsResponse /// [JsonPropertyName("version")] public string Version { get; set; } = string.Empty; + + /// + /// True if content safety if enabled, false otherwise. + /// + [JsonPropertyName("contentSafetyStatus")] + public bool IsContentSafetyEnabled { get; set; } = false; } /// diff --git a/webapi/Options/BotSchemaOptions.cs b/webapi/Options/ChatArchiveSchemaInfo.cs similarity index 59% rename from webapi/Options/BotSchemaOptions.cs rename to webapi/Options/ChatArchiveSchemaInfo.cs index f8ee76cfd..a9a1df5fc 100644 --- a/webapi/Options/BotSchemaOptions.cs +++ b/webapi/Options/ChatArchiveSchemaInfo.cs @@ -5,21 +5,19 @@ namespace CopilotChat.WebApi.Options; /// -/// Configuration options for the bot file schema that is supported by this application. +/// Information on schema used to serialize chat archives. /// -public class BotSchemaOptions +public record ChatArchiveSchemaInfo { - public const string PropertyName = "BotSchema"; - /// /// The name of the schema. /// [Required, NotEmptyOrWhitespace] - public string Name { get; set; } = string.Empty; + public string Name { get; init; } = "CopilotChat"; /// /// The version of the schema. /// [Range(0, int.MaxValue)] - public int Version { get; set; } + public int Version { get; init; } = 1; } diff --git a/webapi/Services/AzureContentSafety.cs b/webapi/Services/AzureContentSafety.cs index 53c2fcac1..df07bb2e0 100644 --- a/webapi/Services/AzureContentSafety.cs +++ b/webapi/Services/AzureContentSafety.cs @@ -10,9 +10,7 @@ using System.Threading; using System.Threading.Tasks; using CopilotChat.WebApi.Models.Response; -using CopilotChat.WebApi.Options; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Diagnostics; namespace CopilotChat.WebApi.Services; @@ -35,29 +33,19 @@ public sealed class AzureContentSafety : IContentSafetyService { private const string HttpUserAgent = "Copilot Chat"; - private readonly Uri _endpoint; + private readonly string _endpoint; private readonly HttpClient _httpClient; private readonly HttpClientHandler? _httpClientHandler; - /// - /// Options for the content safety. - /// - private readonly ContentSafetyOptions _contentSafetyOptions; - - /// - public ContentSafetyOptions Options => this._contentSafetyOptions; - /// /// Initializes a new instance of the class. /// /// Endpoint for service API call. /// The API key. - /// Content safety options from appsettings. /// Instance of to setup specific scenarios. - public AzureContentSafety(Uri endpoint, string apiKey, ContentSafetyOptions contentSafetyOptions, HttpClientHandler httpClientHandler) + public AzureContentSafety(string endpoint, string apiKey, HttpClientHandler httpClientHandler) { this._endpoint = endpoint; - this._contentSafetyOptions = contentSafetyOptions; this._httpClient = new(httpClientHandler); this._httpClient.DefaultRequestHeaders.Add("User-Agent", HttpUserAgent); @@ -71,11 +59,9 @@ public AzureContentSafety(Uri endpoint, string apiKey, ContentSafetyOptions cont /// /// Endpoint for service API call. /// The API key. - /// Content safety options from appsettings. - public AzureContentSafety(Uri endpoint, string apiKey, ContentSafetyOptions contentSafetyOptions) + public AzureContentSafety(string endpoint, string apiKey) { this._endpoint = endpoint; - this._contentSafetyOptions = contentSafetyOptions; this._httpClientHandler = new() { CheckCertificateRevocationList = true }; this._httpClient = new(this._httpClientHandler); @@ -87,21 +73,8 @@ public AzureContentSafety(Uri endpoint, string apiKey, ContentSafetyOptions cont } /// - public bool ContentSafetyStatus(ILogger logger) - { - if (this._endpoint is null) - { - logger.LogWarning("Content Safety is missing a valid endpoint. Please check the configuration."); - return false; - } - - return this._contentSafetyOptions.Enabled; - } - - /// - public List ParseViolatedCategories(ImageAnalysisResponse imageAnalysisResponse, short? threshold) + public List ParseViolatedCategories(ImageAnalysisResponse imageAnalysisResponse, short threshold) { - threshold = threshold != null ? threshold : this._contentSafetyOptions.ViolationThreshold; var violatedCategories = new List(); foreach (var property in typeof(ImageAnalysisResponse).GetProperties()) @@ -132,8 +105,8 @@ public async Task ImageAnalysisAsync(IFormFile formFile, Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"), }; - var response = await this._httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); - var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var response = await this._httpClient.SendAsync(httpRequestMessage, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); if (!response.IsSuccessStatusCode || body is null) { throw new SKException($"[Content Safety] Failed to analyze image. {response.StatusCode}"); diff --git a/webapi/Services/IContentSafetyService.cs b/webapi/Services/IContentSafetyService.cs index 7128081fb..4f9420b1d 100644 --- a/webapi/Services/IContentSafetyService.cs +++ b/webapi/Services/IContentSafetyService.cs @@ -5,9 +5,7 @@ using System.Threading; using System.Threading.Tasks; using CopilotChat.WebApi.Models.Response; -using CopilotChat.WebApi.Options; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; namespace CopilotChat.WebApi.Services; @@ -16,18 +14,6 @@ namespace CopilotChat.WebApi.Services; /// public interface IContentSafetyService : IDisposable { - /// - /// Gets the options for the content safety. - /// - ContentSafetyOptions Options { get; } - - /// - /// Checks the state of the content safety. - /// - /// Logger. - /// True if content safety is enabled with non-null endpoint. - bool ContentSafetyStatus(ILogger logger); - /// /// Invokes a sync API to perform harmful content analysis on image. /// @@ -40,7 +26,7 @@ public interface IContentSafetyService : IDisposable /// Parse the analysis result and return the violated categories. /// /// The content analysis result. - /// Optional violation threshold. If not specified, threshold should be pulled from Options. + /// Optional violation threshold. /// The list of violated category names. Will return an empty list if there is no violation. - List ParseViolatedCategories(ImageAnalysisResponse imageAnalysisResponse, short? threshold); + List ParseViolatedCategories(ImageAnalysisResponse imageAnalysisResponse, short threshold); } diff --git a/webapi/Services/MaintenanceMiddleware.cs b/webapi/Services/MaintenanceMiddleware.cs index 3f101e6fa..6e8983c71 100644 --- a/webapi/Services/MaintenanceMiddleware.cs +++ b/webapi/Services/MaintenanceMiddleware.cs @@ -29,14 +29,14 @@ public class MaintenanceMiddleware public MaintenanceMiddleware( RequestDelegate next, IReadOnlyList actions, - IOptions servicetOptions, + IOptions serviceOptions, IHubContext messageRelayHubContext, ILogger logger) { this._next = next; this._actions = actions; - this._serviceOptions = servicetOptions; + this._serviceOptions = serviceOptions; this._messageRelayHubContext = messageRelayHubContext; this._logger = logger; } diff --git a/webapi/Skills/ChatSkills/SemanticMemoryRetriever.cs b/webapi/Skills/ChatSkills/SemanticMemoryRetriever.cs index 9321145e4..337f99fdf 100644 --- a/webapi/Skills/ChatSkills/SemanticMemoryRetriever.cs +++ b/webapi/Skills/ChatSkills/SemanticMemoryRetriever.cs @@ -77,7 +77,7 @@ public SemanticMemoryRetriever( List<(Citation Citation, Citation.Partition Memory)> relevantMemories = new(); foreach (var memoryName in this._memoryNames) { - await SearchMemoryAsync(memoryName).ConfigureAwait(false); + await SearchMemoryAsync(memoryName); } var builderMemory = new StringBuilder(); @@ -147,8 +147,7 @@ await this._memoryClient.SearchMemoryAsync( query, this.CalculateRelevanceThreshold(memoryName, chatSession!.MemoryBalance), chatId, - memoryName) - .ConfigureAwait(false); + memoryName); foreach (var result in searchResult.Results.SelectMany(c => c.Partitions.Select(p => (c, p)))) { diff --git a/webapi/appsettings.json b/webapi/appsettings.json index 1b66af79d..0411c9ec5 100644 --- a/webapi/appsettings.json +++ b/webapi/appsettings.json @@ -1,5 +1,5 @@ // -// # CopilotChat Application Settings +// # Chat Copilot Application Settings // // # Quickstart // - Update the "Completion" and "Embedding" sections below to use your AI services. @@ -178,11 +178,6 @@ "http://localhost:3000", "https://localhost:3000" ], - // The schema information for a serialized bot that is supported by this application. - "BotSchema": { - "Name": "CopilotChat", - "Version": 1 - }, // // Semantic Memory configuration - https://github.com/microsoft/semantic-memory // - ContentStorageType is the storage configuration for memory transfer: "AzureBlobs" or "SimpleFileStorage" diff --git a/webapp/src/components/views/BackendProbe.tsx b/webapp/src/components/views/BackendProbe.tsx index e362b4648..2403b5fbc 100644 --- a/webapp/src/components/views/BackendProbe.tsx +++ b/webapp/src/components/views/BackendProbe.tsx @@ -23,7 +23,7 @@ export const BackendProbe: FC = ({ uri, onBackendFound }) => { const dispatch = useAppDispatch(); const { isMaintenance } = useAppSelector((state: RootState) => state.app); const healthUrl = new URL('healthz', uri); - const migrationUrl = new URL('maintenancestatus', uri); + const migrationUrl = new URL('maintenanceStatus', uri); const [model, setModel] = useState(null); diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index f077aca3c..fd4061a3b 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -163,7 +163,7 @@ export const useChat = () => { const loadChats = async () => { try { const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); - const chatSessions = await chatService.getAllChatsAsync(userId, accessToken); + const chatSessions = await chatService.getAllChatsAsync(accessToken); if (chatSessions.length > 0) { const loadedConversations: Conversations = {}; @@ -324,7 +324,7 @@ export const useChat = () => { const joinChat = async (chatId: string) => { try { const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); - await chatService.joinChatAsync(userId, chatId, accessToken).then(async (result: IChatSession) => { + await chatService.joinChatAsync(chatId, accessToken).then(async (result: IChatSession) => { // Get chat messages const chatMessages = await chatService.getChatMessagesAsync(result.id, 0, 100, accessToken); diff --git a/webapp/src/libs/models/ServiceOptions.ts b/webapp/src/libs/models/ServiceOptions.ts index b5335f6f3..6e6d7914d 100644 --- a/webapp/src/libs/models/ServiceOptions.ts +++ b/webapp/src/libs/models/ServiceOptions.ts @@ -8,4 +8,5 @@ export interface MemoryStore { export interface ServiceOptions { memoryStore: MemoryStore; version: string; + isContentSafetyEnabled: boolean; } diff --git a/webapp/src/libs/services/BotService.ts b/webapp/src/libs/services/BotService.ts index a5aa1f37c..83a686e94 100644 --- a/webapp/src/libs/services/BotService.ts +++ b/webapp/src/libs/services/BotService.ts @@ -9,7 +9,7 @@ export class BotService extends BaseService { // TODO: [Issue #47] Add type for result. See Bot.cs const result = await this.getResponseAsync( { - commandPath: `bot/download/${chatId}`, + commandPath: `chats/archives/${chatId}`, method: 'GET', }, accessToken, @@ -21,7 +21,7 @@ export class BotService extends BaseService { public uploadAsync = async (bot: Bot, accessToken: string) => { const result = await this.getResponseAsync( { - commandPath: 'bot/upload', + commandPath: 'chats/archives', method: 'Post', body: bot, }, diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 3a9768026..94398f8bf 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -21,7 +21,7 @@ export class ChatService extends BaseService { const result = await this.getResponseAsync( { - commandPath: 'chatSession/create', + commandPath: 'chats', method: 'POST', body, }, @@ -34,7 +34,7 @@ export class ChatService extends BaseService { public getChatAsync = async (chatId: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `chatSession/getChat/${chatId}`, + commandPath: `chats/${chatId}`, method: 'GET', }, accessToken, @@ -43,10 +43,10 @@ export class ChatService extends BaseService { return result; }; - public getAllChatsAsync = async (userId: string, accessToken: string): Promise => { + public getAllChatsAsync = async (accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `chatSession/getAllChats/${userId}`, + commandPath: 'chats', method: 'GET', }, accessToken, @@ -62,7 +62,7 @@ export class ChatService extends BaseService { ): Promise => { const result = await this.getResponseAsync( { - commandPath: `chatSession/getChatMessages/${chatId}?startIdx=${startIdx}&count=${count}`, + commandPath: `chats/${chatId}/messages/?startIdx=${startIdx}&count=${count}`, method: 'GET', }, accessToken, @@ -89,8 +89,8 @@ export class ChatService extends BaseService { const result = await this.getResponseAsync( { - commandPath: `chatSession/edit`, - method: 'POST', + commandPath: `chats/${chatId}`, + method: 'PATCH', body, }, accessToken, @@ -102,7 +102,7 @@ export class ChatService extends BaseService { public deleteChatAsync = async (chatId: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `chatSession/${chatId}`, + commandPath: `chats/${chatId}`, method: 'DELETE', }, accessToken, @@ -167,9 +167,11 @@ export class ChatService extends BaseService { ask.variables = ask.variables ? ask.variables.concat(openApiSkillVariables) : openApiSkillVariables; } + const chatId = ask.variables?.find((variable) => variable.key === 'chatId')?.value as string; + const result = await this.getResponseAsync( { - commandPath: processPlan ? 'processplan' : 'chat', + commandPath: processPlan ? `chats/${chatId}/processPlan` : `chats/${chatId}/messages`, method: 'POST', body: ask, }, @@ -180,17 +182,11 @@ export class ChatService extends BaseService { return result; }; - public joinChatAsync = async (userId: string, chatId: string, accessToken: string): Promise => { - const body: IChatParticipant = { - userId, - chatId, - }; - + public joinChatAsync = async (chatId: string, accessToken: string): Promise => { await this.getResponseAsync( { - commandPath: `chatParticipant/join`, + commandPath: `chats/${chatId}/participants`, method: 'POST', - body, }, accessToken, ); @@ -201,7 +197,7 @@ export class ChatService extends BaseService { public getChatMemorySourcesAsync = async (chatId: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `chatSession/${chatId}/sources`, + commandPath: `chats/${chatId}/documents`, method: 'GET', }, accessToken, @@ -213,7 +209,7 @@ export class ChatService extends BaseService { public getAllChatParticipantsAsync = async (chatId: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `chatParticipant/getAllParticipants/${chatId}`, + commandPath: `chats/${chatId}/participants`, method: 'GET', }, accessToken, @@ -238,7 +234,7 @@ export class ChatService extends BaseService { ): Promise => { const result = await this.getResponseAsync( { - commandPath: `chatMemory/${chatId}/${memoryName}`, + commandPath: `chats/${chatId}/?memoryType=${memoryName}`, method: 'GET', }, accessToken, @@ -250,7 +246,7 @@ export class ChatService extends BaseService { public getServiceOptionsAsync = async (accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `serviceOptions`, + commandPath: `options`, method: 'GET', }, accessToken, @@ -262,7 +258,7 @@ export class ChatService extends BaseService { public getPluginManifest = async (manifestDomain: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `getPluginManifest`, + commandPath: `pluginManifest`, method: 'GET', query: new URLSearchParams({ manifestDomain: encodeURIComponent(manifestDomain) }), }, diff --git a/webapp/src/libs/services/DocumentImportService.ts b/webapp/src/libs/services/DocumentImportService.ts index d8b7ec89e..80f5661cf 100644 --- a/webapp/src/libs/services/DocumentImportService.ts +++ b/webapp/src/libs/services/DocumentImportService.ts @@ -2,6 +2,7 @@ import { IChatMessage } from '../models/ChatMessage'; import { BaseService } from './BaseService'; +import { ServiceOptions } from '../models/ServiceOptions'; export class DocumentImportService extends BaseService { public importDocumentAsync = async ( @@ -27,12 +28,14 @@ export class DocumentImportService extends BaseService { }; public getContentSafetyStatusAsync = async (accessToken: string): Promise => { - return await this.getResponseAsync( + const serviceOptions = await this.getResponseAsync( { commandPath: 'contentSafety/status', method: 'GET', }, accessToken, ); + + return serviceOptions.isContentSafetyEnabled; }; } diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts index e7cfa4266..813634a22 100644 --- a/webapp/src/redux/features/app/AppState.ts +++ b/webapp/src/redux/features/app/AppState.ts @@ -142,6 +142,6 @@ export const initialState: AppState = { tokenUsage: {}, features: Features, settings: Settings, - serviceOptions: { memoryStore: { types: [], selectedType: '' }, version: '' }, + serviceOptions: { memoryStore: { types: [], selectedType: '' }, version: '', isContentSafetyEnabled: false }, isMaintenance: false, }; From 91db17afaeec98be18d07c29d13de13186846009 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Fri, 29 Sep 2023 18:40:15 -0700 Subject: [PATCH 2/8] Corrections and refinements --- webapi/Controllers/ChatHistoryController.cs | 6 ++++-- webapi/Controllers/ChatMemoryController.cs | 10 +++++----- webapi/Controllers/ServiceInfoController.cs | 6 +++--- webapi/Models/Response/CreateChatResponse.cs | 2 +- webapi/Models/Response/ImageAnalysisResponse.cs | 2 +- ...viceOptionsResponse.cs => ServiceInfoResponse.cs} | 6 +++--- webapp/src/App.tsx | 10 +++++----- webapp/src/components/chat/tabs/DocumentsTab.tsx | 8 ++++---- .../header/settings-dialog/SettingsDialog.tsx | 4 ++-- webapp/src/libs/hooks/useChat.ts | 6 +++--- .../models/{ServiceOptions.ts => ServiceInfo.ts} | 2 +- webapp/src/libs/services/ChatService.ts | 12 ++++++------ webapp/src/libs/services/DocumentImportService.ts | 8 ++++---- webapp/src/redux/features/app/AppState.ts | 6 +++--- webapp/src/redux/features/app/appSlice.ts | 8 ++++---- 15 files changed, 49 insertions(+), 47 deletions(-) rename webapi/Models/Response/{ServiceOptionsResponse.cs => ServiceInfoResponse.cs} (87%) rename webapp/src/libs/models/{ServiceOptions.ts => ServiceInfo.ts} (86%) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index b6bb58d57..a506bb753 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -9,6 +9,7 @@ using CopilotChat.WebApi.Extensions; using CopilotChat.WebApi.Hubs; using CopilotChat.WebApi.Models.Request; +using CopilotChat.WebApi.Models.Response; using CopilotChat.WebApi.Models.Storage; using CopilotChat.WebApi.Options; using CopilotChat.WebApi.Skills.Utils; @@ -33,6 +34,7 @@ public class ChatHistoryController : ControllerBase { private const string ChatEditedClientCall = "ChatEdited"; private const string ChatDeletedClientCall = "ChatDeleted"; + private const string GetChatRoute = "GetChatRoute"; private readonly ILogger _logger; private readonly ISemanticMemoryClient _memoryClient; @@ -109,7 +111,7 @@ public async Task CreateChatSessionAsync( this._logger.LogDebug("Created chat session with id {0}.", newChat.Id); - return this.CreatedAtAction(nameof(GetChatSessionByIdAsync), newChat.Id, chatMessage); + return this.CreatedAtRoute(GetChatRoute, new { chatId = newChat.Id }, new CreateChatResponse(newChat, chatMessage)); } /// @@ -117,7 +119,7 @@ public async Task CreateChatSessionAsync( /// /// The chat id. [HttpGet] - [Route("chats/{chatId:guid}")] + [Route("chats/{chatId:guid}", Name = GetChatRoute)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/webapi/Controllers/ChatMemoryController.cs b/webapi/Controllers/ChatMemoryController.cs index b83221ade..8300f279b 100644 --- a/webapi/Controllers/ChatMemoryController.cs +++ b/webapi/Controllers/ChatMemoryController.cs @@ -51,7 +51,7 @@ public ChatMemoryController( /// /// The semantic text memory instance. /// The chat id. - /// Type of memory. Must map to a member of . + /// Type of memory. Must map to a member of . [HttpGet] [Route("chats/{chatId:guid}/memories")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -60,17 +60,17 @@ public ChatMemoryController( public async Task GetSemanticMemoriesAsync( [FromServices] ISemanticMemoryClient memoryClient, [FromRoute] string chatId, - [FromQuery] string memoryType) + [FromQuery] string type) { // Sanitize the log input by removing new line characters. // https://github.com/microsoft/chat-copilot/security/code-scanning/1 var sanitizedChatId = GetSanitizedParameter(chatId); // Map the requested memoryType to the memory store container name - if (!this._promptOptions.TryGetMemoryContainerName(memoryType, out string memoryContainerName)) + if (!this._promptOptions.TryGetMemoryContainerName(type, out string memoryContainerName)) { - this._logger.LogWarning("Memory type: {0} is invalid.", memoryType); - return this.BadRequest($"Memory type: {memoryType} is invalid."); + this._logger.LogWarning("Memory type: {0} is invalid.", type); + return this.BadRequest($"Memory type: {type} is invalid."); } // Make sure the chat session exists. diff --git a/webapi/Controllers/ServiceInfoController.cs b/webapi/Controllers/ServiceInfoController.cs index ee40ed846..0624c2e02 100644 --- a/webapi/Controllers/ServiceInfoController.cs +++ b/webapi/Controllers/ServiceInfoController.cs @@ -49,14 +49,14 @@ public ServiceInfoController( /// /// Return information on running service. /// - [Route("options")] + [Route("info")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public IActionResult GetServiceOptions() { - var response = new ServiceOptionsResponse() + var response = new ServiceInfoResponse() { - MemoryStore = new MemoryStoreOptionResponse() + MemoryStore = new MemoryStoreInfoResponse() { Types = Enum.GetNames(typeof(MemoryStoreType)), SelectedType = this.memoryOptions.GetMemoryStoreType(this.Configuration).ToString(), diff --git a/webapi/Models/Response/CreateChatResponse.cs b/webapi/Models/Response/CreateChatResponse.cs index eeb4d4d1e..0cf3e72e6 100644 --- a/webapi/Models/Response/CreateChatResponse.cs +++ b/webapi/Models/Response/CreateChatResponse.cs @@ -6,7 +6,7 @@ namespace CopilotChat.WebApi.Models.Response; /// -/// Response object definition to the 'chatSession/create' request. +/// Response object definition to the POST to 'chats' request. /// This groups the initial bot message with the chat session /// to avoid making two requests. /// diff --git a/webapi/Models/Response/ImageAnalysisResponse.cs b/webapi/Models/Response/ImageAnalysisResponse.cs index 5bcb34c2e..e75bd10ea 100644 --- a/webapi/Models/Response/ImageAnalysisResponse.cs +++ b/webapi/Models/Response/ImageAnalysisResponse.cs @@ -6,7 +6,7 @@ namespace CopilotChat.WebApi.Models.Response; /// -/// Response definition to the /contentsafety/image:analyze +/// Response definition to image content safety analysis requests. /// endpoint made by the AzureContentSafety. /// public class ImageAnalysisResponse diff --git a/webapi/Models/Response/ServiceOptionsResponse.cs b/webapi/Models/Response/ServiceInfoResponse.cs similarity index 87% rename from webapi/Models/Response/ServiceOptionsResponse.cs rename to webapi/Models/Response/ServiceInfoResponse.cs index 1ac2de386..0a1c903c1 100644 --- a/webapi/Models/Response/ServiceOptionsResponse.cs +++ b/webapi/Models/Response/ServiceInfoResponse.cs @@ -9,13 +9,13 @@ namespace CopilotChat.WebApi.Models.Response; /// /// Information on running service. /// -public class ServiceOptionsResponse +public class ServiceInfoResponse { /// /// Configured memory store. /// [JsonPropertyName("memoryStore")] - public MemoryStoreOptionResponse MemoryStore { get; set; } = new MemoryStoreOptionResponse(); + public MemoryStoreInfoResponse MemoryStore { get; set; } = new MemoryStoreInfoResponse(); /// /// Version of this application. @@ -33,7 +33,7 @@ public class ServiceOptionsResponse /// /// Response to memoryStoreType request. /// -public class MemoryStoreOptionResponse +public class MemoryStoreInfoResponse { /// /// All the available memory store types. diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 3274d4420..df9b4e786 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -14,7 +14,7 @@ import { AlertType } from './libs/models/AlertType'; import { useAppDispatch, useAppSelector } from './redux/app/hooks'; import { RootState } from './redux/app/store'; import { FeatureKeys } from './redux/features/app/AppState'; -import { addAlert, setActiveUserInfo, setServiceOptions } from './redux/features/app/appSlice'; +import { addAlert, setActiveUserInfo, setServiceInfo } from './redux/features/app/appSlice'; import { semanticKernelDarkTheme, semanticKernelLightTheme } from './styles'; export const useClasses = makeStyles({ @@ -121,10 +121,10 @@ const App: FC = () => { // Check if content safety is enabled file.getContentSafetyStatus(), - // Load service options - chat.getServiceOptions().then((serviceOptions) => { - if (serviceOptions) { - dispatch(setServiceOptions(serviceOptions)); + // Load service information + chat.getServiceInfo().then((serviceInfo) => { + if (serviceInfo) { + dispatch(setServiceInfo(serviceInfo)); } }), ]); diff --git a/webapp/src/components/chat/tabs/DocumentsTab.tsx b/webapp/src/components/chat/tabs/DocumentsTab.tsx index 3dd7a49d7..52c604805 100644 --- a/webapp/src/components/chat/tabs/DocumentsTab.tsx +++ b/webapp/src/components/chat/tabs/DocumentsTab.tsx @@ -87,7 +87,7 @@ export const DocumentsTab: React.FC = () => { const chat = useChat(); const fileHandler = useFile(); - const { serviceOptions } = useAppSelector((state: RootState) => state.app); + const { serviceInfo } = useAppSelector((state: RootState) => state.app); const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations); const { importingDocuments } = conversations[selectedId]; @@ -155,17 +155,17 @@ export const DocumentsTab: React.FC = () => {
- {serviceOptions.memoryStore.types.map((storeType) => { + {serviceInfo.memoryStore.types.map((storeType) => { return ( ); })} diff --git a/webapp/src/components/header/settings-dialog/SettingsDialog.tsx b/webapp/src/components/header/settings-dialog/SettingsDialog.tsx index d7dd52306..90fceda23 100644 --- a/webapp/src/components/header/settings-dialog/SettingsDialog.tsx +++ b/webapp/src/components/header/settings-dialog/SettingsDialog.tsx @@ -56,7 +56,7 @@ interface ISettingsDialogProps { export const SettingsDialog: React.FC = ({ open, closeDialog }) => { const classes = useClasses(); const dialogClasses = useDialogClasses(); - const { serviceOptions, settings, tokenUsage } = useAppSelector((state: RootState) => state.app); + const { serviceInfo, settings, tokenUsage } = useAppSelector((state: RootState) => state.app); return ( = ({ open, closeDial - Backend version: {serviceOptions.version} + Backend version: {serviceInfo.version}
Frontend version: {process.env.REACT_APP_SK_VERSION ?? '-'}
diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index fd4061a3b..6f29a71f8 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -371,9 +371,9 @@ export const useChat = () => { } }; - const getServiceOptions = async () => { + const getServiceInfo = async () => { try { - return await chatService.getServiceOptionsAsync(await AuthHelper.getSKaaSAccessToken(instance, inProgress)); + return await chatService.getServiceInfoAsync(await AuthHelper.getSKaaSAccessToken(instance, inProgress)); } catch (e: any) { const errorMessage = `Error getting service options. Details: ${getErrorDetails(e)}`; dispatch(addAlert({ message: errorMessage, type: AlertType.Error })); @@ -448,7 +448,7 @@ export const useChat = () => { importDocument, joinChat, editChat, - getServiceOptions, + getServiceInfo, deleteChat, processPlan, }; diff --git a/webapp/src/libs/models/ServiceOptions.ts b/webapp/src/libs/models/ServiceInfo.ts similarity index 86% rename from webapp/src/libs/models/ServiceOptions.ts rename to webapp/src/libs/models/ServiceInfo.ts index 6e6d7914d..d5936d390 100644 --- a/webapp/src/libs/models/ServiceOptions.ts +++ b/webapp/src/libs/models/ServiceInfo.ts @@ -5,7 +5,7 @@ export interface MemoryStore { selectedType: string; } -export interface ServiceOptions { +export interface ServiceInfo { memoryStore: MemoryStore; version: string; isContentSafetyEnabled: boolean; diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 94398f8bf..9e17729ed 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -7,7 +7,7 @@ import { IChatParticipant } from '../models/ChatParticipant'; import { IChatSession, ICreateChatSessionResponse } from '../models/ChatSession'; import { IChatUser } from '../models/ChatUser'; import { PluginManifest } from '../models/PluginManifest'; -import { ServiceOptions } from '../models/ServiceOptions'; +import { ServiceInfo } from '../models/ServiceInfo'; import { IAsk, IAskVariables } from '../semantic-kernel/model/Ask'; import { IAskResult } from '../semantic-kernel/model/AskResult'; import { ICustomPlugin } from '../semantic-kernel/model/CustomPlugin'; @@ -62,7 +62,7 @@ export class ChatService extends BaseService { ): Promise => { const result = await this.getResponseAsync( { - commandPath: `chats/${chatId}/messages/?startIdx=${startIdx}&count=${count}`, + commandPath: `chats/${chatId}/messages?startIdx=${startIdx}&count=${count}`, method: 'GET', }, accessToken, @@ -234,7 +234,7 @@ export class ChatService extends BaseService { ): Promise => { const result = await this.getResponseAsync( { - commandPath: `chats/${chatId}/?memoryType=${memoryName}`, + commandPath: `chats/${chatId}/memories?type=${memoryName}`, method: 'GET', }, accessToken, @@ -243,10 +243,10 @@ export class ChatService extends BaseService { return result; }; - public getServiceOptionsAsync = async (accessToken: string): Promise => { - const result = await this.getResponseAsync( + public getServiceInfoAsync = async (accessToken: string): Promise => { + const result = await this.getResponseAsync( { - commandPath: `options`, + commandPath: `info`, method: 'GET', }, accessToken, diff --git a/webapp/src/libs/services/DocumentImportService.ts b/webapp/src/libs/services/DocumentImportService.ts index 80f5661cf..af7a9411b 100644 --- a/webapp/src/libs/services/DocumentImportService.ts +++ b/webapp/src/libs/services/DocumentImportService.ts @@ -2,7 +2,7 @@ import { IChatMessage } from '../models/ChatMessage'; import { BaseService } from './BaseService'; -import { ServiceOptions } from '../models/ServiceOptions'; +import { ServiceInfo } from '../models/ServiceInfo'; export class DocumentImportService extends BaseService { public importDocumentAsync = async ( @@ -28,14 +28,14 @@ export class DocumentImportService extends BaseService { }; public getContentSafetyStatusAsync = async (accessToken: string): Promise => { - const serviceOptions = await this.getResponseAsync( + const serviceInfo = await this.getResponseAsync( { - commandPath: 'contentSafety/status', + commandPath: 'info', method: 'GET', }, accessToken, ); - return serviceOptions.isContentSafetyEnabled; + return serviceInfo.isContentSafetyEnabled; }; } diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts index 813634a22..5e3745cbc 100644 --- a/webapp/src/redux/features/app/AppState.ts +++ b/webapp/src/redux/features/app/AppState.ts @@ -3,7 +3,7 @@ import { AuthHelper } from '../../../libs/auth/AuthHelper'; import { AlertType } from '../../../libs/models/AlertType'; import { IChatUser } from '../../../libs/models/ChatUser'; -import { ServiceOptions } from '../../../libs/models/ServiceOptions'; +import { ServiceInfo } from '../../../libs/models/ServiceInfo'; import { TokenUsage } from '../../../libs/models/TokenUsage'; // This is the default user information when authentication is set to 'None'. @@ -56,7 +56,7 @@ export interface AppState { tokenUsage: TokenUsage; features: Record; settings: Setting[]; - serviceOptions: ServiceOptions; + serviceInfo: ServiceInfo; isMaintenance: boolean; } @@ -142,6 +142,6 @@ export const initialState: AppState = { tokenUsage: {}, features: Features, settings: Settings, - serviceOptions: { memoryStore: { types: [], selectedType: '' }, version: '', isContentSafetyEnabled: false }, + serviceInfo: { memoryStore: { types: [], selectedType: '' }, version: '', isContentSafetyEnabled: false }, isMaintenance: false, }; diff --git a/webapp/src/redux/features/app/appSlice.ts b/webapp/src/redux/features/app/appSlice.ts index 820e8b463..486e7eb52 100644 --- a/webapp/src/redux/features/app/appSlice.ts +++ b/webapp/src/redux/features/app/appSlice.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Constants } from '../../../Constants'; -import { ServiceOptions } from '../../../libs/models/ServiceOptions'; +import { ServiceInfo } from '../../../libs/models/ServiceInfo'; import { TokenUsage, TokenUsageFunctionNameMap } from '../../../libs/models/TokenUsage'; import { ActiveUserInfo, Alert, AppState, FeatureKeys, initialState } from './AppState'; @@ -68,8 +68,8 @@ export const appSlice = createSlice({ }, }; }, - setServiceOptions: (state: AppState, action: PayloadAction) => { - state.serviceOptions = action.payload; + setServiceInfo: (state: AppState, action: PayloadAction) => { + state.serviceInfo = action.payload; }, }, }); @@ -82,7 +82,7 @@ export const { toggleFeatureFlag, toggleFeatureState, updateTokenUsage, - setServiceOptions, + setServiceInfo, setMaintenance, } = appSlice.actions; From 35748087e7651f879f64cff03fc44149a508067e Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 2 Oct 2023 16:51:43 -0700 Subject: [PATCH 3/8] Update webapi/Models/Response/CreateChatResponse.cs Co-authored-by: Desmond Howard --- webapi/Models/Response/CreateChatResponse.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Models/Response/CreateChatResponse.cs b/webapi/Models/Response/CreateChatResponse.cs index 0cf3e72e6..3adcabf1e 100644 --- a/webapi/Models/Response/CreateChatResponse.cs +++ b/webapi/Models/Response/CreateChatResponse.cs @@ -6,7 +6,7 @@ namespace CopilotChat.WebApi.Models.Response; /// -/// Response object definition to the POST to 'chats' request. +/// Response object definition to the 'chats' POST request. /// This groups the initial bot message with the chat session /// to avoid making two requests. /// From 7a5f04946be26e9c2d372302a65e3f58973ddab3 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 2 Oct 2023 16:54:52 -0700 Subject: [PATCH 4/8] Update webapi/Models/Response/ServiceInfoResponse.cs Co-authored-by: Desmond Howard --- webapi/Models/Response/ServiceInfoResponse.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Models/Response/ServiceInfoResponse.cs b/webapi/Models/Response/ServiceInfoResponse.cs index 0a1c903c1..7f83f59b7 100644 --- a/webapi/Models/Response/ServiceInfoResponse.cs +++ b/webapi/Models/Response/ServiceInfoResponse.cs @@ -26,7 +26,7 @@ public class ServiceInfoResponse /// /// True if content safety if enabled, false otherwise. /// - [JsonPropertyName("contentSafetyStatus")] + [JsonPropertyName("isContentSafetyEnabled")] public bool IsContentSafetyEnabled { get; set; } = false; } From fc2dd4cf9133ba5374286c573f17fb5c24aaf3dc Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 2 Oct 2023 16:58:38 -0700 Subject: [PATCH 5/8] Update webapp/src/libs/services/ChatService.ts Co-authored-by: Desmond Howard --- webapp/src/libs/services/ChatService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 9e17729ed..c25e01110 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -171,7 +171,7 @@ export class ChatService extends BaseService { const result = await this.getResponseAsync( { - commandPath: processPlan ? `chats/${chatId}/processPlan` : `chats/${chatId}/messages`, + commandPath: `chats/${chatId}/${processPlan ? 'processPlan' : 'chat'}`, method: 'POST', body: ask, }, From daed3ddb18d6a0f2a7c61ea09bc842e3b613cb56 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 2 Oct 2023 17:01:13 -0700 Subject: [PATCH 6/8] Update webapp/src/libs/services/BotService.ts Co-authored-by: Desmond Howard --- webapp/src/libs/services/BotService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/libs/services/BotService.ts b/webapp/src/libs/services/BotService.ts index 83a686e94..4bf713b55 100644 --- a/webapp/src/libs/services/BotService.ts +++ b/webapp/src/libs/services/BotService.ts @@ -22,7 +22,7 @@ export class BotService extends BaseService { const result = await this.getResponseAsync( { commandPath: 'chats/archives', - method: 'Post', + method: 'POST', body: bot, }, accessToken, From c092be3f4f98671564390adf0e6b1dd666f57764 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 2 Oct 2023 18:07:54 -0700 Subject: [PATCH 7/8] Address PR comments --- webapi/Controllers/ChatArchiveController.cs | 6 +++--- webapi/Controllers/ChatController.cs | 8 ++++---- webapi/Controllers/ServiceInfoController.cs | 2 +- webapi/Models/Request/ExecutePlanParameters.cs | 5 ----- webapp/src/components/chat/chat-list/ChatList.tsx | 4 ++-- webapp/src/libs/hooks/useChat.ts | 8 ++++---- webapp/src/libs/models/{Bot.ts => ChatArchive.ts} | 2 +- .../services/{BotService.ts => ChatArchiveService.ts} | 10 +++++----- webapp/src/libs/services/ChatService.ts | 4 ++-- 9 files changed, 22 insertions(+), 27 deletions(-) rename webapp/src/libs/models/{Bot.ts => ChatArchive.ts} (92%) rename webapp/src/libs/services/{BotService.ts => ChatArchiveService.ts} (71%) diff --git a/webapi/Controllers/ChatArchiveController.cs b/webapi/Controllers/ChatArchiveController.cs index 87d83e8e8..70579c1b0 100644 --- a/webapi/Controllers/ChatArchiveController.cs +++ b/webapi/Controllers/ChatArchiveController.cs @@ -65,7 +65,7 @@ public ChatArchiveController( /// Cancellation token. /// The serialized chat archive object of the chat id. [HttpGet] - [Route("chats/archives/{chatId:guid}")] + [Route("chats/{chatId:guid}/archive")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -74,9 +74,9 @@ public ChatArchiveController( { this._logger.LogDebug("Received call to download a chat archive"); - var memory = await this.CreateChatArchiveAsync(chatId, cancellationToken); + var chatArchive = await this.CreateChatArchiveAsync(chatId, cancellationToken); - return this.Ok(memory); + return this.Ok(chatArchive); } /// diff --git a/webapi/Controllers/ChatController.cs b/webapi/Controllers/ChatController.cs index cfbdeb147..ba92c3d50 100644 --- a/webapi/Controllers/ChatController.cs +++ b/webapi/Controllers/ChatController.cs @@ -94,7 +94,7 @@ public async Task ChatAsync( [FromBody] Ask ask, [FromRoute] Guid chatId) { - this._logger.LogDebug("/chat request received."); + this._logger.LogDebug("Chat message received."); return await this.HandleRequest(ChatFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); } @@ -112,7 +112,7 @@ public async Task ChatAsync( /// Prompt along with its parameters. /// Chat ID. /// Results containing the response from the model. - [Route("chats/{chatId:guid}/processPlan")] + [Route("chats/{chatId:guid}/plan")] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -130,7 +130,7 @@ public async Task ProcessPlanAsync( [FromBody] ExecutePlanParameters ask, [FromRoute] Guid chatId) { - this._logger.LogDebug("/processplan request received."); + this._logger.LogDebug("plan request received."); return await this.HandleRequest(ProcessPlanFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); } @@ -263,7 +263,7 @@ private Dictionary GetPluginAuthHeaders(IHeaderDictionary header /// The domain of the manifest. /// The plugin's manifest JSON. [HttpGet] - [Route("pluginManifest")] + [Route("plugins")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetPluginManifest([FromQuery] Uri manifestDomain) { diff --git a/webapi/Controllers/ServiceInfoController.cs b/webapi/Controllers/ServiceInfoController.cs index 0624c2e02..bb4ba090b 100644 --- a/webapi/Controllers/ServiceInfoController.cs +++ b/webapi/Controllers/ServiceInfoController.cs @@ -52,7 +52,7 @@ public ServiceInfoController( [Route("info")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult GetServiceOptions() + public IActionResult GetServiceInfo() { var response = new ServiceInfoResponse() { diff --git a/webapi/Models/Request/ExecutePlanParameters.cs b/webapi/Models/Request/ExecutePlanParameters.cs index 7bea6b332..ff0bcb6d7 100644 --- a/webapi/Models/Request/ExecutePlanParameters.cs +++ b/webapi/Models/Request/ExecutePlanParameters.cs @@ -7,9 +7,4 @@ namespace CopilotChat.WebApi.Models.Request; public class ExecutePlanParameters : Ask { public ProposedPlan? Plan { get; set; } - - /// - /// Id of the message containing proposed plan previously saved in chat history, if any. - /// - public string? MessageId { get; set; } } diff --git a/webapp/src/components/chat/chat-list/ChatList.tsx b/webapp/src/components/chat/chat-list/ChatList.tsx index e6be89fb7..5b9d3252c 100644 --- a/webapp/src/components/chat/chat-list/ChatList.tsx +++ b/webapp/src/components/chat/chat-list/ChatList.tsx @@ -13,7 +13,7 @@ import { import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { useChat, useFile } from '../../../libs/hooks'; import { AlertType } from '../../../libs/models/AlertType'; -import { Bot } from '../../../libs/models/Bot'; +import { ChatArchive } from '../../../libs/models/ChatArchive'; import { useAppDispatch, useAppSelector } from '../../../redux/app/hooks'; import { RootState } from '../../../redux/app/store'; import { addAlert } from '../../../redux/features/app/appSlice'; @@ -161,7 +161,7 @@ export const ChatList: FC = () => { const fileUploaderRef = useRef(null); const onUpload = useCallback( (file: File) => { - fileHandler.loadFile(file, chat.uploadBot).catch((error) => + fileHandler.loadFile(file, chat.uploadBot).catch((error) => dispatch( addAlert({ message: `Failed to parse uploaded file. ${error instanceof Error ? error.message : ''}`, diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index 6f29a71f8..942818d1c 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -18,13 +18,13 @@ import { import { Plugin } from '../../redux/features/plugins/PluginsState'; import { AuthHelper } from '../auth/AuthHelper'; import { AlertType } from '../models/AlertType'; -import { Bot } from '../models/Bot'; +import { ChatArchive } from '../models/ChatArchive'; import { AuthorRoles, ChatMessageType, IChatMessage } from '../models/ChatMessage'; import { IChatSession, ICreateChatSessionResponse } from '../models/ChatSession'; import { IChatUser } from '../models/ChatUser'; import { TokenUsage } from '../models/TokenUsage'; import { IAskVariables } from '../semantic-kernel/model/Ask'; -import { BotService } from '../services/BotService'; +import { ChatArchiveService } from '../services/ChatArchiveService'; import { ChatService } from '../services/ChatService'; import { DocumentImportService } from '../services/DocumentImportService'; @@ -51,7 +51,7 @@ export const useChat = () => { const { conversations } = useAppSelector((state: RootState) => state.conversations); const { activeUserInfo, features } = useAppSelector((state: RootState) => state.app); - const botService = new BotService(); + const botService = new ChatArchiveService(); const chatService = new ChatService(); const documentImportService = new DocumentImportService(); @@ -221,7 +221,7 @@ export const useChat = () => { return undefined; }; - const uploadBot = async (bot: Bot) => { + const uploadBot = async (bot: ChatArchive) => { try { const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); await botService.uploadAsync(bot, accessToken).then(async (chatSession: IChatSession) => { diff --git a/webapp/src/libs/models/Bot.ts b/webapp/src/libs/models/ChatArchive.ts similarity index 92% rename from webapp/src/libs/models/Bot.ts rename to webapp/src/libs/models/ChatArchive.ts index e41b15416..e2f95c699 100644 --- a/webapp/src/libs/models/Bot.ts +++ b/webapp/src/libs/models/ChatArchive.ts @@ -2,7 +2,7 @@ import { IChatMessage } from './ChatMessage'; -export interface Bot { +export interface ChatArchive { Schema: { Name: string; Version: number }; Configurations: { EmbeddingAIService: string; EmbeddingDeploymentOrModelId: string }; ChatTitle: string; diff --git a/webapp/src/libs/services/BotService.ts b/webapp/src/libs/services/ChatArchiveService.ts similarity index 71% rename from webapp/src/libs/services/BotService.ts rename to webapp/src/libs/services/ChatArchiveService.ts index 83a686e94..1899ae850 100644 --- a/webapp/src/libs/services/BotService.ts +++ b/webapp/src/libs/services/ChatArchiveService.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. -import { Bot } from '../models/Bot'; +import { ChatArchive } from '../models/ChatArchive'; import { IChatSession } from '../models/ChatSession'; import { BaseService } from './BaseService'; -export class BotService extends BaseService { +export class ChatArchiveService extends BaseService { public downloadAsync = async (chatId: string, accessToken: string) => { // TODO: [Issue #47] Add type for result. See Bot.cs const result = await this.getResponseAsync( { - commandPath: `chats/archives/${chatId}`, + commandPath: `chats/${chatId}/archive`, method: 'GET', }, accessToken, @@ -18,12 +18,12 @@ export class BotService extends BaseService { return result; }; - public uploadAsync = async (bot: Bot, accessToken: string) => { + public uploadAsync = async (chatArchive: ChatArchive, accessToken: string) => { const result = await this.getResponseAsync( { commandPath: 'chats/archives', method: 'Post', - body: bot, + body: chatArchive, }, accessToken, ); diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index 9e17729ed..9c06bb4f0 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -171,7 +171,7 @@ export class ChatService extends BaseService { const result = await this.getResponseAsync( { - commandPath: processPlan ? `chats/${chatId}/processPlan` : `chats/${chatId}/messages`, + commandPath: processPlan ? `chats/${chatId}/plan` : `chats/${chatId}/messages`, method: 'POST', body: ask, }, @@ -258,7 +258,7 @@ export class ChatService extends BaseService { public getPluginManifest = async (manifestDomain: string, accessToken: string): Promise => { const result = await this.getResponseAsync( { - commandPath: `pluginManifest`, + commandPath: `plugins`, method: 'GET', query: new URLSearchParams({ manifestDomain: encodeURIComponent(manifestDomain) }), }, From 15fde8c996bad3515d1804d86a15face9c95ada2 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Mon, 2 Oct 2023 19:00:01 -0700 Subject: [PATCH 8/8] Fix errors stemming from merge --- webapp/src/components/open-api-plugins/PluginGallery.tsx | 6 +++--- webapp/src/redux/features/app/AppState.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/open-api-plugins/PluginGallery.tsx b/webapp/src/components/open-api-plugins/PluginGallery.tsx index 08a92df24..9884edf39 100644 --- a/webapp/src/components/open-api-plugins/PluginGallery.tsx +++ b/webapp/src/components/open-api-plugins/PluginGallery.tsx @@ -65,7 +65,7 @@ export const PluginGallery: React.FC = () => { const dispatch = useDispatch(); const { plugins } = useAppSelector((state: RootState) => state.plugins); - const { serviceOptions } = useAppSelector((state: RootState) => state.app); + const { serviceInfo } = useAppSelector((state: RootState) => state.app); const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations); const [open, setOpen] = useState(false); @@ -75,7 +75,7 @@ export const PluginGallery: React.FC = () => { useEffect(() => { function updateHostedPlugin() { setHostedPlugins([]); - serviceOptions.availablePlugins.forEach((availablePlugin) => { + serviceInfo.availablePlugins.forEach((availablePlugin) => { getPluginManifest(availablePlugin.manifestDomain) .then((manifest) => { const newHostedPlugin = { @@ -103,7 +103,7 @@ export const PluginGallery: React.FC = () => { updateHostedPlugin(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [conversations[selectedId], open, serviceOptions.availablePlugins]); + }, [conversations[selectedId], open, serviceInfo.availablePlugins]); return (