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..70579c1b0 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/{chatId:guid}/archive")] [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 chatArchive = await this.CreateChatArchiveAsync(chatId, cancellationToken); - return this.Ok(memory); + return this.Ok(chatArchive); } /// - /// 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 a763bb297..a008418d5 100644 --- a/webapi/Controllers/ChatController.cs +++ b/webapi/Controllers/ChatController.cs @@ -82,8 +82,9 @@ public ChatController( /// 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)] @@ -98,10 +99,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); + this._logger.LogDebug("Chat message received."); + + return await this.HandleRequest(ChatFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); } /// @@ -115,8 +118,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}/plan")] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -131,10 +135,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); + this._logger.LogDebug("plan request received."); + + return await this.HandleRequest(ProcessPlanFunctionName, kernel, messageRelayHubContext, planner, askConverter, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); } #region Private Methods @@ -151,6 +157,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, @@ -161,17 +168,13 @@ private async Task HandleRequest( ChatSessionRepository chatSessionRepository, ChatParticipantRepository chatParticipantRepository, IAuthInfo authInfo, - Ask ask) + Ask ask, + string chatId) { // Put ask's variables in the context we will use. var contextVariables = askConverter.GetContextVariables(ask); // Verify that the chat exists and that the user has access to it. - if (!contextVariables.TryGetValue("chatId", out string? chatId)) - { - return this.BadRequest("ChatId not specified."); - } - ChatSession? chat = null; #pragma warning disable CA1508 // Avoid dead conditional code. It's giving out false positives on chat == null. if (!(await chatSessionRepository.TryFindByIdAsync(chatId, callback: c => chat = c)) || chat == null) diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs index 01cfd1605..a506bb753 100644 --- a/webapi/Controllers/ChatHistoryController.cs +++ b/webapi/Controllers/ChatHistoryController.cs @@ -34,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; @@ -81,7 +82,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 +110,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.CreatedAtRoute(GetChatRoute, new { chatId = newChat.Id }, new CreateChatResponse(newChat, chatMessage)); } /// @@ -120,8 +119,7 @@ public async Task CreateChatSessionAsync( /// /// The chat id. [HttpGet] - [ActionName("GetChatSessionByIdAsync")] - [Route("chatSession/getChat/{chatId:guid}")] + [Route("chats/{chatId:guid}", Name = GetChatRoute)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -143,11 +141,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 +161,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 +176,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 +203,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 +230,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 +245,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 +258,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 31a967e70..ce33d920d 100644 --- a/webapi/Controllers/ChatMemoryController.cs +++ b/webapi/Controllers/ChatMemoryController.cs @@ -51,24 +51,24 @@ 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("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 type) { // Sanitize the log input by removing new line characters. // https://github.com/microsoft/chat-copilot/security/code-scanning/1 var sanitizedChatId = GetSanitizedParameter(chatId); - var sanitizedMemoryType = GetSanitizedParameter(memoryType); + var sanitizedMemoryType = GetSanitizedParameter(type); // 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.", sanitizedMemoryType); return this.BadRequest($"Memory type: {sanitizedMemoryType} is invalid."); @@ -100,8 +100,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 db0133a37..22cb72ec4 100644 --- a/webapi/Controllers/ServiceInfoController.cs +++ b/webapi/Controllers/ServiceInfoController.cs @@ -31,6 +31,7 @@ public class ServiceInfoController : ControllerBase private readonly ChatAuthenticationOptions _chatAuthenticationOptions; private readonly FrontendOptions _frontendOptions; private readonly IEnumerable availablePlugins; + private readonly ContentSafetyOptions _contentSafetyOptions; public ServiceInfoController( ILogger logger, @@ -38,7 +39,8 @@ public ServiceInfoController( IOptions memoryOptions, IOptions chatAuthenticationOptions, IOptions frontendOptions, - IDictionary availablePlugins) + IDictionary availablePlugins, + IOptions contentSafetyOptions) { this._logger = logger; this.Configuration = configuration; @@ -46,25 +48,27 @@ public ServiceInfoController( this._chatAuthenticationOptions = chatAuthenticationOptions.Value; this._frontendOptions = frontendOptions.Value; this.availablePlugins = this.SanitizePlugins(availablePlugins); + this._contentSafetyOptions = contentSafetyOptions.Value; } /// - /// Return the memory store type that is configured. + /// Return information on running service. /// - [Route("serviceOptions")] + [Route("info")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult GetServiceOptions() + public IActionResult GetServiceInfo() { - 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(), }, AvailablePlugins = this.availablePlugins, - 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 2dedebc28..7bfb865af 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -50,8 +50,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/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/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/CreateChatResponse.cs b/webapi/Models/Response/CreateChatResponse.cs index eeb4d4d1e..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 'chatSession/create' request. +/// Response object definition to the 'chats' POST 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 72% rename from webapi/Models/Response/ServiceOptionsResponse.cs rename to webapi/Models/Response/ServiceInfoResponse.cs index 85b3f4900..fb8f8fc47 100644 --- a/webapi/Models/Response/ServiceOptionsResponse.cs +++ b/webapi/Models/Response/ServiceInfoResponse.cs @@ -7,13 +7,16 @@ namespace CopilotChat.WebApi.Models.Response; -public class ServiceOptionsResponse +/// +/// Information on running service. +/// +public class ServiceInfoResponse { /// /// Configured memory store. /// [JsonPropertyName("memoryStore")] - public MemoryStoreOptionResponse MemoryStore { get; set; } = new MemoryStoreOptionResponse(); + public MemoryStoreInfoResponse MemoryStore { get; set; } = new MemoryStoreInfoResponse(); /// /// All the available plugins. @@ -26,12 +29,18 @@ public class ServiceOptionsResponse /// [JsonPropertyName("version")] public string Version { get; set; } = string.Empty; + + /// + /// True if content safety if enabled, false otherwise. + /// + [JsonPropertyName("isContentSafetyEnabled")] + public bool IsContentSafetyEnabled { get; set; } = false; } /// /// Response to memoryStoreType request. /// -public class MemoryStoreOptionResponse +public class MemoryStoreInfoResponse { /// /// All the available memory store types. 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 00b5e4eaa..f24ddc2f0 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. @@ -190,11 +190,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/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/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/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/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 ( = ({ 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 45b161f1e..3a56348ad 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(); @@ -164,7 +164,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 = {}; @@ -223,7 +223,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) => { @@ -327,7 +327,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); @@ -375,9 +375,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 })); @@ -452,7 +452,7 @@ export const useChat = () => { importDocument, joinChat, editChat, - getServiceOptions, + getServiceInfo, deleteChat, processPlan, }; 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/models/ServiceOptions.ts b/webapp/src/libs/models/ServiceInfo.ts similarity index 81% rename from webapp/src/libs/models/ServiceOptions.ts rename to webapp/src/libs/models/ServiceInfo.ts index aed3e50ed..d74927742 100644 --- a/webapp/src/libs/models/ServiceOptions.ts +++ b/webapp/src/libs/models/ServiceInfo.ts @@ -10,8 +10,9 @@ export interface HostedPlugin { manifestDomain: string; } -export interface ServiceOptions { +export interface ServiceInfo { memoryStore: MemoryStore; availablePlugins: HostedPlugin[]; version: string; + isContentSafetyEnabled: boolean; } diff --git a/webapp/src/libs/services/BotService.ts b/webapp/src/libs/services/ChatArchiveService.ts similarity index 63% rename from webapp/src/libs/services/BotService.ts rename to webapp/src/libs/services/ChatArchiveService.ts index a5aa1f37c..7dd47d6f6 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: `bot/download/${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: 'bot/upload', - method: 'Post', - body: bot, + commandPath: 'chats/archives', + method: 'POST', + body: chatArchive, }, accessToken, ); diff --git a/webapp/src/libs/services/ChatService.ts b/webapp/src/libs/services/ChatService.ts index af33878bc..abab195c0 100644 --- a/webapp/src/libs/services/ChatService.ts +++ b/webapp/src/libs/services/ChatService.ts @@ -6,7 +6,7 @@ import { IChatMessage } from '../models/ChatMessage'; import { IChatParticipant } from '../models/ChatParticipant'; import { IChatSession, ICreateChatSessionResponse } from '../models/ChatSession'; import { IChatUser } from '../models/ChatUser'; -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'; @@ -20,7 +20,7 @@ export class ChatService extends BaseService { const result = await this.getResponseAsync( { - commandPath: 'chatSession/create', + commandPath: 'chats', method: 'POST', body, }, @@ -33,7 +33,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, @@ -42,10 +42,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, @@ -61,7 +61,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: `chats/${chatId}/${processPlan ? 'processPlan' : 'chat'}`, 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}/memories?type=${memoryName}`, method: 'GET', }, accessToken, @@ -247,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: `serviceOptions`, + commandPath: `info`, method: 'GET', }, accessToken, diff --git a/webapp/src/libs/services/DocumentImportService.ts b/webapp/src/libs/services/DocumentImportService.ts index d8b7ec89e..af7a9411b 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 { ServiceInfo } from '../models/ServiceInfo'; 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 serviceInfo = await this.getResponseAsync( { - commandPath: 'contentSafety/status', + commandPath: 'info', method: 'GET', }, accessToken, ); + + return serviceInfo.isContentSafetyEnabled; }; } diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts index 46de4e883..88d1c1d97 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,10 +142,11 @@ export const initialState: AppState = { tokenUsage: {}, features: Features, settings: Settings, - serviceOptions: { + serviceInfo: { memoryStore: { types: [], selectedType: '' }, availablePlugins: [], 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;