From fe6c77ce9e1ad8193ed61ff82c23a674831ea39a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Sep 2023 12:16:00 -0700 Subject: [PATCH 01/28] Migration POC --- webapi/Controllers/BotController.cs | 10 +- webapi/Controllers/ChatMigrationController.cs | 115 +++++++++++++++ .../ISemanticMemoryClientExtensions.cs | 15 +- webapi/Extensions/ServiceExtensions.cs | 9 ++ webapi/Program.cs | 2 + webapi/Services/ChatMemoryMigrationService.cs | 110 ++++++++++++++ webapi/Services/ChatMigrationMiddleware.cs | 96 +++++++++++++ webapi/Services/ChatMigrationMonitor.cs | 136 ++++++++++++++++++ .../Services/IChatMemoryMigrationService.cs | 18 +++ webapi/Services/IChatMigrationMonitor.cs | 35 +++++ webapi/Storage/ChatMemorySourceRepository.cs | 9 ++ webapi/Storage/ChatSessionRepository.cs | 12 ++ webapp/src/App.tsx | 9 +- webapp/src/components/views/BackendProbe.tsx | 82 ++++++++--- webapp/src/redux/features/app/AppState.ts | 2 + webapp/src/redux/features/app/appSlice.ts | 4 + .../message-relay/signalRHubConnection.ts | 12 +- 17 files changed, 651 insertions(+), 25 deletions(-) create mode 100644 webapi/Controllers/ChatMigrationController.cs create mode 100644 webapi/Services/ChatMemoryMigrationService.cs create mode 100644 webapi/Services/ChatMigrationMiddleware.cs create mode 100644 webapi/Services/ChatMigrationMonitor.cs create mode 100644 webapi/Services/IChatMemoryMigrationService.cs create mode 100644 webapi/Services/IChatMigrationMonitor.cs diff --git a/webapi/Controllers/BotController.cs b/webapi/Controllers/BotController.cs index 70408a0fa..0bdc6af2f 100644 --- a/webapi/Controllers/BotController.cs +++ b/webapi/Controllers/BotController.cs @@ -265,11 +265,11 @@ private async Task CreateBotAsync(IKernel kernel, Guid chatId) await this.GetMemoryRecordsAndAppendToEmbeddingsAsync(kernel: kernel, collectionName: collection, embeddings: bot.Embeddings); } - // get the document memory collection names (global scope) - await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( - kernel: kernel, - collectionName: this._documentMemoryOptions.GlobalDocumentCollectionName, - embeddings: bot.DocumentEmbeddings); + //// get the document memory collection names (global scope) $$$ + //await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( + // kernel: kernel, + // collectionName: this._documentMemoryOptions.GlobalDocumentCollectionName, + // embeddings: bot.DocumentEmbeddings); // get the document memory collection names (user scope) await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( diff --git a/webapi/Controllers/ChatMigrationController.cs b/webapi/Controllers/ChatMigrationController.cs new file mode 100644 index 000000000..cf83d74b8 --- /dev/null +++ b/webapi/Controllers/ChatMigrationController.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using CopilotChat.WebApi.Auth; +using CopilotChat.WebApi.Hubs; +using CopilotChat.WebApi.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; + +namespace CopilotChat.WebApi.Controllers; + +/// +/// $$$ +/// +[ApiController] +public class ChatMigrationController : ControllerBase +{ + private const string GlobalChatMigrationActiveCall = "GlobalChatMigrationActive"; + private const string GlobalChatMigrationCompleteCall = "GlobalChatMigrationComplete"; + + private readonly ILogger _logger; + private readonly IAuthInfo _authInfo; + + /// + /// Initializes a new instance of the class. + /// + public ChatMigrationController( + ILogger logger, + IAuthInfo authInfo) + { + this._logger = logger; + this._authInfo = authInfo; + } + + /// + /// $$$ + /// + [Route("maintenance/")] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> MigrateStatusAsync( + [FromServices] IKernel kernel, + [FromServices] IChatMigrationMonitor migrationMonitor, + [FromServices] IHubContext messageRelayHubContext, + CancellationToken cancelToken = default) + { + var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(kernel.Memory, cancelToken).ConfigureAwait(false); + + if (migrationStatus != ChatVersionStatus.None) + { + await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationActiveCall, "Chat migration in progress", cancelToken).ConfigureAwait(false); + } + else + { + await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationCompleteCall, "Chat migration completed.", cancelToken).ConfigureAwait(false); + } + + return this.Ok($"Migration status: {migrationStatus.Label}"); + } + + ///// + ///// $$$ + ///// + //[Route("chats/version/")] + //[HttpPost] + //[ProducesResponseType(StatusCodes.Status200OK)] + //[ProducesResponseType(StatusCodes.Status400BadRequest)] + //public async Task MigrateChatsAsync( + // [FromServices] IChatMigrationMonitor migrationMonitor, + // [FromServices] IChatMemoryMigrationService migrationService, + // [FromServices] IHubContext messageRelayHubContext) + //{ + // // $$$ HOOK / ENTRY POINT ??? + // // Monitor caches status for minimum middle-ware impact + // var migrationStatus = await migrationMonitor.GetCurrentStatusAsync().ConfigureAwait(false); + + // if (migrationStatus != ChatVersionStatus.None) + // { + // await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationActiveCall, "Chat migration in progress").ConfigureAwait(false); + // } + + // if (migrationStatus == ChatVersionStatus.RequiresUpgrade) + // { + // try + // { + // // All documents will need to be re-imported + // await this.RemoveMemorySourcesAsync(); + // // Migrate all chats to single index + // await migrationService.MigrateAsync(); + + // await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationCompleteCall, "Chat migration complete").ConfigureAwait(false); + // } + // catch (Exception ex) when (!ex.IsCriticalException()) + // { + // return this.Problem(ex.Message); + // } + // } + + // return this.Ok(); + //} + + //private async Task RemoveMemorySourcesAsync() + //{ + // var documentMemories = await this._sourceRepository.GetAllAsync().ConfigureAwait(false); + // foreach (var document in documentMemories) // $$$ PARRALLEL ??? + // { + // await this._sourceRepository.DeleteAsync(document).ConfigureAwait(false); + // } + //} +} diff --git a/webapi/Extensions/ISemanticMemoryClientExtensions.cs b/webapi/Extensions/ISemanticMemoryClientExtensions.cs index 0d47a931d..13cede3da 100644 --- a/webapi/Extensions/ISemanticMemoryClientExtensions.cs +++ b/webapi/Extensions/ISemanticMemoryClientExtensions.cs @@ -110,11 +110,23 @@ public static async Task StoreDocumentAsync( await memoryClient.ImportDocumentAsync(uploadRequest, cancelToken); } + public static Task StoreMemoryAsync( + this ISemanticMemoryClient memoryClient, + string indexName, + string chatId, + string memoryName, + string memory, + CancellationToken cancelToken = default) + { + return memoryClient.StoreMemoryAsync(indexName, chatId, memoryName, memoryId: Guid.NewGuid().ToString(), memory, cancelToken); + } + public static async Task StoreMemoryAsync( this ISemanticMemoryClient memoryClient, string indexName, string chatId, string memoryName, + string memoryId, string memory, CancellationToken cancelToken = default) { @@ -124,10 +136,9 @@ public static async Task StoreMemoryAsync( await writer.FlushAsync(); stream.Position = 0; - var id = Guid.NewGuid().ToString(); var uploadRequest = new DocumentUploadRequest { - DocumentId = id, + DocumentId = memoryId, Index = indexName, Files = new() diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index e3c654eeb..3272faebf 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -7,6 +7,7 @@ using CopilotChat.WebApi.Auth; using CopilotChat.WebApi.Models.Storage; using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Services; using CopilotChat.WebApi.Storage; using CopilotChat.WebApi.Utilities; using Microsoft.AspNetCore.Authentication; @@ -85,6 +86,14 @@ internal static IServiceCollection AddUtilities(this IServiceCollection services return services.AddScoped(); } + internal static IServiceCollection AddMigrationServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + /// /// Add CORS settings. /// diff --git a/webapi/Program.cs b/webapi/Program.cs index ab5dd9648..c3a9c4601 100644 --- a/webapi/Program.cs +++ b/webapi/Program.cs @@ -68,6 +68,7 @@ public static async Task Main(string[] args) // Add in the rest of the services. builder.Services + .AddMigrationServices() .AddEndpointsApiExplorer() .AddSwaggerGen() .AddCorsPolicy(builder.Configuration) @@ -83,6 +84,7 @@ public static async Task Main(string[] args) app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); + app.UseMiddleware(); app.MapControllers() .RequireAuthorization(); app.MapHealthChecks("/healthz"); diff --git a/webapi/Services/ChatMemoryMigrationService.cs b/webapi/Services/ChatMemoryMigrationService.cs new file mode 100644 index 000000000..495fe7ebe --- /dev/null +++ b/webapi/Services/ChatMemoryMigrationService.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CopilotChat.WebApi.Extensions; +using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Storage; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticMemory; + +namespace CopilotChat.WebApi.Services; + +/// +/// $$$ +/// +public class ChatMemoryMigrationService : IChatMemoryMigrationService +{ + private readonly ILogger _logger; + private readonly ISemanticMemoryClient _memoryClient; + private readonly ChatSessionRepository _chatSessionRepository; + private readonly PromptsOptions _promptOptions; + + /// + /// Initializes a new instance of the class. + /// + public ChatMemoryMigrationService( + ILogger logger, + IOptions documentMemoryOptions, + IOptions promptOptions, + ISemanticMemoryClient memoryClient, + ChatSessionRepository chatSessionRepository) + { + this._logger = logger; + this._promptOptions = promptOptions.Value; + this._memoryClient = memoryClient; + this._chatSessionRepository = chatSessionRepository; + } + + /// + /// Migrates all non-document memory to the semantic-memory index. + /// + public async Task MigrateAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default) + { + var shouldMigrate = false; + + var tokenMemory = await GetTokenMemory(cancelToken).ConfigureAwait(false); + if (tokenMemory == null) + { + // Create token memory + var token = Guid.NewGuid().ToString(); + await SetTokenMemory(token, cancelToken).ConfigureAwait(false); + // Allow writes that are racing time to land + await Task.Delay(TimeSpan.FromSeconds(5), cancelToken).ConfigureAwait(false); + // Retrieve token memory + tokenMemory = await GetTokenMemory(cancelToken).ConfigureAwait(false); + // Set migrate flag if token matches + shouldMigrate = tokenMemory != null && tokenMemory.Metadata.Text.Equals(token, StringComparison.OrdinalIgnoreCase); + } + + if (!shouldMigrate) + { + return; + } + + // Extract and store memories, using the original id to avoid duplication should a retry be required. + await foreach ((string chatId, string memoryName, string memoryId, string memoryText) in QueryMemoriesAsync()) + { + // $$$ PARRALLEL ??? + await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, chatId, memoryName, memoryId, memoryText, cancelToken); + } + + await SetTokenMemory("Done", cancelToken).ConfigureAwait(false); // $$$ DONE Const + + // Inline function to extract all memories for a given chat and memory type. + async IAsyncEnumerable<(string chatId, string memoryName, string memoryId, string memoryText)> QueryMemoriesAsync() + { + var chats = await this._chatSessionRepository.GetAllChatsAsync().ConfigureAwait(false); + foreach (var chat in chats) + { + foreach (var memoryType in this._promptOptions.MemoryMap.Keys) + { + var indexName = $"{chat.Id}-{memoryType}"; + var memories = await memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancelToken).ToArrayAsync(cancelToken); + + foreach (var memory in memories) + { + yield return (chat.Id, memoryType, memory.Metadata.Id, memory.Metadata.Text); + } + } + } + } + + // Inline function to read the token memory + async Task GetTokenMemory(CancellationToken cancelToken) + { + return await memory.GetAsync(this._promptOptions.MemoryIndexName, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancelToken).ConfigureAwait(false); + } + + // Inline function to write the token memory + async Task SetTokenMemory(string token, CancellationToken cancelToken) + { + await memory.SaveInformationAsync(this._promptOptions.MemoryIndexName, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancelToken).ConfigureAwait(false); + } + } +} diff --git a/webapi/Services/ChatMigrationMiddleware.cs b/webapi/Services/ChatMigrationMiddleware.cs new file mode 100644 index 000000000..83c72e16f --- /dev/null +++ b/webapi/Services/ChatMigrationMiddleware.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using CopilotChat.WebApi.Controllers; +using CopilotChat.WebApi.Hubs; +using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Storage; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel; + +namespace CopilotChat.WebApi.Services; + +/// +/// $$$ +/// +public class ChatMigrationMiddleware +{ + private const string GlobalChatMigrationActiveCall = "GlobalChatMigrationActive"; // $$$ DUPE + private const string GlobalChatMigrationCompleteCall = "GlobalChatMigrationComplete"; + + private readonly RequestDelegate _next; + private readonly IOptions _documentMemoryOptions; + private readonly IOptions _promptOptions; + private readonly ChatMemorySourceRepository _sourceRepository; + private readonly IChatMigrationMonitor _migrationMonitor; + private readonly IChatMemoryMigrationService _migrationService; + private readonly IHubContext _messageRelayHubContext; + private readonly ILogger _logger; + + public ChatMigrationMiddleware( + RequestDelegate next, + IOptions documentMemoryOptions, + IOptions promptOptions, + ChatMemorySourceRepository sourceRepository, + IChatMigrationMonitor migrationMonitor, + IChatMemoryMigrationService migrationService, + IHubContext messageRelayHubContext, + ILogger logger) + + { + this._next = next; + this._documentMemoryOptions = documentMemoryOptions; + this._promptOptions = promptOptions; + this._sourceRepository = sourceRepository; + this._migrationMonitor = migrationMonitor; + this._migrationService = migrationService; + this._messageRelayHubContext = messageRelayHubContext; + this._logger = logger; + } + + public async Task Invoke(HttpContext ctx, IKernel kernel) + { + // Monitor caches status for minimum middle-ware impact + var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(kernel.Memory).ConfigureAwait(false); + + if (migrationStatus != ChatVersionStatus.None) + { + await this._messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationActiveCall, "Chat migration in progress").ConfigureAwait(false); + } + + if (migrationStatus == ChatVersionStatus.RequiresUpgrade) + { + // $$$ BACKGROUND JOB + try + { + // All documents will need to be re-imported + await this.RemoveMemorySourcesAsync(); + + // Migrate all chats to single index + await this._migrationService.MigrateAsync(kernel.Memory); + + await this._messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationCompleteCall, "Chat migration complete").ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + this._logger.LogError(ex, "Error migrating chat memories"); + //return this.Problem(ex.Message); $$$ + } + } + + await this._next(ctx); + } + + private async Task RemoveMemorySourcesAsync() + { + var documentMemories = await this._sourceRepository.GetAllAsync().ConfigureAwait(false); + foreach (var document in documentMemories) // $$$ PARALLEL ??? + { + await this._sourceRepository.DeleteAsync(document).ConfigureAwait(false); + } + } +} diff --git a/webapi/Services/ChatMigrationMonitor.cs b/webapi/Services/ChatMigrationMonitor.cs new file mode 100644 index 000000000..155a02011 --- /dev/null +++ b/webapi/Services/ChatMigrationMonitor.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CopilotChat.WebApi.Options; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Memory; + +namespace CopilotChat.WebApi.Services; + +/// +/// $$$ +/// +public class ChatMigrationMonitor : IChatMigrationMonitor +{ + internal const string MigrationKey = "migrate-00000000-0000-0000-0000-000000000000"; + + private static ChatVersionStatus? _cachedStatus; + private static bool? _hasCurrentIndex; + + private readonly ILogger _logger; + private readonly string _indexNameAllMemory; + + /// + /// Initializes a new instance of the class. + /// + public ChatMigrationMonitor( + ILogger logger, + IOptions promptOptions) + { + this._logger = logger; + this._indexNameAllMemory = promptOptions.Value.MemoryIndexName; + } + + /// + /// $$$ + /// + public async Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default) // $$$ RETURN string / id ??? + { + if (_cachedStatus == null) + { + // Attempt to determine migration status looking at index existence. (Once) + Interlocked.CompareExchange( + ref _cachedStatus, + await QueryCollectionAsync().ConfigureAwait(false), + null); + + if (_cachedStatus == null) + { + // Attempt to determine migration status looking at index state. + _cachedStatus = await QueryStatusAsync().ConfigureAwait(false); + } + } + else + { + // Refresh status if we have a cached value for any state other than: ChatVersionStatus.None. + switch (_cachedStatus) + { + case (ChatVersionStatus s) when (s == ChatVersionStatus.RequiresUpgrade || s == ChatVersionStatus.Upgrading): + _cachedStatus = await QueryStatusAsync().ConfigureAwait(false); + break; + + default: // ChatVersionStatus.None + break; + } + } + + return _cachedStatus ?? ChatVersionStatus.None; + + // Inline function to determine if the new "target" index already exists. + // If not, we need to upgrade; otherwise, futher inspection is required. + async Task QueryCollectionAsync() + { + try + { + if (_hasCurrentIndex == null) + { + // Cache "found" index state to reduce query count and avoid handling truth mutation. + var collections = await memory.GetCollectionsAsync(cancelToken).ConfigureAwait(false); + + // Does the new "target" index already exist? + _hasCurrentIndex = collections.Any(c => c.Equals(this._indexNameAllMemory, StringComparison.OrdinalIgnoreCase)); + + if (!_hasCurrentIndex ?? false) + { + return ChatVersionStatus.RequiresUpgrade; // No index == update required + } + } + } + catch (SKException exception) + { + this._logger.LogError(exception, "Unable to search collections"); + } + + return null; // Further inspection required + } + + async Task QueryStatusAsync() + { + if (_hasCurrentIndex ?? false) + { + try + { + var result = + await memory.GetAsync( + this._indexNameAllMemory, + MigrationKey, + withEmbedding: false, + cancelToken).ConfigureAwait(false); + + if (result != null) + { + var text = result.Metadata.Text; // $$$ MODEL: Status, Initiator, Timestamp + + if (!string.IsNullOrWhiteSpace(text) && text.Equals("Done", StringComparison.OrdinalIgnoreCase)) // $$$ DONE Const + { + return ChatVersionStatus.None; + } + + return ChatVersionStatus.Upgrading; + } + } + catch (SKException exception) + { + this._logger.LogError(exception, "Unable to search collection {0}", this._indexNameAllMemory); + } + } + + return ChatVersionStatus.RequiresUpgrade; + } + } +} diff --git a/webapi/Services/IChatMemoryMigrationService.cs b/webapi/Services/IChatMemoryMigrationService.cs new file mode 100644 index 000000000..af627f48c --- /dev/null +++ b/webapi/Services/IChatMemoryMigrationService.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Memory; + +namespace CopilotChat.WebApi.Services; + +/// +/// $$$ +/// +public interface IChatMemoryMigrationService +{ + /// + /// $$$ + /// + Task MigrateAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default); +} diff --git a/webapi/Services/IChatMigrationMonitor.cs b/webapi/Services/IChatMigrationMonitor.cs new file mode 100644 index 000000000..b441d95ea --- /dev/null +++ b/webapi/Services/IChatMigrationMonitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Memory; + +namespace CopilotChat.WebApi.Services; + +/// +/// $$$ +/// +public sealed class ChatVersionStatus +{ + public static ChatVersionStatus None { get; } = new ChatVersionStatus(nameof(None)); + public static ChatVersionStatus RequiresUpgrade { get; } = new ChatVersionStatus(nameof(RequiresUpgrade)); + public static ChatVersionStatus Upgrading { get; } = new ChatVersionStatus(nameof(Upgrading)); + + public string Label { get; } + + private ChatVersionStatus(string label) + { + this.Label = label; + } +} + +/// +/// $$$ +/// +public interface IChatMigrationMonitor +{ + /// + /// $$$ + /// + Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default); +} diff --git a/webapi/Storage/ChatMemorySourceRepository.cs b/webapi/Storage/ChatMemorySourceRepository.cs index 7444edc2c..427a701e6 100644 --- a/webapi/Storage/ChatMemorySourceRepository.cs +++ b/webapi/Storage/ChatMemorySourceRepository.cs @@ -41,4 +41,13 @@ public Task> FindByNameAsync(string name) { return base.StorageContext.QueryEntitiesAsync(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } + + /// + /// Retrieves all memory sources. + /// + /// A list of memory sources. + public Task> GetAllAsync() + { + return base.StorageContext.QueryEntitiesAsync(e => true); + } } diff --git a/webapi/Storage/ChatSessionRepository.cs b/webapi/Storage/ChatSessionRepository.cs index 3875fb051..98da39e24 100644 --- a/webapi/Storage/ChatSessionRepository.cs +++ b/webapi/Storage/ChatSessionRepository.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using CopilotChat.WebApi.Models.Storage; namespace CopilotChat.WebApi.Storage; @@ -17,4 +20,13 @@ public ChatSessionRepository(IStorageContext storageContext) : base(storageContext) { } + + /// + /// Retrieves all chat sessions. + /// + /// A list of ChatMessages. + public Task> GetAllChatsAsync() + { + return base.StorageContext.QueryEntitiesAsync(e => true); + } } diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 9853b6da3..3cbaab9f7 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -63,12 +63,14 @@ const App: FC = () => { const dispatch = useAppDispatch(); const { instance, inProgress } = useMsal(); - const { activeUserInfo, features } = useAppSelector((state: RootState) => state.app); + const { activeUserInfo, features, isMigrating } = useAppSelector((state: RootState) => state.app); const isAuthenticated = useIsAuthenticated(); const chat = useChat(); const file = useFile(); + console.log(`# ${isMigrating} (APP)`); // $$$ + useEffect(() => { if (isAuthenticated) { if (appState === AppState.SettingUserInfo) { @@ -125,6 +127,11 @@ const App: FC = () => { ]); } + if (isMigrating) + { + setAppState(AppState.ProbeForBackend); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [instance, inProgress, isAuthenticated, appState]); diff --git a/webapp/src/components/views/BackendProbe.tsx b/webapp/src/components/views/BackendProbe.tsx index f160ed859..ade38d609 100644 --- a/webapp/src/components/views/BackendProbe.tsx +++ b/webapp/src/components/views/BackendProbe.tsx @@ -3,6 +3,8 @@ import { Body1, Spinner, Title3 } from '@fluentui/react-components'; import { FC, useEffect } from 'react'; import { useSharedClasses } from '../../styles'; +import { useAppSelector } from '../../redux/app/hooks'; +import { RootState } from '../../redux/app/store'; interface IData { uri: string; @@ -11,20 +13,53 @@ interface IData { export const BackendProbe: FC = ({ uri, onBackendFound }) => { const classes = useSharedClasses(); + const { isMigrating } = useAppSelector((state: RootState) => state.app); + const healthUrl = new URL('healthz', uri); + const migrationUrl = new URL('maintenance', uri); + + console.log(`# ${isMigrating} (PROBE)`); // $$$ + useEffect(() => { const timer = setInterval(() => { - const requestUrl = new URL('healthz', uri); - const fetchAsync = async () => { - const result = await fetch(requestUrl); + const fetchHealthAsync = async () => { + const result = await fetch(healthUrl); + + if (isMigrating) { + console.log(`# MIGRATING`); // $$$ + return; + } if (result.ok) { + console.log(`# HEALTH DONE`); // $$$ + onBackendFound(); + } + + console.log(`# HEALTH NEXT`); // $$$ + }; + + const fetchMigrationAsync = async () => { + const result = await fetch(migrationUrl); + + if (!result.ok) { + return; + } + + const text = "test" + 1; // $$$ + + if (text === 'None') { onBackendFound(); } }; - fetchAsync().catch(() => { + fetchHealthAsync().catch(() => { // Ignore - this page is just a probe, so we don't need to show any errors if backend is not found }); + + if (isMigrating) { + fetchMigrationAsync().catch(() => { + // Ignore - this page is just a probe, so we don't need to show any errors if backend is not found + }); + } }, 3000); return () => { @@ -33,17 +68,32 @@ export const BackendProbe: FC = ({ uri, onBackendFound }) => { }); return ( -
- Looking for your backend - - - This sample expects to find a Semantic Kernel service from webapi/ running at{' '} - {uri} - - - Run your Semantic Kernel service locally using Visual Studio, Visual Studio Code or by typing the - following command: dotnet run - -
+ <> + {isMigrating ? +
+ Migrating chat memories... + + + An upgrade requires that all non-document memories be migrated. This might take several minutes... + + + Note: Any previous documents memories will need to be re-imported. + +
+ : +
+ Looking for your backend + + + This sample expects to find a Semantic Kernel service from webapi/ running at{' '} + {uri} + + + Run your Semantic Kernel service locally using Visual Studio, Visual Studio Code or by typing the + following command: dotnet run + +
+ } + ); }; diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts index c28a552a7..b1241d72d 100644 --- a/webapp/src/redux/features/app/AppState.ts +++ b/webapp/src/redux/features/app/AppState.ts @@ -40,6 +40,7 @@ export interface AppState { features: Record; settings: Setting[]; serviceOptions: ServiceOptions; + isMigrating: boolean, } export enum FeatureKeys { @@ -126,4 +127,5 @@ export const initialState: AppState = { features: Features, settings: Settings, serviceOptions: { memoryStore: { types: [], selectedType: '' }, version: '' }, + isMigrating: false, }; diff --git a/webapp/src/redux/features/app/appSlice.ts b/webapp/src/redux/features/app/appSlice.ts index 1d4ef00f2..a013f56e4 100644 --- a/webapp/src/redux/features/app/appSlice.ts +++ b/webapp/src/redux/features/app/appSlice.ts @@ -10,6 +10,9 @@ export const appSlice = createSlice({ name: 'app', initialState, reducers: { + setMigration: (state: AppState, action: PayloadAction) => { + state.isMigrating = action.payload; + }, setAlerts: (state: AppState, action: PayloadAction) => { state.alerts = action.payload; }, @@ -80,6 +83,7 @@ export const { toggleFeatureState, updateTokenUsage, setServiceOptions, + setMigration, } = appSlice.actions; export default appSlice.reducer; diff --git a/webapp/src/redux/features/message-relay/signalRHubConnection.ts b/webapp/src/redux/features/message-relay/signalRHubConnection.ts index 884123d6e..91a8f2caf 100644 --- a/webapp/src/redux/features/message-relay/signalRHubConnection.ts +++ b/webapp/src/redux/features/message-relay/signalRHubConnection.ts @@ -9,7 +9,7 @@ import { AuthorRoles, ChatMessageType, IChatMessage } from '../../../libs/models import { IChatUser } from '../../../libs/models/ChatUser'; import { PlanState } from '../../../libs/models/Plan'; import { StoreMiddlewareAPI } from '../../app/store'; -import { addAlert } from '../app/appSlice'; +import { addAlert, setMigration } from '../app/appSlice'; import { ChatState } from '../conversations/ChatState'; /* @@ -27,6 +27,8 @@ const enum SignalRCallbackMethods { GlobalDocumentUploaded = 'GlobalDocumentUploaded', ChatEdited = 'ChatEdited', ChatDeleted = 'ChatDeleted', + GlobalChatMigrationActive = 'GlobalChatMigrationActive', + GlobalChatMigrationComplete = 'GlobalChatMigrationComplete', } // Set up a SignalR connection to the messageRelayHub on the server @@ -215,6 +217,14 @@ const registerSignalREvents = (hubConnection: signalR.HubConnection, store: Stor }); } }); + + hubConnection.on(SignalRCallbackMethods.GlobalChatMigrationActive, () => { + store.dispatch(setMigration(true)); + }); + + hubConnection.on(SignalRCallbackMethods.GlobalChatMigrationComplete, () => { + store.dispatch(setMigration(false)); + }); }; // This is a singleton instance of the SignalR connection From b9d945509fd62cf553fccdc1c44650c8cb918b20 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Sep 2023 12:55:04 -0700 Subject: [PATCH 02/28] Typo cleanup --- webapi/Controllers/ChatMigrationController.cs | 50 ------------------- webapi/Services/ChatMemoryMigrationService.cs | 1 - webapi/Services/ChatMigrationMonitor.cs | 2 +- 3 files changed, 1 insertion(+), 52 deletions(-) diff --git a/webapi/Controllers/ChatMigrationController.cs b/webapi/Controllers/ChatMigrationController.cs index cf83d74b8..be3923c5e 100644 --- a/webapi/Controllers/ChatMigrationController.cs +++ b/webapi/Controllers/ChatMigrationController.cs @@ -62,54 +62,4 @@ public async Task> MigrateStatusAsync( return this.Ok($"Migration status: {migrationStatus.Label}"); } - - ///// - ///// $$$ - ///// - //[Route("chats/version/")] - //[HttpPost] - //[ProducesResponseType(StatusCodes.Status200OK)] - //[ProducesResponseType(StatusCodes.Status400BadRequest)] - //public async Task MigrateChatsAsync( - // [FromServices] IChatMigrationMonitor migrationMonitor, - // [FromServices] IChatMemoryMigrationService migrationService, - // [FromServices] IHubContext messageRelayHubContext) - //{ - // // $$$ HOOK / ENTRY POINT ??? - // // Monitor caches status for minimum middle-ware impact - // var migrationStatus = await migrationMonitor.GetCurrentStatusAsync().ConfigureAwait(false); - - // if (migrationStatus != ChatVersionStatus.None) - // { - // await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationActiveCall, "Chat migration in progress").ConfigureAwait(false); - // } - - // if (migrationStatus == ChatVersionStatus.RequiresUpgrade) - // { - // try - // { - // // All documents will need to be re-imported - // await this.RemoveMemorySourcesAsync(); - // // Migrate all chats to single index - // await migrationService.MigrateAsync(); - - // await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationCompleteCall, "Chat migration complete").ConfigureAwait(false); - // } - // catch (Exception ex) when (!ex.IsCriticalException()) - // { - // return this.Problem(ex.Message); - // } - // } - - // return this.Ok(); - //} - - //private async Task RemoveMemorySourcesAsync() - //{ - // var documentMemories = await this._sourceRepository.GetAllAsync().ConfigureAwait(false); - // foreach (var document in documentMemories) // $$$ PARRALLEL ??? - // { - // await this._sourceRepository.DeleteAsync(document).ConfigureAwait(false); - // } - //} } diff --git a/webapi/Services/ChatMemoryMigrationService.cs b/webapi/Services/ChatMemoryMigrationService.cs index 495fe7ebe..a27c9dc5f 100644 --- a/webapi/Services/ChatMemoryMigrationService.cs +++ b/webapi/Services/ChatMemoryMigrationService.cs @@ -70,7 +70,6 @@ public async Task MigrateAsync(ISemanticTextMemory memory, CancellationToken can // Extract and store memories, using the original id to avoid duplication should a retry be required. await foreach ((string chatId, string memoryName, string memoryId, string memoryText) in QueryMemoriesAsync()) { - // $$$ PARRALLEL ??? await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, chatId, memoryName, memoryId, memoryText, cancelToken); } diff --git a/webapi/Services/ChatMigrationMonitor.cs b/webapi/Services/ChatMigrationMonitor.cs index 155a02011..16fc5af7d 100644 --- a/webapi/Services/ChatMigrationMonitor.cs +++ b/webapi/Services/ChatMigrationMonitor.cs @@ -72,7 +72,7 @@ await QueryCollectionAsync().ConfigureAwait(false), return _cachedStatus ?? ChatVersionStatus.None; // Inline function to determine if the new "target" index already exists. - // If not, we need to upgrade; otherwise, futher inspection is required. + // If not, we need to upgrade; otherwise, further inspection is required. async Task QueryCollectionAsync() { try From 9ed3b86bf0158111f5c8b3bad7763fbba65e475d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Sep 2023 12:59:59 -0700 Subject: [PATCH 03/28] Namespace cleanup (dotnet-format) --- webapi/Storage/ChatSessionRepository.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/webapi/Storage/ChatSessionRepository.cs b/webapi/Storage/ChatSessionRepository.cs index 98da39e24..726b50d2e 100644 --- a/webapi/Storage/ChatSessionRepository.cs +++ b/webapi/Storage/ChatSessionRepository.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using CopilotChat.WebApi.Models.Storage; From 9285c964c6ea2181df605518946624986ff57e64 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Sep 2023 13:22:40 -0700 Subject: [PATCH 04/28] Clean-up and consolidation --- webapi/Controllers/ChatMigrationController.cs | 17 +++------ webapi/Services/ChatMemoryMigrationService.cs | 8 ++--- webapi/Services/ChatMigrationMiddleware.cs | 9 ++--- webapi/Services/ChatMigrationMonitor.cs | 31 ++++++++-------- .../Services/IChatMemoryMigrationService.cs | 5 +-- webapi/Services/IChatMigrationMonitor.cs | 35 ++++++++++++++----- 6 files changed, 55 insertions(+), 50 deletions(-) diff --git a/webapi/Controllers/ChatMigrationController.cs b/webapi/Controllers/ChatMigrationController.cs index be3923c5e..ecb0af59b 100644 --- a/webapi/Controllers/ChatMigrationController.cs +++ b/webapi/Controllers/ChatMigrationController.cs @@ -14,14 +14,11 @@ namespace CopilotChat.WebApi.Controllers; /// -/// $$$ +/// Controller for reporting the status of chat migration. /// [ApiController] public class ChatMigrationController : ControllerBase { - private const string GlobalChatMigrationActiveCall = "GlobalChatMigrationActive"; - private const string GlobalChatMigrationCompleteCall = "GlobalChatMigrationComplete"; - private readonly ILogger _logger; private readonly IAuthInfo _authInfo; @@ -37,13 +34,13 @@ public ChatMigrationController( } /// - /// $$$ + /// Route for reporting the status of chat migration. /// [Route("maintenance/")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> MigrateStatusAsync( + public async Task> MigrateStatusAsync( [FromServices] IKernel kernel, [FromServices] IChatMigrationMonitor migrationMonitor, [FromServices] IHubContext messageRelayHubContext, @@ -51,13 +48,9 @@ public async Task> MigrateStatusAsync( { var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(kernel.Memory, cancelToken).ConfigureAwait(false); - if (migrationStatus != ChatVersionStatus.None) - { - await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationActiveCall, "Chat migration in progress", cancelToken).ConfigureAwait(false); - } - else + if (migrationStatus != ChatMigrationStatus.None) { - await messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationCompleteCall, "Chat migration completed.", cancelToken).ConfigureAwait(false); + await messageRelayHubContext.Clients.All.SendAsync(ChatMigrationMiddleware.GlobalChatMigrationActiveCall, "Chat migration in progress", cancelToken).ConfigureAwait(false); } return this.Ok($"Migration status: {migrationStatus.Label}"); diff --git a/webapi/Services/ChatMemoryMigrationService.cs b/webapi/Services/ChatMemoryMigrationService.cs index a27c9dc5f..65e5ad386 100644 --- a/webapi/Services/ChatMemoryMigrationService.cs +++ b/webapi/Services/ChatMemoryMigrationService.cs @@ -16,7 +16,7 @@ namespace CopilotChat.WebApi.Services; /// -/// $$$ +/// Service implementation of . /// public class ChatMemoryMigrationService : IChatMemoryMigrationService { @@ -41,9 +41,7 @@ public ChatMemoryMigrationService( this._chatSessionRepository = chatSessionRepository; } - /// - /// Migrates all non-document memory to the semantic-memory index. - /// + /// public async Task MigrateAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default) { var shouldMigrate = false; @@ -73,7 +71,7 @@ public async Task MigrateAsync(ISemanticTextMemory memory, CancellationToken can await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, chatId, memoryName, memoryId, memoryText, cancelToken); } - await SetTokenMemory("Done", cancelToken).ConfigureAwait(false); // $$$ DONE Const + await SetTokenMemory(ChatMigrationMonitor.MigrationCompletionToken, cancelToken).ConfigureAwait(false); // Inline function to extract all memories for a given chat and memory type. async IAsyncEnumerable<(string chatId, string memoryName, string memoryId, string memoryText)> QueryMemoriesAsync() diff --git a/webapi/Services/ChatMigrationMiddleware.cs b/webapi/Services/ChatMigrationMiddleware.cs index 83c72e16f..caa3b3365 100644 --- a/webapi/Services/ChatMigrationMiddleware.cs +++ b/webapi/Services/ChatMigrationMiddleware.cs @@ -19,8 +19,7 @@ namespace CopilotChat.WebApi.Services; /// public class ChatMigrationMiddleware { - private const string GlobalChatMigrationActiveCall = "GlobalChatMigrationActive"; // $$$ DUPE - private const string GlobalChatMigrationCompleteCall = "GlobalChatMigrationComplete"; + internal const string GlobalChatMigrationActiveCall = "GlobalChatMigrationActive"; private readonly RequestDelegate _next; private readonly IOptions _documentMemoryOptions; @@ -57,12 +56,12 @@ public async Task Invoke(HttpContext ctx, IKernel kernel) // Monitor caches status for minimum middle-ware impact var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(kernel.Memory).ConfigureAwait(false); - if (migrationStatus != ChatVersionStatus.None) + if (migrationStatus != ChatMigrationStatus.None) { await this._messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationActiveCall, "Chat migration in progress").ConfigureAwait(false); } - if (migrationStatus == ChatVersionStatus.RequiresUpgrade) + if (migrationStatus == ChatMigrationStatus.RequiresUpgrade) { // $$$ BACKGROUND JOB try @@ -72,8 +71,6 @@ public async Task Invoke(HttpContext ctx, IKernel kernel) // Migrate all chats to single index await this._migrationService.MigrateAsync(kernel.Memory); - - await this._messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationCompleteCall, "Chat migration complete").ConfigureAwait(false); } catch (Exception ex) when (!ex.IsCriticalException()) { diff --git a/webapi/Services/ChatMigrationMonitor.cs b/webapi/Services/ChatMigrationMonitor.cs index 16fc5af7d..dbcb3b678 100644 --- a/webapi/Services/ChatMigrationMonitor.cs +++ b/webapi/Services/ChatMigrationMonitor.cs @@ -13,13 +13,14 @@ namespace CopilotChat.WebApi.Services; /// -/// $$$ +/// Service implementation of . /// public class ChatMigrationMonitor : IChatMigrationMonitor { + internal const string MigrationCompletionToken = "DONE"; internal const string MigrationKey = "migrate-00000000-0000-0000-0000-000000000000"; - private static ChatVersionStatus? _cachedStatus; + private static ChatMigrationStatus? _cachedStatus; private static bool? _hasCurrentIndex; private readonly ILogger _logger; @@ -36,10 +37,8 @@ public ChatMigrationMonitor( this._indexNameAllMemory = promptOptions.Value.MemoryIndexName; } - /// - /// $$$ - /// - public async Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default) // $$$ RETURN string / id ??? + /// + public async Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default) { if (_cachedStatus == null) { @@ -60,7 +59,7 @@ await QueryCollectionAsync().ConfigureAwait(false), // Refresh status if we have a cached value for any state other than: ChatVersionStatus.None. switch (_cachedStatus) { - case (ChatVersionStatus s) when (s == ChatVersionStatus.RequiresUpgrade || s == ChatVersionStatus.Upgrading): + case (ChatMigrationStatus s) when (s == ChatMigrationStatus.RequiresUpgrade || s == ChatMigrationStatus.Upgrading): _cachedStatus = await QueryStatusAsync().ConfigureAwait(false); break; @@ -69,11 +68,11 @@ await QueryCollectionAsync().ConfigureAwait(false), } } - return _cachedStatus ?? ChatVersionStatus.None; + return _cachedStatus ?? ChatMigrationStatus.None; // Inline function to determine if the new "target" index already exists. // If not, we need to upgrade; otherwise, further inspection is required. - async Task QueryCollectionAsync() + async Task QueryCollectionAsync() { try { @@ -87,7 +86,7 @@ await QueryCollectionAsync().ConfigureAwait(false), if (!_hasCurrentIndex ?? false) { - return ChatVersionStatus.RequiresUpgrade; // No index == update required + return ChatMigrationStatus.RequiresUpgrade; // No index == update required } } } @@ -99,7 +98,7 @@ await QueryCollectionAsync().ConfigureAwait(false), return null; // Further inspection required } - async Task QueryStatusAsync() + async Task QueryStatusAsync() { if (_hasCurrentIndex ?? false) { @@ -114,14 +113,14 @@ await memory.GetAsync( if (result != null) { - var text = result.Metadata.Text; // $$$ MODEL: Status, Initiator, Timestamp + var text = result.Metadata.Text; - if (!string.IsNullOrWhiteSpace(text) && text.Equals("Done", StringComparison.OrdinalIgnoreCase)) // $$$ DONE Const + if (!string.IsNullOrWhiteSpace(text) && text.Equals(MigrationCompletionToken, StringComparison.OrdinalIgnoreCase)) { - return ChatVersionStatus.None; + return ChatMigrationStatus.None; } - return ChatVersionStatus.Upgrading; + return ChatMigrationStatus.Upgrading; } } catch (SKException exception) @@ -130,7 +129,7 @@ await memory.GetAsync( } } - return ChatVersionStatus.RequiresUpgrade; + return ChatMigrationStatus.RequiresUpgrade; } } } diff --git a/webapi/Services/IChatMemoryMigrationService.cs b/webapi/Services/IChatMemoryMigrationService.cs index af627f48c..2e2d91e0f 100644 --- a/webapi/Services/IChatMemoryMigrationService.cs +++ b/webapi/Services/IChatMemoryMigrationService.cs @@ -7,12 +7,13 @@ namespace CopilotChat.WebApi.Services; /// -/// $$$ +/// Defines contract for migrating chat memory. /// public interface IChatMemoryMigrationService { /// - /// $$$ + /// Migrates all non-document memory to the semantic-memory index. + /// Subsequent/redunant migration is non-destructive/no-impact to migrated index. /// Task MigrateAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default); } diff --git a/webapi/Services/IChatMigrationMonitor.cs b/webapi/Services/IChatMigrationMonitor.cs index b441d95ea..efedd3fcb 100644 --- a/webapi/Services/IChatMigrationMonitor.cs +++ b/webapi/Services/IChatMigrationMonitor.cs @@ -7,29 +7,46 @@ namespace CopilotChat.WebApi.Services; /// -/// $$$ +/// Set of migration states/status for chat memory migration. /// -public sealed class ChatVersionStatus +/// +/// Raison d'etre: Interlocked.CompareExchange doesn't work with enums. +/// +public sealed class ChatMigrationStatus { - public static ChatVersionStatus None { get; } = new ChatVersionStatus(nameof(None)); - public static ChatVersionStatus RequiresUpgrade { get; } = new ChatVersionStatus(nameof(RequiresUpgrade)); - public static ChatVersionStatus Upgrading { get; } = new ChatVersionStatus(nameof(Upgrading)); + /// + /// Represents state where no migration is required or in progress. + /// + public static ChatMigrationStatus None { get; } = new ChatMigrationStatus(nameof(None)); + + /// + /// Represents state where no migration is required. + /// + public static ChatMigrationStatus RequiresUpgrade { get; } = new ChatMigrationStatus(nameof(RequiresUpgrade)); + /// + /// Represents state where no migration is in progress. + /// + public static ChatMigrationStatus Upgrading { get; } = new ChatMigrationStatus(nameof(Upgrading)); + + /// + /// The state label (no functional impact, but helps debugging). + /// public string Label { get; } - private ChatVersionStatus(string label) + private ChatMigrationStatus(string label) { this.Label = label; } } /// -/// $$$ +/// Contract for monitoring the status of chat memory migration. /// public interface IChatMigrationMonitor { /// - /// $$$ + /// Inspects the current state of affairs to determine the chat migration status. /// - Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default); + Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default); } From 834802bf24454754e5b76a3bb7903b74744beec2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Sep 2023 13:27:47 -0700 Subject: [PATCH 05/28] Spell-check --- webapi/Services/IChatMigrationMonitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Services/IChatMigrationMonitor.cs b/webapi/Services/IChatMigrationMonitor.cs index efedd3fcb..424433247 100644 --- a/webapi/Services/IChatMigrationMonitor.cs +++ b/webapi/Services/IChatMigrationMonitor.cs @@ -10,7 +10,7 @@ namespace CopilotChat.WebApi.Services; /// Set of migration states/status for chat memory migration. /// /// -/// Raison d'etre: Interlocked.CompareExchange doesn't work with enums. +/// Interlocked.CompareExchange doesn't work with enums. /// public sealed class ChatMigrationStatus { From 9230fc8e82b94d3f8d92baf972d4f36ae40e2165 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Sep 2023 13:33:17 -0700 Subject: [PATCH 06/28] Update migration route --- webapi/Controllers/ChatMigrationController.cs | 2 +- webapp/src/components/views/BackendProbe.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapi/Controllers/ChatMigrationController.cs b/webapi/Controllers/ChatMigrationController.cs index ecb0af59b..45be3c674 100644 --- a/webapi/Controllers/ChatMigrationController.cs +++ b/webapi/Controllers/ChatMigrationController.cs @@ -36,7 +36,7 @@ public ChatMigrationController( /// /// Route for reporting the status of chat migration. /// - [Route("maintenance/")] + [Route("migrationstatus/")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] diff --git a/webapp/src/components/views/BackendProbe.tsx b/webapp/src/components/views/BackendProbe.tsx index ade38d609..e8f5cb80e 100644 --- a/webapp/src/components/views/BackendProbe.tsx +++ b/webapp/src/components/views/BackendProbe.tsx @@ -15,7 +15,7 @@ export const BackendProbe: FC = ({ uri, onBackendFound }) => { const classes = useSharedClasses(); const { isMigrating } = useAppSelector((state: RootState) => state.app); const healthUrl = new URL('healthz', uri); - const migrationUrl = new URL('maintenance', uri); + const migrationUrl = new URL('migrationstatus', uri); console.log(`# ${isMigrating} (PROBE)`); // $$$ From 8d73a0ed8c1ce1ab2082a9ba311cb42a092e4bd8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 12 Sep 2023 11:12:57 -0700 Subject: [PATCH 07/28] Merged to general maintenance hook --- webapi/Controllers/BotController.cs | 160 +----------------- webapi/Controllers/ChatMigrationController.cs | 58 ------- webapi/Controllers/MaintenanceController.cs | 13 +- webapi/Services/ChatMigrationMiddleware.cs | 93 ---------- webapi/Services/MaintenanceMiddleware.cs | 2 + 5 files changed, 20 insertions(+), 306 deletions(-) delete mode 100644 webapi/Controllers/ChatMigrationController.cs delete mode 100644 webapi/Services/ChatMigrationMiddleware.cs diff --git a/webapi/Controllers/BotController.cs b/webapi/Controllers/BotController.cs index 0bdc6af2f..d2508b4f1 100644 --- a/webapi/Controllers/BotController.cs +++ b/webapi/Controllers/BotController.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using CopilotChat.WebApi.Auth; using CopilotChat.WebApi.Extensions; @@ -27,7 +26,6 @@ public class BotController : ControllerBase { private readonly ILogger _logger; private readonly IMemoryStore? _memoryStore; - private readonly ISemanticTextMemory _semanticMemory; private readonly ChatSessionRepository _chatRepository; private readonly ChatMessageRepository _chatMessageRepository; private readonly ChatParticipantRepository _chatParticipantRepository; @@ -47,7 +45,6 @@ public class BotController : ControllerBase /// The logger. public BotController( IMemoryStore memoryStore, - ISemanticTextMemory semanticMemory, ChatSessionRepository chatRepository, ChatMessageRepository chatMessageRepository, ChatParticipantRepository chatParticipantRepository, @@ -58,7 +55,6 @@ public BotController( { this._memoryStore = memoryStore; this._logger = logger; - this._semanticMemory = semanticMemory; this._chatRepository = chatRepository; this._chatMessageRepository = chatMessageRepository; this._chatParticipantRepository = chatParticipantRepository; @@ -67,81 +63,6 @@ public BotController( this._documentMemoryOptions = documentMemoryOptions.Value; } - /// - /// Upload a bot. - /// - /// The Semantic Kernel instance. - /// The auth info instance. - /// The bot object from the message body - /// The cancellation token. - /// The HTTP action result with new chat session object. - [HttpPost] - [Route("bot/upload")] - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> UploadAsync( - [FromServices] IKernel kernel, - [FromServices] IAuthInfo authInfo, - [FromBody] Bot bot, - CancellationToken cancellationToken) - { - this._logger.LogDebug("Received call to upload a bot"); - - if (!this.IsBotCompatible( - externalBotSchema: bot.Schema, - externalBotEmbeddingConfig: bot.EmbeddingConfigurations)) - { - return this.BadRequest("Incompatible schema. " + - $"The supported bot schema is {this._botSchemaOptions.Name}/{this._botSchemaOptions.Version} " + - $"for the {this._embeddingConfig.DeploymentOrModelId} model from {this._embeddingConfig.AIService}. " + - $"But the uploaded file is with schema {bot.Schema.Name}/{bot.Schema.Version} " + - $"for the {bot.EmbeddingConfigurations.DeploymentOrModelId} model from {bot.EmbeddingConfigurations.AIService}."); - } - - string chatTitle = $"{bot.ChatTitle} - Clone"; - string chatId = string.Empty; - ChatSession newChat; - - // Upload chat history into chat repository and embeddings into memory. - - // Create a new chat and get the chat id. - newChat = new ChatSession(chatTitle, bot.SystemDescription); - await this._chatRepository.CreateAsync(newChat); - await this._chatParticipantRepository.CreateAsync(new ChatParticipant(authInfo.UserId, newChat.Id)); - chatId = newChat.Id; - - string oldChatId = bot.ChatHistory.First().ChatId; - - // Update the app's chat storage. - foreach (var message in bot.ChatHistory) - { - var chatMessage = new ChatMessage( - message.UserId, - message.UserName, - chatId, - message.Content, - message.Prompt, - message.Citations, - ChatMessage.AuthorRoles.Participant) - { - Timestamp = message.Timestamp - }; - await this._chatMessageRepository.CreateAsync(chatMessage); - } - - // Update the memory. - await this.BulkUpsertMemoryRecordsAsync(oldChatId, chatId, bot.Embeddings, cancellationToken); - - // TODO: [Issue #47] Revert changes if any of the actions failed - - return this.CreatedAtAction( - nameof(ChatHistoryController.GetChatSessionByIdAsync), - nameof(ChatHistoryController).Replace("Controller", string.Empty, StringComparison.OrdinalIgnoreCase), - new { chatId }, - newChat); - } - /// /// Download a bot. /// @@ -165,26 +86,6 @@ public async Task> UploadAsync( return this.Ok(memory); } - /// - /// Check if an external bot file is compatible with the application. - /// - /// - /// If the embeddings are not generated from the same model, the bot file is not compatible. - /// - /// The external bot schema. - /// The external bot embedding configuration. - /// True if the bot file is compatible with the app; otherwise false. - private bool IsBotCompatible( - BotSchemaOptions externalBotSchema, - BotEmbeddingConfig externalBotEmbeddingConfig) - { - // The app can define what schema/version it supports before the community comes out with an open schema. - return externalBotSchema.Name.Equals(this._botSchemaOptions.Name, StringComparison.OrdinalIgnoreCase) - && externalBotSchema.Version == this._botSchemaOptions.Version - && externalBotEmbeddingConfig.AIService == this._embeddingConfig.AIService - && externalBotEmbeddingConfig.DeploymentOrModelId.Equals(this._embeddingConfig.DeploymentOrModelId, StringComparison.OrdinalIgnoreCase); - } - /// /// Get memory from memory store and append the memory records to a given list. /// It will update the memory collection name in the new list if the newCollectionName is provided. @@ -206,8 +107,8 @@ private async Task GetMemoryRecordsAndAppendToEmbeddingsAsync( { collectionMemoryRecords = await kernel.Memory.SearchAsync( collectionName, - "abc", // dummy query since we don't care about relevance. An empty string will cause exception. - limit: 999999999, // temp solution to get as much as record as a workaround. + "*", // dummy query since we don't care about relevance. An empty string will cause exception. + limit: int.MaxValue, // temp solution to get as much as record as a workaround. minRelevanceScore: -1, // no relevance required since the collection only has one entry withEmbeddings: true, cancellationToken: default @@ -256,7 +157,6 @@ private async Task CreateBotAsync(IKernel kernel, Guid chatId) bot.ChatHistory = await this.GetAllChatMessagesAsync(chatIdString); // get the memory collections associated with this chat - // TODO: [Issue #47] filtering memory collections by name might be fragile. var chatCollections = (await kernel.Memory.GetCollectionsAsync()) .Where(collection => collection.StartsWith(chatIdString, StringComparison.OrdinalIgnoreCase)); @@ -265,11 +165,11 @@ private async Task CreateBotAsync(IKernel kernel, Guid chatId) await this.GetMemoryRecordsAndAppendToEmbeddingsAsync(kernel: kernel, collectionName: collection, embeddings: bot.Embeddings); } - //// get the document memory collection names (global scope) $$$ - //await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( - // kernel: kernel, - // collectionName: this._documentMemoryOptions.GlobalDocumentCollectionName, - // embeddings: bot.DocumentEmbeddings); + // get the document memory collection names (global scope) + await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( + kernel: kernel, + collectionName: this._documentMemoryOptions.GlobalDocumentCollectionName, + embeddings: bot.DocumentEmbeddings); // get the document memory collection names (user scope) await this.GetMemoryRecordsAndAppendToEmbeddingsAsync( @@ -291,50 +191,4 @@ private async Task> GetAllChatMessagesAsync(string chatId) return (await this._chatMessageRepository.FindByChatIdAsync(chatId)) .OrderByDescending(m => m.Timestamp).ToList(); } - - /// - /// Bulk upsert memory records into memory store. - /// - /// The original chat id of the memory records. - /// The new chat id that will replace the original chat id. - /// The list of embeddings of the chat id. - /// The function doesn't return anything. - private async Task BulkUpsertMemoryRecordsAsync(string oldChatId, string chatId, List>> embeddings, CancellationToken cancellationToken = default) - { - foreach (var collection in embeddings) - { - foreach (var record in collection.Value) - { - if (record != null && record.Embedding != null) - { - var newCollectionName = collection.Key.Replace(oldChatId, chatId, StringComparison.OrdinalIgnoreCase); - - if (this._memoryStore == null) - { - await this._semanticMemory.SaveInformationAsync( - collection: newCollectionName, - text: record.Metadata.Text, - id: record.Metadata.Id, - cancellationToken: cancellationToken); - } - else - { - MemoryRecord data = MemoryRecord.LocalRecord( - id: record.Metadata.Id, - text: record.Metadata.Text, - embedding: record.Embedding.Value, - description: null, - additionalMetadata: null); - - if (!(await this._memoryStore.DoesCollectionExistAsync(newCollectionName, default))) - { - await this._memoryStore.CreateCollectionAsync(newCollectionName, default); - } - - await this._memoryStore.UpsertAsync(newCollectionName, data, default); - } - } - } - } - } } diff --git a/webapi/Controllers/ChatMigrationController.cs b/webapi/Controllers/ChatMigrationController.cs deleted file mode 100644 index 45be3c674..000000000 --- a/webapi/Controllers/ChatMigrationController.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; -using CopilotChat.WebApi.Auth; -using CopilotChat.WebApi.Hubs; -using CopilotChat.WebApi.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; - -namespace CopilotChat.WebApi.Controllers; - -/// -/// Controller for reporting the status of chat migration. -/// -[ApiController] -public class ChatMigrationController : ControllerBase -{ - private readonly ILogger _logger; - private readonly IAuthInfo _authInfo; - - /// - /// Initializes a new instance of the class. - /// - public ChatMigrationController( - ILogger logger, - IAuthInfo authInfo) - { - this._logger = logger; - this._authInfo = authInfo; - } - - /// - /// Route for reporting the status of chat migration. - /// - [Route("migrationstatus/")] - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> MigrateStatusAsync( - [FromServices] IKernel kernel, - [FromServices] IChatMigrationMonitor migrationMonitor, - [FromServices] IHubContext messageRelayHubContext, - CancellationToken cancelToken = default) - { - var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(kernel.Memory, cancelToken).ConfigureAwait(false); - - if (migrationStatus != ChatMigrationStatus.None) - { - await messageRelayHubContext.Clients.All.SendAsync(ChatMigrationMiddleware.GlobalChatMigrationActiveCall, "Chat migration in progress", cancelToken).ConfigureAwait(false); - } - - return this.Ok($"Migration status: {migrationStatus.Label}"); - } -} diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index 8f2ce3bb7..ce9a9b771 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. 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; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -46,13 +48,20 @@ public MaintenanceController( [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public ActionResult GetMaintenanceStatus( - [FromServices] IKernel kernel, + public async Task> GetMaintenanceStatusAsync( + [FromServices] IChatMigrationMonitor migrationMonitor, [FromServices] IHubContext messageRelayHubContext, CancellationToken cancellationToken = default) { MigrationResult? result = null; + var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(null!, cancellationToken).ConfigureAwait(false); // $$$ MEMORY OR ??? + + if (migrationStatus != ChatMigrationStatus.None) + { + result = new MigrationResult(); // $$$ Update UI + } + if (this._serviceOptions.Value.InMaintenance) { result = new MigrationResult(); diff --git a/webapi/Services/ChatMigrationMiddleware.cs b/webapi/Services/ChatMigrationMiddleware.cs deleted file mode 100644 index caa3b3365..000000000 --- a/webapi/Services/ChatMigrationMiddleware.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using CopilotChat.WebApi.Controllers; -using CopilotChat.WebApi.Hubs; -using CopilotChat.WebApi.Options; -using CopilotChat.WebApi.Storage; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.SemanticKernel; - -namespace CopilotChat.WebApi.Services; - -/// -/// $$$ -/// -public class ChatMigrationMiddleware -{ - internal const string GlobalChatMigrationActiveCall = "GlobalChatMigrationActive"; - - private readonly RequestDelegate _next; - private readonly IOptions _documentMemoryOptions; - private readonly IOptions _promptOptions; - private readonly ChatMemorySourceRepository _sourceRepository; - private readonly IChatMigrationMonitor _migrationMonitor; - private readonly IChatMemoryMigrationService _migrationService; - private readonly IHubContext _messageRelayHubContext; - private readonly ILogger _logger; - - public ChatMigrationMiddleware( - RequestDelegate next, - IOptions documentMemoryOptions, - IOptions promptOptions, - ChatMemorySourceRepository sourceRepository, - IChatMigrationMonitor migrationMonitor, - IChatMemoryMigrationService migrationService, - IHubContext messageRelayHubContext, - ILogger logger) - - { - this._next = next; - this._documentMemoryOptions = documentMemoryOptions; - this._promptOptions = promptOptions; - this._sourceRepository = sourceRepository; - this._migrationMonitor = migrationMonitor; - this._migrationService = migrationService; - this._messageRelayHubContext = messageRelayHubContext; - this._logger = logger; - } - - public async Task Invoke(HttpContext ctx, IKernel kernel) - { - // Monitor caches status for minimum middle-ware impact - var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(kernel.Memory).ConfigureAwait(false); - - if (migrationStatus != ChatMigrationStatus.None) - { - await this._messageRelayHubContext.Clients.All.SendAsync(GlobalChatMigrationActiveCall, "Chat migration in progress").ConfigureAwait(false); - } - - if (migrationStatus == ChatMigrationStatus.RequiresUpgrade) - { - // $$$ BACKGROUND JOB - try - { - // All documents will need to be re-imported - await this.RemoveMemorySourcesAsync(); - - // Migrate all chats to single index - await this._migrationService.MigrateAsync(kernel.Memory); - } - catch (Exception ex) when (!ex.IsCriticalException()) - { - this._logger.LogError(ex, "Error migrating chat memories"); - //return this.Problem(ex.Message); $$$ - } - } - - await this._next(ctx); - } - - private async Task RemoveMemorySourcesAsync() - { - var documentMemories = await this._sourceRepository.GetAllAsync().ConfigureAwait(false); - foreach (var document in documentMemories) // $$$ PARALLEL ??? - { - await this._sourceRepository.DeleteAsync(document).ConfigureAwait(false); - } - } -} diff --git a/webapi/Services/MaintenanceMiddleware.cs b/webapi/Services/MaintenanceMiddleware.cs index c697a06a4..979230362 100644 --- a/webapi/Services/MaintenanceMiddleware.cs +++ b/webapi/Services/MaintenanceMiddleware.cs @@ -24,6 +24,8 @@ public class MaintenanceMiddleware public MaintenanceMiddleware( RequestDelegate next, + //IChatMigrationMonitor migrationMonitor, $$$ PATTERN ?? + //IChatMemoryMigrationService migrationService, IOptions servicetOptions, IHubContext messageRelayHubContext, ILogger logger) From 29dd9ec3eb740ed838d113040b3e78fdc92f26e8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 12 Sep 2023 11:43:37 -0700 Subject: [PATCH 08/28] Merge/refactor check-point --- webapi/Controllers/MaintenanceController.cs | 15 ++++++---- webapi/Models/Response/MaintenanceResult.cs | 33 +++++++++++++++++++++ webapi/Models/Response/MigrationResult.cs | 24 --------------- 3 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 webapi/Models/Response/MaintenanceResult.cs delete mode 100644 webapi/Models/Response/MigrationResult.cs diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index ce9a9b771..fd879db6e 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.SemanticKernel; namespace CopilotChat.WebApi.Controllers; @@ -48,23 +47,29 @@ public MaintenanceController( [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> GetMaintenanceStatusAsync( + public async Task> GetMaintenanceStatusAsync( [FromServices] IChatMigrationMonitor migrationMonitor, [FromServices] IHubContext messageRelayHubContext, CancellationToken cancellationToken = default) { - MigrationResult? result = null; + MaintenanceResult? result = null; var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(null!, cancellationToken).ConfigureAwait(false); // $$$ MEMORY OR ??? if (migrationStatus != ChatMigrationStatus.None) { - result = new MigrationResult(); // $$$ Update UI + result = + new MaintenanceResult + { + Title = "$$$", + Message = "$$$", + Note = "$$$", + }; } if (this._serviceOptions.Value.InMaintenance) { - result = new MigrationResult(); + result = new MaintenanceResult(); } return this.Ok(result); diff --git a/webapi/Models/Response/MaintenanceResult.cs b/webapi/Models/Response/MaintenanceResult.cs new file mode 100644 index 000000000..25c98083f --- /dev/null +++ b/webapi/Models/Response/MaintenanceResult.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Defines optional messaging for maintenace mode. +/// +public class MaintenanceResult +{ + /// + /// The maintenace notification title. + /// + /// + /// Will utilize default if not defined. + /// + public string Title { get; set; } = string.Empty; + + /// + /// The maintenace notification message. + /// + /// + /// Will utilize default if not defined. + /// + public string Message { get; set; } = string.Empty; + + /// + /// The maintenace notification note. + /// + /// + /// Will utilize default if not defined. + /// + public string? Note { get; set; } +} diff --git a/webapi/Models/Response/MigrationResult.cs b/webapi/Models/Response/MigrationResult.cs deleted file mode 100644 index d41301a72..000000000 --- a/webapi/Models/Response/MigrationResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace CopilotChat.WebApi.Models.Response; - -/// -/// $$$ -/// -public class MigrationResult -{ - /// - /// $$$ - /// - public string Title { get; set; } = string.Empty; - - /// - /// $$$ - /// - public string Message { get; set; } = string.Empty; - - /// - /// $$$ - /// - public string? Note { get; set; } -} From ac340178bb31fddd9eda337a73f0f2fda7a220b0 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 14 Sep 2023 19:03:58 -0700 Subject: [PATCH 09/28] Checkpoint --- webapi/Controllers/MaintenanceController.cs | 12 ++--- webapi/Extensions/ServiceExtensions.cs | 9 ++++ webapi/Services/IMaintenanceAction.cs | 19 +++++++ webapi/Services/MaintenanceMiddleware.cs | 30 +++++++++-- .../ChatMemoryMigrationService.cs | 33 ++++++++---- .../ChatMigrationMaintenanceAction.cs | 54 +++++++++++++++++++ .../ChatMigrationMonitor.cs | 7 +-- .../ChatMigrationStatus.cs} | 17 +----- .../IChatMemoryMigrationService.cs | 5 +- .../MemoryMigration/IChatMigrationMonitor.cs | 17 ++++++ 10 files changed, 162 insertions(+), 41 deletions(-) create mode 100644 webapi/Services/IMaintenanceAction.cs rename webapi/Services/{ => MemoryMigration}/ChatMemoryMigrationService.cs (75%) create mode 100644 webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs rename webapi/Services/{ => MemoryMigration}/ChatMigrationMonitor.cs (94%) rename webapi/Services/{IChatMigrationMonitor.cs => MemoryMigration/ChatMigrationStatus.cs} (68%) rename webapi/Services/{ => MemoryMigration}/IChatMemoryMigrationService.cs (71%) create mode 100644 webapi/Services/MemoryMigration/IChatMigrationMonitor.cs diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index fd879db6e..b8d3217c3 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -6,7 +6,7 @@ using CopilotChat.WebApi.Hubs; using CopilotChat.WebApi.Models.Response; using CopilotChat.WebApi.Options; -using CopilotChat.WebApi.Services; +using CopilotChat.WebApi.Services.MemoryMigration; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -48,22 +48,22 @@ public MaintenanceController( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> GetMaintenanceStatusAsync( - [FromServices] IChatMigrationMonitor migrationMonitor, + [FromServices] IChatMigrationMonitor migrationMonitor, // $$$ WRONG INTERFACE [FromServices] IHubContext messageRelayHubContext, CancellationToken cancellationToken = default) { MaintenanceResult? result = null; - var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(null!, cancellationToken).ConfigureAwait(false); // $$$ MEMORY OR ??? + var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(cancellationToken).ConfigureAwait(false); if (migrationStatus != ChatMigrationStatus.None) { result = new MaintenanceResult { - Title = "$$$", - Message = "$$$", - Note = "$$$", + Title = "Migrating Chat Memory", + Message = "An upgrade requires that all non-document memories be migrated. This may take several minutes...", + Note = "Note: All document memories will need to be re-imported.", }; } diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index dde409608..cdbbe6277 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -8,6 +8,7 @@ using CopilotChat.WebApi.Models.Storage; using CopilotChat.WebApi.Options; using CopilotChat.WebApi.Services; +using CopilotChat.WebApi.Services.MemoryMigration; using CopilotChat.WebApi.Storage; using CopilotChat.WebApi.Utilities; using Microsoft.AspNetCore.Authentication; @@ -92,6 +93,14 @@ internal static IServiceCollection AddMigrationServices(this IServiceCollection { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // $$$ MIGRATION + services.AddSingleton>( + sp => + (IReadOnlyList) + new[] + { + sp.GetRequiredService(), + }); return services; } diff --git a/webapi/Services/IMaintenanceAction.cs b/webapi/Services/IMaintenanceAction.cs new file mode 100644 index 000000000..ba2b18350 --- /dev/null +++ b/webapi/Services/IMaintenanceAction.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using System.Threading; + +namespace CopilotChat.WebApi.Services; + +/// +/// Defines discrete maintenace action responsible for both inspecting state +/// and performing maintenace. +/// +public interface IMaintenanceAction +{ + /// + /// Calling site to initiate maintenance action. + /// + /// true if maintenance needed or in progress + Task InvokeAsync(CancellationToken cancellation = default); +} diff --git a/webapi/Services/MaintenanceMiddleware.cs b/webapi/Services/MaintenanceMiddleware.cs index 979230362..2baade736 100644 --- a/webapi/Services/MaintenanceMiddleware.cs +++ b/webapi/Services/MaintenanceMiddleware.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using System.Threading.Tasks; using CopilotChat.WebApi.Controllers; using CopilotChat.WebApi.Hubs; @@ -18,20 +19,23 @@ namespace CopilotChat.WebApi.Services; public class MaintenanceMiddleware { private readonly RequestDelegate _next; + private readonly IReadOnlyList _actions; private readonly IOptions _serviceOptions; private readonly IHubContext _messageRelayHubContext; private readonly ILogger _logger; + private bool? _isInMaintenance; + public MaintenanceMiddleware( RequestDelegate next, - //IChatMigrationMonitor migrationMonitor, $$$ PATTERN ?? - //IChatMemoryMigrationService migrationService, + IReadOnlyList actions, IOptions servicetOptions, IHubContext messageRelayHubContext, ILogger logger) { this._next = next; + this._actions = actions; this._serviceOptions = servicetOptions; this._messageRelayHubContext = messageRelayHubContext; this._logger = logger; @@ -39,11 +43,31 @@ public MaintenanceMiddleware( public async Task Invoke(HttpContext ctx, IKernel kernel) { - if (this._serviceOptions.Value.InMaintenance) + // Skip inspection if _isInMaintenance explicitly false. + if (!(this._isInMaintenance ?? false)) + { + // Maintance never false => true; always true => false or just false; + this._isInMaintenance = await this.InspectMaintenanceActionAsync().ConfigureAwait(false); + } + + // In mainteance if actions say so or explicitly configured. + if ((this._isInMaintenance ?? false) || this._serviceOptions.Value.InMaintenance) { await this._messageRelayHubContext.Clients.All.SendAsync(MaintenanceController.GlobalSiteMaintenance, "Site undergoing maintenance...").ConfigureAwait(false); } await this._next(ctx); } + + private async Task InspectMaintenanceActionAsync() + { + bool inMaintenance = false; + + foreach (var action in this._actions) + { + inMaintenance |= await action.InvokeAsync().ConfigureAwait(false); + } + + return inMaintenance; + } } diff --git a/webapi/Services/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs similarity index 75% rename from webapi/Services/ChatMemoryMigrationService.cs rename to webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs index 65e5ad386..a1f3e995b 100644 --- a/webapi/Services/ChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs @@ -13,7 +13,7 @@ using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticMemory; -namespace CopilotChat.WebApi.Services; +namespace CopilotChat.WebApi.Services.MemoryMigration; /// /// Service implementation of . @@ -21,8 +21,10 @@ namespace CopilotChat.WebApi.Services; public class ChatMemoryMigrationService : IChatMemoryMigrationService { private readonly ILogger _logger; + private readonly ISemanticTextMemory memory; // $$$ private readonly ISemanticMemoryClient _memoryClient; private readonly ChatSessionRepository _chatSessionRepository; + private readonly ChatMemorySourceRepository _memorySourceRepository; private readonly PromptsOptions _promptOptions; /// @@ -33,29 +35,31 @@ public ChatMemoryMigrationService( IOptions documentMemoryOptions, IOptions promptOptions, ISemanticMemoryClient memoryClient, - ChatSessionRepository chatSessionRepository) + ChatSessionRepository chatSessionRepository, + ChatMemorySourceRepository memorySourceRepository) { this._logger = logger; this._promptOptions = promptOptions.Value; this._memoryClient = memoryClient; this._chatSessionRepository = chatSessionRepository; + this._memorySourceRepository = memorySourceRepository; } /// - public async Task MigrateAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default) + public async Task MigrateAsync(CancellationToken cancellationToken = default) { var shouldMigrate = false; - var tokenMemory = await GetTokenMemory(cancelToken).ConfigureAwait(false); + var tokenMemory = await GetTokenMemory(cancellationToken).ConfigureAwait(false); if (tokenMemory == null) { // Create token memory var token = Guid.NewGuid().ToString(); - await SetTokenMemory(token, cancelToken).ConfigureAwait(false); + await SetTokenMemory(token, cancellationToken).ConfigureAwait(false); // Allow writes that are racing time to land - await Task.Delay(TimeSpan.FromSeconds(5), cancelToken).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); // Retrieve token memory - tokenMemory = await GetTokenMemory(cancelToken).ConfigureAwait(false); + tokenMemory = await GetTokenMemory(cancellationToken).ConfigureAwait(false); // Set migrate flag if token matches shouldMigrate = tokenMemory != null && tokenMemory.Metadata.Text.Equals(token, StringComparison.OrdinalIgnoreCase); } @@ -65,13 +69,15 @@ public async Task MigrateAsync(ISemanticTextMemory memory, CancellationToken can return; } + await RemoveMemorySourcesAsync().ConfigureAwait(false); + // Extract and store memories, using the original id to avoid duplication should a retry be required. await foreach ((string chatId, string memoryName, string memoryId, string memoryText) in QueryMemoriesAsync()) { - await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, chatId, memoryName, memoryId, memoryText, cancelToken); + await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, chatId, memoryName, memoryId, memoryText, cancellationToken); } - await SetTokenMemory(ChatMigrationMonitor.MigrationCompletionToken, cancelToken).ConfigureAwait(false); + await SetTokenMemory(ChatMigrationMonitor.MigrationCompletionToken, cancellationToken).ConfigureAwait(false); // Inline function to extract all memories for a given chat and memory type. async IAsyncEnumerable<(string chatId, string memoryName, string memoryId, string memoryText)> QueryMemoriesAsync() @@ -82,7 +88,7 @@ public async Task MigrateAsync(ISemanticTextMemory memory, CancellationToken can foreach (var memoryType in this._promptOptions.MemoryMap.Keys) { var indexName = $"{chat.Id}-{memoryType}"; - var memories = await memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancelToken).ToArrayAsync(cancelToken); + var memories = await memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); foreach (var memory in memories) { @@ -103,5 +109,12 @@ async Task SetTokenMemory(string token, CancellationToken cancelToken) { await memory.SaveInformationAsync(this._promptOptions.MemoryIndexName, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancelToken).ConfigureAwait(false); } + + async Task RemoveMemorySourcesAsync() + { + var documentMemories = await this._memorySourceRepository.GetAllAsync().ConfigureAwait(false); + + await Task.WhenAll(documentMemories.Select(memory => this._memorySourceRepository.DeleteAsync(memory))).ConfigureAwait(false); + } } } diff --git a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs new file mode 100644 index 000000000..5829b20b7 --- /dev/null +++ b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace CopilotChat.WebApi.Services.MemoryMigration; + +/// +/// Middleware for determining is site is undergoing maintenance. +/// +public class ChatMigrationMaintenanceAction : IMaintenanceAction +{ + private readonly IChatMigrationMonitor _migrationMonitor; + private readonly IChatMemoryMigrationService _migrationService; + private readonly ILogger _logger; + + public ChatMigrationMaintenanceAction( + IChatMigrationMonitor migrationMonitor, + IChatMemoryMigrationService migrationService, + ILogger logger) + + { + this._migrationMonitor = migrationMonitor; + this._migrationService = migrationService; + this._logger = logger; + } + + public async Task InvokeAsync(CancellationToken cancellation = default) + { + var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(cancellation).ConfigureAwait(false); + + if (migrationStatus != ChatMigrationStatus.None) + { + return true; + } + + if (migrationStatus == ChatMigrationStatus.RequiresUpgrade) + { + try + { + // Migrate all chats to single index + await this._migrationService.MigrateAsync(cancellation).ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + this._logger.LogError(ex, "Error migrating chat memories"); + } + } + + return false; + } +} diff --git a/webapi/Services/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs similarity index 94% rename from webapi/Services/ChatMigrationMonitor.cs rename to webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index dbcb3b678..4ed5ad4c8 100644 --- a/webapi/Services/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -10,7 +10,7 @@ using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Memory; -namespace CopilotChat.WebApi.Services; +namespace CopilotChat.WebApi.Services.MemoryMigration; /// /// Service implementation of . @@ -25,6 +25,7 @@ public class ChatMigrationMonitor : IChatMigrationMonitor private readonly ILogger _logger; private readonly string _indexNameAllMemory; + private readonly ISemanticTextMemory memory; // $$$ /// /// Initializes a new instance of the class. @@ -38,7 +39,7 @@ public ChatMigrationMonitor( } /// - public async Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default) + public async Task GetCurrentStatusAsync(CancellationToken cancelToken = default) { if (_cachedStatus == null) { @@ -59,7 +60,7 @@ await QueryCollectionAsync().ConfigureAwait(false), // Refresh status if we have a cached value for any state other than: ChatVersionStatus.None. switch (_cachedStatus) { - case (ChatMigrationStatus s) when (s == ChatMigrationStatus.RequiresUpgrade || s == ChatMigrationStatus.Upgrading): + case ChatMigrationStatus s when s == ChatMigrationStatus.RequiresUpgrade || s == ChatMigrationStatus.Upgrading: _cachedStatus = await QueryStatusAsync().ConfigureAwait(false); break; diff --git a/webapi/Services/IChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationStatus.cs similarity index 68% rename from webapi/Services/IChatMigrationMonitor.cs rename to webapi/Services/MemoryMigration/ChatMigrationStatus.cs index 424433247..190505768 100644 --- a/webapi/Services/IChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationStatus.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Threading; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Memory; - -namespace CopilotChat.WebApi.Services; +namespace CopilotChat.WebApi.Services.MemoryMigration; /// /// Set of migration states/status for chat memory migration. @@ -39,14 +35,3 @@ private ChatMigrationStatus(string label) this.Label = label; } } - -/// -/// Contract for monitoring the status of chat memory migration. -/// -public interface IChatMigrationMonitor -{ - /// - /// Inspects the current state of affairs to determine the chat migration status. - /// - Task GetCurrentStatusAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default); -} diff --git a/webapi/Services/IChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs similarity index 71% rename from webapi/Services/IChatMemoryMigrationService.cs rename to webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs index 2e2d91e0f..3fd94ec2e 100644 --- a/webapi/Services/IChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs @@ -2,9 +2,8 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Memory; -namespace CopilotChat.WebApi.Services; +namespace CopilotChat.WebApi.Services.MemoryMigration; /// /// Defines contract for migrating chat memory. @@ -15,5 +14,5 @@ public interface IChatMemoryMigrationService /// Migrates all non-document memory to the semantic-memory index. /// Subsequent/redunant migration is non-destructive/no-impact to migrated index. /// - Task MigrateAsync(ISemanticTextMemory memory, CancellationToken cancelToken = default); + Task MigrateAsync(CancellationToken cancelToken = default); } diff --git a/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs new file mode 100644 index 000000000..8c2968237 --- /dev/null +++ b/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace CopilotChat.WebApi.Services.MemoryMigration; + +/// +/// Contract for monitoring the status of chat memory migration. +/// +public interface IChatMigrationMonitor +{ + /// + /// Inspects the current state of affairs to determine the chat migration status. + /// + Task GetCurrentStatusAsync(CancellationToken cancelToken = default); +} From 1d4a307db402cbb9641b92a555e831ce687bf9e1 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 15 Sep 2023 09:31:53 -0700 Subject: [PATCH 10/28] Spelling --- webapi/Models/Response/MaintenanceResult.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapi/Models/Response/MaintenanceResult.cs b/webapi/Models/Response/MaintenanceResult.cs index 25c98083f..688d164b9 100644 --- a/webapi/Models/Response/MaintenanceResult.cs +++ b/webapi/Models/Response/MaintenanceResult.cs @@ -3,12 +3,12 @@ namespace CopilotChat.WebApi.Models.Response; /// -/// Defines optional messaging for maintenace mode. +/// Defines optional messaging for maintenance mode. /// public class MaintenanceResult { /// - /// The maintenace notification title. + /// The maintenance notification title. /// /// /// Will utilize default if not defined. @@ -16,7 +16,7 @@ public class MaintenanceResult public string Title { get; set; } = string.Empty; /// - /// The maintenace notification message. + /// The maintenance notification message. /// /// /// Will utilize default if not defined. @@ -24,7 +24,7 @@ public class MaintenanceResult public string Message { get; set; } = string.Empty; /// - /// The maintenace notification note. + /// The maintenance notification note. /// /// /// Will utilize default if not defined. From 30cd475ab1f1878cc3204204c6b68dd24163e09c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 15 Sep 2023 12:34:23 -0700 Subject: [PATCH 11/28] Typos and rename --- .../ISemanticMemoryClientExtensions.cs | 26 +++++++++---------- webapi/Program.cs | 1 - webapi/Services/IMaintenanceAction.cs | 2 +- webapi/Services/MaintenanceMiddleware.cs | 4 +-- .../ChatMemoryMigrationService.cs | 10 +++---- .../MemoryMigration/ChatMigrationMonitor.cs | 8 +++--- .../IChatMemoryMigrationService.cs | 2 +- .../MemoryMigration/IChatMigrationMonitor.cs | 2 +- .../ChatSkills/SemanticChatMemoryExtractor.cs | 2 +- 9 files changed, 28 insertions(+), 29 deletions(-) diff --git a/webapi/Extensions/ISemanticMemoryClientExtensions.cs b/webapi/Extensions/ISemanticMemoryClientExtensions.cs index a6404e943..c4eac65ff 100644 --- a/webapi/Extensions/ISemanticMemoryClientExtensions.cs +++ b/webapi/Extensions/ISemanticMemoryClientExtensions.cs @@ -60,9 +60,9 @@ public static Task SearchMemoryAsync( float relevanceThreshold, string chatId, string? memoryName = null, - CancellationToken cancelToken = default) + CancellationToken cancellationToken = default) { - return memoryClient.SearchMemoryAsync(indexName, query, relevanceThreshold, resultCount: -1, chatId, memoryName, cancelToken); + return memoryClient.SearchMemoryAsync(indexName, query, relevanceThreshold, resultCount: -1, chatId, memoryName, cancellationToken); } public static async Task SearchMemoryAsync( @@ -73,7 +73,7 @@ public static async Task SearchMemoryAsync( int resultCount, string chatId, string? memoryName = null, - CancellationToken cancelToken = default) + CancellationToken cancellationToken = default) { var filter = new MemoryFilter @@ -94,7 +94,7 @@ await memoryClient.SearchAsync( indexName, filter, resultCount, - cancelToken) + cancellationToken) .ConfigureAwait(false); return searchResult; @@ -108,7 +108,7 @@ public static async Task StoreDocumentAsync( string memoryName, string fileName, Stream fileContent, - CancellationToken cancelToken = default) + CancellationToken cancellationToken = default) { var uploadRequest = new DocumentUploadRequest @@ -121,7 +121,7 @@ public static async Task StoreDocumentAsync( uploadRequest.Tags.Add(MemoryTags.TagChatId, chatId); uploadRequest.Tags.Add(MemoryTags.TagMemory, memoryName); - await memoryClient.ImportDocumentAsync(uploadRequest, cancelToken); + await memoryClient.ImportDocumentAsync(uploadRequest, cancellationToken); } public static Task StoreMemoryAsync( @@ -130,9 +130,9 @@ public static Task StoreMemoryAsync( string chatId, string memoryName, string memory, - CancellationToken cancelToken = default) + CancellationToken cancellationToken = default) { - return memoryClient.StoreMemoryAsync(indexName, chatId, memoryName, memoryId: Guid.NewGuid().ToString(), memory, cancelToken); + return memoryClient.StoreMemoryAsync(indexName, chatId, memoryName, memoryId: Guid.NewGuid().ToString(), memory, cancellationToken); } public static async Task StoreMemoryAsync( @@ -142,7 +142,7 @@ public static async Task StoreMemoryAsync( string memoryName, string memoryId, string memory, - CancellationToken cancelToken = default) + CancellationToken cancellationToken = default) { using var stream = new MemoryStream(); using var writer = new StreamWriter(stream); @@ -165,19 +165,19 @@ public static async Task StoreMemoryAsync( uploadRequest.Tags.Add(MemoryTags.TagChatId, chatId); uploadRequest.Tags.Add(MemoryTags.TagMemory, memoryName); - await memoryClient.ImportDocumentAsync(uploadRequest, cancelToken); + await memoryClient.ImportDocumentAsync(uploadRequest, cancellationToken); } public static async Task RemoveChatMemoriesAsync( this ISemanticMemoryClient memoryClient, string indexName, string chatId, - CancellationToken cancelToken = default) + CancellationToken cancellationToken = default) { - var memories = await memoryClient.SearchMemoryAsync(indexName, "*", 0.0F, chatId, cancelToken: cancelToken); + var memories = await memoryClient.SearchMemoryAsync(indexName, "*", 0.0F, chatId, cancellationToken: cancellationToken); foreach (var memory in memories.Results) { - await memoryClient.DeleteDocumentAsync(indexName, memory.Link, cancelToken); + await memoryClient.DeleteDocumentAsync(indexName, memory.Link, cancellationToken); } } } diff --git a/webapi/Program.cs b/webapi/Program.cs index c09aa2ec1..3bf1c7be1 100644 --- a/webapi/Program.cs +++ b/webapi/Program.cs @@ -47,7 +47,6 @@ public static async Task Main(string[] args) .AddCopilotChatAuthentication(builder.Configuration) .AddCopilotChatAuthorization(); - // Configure and add semantic services builder .AddBotConfig() diff --git a/webapi/Services/IMaintenanceAction.cs b/webapi/Services/IMaintenanceAction.cs index ba2b18350..023d20b96 100644 --- a/webapi/Services/IMaintenanceAction.cs +++ b/webapi/Services/IMaintenanceAction.cs @@ -6,7 +6,7 @@ namespace CopilotChat.WebApi.Services; /// -/// Defines discrete maintenace action responsible for both inspecting state +/// Defines discrete maintenance action responsible for both inspecting state /// and performing maintenace. /// public interface IMaintenanceAction diff --git a/webapi/Services/MaintenanceMiddleware.cs b/webapi/Services/MaintenanceMiddleware.cs index 2baade736..0e3dcbfa0 100644 --- a/webapi/Services/MaintenanceMiddleware.cs +++ b/webapi/Services/MaintenanceMiddleware.cs @@ -46,11 +46,11 @@ public async Task Invoke(HttpContext ctx, IKernel kernel) // Skip inspection if _isInMaintenance explicitly false. if (!(this._isInMaintenance ?? false)) { - // Maintance never false => true; always true => false or just false; + // Maintenance never false => true; always true => false or just false; this._isInMaintenance = await this.InspectMaintenanceActionAsync().ConfigureAwait(false); } - // In mainteance if actions say so or explicitly configured. + // In maintenance if actions say so or explicitly configured. if ((this._isInMaintenance ?? false) || this._serviceOptions.Value.InMaintenance) { await this._messageRelayHubContext.Clients.All.SendAsync(MaintenanceController.GlobalSiteMaintenance, "Site undergoing maintenance...").ConfigureAwait(false); diff --git a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs index a1f3e995b..f745466f1 100644 --- a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs @@ -88,7 +88,7 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) foreach (var memoryType in this._promptOptions.MemoryMap.Keys) { var indexName = $"{chat.Id}-{memoryType}"; - var memories = await memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); + var memories = await this.memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); foreach (var memory in memories) { @@ -99,15 +99,15 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) } // Inline function to read the token memory - async Task GetTokenMemory(CancellationToken cancelToken) + async Task GetTokenMemory(CancellationToken cancellationToken) { - return await memory.GetAsync(this._promptOptions.MemoryIndexName, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancelToken).ConfigureAwait(false); + return await this.memory.GetAsync(this._promptOptions.MemoryIndexName, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken).ConfigureAwait(false); } // Inline function to write the token memory - async Task SetTokenMemory(string token, CancellationToken cancelToken) + async Task SetTokenMemory(string token, CancellationToken cancellationToken) { - await memory.SaveInformationAsync(this._promptOptions.MemoryIndexName, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancelToken).ConfigureAwait(false); + await this.memory.SaveInformationAsync(this._promptOptions.MemoryIndexName, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken).ConfigureAwait(false); } async Task RemoveMemorySourcesAsync() diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index 4ed5ad4c8..7bd63d802 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -39,7 +39,7 @@ public ChatMigrationMonitor( } /// - public async Task GetCurrentStatusAsync(CancellationToken cancelToken = default) + public async Task GetCurrentStatusAsync(CancellationToken cancellationToken = default) { if (_cachedStatus == null) { @@ -80,7 +80,7 @@ await QueryCollectionAsync().ConfigureAwait(false), if (_hasCurrentIndex == null) { // Cache "found" index state to reduce query count and avoid handling truth mutation. - var collections = await memory.GetCollectionsAsync(cancelToken).ConfigureAwait(false); + var collections = await this.memory.GetCollectionsAsync(cancellationToken).ConfigureAwait(false); // Does the new "target" index already exist? _hasCurrentIndex = collections.Any(c => c.Equals(this._indexNameAllMemory, StringComparison.OrdinalIgnoreCase)); @@ -106,11 +106,11 @@ async Task QueryStatusAsync() try { var result = - await memory.GetAsync( + await this.memory.GetAsync( this._indexNameAllMemory, MigrationKey, withEmbedding: false, - cancelToken).ConfigureAwait(false); + cancellationToken).ConfigureAwait(false); if (result != null) { diff --git a/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs index 3fd94ec2e..0601b3914 100644 --- a/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs @@ -14,5 +14,5 @@ public interface IChatMemoryMigrationService /// Migrates all non-document memory to the semantic-memory index. /// Subsequent/redunant migration is non-destructive/no-impact to migrated index. /// - Task MigrateAsync(CancellationToken cancelToken = default); + Task MigrateAsync(CancellationToken cancellationToken = default); } diff --git a/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs index 8c2968237..391f7558e 100644 --- a/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs @@ -13,5 +13,5 @@ public interface IChatMigrationMonitor /// /// Inspects the current state of affairs to determine the chat migration status. /// - Task GetCurrentStatusAsync(CancellationToken cancelToken = default); + Task GetCurrentStatusAsync(CancellationToken cancellationToken = default); } diff --git a/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs b/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs index ada6d0682..ac7ad4d8d 100644 --- a/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs +++ b/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs @@ -115,7 +115,7 @@ await memoryClient.SearchMemoryAsync( if (searchResult.Results.Count == 0) { - await memoryClient.StoreMemoryAsync(options.MemoryIndexName, chatId, memoryName, memory, cancelToken: cancellationToken); + await memoryClient.StoreMemoryAsync(options.MemoryIndexName, chatId, memoryName, memory, cancellationToken); } } catch (SKException connectorException) From d51f5dc6a8ace4e967f31a395be487b9ddbad52a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 15 Sep 2023 14:28:15 -0700 Subject: [PATCH 12/28] Refactor kernel initialization --- webapi/Controllers/MaintenanceController.cs | 2 +- webapi/Extensions/SemanticKernelExtensions.cs | 99 ++------- webapi/Extensions/ServiceExtensions.cs | 2 +- .../ChatMemoryMigrationService.cs | 13 +- .../MemoryMigration/ChatMigrationMonitor.cs | 11 +- webapi/Services/SemanticKernelProvider.cs | 191 ++++++++++++++++++ 6 files changed, 222 insertions(+), 96 deletions(-) create mode 100644 webapi/Services/SemanticKernelProvider.cs diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index b8d3217c3..20bf7318d 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -48,7 +48,7 @@ public MaintenanceController( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> GetMaintenanceStatusAsync( - [FromServices] IChatMigrationMonitor migrationMonitor, // $$$ WRONG INTERFACE + [FromServices] IChatMigrationMonitor migrationMonitor, // $$$ WRONG INTERFACE (IReadOnlyList actions) [FromServices] IHubContext messageRelayHubContext, CancellationToken cancellationToken = default) { diff --git a/webapi/Extensions/SemanticKernelExtensions.cs b/webapi/Extensions/SemanticKernelExtensions.cs index 4daec1e59..fb6c1ab64 100644 --- a/webapi/Extensions/SemanticKernelExtensions.cs +++ b/webapi/Extensions/SemanticKernelExtensions.cs @@ -16,8 +16,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.Embeddings; -using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Skills.Core; @@ -38,16 +36,16 @@ internal static class SemanticKernelExtensions /// /// Add Semantic Kernel services /// - internal static WebApplicationBuilder AddSemanticKernelServices(this WebApplicationBuilder builder) + public static WebApplicationBuilder AddSemanticKernelServices(this WebApplicationBuilder builder) { + builder.InitializeKernelProvider(); + // Semantic Kernel builder.Services.AddScoped( sp => { - var kernel = Kernel.Builder - .WithLoggerFactory(sp.GetRequiredService()) - .WithCompletionBackend(sp, builder.Configuration) - .Build(); + var provider = sp.GetRequiredService(); + var kernel = provider.GetCompletionKernel(); sp.GetRequiredService()(sp, kernel); return kernel; @@ -67,16 +65,16 @@ internal static WebApplicationBuilder AddSemanticKernelServices(this WebApplicat /// public static WebApplicationBuilder AddPlannerServices(this WebApplicationBuilder builder) { + builder.InitializeKernelProvider(); + builder.Services.AddScoped(sp => { sp.WithBotConfig(builder.Configuration); var plannerOptions = sp.GetRequiredService>(); - var plannerKernel = Kernel.Builder - .WithLoggerFactory(sp.GetRequiredService()) - .WithPlannerBackend(sp, builder.Configuration) - .Build(); + var provider = sp.GetRequiredService(); + var plannerKernel = provider.GetPlannerKernel(); return new CopilotChatPlanner(plannerKernel, plannerOptions?.Value, sp.GetRequiredService>()); }); @@ -133,6 +131,11 @@ public static void ThrowIfFailed(this SKContext context) } } + private static void InitializeKernelProvider(this WebApplicationBuilder builder) + { + builder.Services.AddSingleton(sp => new SemanticKernelProvider(sp, builder.Configuration)); + } + /// /// Register the skills with the kernel. /// @@ -179,80 +182,6 @@ internal static void AddContentSafety(this IServiceCollection services) } } - /// - /// Add the completion backend to the kernel config - /// - private static KernelBuilder WithCompletionBackend(this KernelBuilder kernelBuilder, IServiceProvider provider, IConfiguration configuration) - { - var memoryOptions = provider.GetRequiredService>().Value; - - switch (memoryOptions.TextGeneratorType) - { - case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): - case string y when y.Equals("AzureOpenAIText", StringComparison.OrdinalIgnoreCase): - var azureAIOptions = memoryOptions.GetServiceConfig(configuration, "AzureOpenAIText"); - return kernelBuilder.WithAzureChatCompletionService(azureAIOptions.Deployment, azureAIOptions.Endpoint, azureAIOptions.APIKey); - - case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): - var openAIOptions = memoryOptions.GetServiceConfig(configuration, "OpenAI"); - return kernelBuilder.WithOpenAIChatCompletionService(openAIOptions.TextModel, openAIOptions.APIKey); - - default: - throw new ArgumentException($"Invalid {nameof(memoryOptions.TextGeneratorType)} value in 'SemanticMemory' settings."); - } - } - - /// - /// Add the completion backend to the kernel config for the planner. - /// - private static KernelBuilder WithPlannerBackend(this KernelBuilder kernelBuilder, IServiceProvider provider, IConfiguration configuration) - { - var memoryOptions = provider.GetRequiredService>().Value; - var plannerOptions = provider.GetRequiredService>().Value; - - switch (memoryOptions.TextGeneratorType) - { - case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): - case string y when y.Equals("AzureOpenAIText", StringComparison.OrdinalIgnoreCase): - var azureAIOptions = memoryOptions.GetServiceConfig(configuration, "AzureOpenAIText"); - return kernelBuilder.WithAzureChatCompletionService(plannerOptions.Model, azureAIOptions.Endpoint, azureAIOptions.APIKey); - - case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): - var openAIOptions = memoryOptions.GetServiceConfig(configuration, "OpenAI"); - return kernelBuilder.WithOpenAIChatCompletionService(plannerOptions.Model, openAIOptions.APIKey); - - default: - throw new ArgumentException($"Invalid {nameof(memoryOptions.TextGeneratorType)} value in 'SemanticMemory' settings."); - } - } - - /// - /// Construct IEmbeddingGeneration from - /// - private static ITextEmbeddingGeneration ToTextEmbeddingsService( - this IServiceProvider provider, - IConfiguration configuration, - ILoggerFactory? loggerFactory = null) - { - var logger = provider.GetRequiredService>(); - var memoryOptions = provider.GetRequiredService>().Value; - - switch (memoryOptions.Retrieval.EmbeddingGeneratorType) - { - case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): - case string y when y.Equals("AzureOpenAIEmbedding", StringComparison.OrdinalIgnoreCase): - var azureAIOptions = memoryOptions.GetServiceConfig(configuration, "AzureOpenAIEmbedding"); - return new AzureTextEmbeddingGeneration(azureAIOptions.Deployment, azureAIOptions.Endpoint, azureAIOptions.APIKey, httpClient: null, loggerFactory); - - case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): - var openAIOptions = memoryOptions.GetServiceConfig(configuration, "OpenAI"); - return new OpenAITextEmbeddingGeneration(openAIOptions.EmbeddingModel, openAIOptions.APIKey, organization: null, httpClient: null, loggerFactory); - - default: - throw new ArgumentException($"Invalid {nameof(memoryOptions.Retrieval.EmbeddingGeneratorType)} value in 'SemanticMemory' settings."); - } - } - /// /// Get the embedding model from the configuration. /// diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index cdbbe6277..97bdaabb7 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -93,7 +93,7 @@ internal static IServiceCollection AddMigrationServices(this IServiceCollection { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); // $$$ MIGRATION + services.AddSingleton(); services.AddSingleton>( sp => (IReadOnlyList) diff --git a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs index f745466f1..e02bafb0d 100644 --- a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs @@ -21,7 +21,7 @@ namespace CopilotChat.WebApi.Services.MemoryMigration; public class ChatMemoryMigrationService : IChatMemoryMigrationService { private readonly ILogger _logger; - private readonly ISemanticTextMemory memory; // $$$ + private readonly ISemanticTextMemory _memory; private readonly ISemanticMemoryClient _memoryClient; private readonly ChatSessionRepository _chatSessionRepository; private readonly ChatMemorySourceRepository _memorySourceRepository; @@ -36,13 +36,16 @@ public ChatMemoryMigrationService( IOptions promptOptions, ISemanticMemoryClient memoryClient, ChatSessionRepository chatSessionRepository, - ChatMemorySourceRepository memorySourceRepository) + ChatMemorySourceRepository memorySourceRepository, + SemanticKernelProvider provider) { this._logger = logger; this._promptOptions = promptOptions.Value; this._memoryClient = memoryClient; this._chatSessionRepository = chatSessionRepository; this._memorySourceRepository = memorySourceRepository; + var kernel = provider.GetMigrationKernel(); + this._memory = kernel.Memory; } /// @@ -88,7 +91,7 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) foreach (var memoryType in this._promptOptions.MemoryMap.Keys) { var indexName = $"{chat.Id}-{memoryType}"; - var memories = await this.memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); + var memories = await this._memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); foreach (var memory in memories) { @@ -101,13 +104,13 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) // Inline function to read the token memory async Task GetTokenMemory(CancellationToken cancellationToken) { - return await this.memory.GetAsync(this._promptOptions.MemoryIndexName, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken).ConfigureAwait(false); + return await this._memory.GetAsync(this._promptOptions.MemoryIndexName, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken).ConfigureAwait(false); } // Inline function to write the token memory async Task SetTokenMemory(string token, CancellationToken cancellationToken) { - await this.memory.SaveInformationAsync(this._promptOptions.MemoryIndexName, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken).ConfigureAwait(false); + await this._memory.SaveInformationAsync(this._promptOptions.MemoryIndexName, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken).ConfigureAwait(false); } async Task RemoveMemorySourcesAsync() diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index 7bd63d802..55bfcb5d9 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -25,17 +25,20 @@ public class ChatMigrationMonitor : IChatMigrationMonitor private readonly ILogger _logger; private readonly string _indexNameAllMemory; - private readonly ISemanticTextMemory memory; // $$$ + private readonly ISemanticTextMemory _memory; /// /// Initializes a new instance of the class. /// public ChatMigrationMonitor( ILogger logger, - IOptions promptOptions) + IOptions promptOptions, + SemanticKernelProvider provider) { this._logger = logger; this._indexNameAllMemory = promptOptions.Value.MemoryIndexName; + var kernel = provider.GetMigrationKernel(); + this._memory = kernel.Memory; } /// @@ -80,7 +83,7 @@ await QueryCollectionAsync().ConfigureAwait(false), if (_hasCurrentIndex == null) { // Cache "found" index state to reduce query count and avoid handling truth mutation. - var collections = await this.memory.GetCollectionsAsync(cancellationToken).ConfigureAwait(false); + var collections = await this._memory.GetCollectionsAsync(cancellationToken).ConfigureAwait(false); // Does the new "target" index already exist? _hasCurrentIndex = collections.Any(c => c.Equals(this._indexNameAllMemory, StringComparison.OrdinalIgnoreCase)); @@ -106,7 +109,7 @@ async Task QueryStatusAsync() try { var result = - await this.memory.GetAsync( + await this._memory.GetAsync( this._indexNameAllMemory, MigrationKey, withEmbedding: false, diff --git a/webapi/Services/SemanticKernelProvider.cs b/webapi/Services/SemanticKernelProvider.cs new file mode 100644 index 000000000..e1dafda38 --- /dev/null +++ b/webapi/Services/SemanticKernelProvider.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using CopilotChat.WebApi.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.Embeddings; +using Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch; +using Microsoft.SemanticKernel.Connectors.Memory.Qdrant; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticMemory; +using Microsoft.SemanticMemory.MemoryStorage.Qdrant; + +namespace CopilotChat.WebApi.Services; + +/// +/// Extension methods for registering Semantic Kernel related services. +/// +public sealed class SemanticKernelProvider +{ + private readonly IServiceProvider _serviceProvider; + private readonly IConfiguration _configuration; + + public SemanticKernelProvider(IServiceProvider serviceProvider, IConfiguration configuration) + { + this._serviceProvider = serviceProvider; + this._configuration = configuration; + } + + /// + /// Produce semantic-kernel with only completion services for chat. + /// + public IKernel GetCompletionKernel() + { + var builder = Kernel.Builder.WithLoggerFactory(this._serviceProvider.GetRequiredService()); + + this.WithCompletionBackend(builder); + + return builder.Build(); + } + + /// + /// Produce semantic-kernel with only completion services for planner. + /// + public IKernel GetPlannerKernel() + { + var builder = Kernel.Builder.WithLoggerFactory(this._serviceProvider.GetRequiredService()); + + this.WithPlannerBackend(builder); + + return builder.Build(); + } + + /// + /// Produce semantic-kernel with semantic-memory. + /// + public IKernel GetMigrationKernel() + { + var builder = Kernel.Builder.WithLoggerFactory(this._serviceProvider.GetRequiredService()); + + this.WithEmbeddingBackend(builder); + this.WithSemanticTextMemory(builder); + + return builder.Build(); + } + + /// + /// Add the completion backend to the kernel config + /// + private KernelBuilder WithCompletionBackend(KernelBuilder kernelBuilder) + { + var memoryOptions = this._serviceProvider.GetRequiredService>().Value; + + switch (memoryOptions.TextGeneratorType) + { + case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): + case string y when y.Equals("AzureOpenAIText", StringComparison.OrdinalIgnoreCase): + var azureAIOptions = memoryOptions.GetServiceConfig(this._configuration, "AzureOpenAIText"); + return kernelBuilder.WithAzureChatCompletionService(azureAIOptions.Deployment, azureAIOptions.Endpoint, azureAIOptions.APIKey); + + case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): + var openAIOptions = memoryOptions.GetServiceConfig(this._configuration, "OpenAI"); + return kernelBuilder.WithOpenAIChatCompletionService(openAIOptions.TextModel, openAIOptions.APIKey); + + default: + throw new ArgumentException($"Invalid {nameof(memoryOptions.TextGeneratorType)} value in 'SemanticMemory' settings."); + } + } + + /// + /// Add the completion backend to the kernel config for the planner. + /// + private KernelBuilder WithPlannerBackend(KernelBuilder kernelBuilder) + { + var memoryOptions = this._serviceProvider.GetRequiredService>().Value; + var plannerOptions = this._serviceProvider.GetRequiredService>().Value; + + switch (memoryOptions.TextGeneratorType) + { + case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): + case string y when y.Equals("AzureOpenAIText", StringComparison.OrdinalIgnoreCase): + var azureAIOptions = memoryOptions.GetServiceConfig(this._configuration, "AzureOpenAIText"); + return kernelBuilder.WithAzureChatCompletionService(plannerOptions.Model, azureAIOptions.Endpoint, azureAIOptions.APIKey); + + case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): + var openAIOptions = memoryOptions.GetServiceConfig(this._configuration, "OpenAI"); + return kernelBuilder.WithOpenAIChatCompletionService(plannerOptions.Model, openAIOptions.APIKey); + + default: + throw new ArgumentException($"Invalid {nameof(memoryOptions.TextGeneratorType)} value in 'SemanticMemory' settings."); + } + } + + /// + /// Add the embedding backend to the kernel config + /// + private KernelBuilder WithEmbeddingBackend(KernelBuilder kernelBuilder) + { + var memoryOptions = this._serviceProvider.GetRequiredService>().Value; + + switch (memoryOptions.Retrieval.EmbeddingGeneratorType) + { + case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): + case string y when y.Equals("AzureOpenAIText", StringComparison.OrdinalIgnoreCase): + var azureAIOptions = memoryOptions.GetServiceConfig(this._configuration, "AzureOpenAIEmbedding"); + return kernelBuilder.WithAzureTextEmbeddingGenerationService(azureAIOptions.Deployment, azureAIOptions.Endpoint, azureAIOptions.APIKey); + + case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): + var openAIOptions = memoryOptions.GetServiceConfig(this._configuration, "OpenAI"); + return kernelBuilder.WithOpenAITextEmbeddingGenerationService(openAIOptions.EmbeddingModel, openAIOptions.APIKey); + + default: + throw new ArgumentException($"Invalid {nameof(memoryOptions.Retrieval.EmbeddingGeneratorType)} value in 'SemanticMemory' settings."); + } + } + + /// + /// Add the semantic text memory. + /// + private void WithSemanticTextMemory(KernelBuilder builder) + { + var memoryOptions = this._serviceProvider.GetRequiredService>().Value; + + IMemoryStore memoryStore = CreateMemoryStore(); + +#pragma warning disable CA2000 // Ownership passed to kernel + builder.WithMemory( + new SemanticTextMemory( + memoryStore, + this._serviceProvider.GetRequiredService())); +#pragma warning restore CA2000 // Ownership passed to kernel + + IMemoryStore CreateMemoryStore() + { + switch (memoryOptions.Retrieval.VectorDbType) + { + case string x when x.Equals("SimpleVectorDb", StringComparison.OrdinalIgnoreCase): + return new VolatileMemoryStore(); + + case string x when x.Equals("Qdrant", StringComparison.OrdinalIgnoreCase): + var qdrantConfig = memoryOptions.GetServiceConfig(this._configuration, "Qdrant"); + +#pragma warning disable CA2000 // Ownership passed to QdrantMemoryStore + HttpClient httpClient = new(new HttpClientHandler { CheckCertificateRevocationList = true }); +#pragma warning restore CA2000 // Ownership passed to QdrantMemoryStore + if (!string.IsNullOrWhiteSpace(qdrantConfig.APIKey)) + { + httpClient.DefaultRequestHeaders.Add("api-key", qdrantConfig.APIKey); + } + + return + new QdrantMemoryStore( + httpClient: httpClient, + 1536, + qdrantConfig.Endpoint, + loggerFactory: this._serviceProvider.GetRequiredService()); + + case string x when x.Equals("AzureCognitiveSearch", StringComparison.OrdinalIgnoreCase): + var acsConfig = memoryOptions.GetServiceConfig(this._configuration, "AzureCognitiveSearch"); + return new AzureCognitiveSearchMemoryStore(acsConfig.Endpoint, acsConfig.APIKey); + + default: + throw new InvalidOperationException($"Invalid 'VectorDbType' type '{memoryOptions.Retrieval.VectorDbType}'."); + } + } + } +} From c241714ef3cdcccd889ac8a293bcf005819032ae Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 15 Sep 2023 14:29:19 -0700 Subject: [PATCH 13/28] Spelling --- webapi/Services/IMaintenanceAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Services/IMaintenanceAction.cs b/webapi/Services/IMaintenanceAction.cs index 023d20b96..c2645cad9 100644 --- a/webapi/Services/IMaintenanceAction.cs +++ b/webapi/Services/IMaintenanceAction.cs @@ -7,7 +7,7 @@ namespace CopilotChat.WebApi.Services; /// /// Defines discrete maintenance action responsible for both inspecting state -/// and performing maintenace. +/// and performing maintenance. /// public interface IMaintenanceAction { From 51119c624990ae821fea3772b6f92e6a3a550f62 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 15 Sep 2023 14:31:13 -0700 Subject: [PATCH 14/28] Namespace clean-up --- webapi/Models/Response/Bot.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/webapi/Models/Response/Bot.cs b/webapi/Models/Response/Bot.cs index 1b7cb5a90..bdea1b985 100644 --- a/webapi/Models/Response/Bot.cs +++ b/webapi/Models/Response/Bot.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using CopilotChat.WebApi.Models.Storage; using CopilotChat.WebApi.Options; -using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticMemory; namespace CopilotChat.WebApi.Models.Response; From b59796dd9d76e6ebb3788f6250429409180c52d7 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 15 Sep 2023 14:34:15 -0700 Subject: [PATCH 15/28] Namespace order --- webapi/Services/IMaintenanceAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Services/IMaintenanceAction.cs b/webapi/Services/IMaintenanceAction.cs index c2645cad9..a4848c85c 100644 --- a/webapi/Services/IMaintenanceAction.cs +++ b/webapi/Services/IMaintenanceAction.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; namespace CopilotChat.WebApi.Services; From 81dc4ed17cf85af85b1247bfea4816ddfed96ead Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sat, 16 Sep 2023 11:00:56 -0700 Subject: [PATCH 16/28] Bot Fix --- webapi/Controllers/BotController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Controllers/BotController.cs b/webapi/Controllers/BotController.cs index 5be420b8c..aad9826b6 100644 --- a/webapi/Controllers/BotController.cs +++ b/webapi/Controllers/BotController.cs @@ -114,7 +114,7 @@ private async Task CreateBotAsync(Guid chatId, CancellationToken cancellati foreach (var memory in this._promptOptions.MemoryMap.Keys) { - bot.DocumentEmbeddings.Add( + bot.Embeddings.Add( memory, await this.GetMemoryRecordsAndAppendToEmbeddingsAsync(chatIdString, memory, cancellationToken)); } From 7b1d81f9bd274b56b4c95977df74b3743e53532d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sat, 16 Sep 2023 11:04:24 -0700 Subject: [PATCH 17/28] Fixes and tweaks --- webapi/Extensions/ServiceExtensions.cs | 7 +++++-- webapi/Program.cs | 2 +- webapi/Services/SemanticKernelProvider.cs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index 97bdaabb7..f693aa3aa 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -89,11 +89,14 @@ internal static IServiceCollection AddUtilities(this IServiceCollection services return services.AddScoped(); } - internal static IServiceCollection AddMigrationServices(this IServiceCollection services) + internal static IServiceCollection AddMainetnanceServices(this IServiceCollection services) { + // Inject migration services services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + + // Inject actions so they can be part of the action-list. + services.AddSingleton(); services.AddSingleton>( sp => (IReadOnlyList) diff --git a/webapi/Program.cs b/webapi/Program.cs index 3bf1c7be1..eb0bf7928 100644 --- a/webapi/Program.cs +++ b/webapi/Program.cs @@ -69,7 +69,7 @@ public static async Task Main(string[] args) // Add in the rest of the services. builder.Services - .AddMigrationServices() + .AddMainetnanceServices() .AddEndpointsApiExplorer() .AddSwaggerGen() .AddCorsPolicy(builder.Configuration) diff --git a/webapi/Services/SemanticKernelProvider.cs b/webapi/Services/SemanticKernelProvider.cs index e1dafda38..be4dc682d 100644 --- a/webapi/Services/SemanticKernelProvider.cs +++ b/webapi/Services/SemanticKernelProvider.cs @@ -125,7 +125,7 @@ private KernelBuilder WithEmbeddingBackend(KernelBuilder kernelBuilder) switch (memoryOptions.Retrieval.EmbeddingGeneratorType) { case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): - case string y when y.Equals("AzureOpenAIText", StringComparison.OrdinalIgnoreCase): + case string y when y.Equals("AzureOpenAIEmbedding", StringComparison.OrdinalIgnoreCase): var azureAIOptions = memoryOptions.GetServiceConfig(this._configuration, "AzureOpenAIEmbedding"); return kernelBuilder.WithAzureTextEmbeddingGenerationService(azureAIOptions.Deployment, azureAIOptions.Endpoint, azureAIOptions.APIKey); From 721cef7b26187305ebf697d76629e9eb806455ad Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 18 Sep 2023 18:38:55 -0700 Subject: [PATCH 18/28] Functional testing --- webapi/Controllers/MaintenanceController.cs | 11 ++- .../ISemanticMemoryClientExtensions.cs | 7 +- webapi/Services/MaintenanceMiddleware.cs | 5 +- .../ChatMemoryMigrationService.cs | 65 +++++++++----- .../ChatMigrationMaintenanceAction.cs | 38 ++++---- .../MemoryMigration/ChatMigrationMonitor.cs | 89 ++++++++++--------- webapi/appsettings.json | 2 + webapp/src/components/utils/TextUtils.tsx | 3 + webapp/src/components/views/BackendProbe.tsx | 27 +++--- 9 files changed, 148 insertions(+), 99 deletions(-) diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index 20bf7318d..3cdf331d0 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -48,7 +48,7 @@ public MaintenanceController( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> GetMaintenanceStatusAsync( - [FromServices] IChatMigrationMonitor migrationMonitor, // $$$ WRONG INTERFACE (IReadOnlyList actions) + [FromServices] IChatMigrationMonitor migrationMonitor, [FromServices] IHubContext messageRelayHubContext, CancellationToken cancellationToken = default) { @@ -69,9 +69,14 @@ public MaintenanceController( if (this._serviceOptions.Value.InMaintenance) { - result = new MaintenanceResult(); + result = new MaintenanceResult(); // Default maintenance message } - return this.Ok(result); + if (result != null) + { + return this.Ok(result); + } + + return this.Ok(); } } diff --git a/webapi/Extensions/ISemanticMemoryClientExtensions.cs b/webapi/Extensions/ISemanticMemoryClientExtensions.cs index c4eac65ff..d37888b6b 100644 --- a/webapi/Extensions/ISemanticMemoryClientExtensions.cs +++ b/webapi/Extensions/ISemanticMemoryClientExtensions.cs @@ -45,7 +45,12 @@ public static void AddSemanticMemoryServices(this WebApplicationBuilder appBuild } else { - memoryBuilder.WithCustomOcr(appBuilder.Configuration); + memoryBuilder.WithoutSummarizeHandlers(); + + if (hasOcr) + { + memoryBuilder.WithCustomOcr(appBuilder.Configuration); + } } ISemanticMemoryClient memory = memoryBuilder.FromAppSettings().Build(); diff --git a/webapi/Services/MaintenanceMiddleware.cs b/webapi/Services/MaintenanceMiddleware.cs index 0e3dcbfa0..68137d062 100644 --- a/webapi/Services/MaintenanceMiddleware.cs +++ b/webapi/Services/MaintenanceMiddleware.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Threading.Tasks; using CopilotChat.WebApi.Controllers; @@ -44,14 +45,14 @@ public MaintenanceMiddleware( public async Task Invoke(HttpContext ctx, IKernel kernel) { // Skip inspection if _isInMaintenance explicitly false. - if (!(this._isInMaintenance ?? false)) + if (this._isInMaintenance == null || this._isInMaintenance.Value) { // Maintenance never false => true; always true => false or just false; this._isInMaintenance = await this.InspectMaintenanceActionAsync().ConfigureAwait(false); } // In maintenance if actions say so or explicitly configured. - if ((this._isInMaintenance ?? false) || this._serviceOptions.Value.InMaintenance) + if (this._serviceOptions.Value.InMaintenance) { await this._messageRelayHubContext.Clients.All.SendAsync(MaintenanceController.GlobalSiteMaintenance, "Site undergoing maintenance...").ConfigureAwait(false); } diff --git a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs index e02bafb0d..40319fb80 100644 --- a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs @@ -25,6 +25,7 @@ public class ChatMemoryMigrationService : IChatMemoryMigrationService private readonly ISemanticMemoryClient _memoryClient; private readonly ChatSessionRepository _chatSessionRepository; private readonly ChatMemorySourceRepository _memorySourceRepository; + private readonly string _globalIndex; private readonly PromptsOptions _promptOptions; /// @@ -44,6 +45,7 @@ public ChatMemoryMigrationService( this._memoryClient = memoryClient; this._chatSessionRepository = chatSessionRepository; this._memorySourceRepository = memorySourceRepository; + this._globalIndex = documentMemoryOptions.Value.GlobalDocumentCollectionName; var kernel = provider.GetMigrationKernel(); this._memory = kernel.Memory; } @@ -51,33 +53,44 @@ public ChatMemoryMigrationService( /// public async Task MigrateAsync(CancellationToken cancellationToken = default) { - var shouldMigrate = false; - - var tokenMemory = await GetTokenMemory(cancellationToken).ConfigureAwait(false); - if (tokenMemory == null) + try { - // Create token memory - var token = Guid.NewGuid().ToString(); - await SetTokenMemory(token, cancellationToken).ConfigureAwait(false); - // Allow writes that are racing time to land - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); - // Retrieve token memory - tokenMemory = await GetTokenMemory(cancellationToken).ConfigureAwait(false); - // Set migrate flag if token matches - shouldMigrate = tokenMemory != null && tokenMemory.Metadata.Text.Equals(token, StringComparison.OrdinalIgnoreCase); + await this.InternalMigrateAsync(cancellationToken).ConfigureAwait(false); } + catch (Exception exception) when (!exception.IsCriticalException()) + { + this._logger.LogError(exception, "Error migrating chat memories"); + } + } - if (!shouldMigrate) + private async Task InternalMigrateAsync(CancellationToken cancellationToken = default) + { + var collectionNames = (await this._memory.GetCollectionsAsync(cancellationToken).ConfigureAwait(false)).ToHashSet(StringComparer.OrdinalIgnoreCase); + + var tokenMemory = await GetTokenMemory(cancellationToken).ConfigureAwait(false); + if (tokenMemory != null) { + // Create memory token already exists return; } + // Create memory token + var token = Guid.NewGuid().ToString(); + await SetTokenMemory(token, cancellationToken).ConfigureAwait(false); + await RemoveMemorySourcesAsync().ConfigureAwait(false); + bool needsZombie = true; // Extract and store memories, using the original id to avoid duplication should a retry be required. await foreach ((string chatId, string memoryName, string memoryId, string memoryText) in QueryMemoriesAsync()) { await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, chatId, memoryName, memoryId, memoryText, cancellationToken); + needsZombie = false; + } + + if (needsZombie) + { + await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, Guid.Empty.ToString(), "zombie", Guid.NewGuid().ToString(), "Initialized", cancellationToken); } await SetTokenMemory(ChatMigrationMonitor.MigrationCompletionToken, cancellationToken).ConfigureAwait(false); @@ -91,11 +104,16 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) foreach (var memoryType in this._promptOptions.MemoryMap.Keys) { var indexName = $"{chat.Id}-{memoryType}"; - var memories = await this._memory.SearchAsync(indexName, "*", limit: int.MaxValue, minRelevanceScore: -1, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); - - foreach (var memory in memories) + this._logger.LogCritical($"CHECK: {indexName}"); + if (collectionNames.Contains(indexName)) { - yield return (chat.Id, memoryType, memory.Metadata.Id, memory.Metadata.Text); + this._logger.LogCritical($"MIGRATE: {indexName}"); + var memories = await this._memory.SearchAsync(indexName, "*", limit: 10000, minRelevanceScore: 0, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); + + foreach (var memory in memories) + { + yield return (chat.Id, memoryType, memory.Metadata.Id, memory.Metadata.Text); + } } } } @@ -104,13 +122,20 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) // Inline function to read the token memory async Task GetTokenMemory(CancellationToken cancellationToken) { - return await this._memory.GetAsync(this._promptOptions.MemoryIndexName, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken).ConfigureAwait(false); + try + { + return await this._memory.GetAsync(this._globalIndex, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + return null; + } } // Inline function to write the token memory async Task SetTokenMemory(string token, CancellationToken cancellationToken) { - await this._memory.SaveInformationAsync(this._promptOptions.MemoryIndexName, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken).ConfigureAwait(false); + await this._memory.SaveInformationAsync(this._globalIndex, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken).ConfigureAwait(false); } async Task RemoveMemorySourcesAsync() diff --git a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs index 5829b20b7..12e34a4d4 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs @@ -8,7 +8,7 @@ namespace CopilotChat.WebApi.Services.MemoryMigration; /// -/// Middleware for determining is site is undergoing maintenance. +/// Middleware action to handle memory migration maintenance. /// public class ChatMigrationMaintenanceAction : IMaintenanceAction { @@ -31,24 +31,28 @@ public async Task InvokeAsync(CancellationToken cancellation = default) { var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(cancellation).ConfigureAwait(false); - if (migrationStatus != ChatMigrationStatus.None) - { - return true; - } + this._logger.LogCritical($"ACTION CURRENT: {migrationStatus.Label}"); - if (migrationStatus == ChatMigrationStatus.RequiresUpgrade) + switch (migrationStatus) { - try - { - // Migrate all chats to single index - await this._migrationService.MigrateAsync(cancellation).ConfigureAwait(false); - } - catch (Exception ex) when (!ex.IsCriticalException()) - { - this._logger.LogError(ex, "Error migrating chat memories"); - } + case ChatMigrationStatus s when (s == ChatMigrationStatus.RequiresUpgrade): + try + { + // Migrate all chats to single index + this._migrationService.MigrateAsync(cancellation); // Don't block + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + this._logger.LogError(ex, "Error migrating chat memories"); + } + return true; // In maintenance + + case ChatMigrationStatus s when (s == ChatMigrationStatus.Upgrading): + return true; // In maintenance + + case ChatMigrationStatus s when (s == ChatMigrationStatus.None): + default: + return false; // No maintenance } - - return false; } } diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index 55bfcb5d9..4488bff44 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -7,7 +7,6 @@ using CopilotChat.WebApi.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Memory; namespace CopilotChat.WebApi.Services.MemoryMigration; @@ -15,6 +14,14 @@ namespace CopilotChat.WebApi.Services.MemoryMigration; /// /// Service implementation of . /// +/// +/// Migration is fundamentally determined by presence of the new consolidated index. +/// That is, if the new index exists then migration was considered to have occurred. +/// A tracking record is created in the historical global-document index: +/// to managed race condition during the migration process (having migration triggered a second time while in progress). +/// In the event that somehow two migration processes are initiated in parallel, no duplication will result...only extraneous procesing. +/// If the desire exists to reset/re-execute migration, simply delete the new index. +/// public class ChatMigrationMonitor : IChatMigrationMonitor { internal const string MigrationCompletionToken = "DONE"; @@ -24,6 +31,7 @@ public class ChatMigrationMonitor : IChatMigrationMonitor private static bool? _hasCurrentIndex; private readonly ILogger _logger; + private readonly string _indexNameGlobalDocs; private readonly string _indexNameAllMemory; private readonly ISemanticTextMemory _memory; @@ -32,10 +40,12 @@ public class ChatMigrationMonitor : IChatMigrationMonitor /// public ChatMigrationMonitor( ILogger logger, + IOptions docOptions, IOptions promptOptions, SemanticKernelProvider provider) { this._logger = logger; + this._indexNameGlobalDocs = docOptions.Value.GlobalDocumentCollectionName; this._indexNameAllMemory = promptOptions.Value.MemoryIndexName; var kernel = provider.GetMigrationKernel(); this._memory = kernel.Memory; @@ -44,7 +54,7 @@ public ChatMigrationMonitor( /// public async Task GetCurrentStatusAsync(CancellationToken cancellationToken = default) { - if (_cachedStatus == null) + if (_cachedStatus == null) { // Attempt to determine migration status looking at index existence. (Once) Interlocked.CompareExchange( @@ -63,7 +73,7 @@ await QueryCollectionAsync().ConfigureAwait(false), // Refresh status if we have a cached value for any state other than: ChatVersionStatus.None. switch (_cachedStatus) { - case ChatMigrationStatus s when s == ChatMigrationStatus.RequiresUpgrade || s == ChatMigrationStatus.Upgrading: + case ChatMigrationStatus s when s != ChatMigrationStatus.None: _cachedStatus = await QueryStatusAsync().ConfigureAwait(false); break; @@ -74,13 +84,12 @@ await QueryCollectionAsync().ConfigureAwait(false), return _cachedStatus ?? ChatMigrationStatus.None; - // Inline function to determine if the new "target" index already exists. - // If not, we need to upgrade; otherwise, further inspection is required. + // Reports and caches migration state as either: None or null depending on existince of the target index. async Task QueryCollectionAsync() { - try + if (_hasCurrentIndex == null) { - if (_hasCurrentIndex == null) + try { // Cache "found" index state to reduce query count and avoid handling truth mutation. var collections = await this._memory.GetCollectionsAsync(cancellationToken).ConfigureAwait(false); @@ -88,52 +97,48 @@ await QueryCollectionAsync().ConfigureAwait(false), // Does the new "target" index already exist? _hasCurrentIndex = collections.Any(c => c.Equals(this._indexNameAllMemory, StringComparison.OrdinalIgnoreCase)); - if (!_hasCurrentIndex ?? false) - { - return ChatMigrationStatus.RequiresUpgrade; // No index == update required - } + return (_hasCurrentIndex ?? false) ? ChatMigrationStatus.None : null; + } + catch (Exception exception) when (!exception.IsCriticalException()) + { + this._logger.LogError(exception, "Unable to search collections"); } - } - catch (SKException exception) - { - this._logger.LogError(exception, "Unable to search collections"); } - return null; // Further inspection required + return (_hasCurrentIndex ?? false) ? ChatMigrationStatus.None : null; } + // Note: Only called once determined that target index does not exist. async Task QueryStatusAsync() { - if (_hasCurrentIndex ?? false) + try { - try + var result = + await this._memory.SearchAsync( + this._indexNameGlobalDocs, + MigrationKey, + limit: 1, + minRelevanceScore: -1, + withEmbeddings: false, + cancellationToken) + .SingleOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (result == null) { - var result = - await this._memory.GetAsync( - this._indexNameAllMemory, - MigrationKey, - withEmbedding: false, - cancellationToken).ConfigureAwait(false); - - if (result != null) - { - var text = result.Metadata.Text; - - if (!string.IsNullOrWhiteSpace(text) && text.Equals(MigrationCompletionToken, StringComparison.OrdinalIgnoreCase)) - { - return ChatMigrationStatus.None; - } - - return ChatMigrationStatus.Upgrading; - } + // No migration token + return ChatMigrationStatus.RequiresUpgrade; } - catch (SKException exception) - { - this._logger.LogError(exception, "Unable to search collection {0}", this._indexNameAllMemory); - } - } - return ChatMigrationStatus.RequiresUpgrade; + var isDone = MigrationCompletionToken.Equals(result.Metadata.Text, StringComparison.OrdinalIgnoreCase); + + return isDone ? ChatMigrationStatus.None : ChatMigrationStatus.Upgrading; + } + catch (Exception exception) when (!exception.IsCriticalException()) + { + this._logger.LogWarning("Failure searching collections: {0}\n{1}", this._indexNameGlobalDocs, exception.Message); + return ChatMigrationStatus.RequiresUpgrade; + } } } } diff --git a/webapi/appsettings.json b/webapi/appsettings.json index b7d6f8db4..65710ec19 100644 --- a/webapi/appsettings.json +++ b/webapi/appsettings.json @@ -96,6 +96,8 @@ }, "Cosmos": { "Database": "CopilotChat", + // IMPORTANT: Each container requires a specific partition key. Ensure these are set correctly in your CosmosDB instance. + // See details at ./README.md#1-containers-and-partitionkeys "ChatSessionsContainer": "chatsessions", "ChatMessagesContainer": "chatmessages", "ChatMemorySourcesContainer": "chatmemorysources", diff --git a/webapp/src/components/utils/TextUtils.tsx b/webapp/src/components/utils/TextUtils.tsx index f730ed038..65d35d1fb 100644 --- a/webapp/src/components/utils/TextUtils.tsx +++ b/webapp/src/components/utils/TextUtils.tsx @@ -53,6 +53,9 @@ export function formatChatTextContent(messageContent: string) { * Formats text containing `\n` or `\r` into paragraphs. */ export function formatParagraphTextContent(messageContent: string) { + console.log("****\\/") + console.log(messageContent) + console.log("****/\\") messageContent = messageContent.replaceAll('\r\n', '\n\r'); return ( diff --git a/webapp/src/components/views/BackendProbe.tsx b/webapp/src/components/views/BackendProbe.tsx index 7c53dc6cf..cabb3c6dd 100644 --- a/webapp/src/components/views/BackendProbe.tsx +++ b/webapp/src/components/views/BackendProbe.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import { Body1, Spinner, Title3 } from '@fluentui/react-components'; -import { FC, useEffect, useRef } from 'react'; +import { FC, useEffect, useState } from 'react'; import { useSharedClasses } from '../../styles'; import { useAppDispatch, useAppSelector } from '../../redux/app/hooks'; import { RootState } from '../../redux/app/store'; @@ -25,7 +25,7 @@ export const BackendProbe: FC = ({ uri, onBackendFound }) => { const healthUrl = new URL('healthz', uri); const migrationUrl = new URL('maintenancestatus', uri); - const model = useRef(null); + const [model, setModel] = useState(null); useEffect(() => { const timer = setInterval(() => { @@ -39,19 +39,18 @@ export const BackendProbe: FC = ({ uri, onBackendFound }) => { const fetchMaintenanceAsync = async () => { const result = await fetch(migrationUrl); - if (!result.ok) { return; } - const json: unknown = await result.json(); - - if (json === null) { - dispatch(setMaintenance(false)); - onBackendFound(); - } - - model.current = json as IMaintenance | null; + result.json() + .then(data => { + setModel(data as IMaintenance); + }) + .catch(() => { + dispatch(setMaintenance(false)); + onBackendFound(); + }); }; if (!isMaintenance) { @@ -74,15 +73,15 @@ export const BackendProbe: FC = ({ uri, onBackendFound }) => { <> {isMaintenance ? (
- {model.current?.title ?? 'Site undergoing maintenance...'} + {model?.title ?? 'Site undergoing maintenance...'} - {model.current?.message ?? + {model?.message ?? 'Planned site maintenance is underway. We apologize for the disruption.'} - {model.current?.note ?? + {model?.note ?? "Note: If this message doesn't resolve after a significant duration, refresh the browser."} From 1cf4bf487435aaaf32738b47585e713f7caf95b0 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 18 Sep 2023 18:57:07 -0700 Subject: [PATCH 19/28] Clean-up --- webapi/CopilotChatWebApi.csproj | 2 +- webapi/Services/MaintenanceMiddleware.cs | 1 - .../Services/MemoryMigration/ChatMemoryMigrationService.cs | 2 -- .../MemoryMigration/ChatMigrationMaintenanceAction.cs | 6 ++---- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/webapi/CopilotChatWebApi.csproj b/webapi/CopilotChatWebApi.csproj index 6ebae09eb..ba87a8b92 100644 --- a/webapi/CopilotChatWebApi.csproj +++ b/webapi/CopilotChatWebApi.csproj @@ -24,7 +24,7 @@ - + diff --git a/webapi/Services/MaintenanceMiddleware.cs b/webapi/Services/MaintenanceMiddleware.cs index 68137d062..55c411f72 100644 --- a/webapi/Services/MaintenanceMiddleware.cs +++ b/webapi/Services/MaintenanceMiddleware.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Threading.Tasks; using CopilotChat.WebApi.Controllers; diff --git a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs index 40319fb80..e00bae568 100644 --- a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs @@ -104,10 +104,8 @@ private async Task InternalMigrateAsync(CancellationToken cancellationToken = de foreach (var memoryType in this._promptOptions.MemoryMap.Keys) { var indexName = $"{chat.Id}-{memoryType}"; - this._logger.LogCritical($"CHECK: {indexName}"); if (collectionNames.Contains(indexName)) { - this._logger.LogCritical($"MIGRATE: {indexName}"); var memories = await this._memory.SearchAsync(indexName, "*", limit: 10000, minRelevanceScore: 0, withEmbeddings: false, cancellationToken).ToArrayAsync(cancellationToken); foreach (var memory in memories) diff --git a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs index 12e34a4d4..31433d9b0 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs @@ -31,15 +31,13 @@ public async Task InvokeAsync(CancellationToken cancellation = default) { var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(cancellation).ConfigureAwait(false); - this._logger.LogCritical($"ACTION CURRENT: {migrationStatus.Label}"); - switch (migrationStatus) { case ChatMigrationStatus s when (s == ChatMigrationStatus.RequiresUpgrade): try { - // Migrate all chats to single index - this._migrationService.MigrateAsync(cancellation); // Don't block + // Migrate all chats to single index (in background) + var task = this._migrationService.MigrateAsync(cancellation); } catch (Exception ex) when (!ex.IsCriticalException()) { From 02212547fd881c02c1286fe6ecdf5fbc35727ea8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 18 Sep 2023 19:04:45 -0700 Subject: [PATCH 20/28] Typo --- webapi/Services/MemoryMigration/ChatMigrationMonitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index 4488bff44..82f2e82eb 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -84,7 +84,7 @@ await QueryCollectionAsync().ConfigureAwait(false), return _cachedStatus ?? ChatMigrationStatus.None; - // Reports and caches migration state as either: None or null depending on existince of the target index. + // Reports and caches migration state as either: None or null depending on existence of the target index. async Task QueryCollectionAsync() { if (_hasCurrentIndex == null) From e5fb8b2f2720f8f01ec433d0087f1ff0de5f0f7f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 18 Sep 2023 19:08:16 -0700 Subject: [PATCH 21/28] Typo --- webapi/Services/MemoryMigration/ChatMigrationMonitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index 82f2e82eb..65fa03739 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -19,7 +19,7 @@ namespace CopilotChat.WebApi.Services.MemoryMigration; /// That is, if the new index exists then migration was considered to have occurred. /// A tracking record is created in the historical global-document index: /// to managed race condition during the migration process (having migration triggered a second time while in progress). -/// In the event that somehow two migration processes are initiated in parallel, no duplication will result...only extraneous procesing. +/// In the event that somehow two migration processes are initiated in parallel, no duplication will result...only extraneous processing. /// If the desire exists to reset/re-execute migration, simply delete the new index. /// public class ChatMigrationMonitor : IChatMigrationMonitor From 3a1c694f0e13a581c7bdbbcba1d6fe7032478a40 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 18 Sep 2023 19:42:01 -0700 Subject: [PATCH 22/28] Whitespace --- webapi/Controllers/MaintenanceController.cs | 2 +- webapi/Services/MemoryMigration/ChatMigrationMonitor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index 3cdf331d0..d74460b26 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -69,7 +69,7 @@ public MaintenanceController( if (this._serviceOptions.Value.InMaintenance) { - result = new MaintenanceResult(); // Default maintenance message + result = new MaintenanceResult(); // Default maintenance message } if (result != null) diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index 65fa03739..9be8d5618 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -54,7 +54,7 @@ public ChatMigrationMonitor( /// public async Task GetCurrentStatusAsync(CancellationToken cancellationToken = default) { - if (_cachedStatus == null) + if (_cachedStatus == null) { // Attempt to determine migration status looking at index existence. (Once) Interlocked.CompareExchange( From 8b079676d3967d0bd953e051b256aad9abca86b6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 19 Sep 2023 11:15:42 -0700 Subject: [PATCH 23/28] Whitespace --- webapp/src/components/views/BackendProbe.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/components/views/BackendProbe.tsx b/webapp/src/components/views/BackendProbe.tsx index 361b3ad3f..e362b4648 100644 --- a/webapp/src/components/views/BackendProbe.tsx +++ b/webapp/src/components/views/BackendProbe.tsx @@ -39,6 +39,7 @@ export const BackendProbe: FC = ({ uri, onBackendFound }) => { const fetchMaintenanceAsync = async () => { const result = await fetch(migrationUrl); + if (!result.ok) { return; } From daae03f1fbca5c3cb826e933bae37f9434b7c49f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 19 Sep 2023 11:20:55 -0700 Subject: [PATCH 24/28] Remove edit --- webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs b/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs index 4f5bea98f..ef11ba625 100644 --- a/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs +++ b/webapi/Skills/ChatSkills/SemanticChatMemoryExtractor.cs @@ -114,7 +114,7 @@ await memoryClient.SearchMemoryAsync( if (searchResult.Results.Count == 0) { - await memoryClient.StoreMemoryAsync(options.MemoryIndexName, chatId, memoryName, memory, cancellationToken); + await memoryClient.StoreMemoryAsync(options.MemoryIndexName, chatId, memoryName, memory, cancellationToken: cancellationToken); } } catch (Exception exception) when (!exception.IsCriticalException()) From dd59889ed38e2a65728265c397871de8f6c8ca8b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 19 Sep 2023 14:17:35 -0700 Subject: [PATCH 25/28] Remove "ConfigureAwait" --- webapi/Controllers/MaintenanceController.cs | 2 +- .../ISemanticMemoryClientExtensions.cs | 3 +-- webapi/Services/MaintenanceMiddleware.cs | 6 ++--- .../ChatMemoryMigrationService.cs | 22 +++++++++---------- .../ChatMigrationMaintenanceAction.cs | 2 +- .../MemoryMigration/ChatMigrationMonitor.cs | 10 ++++----- 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index d74460b26..f3e54dd41 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -54,7 +54,7 @@ public MaintenanceController( { MaintenanceResult? result = null; - var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(cancellationToken).ConfigureAwait(false); + var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(cancellationToken); if (migrationStatus != ChatMigrationStatus.None) { diff --git a/webapi/Extensions/ISemanticMemoryClientExtensions.cs b/webapi/Extensions/ISemanticMemoryClientExtensions.cs index d37888b6b..c083dc2ae 100644 --- a/webapi/Extensions/ISemanticMemoryClientExtensions.cs +++ b/webapi/Extensions/ISemanticMemoryClientExtensions.cs @@ -99,8 +99,7 @@ await memoryClient.SearchAsync( indexName, filter, resultCount, - cancellationToken) - .ConfigureAwait(false); + cancellationToken); return searchResult; } diff --git a/webapi/Services/MaintenanceMiddleware.cs b/webapi/Services/MaintenanceMiddleware.cs index 55c411f72..3f101e6fa 100644 --- a/webapi/Services/MaintenanceMiddleware.cs +++ b/webapi/Services/MaintenanceMiddleware.cs @@ -47,13 +47,13 @@ public async Task Invoke(HttpContext ctx, IKernel kernel) if (this._isInMaintenance == null || this._isInMaintenance.Value) { // Maintenance never false => true; always true => false or just false; - this._isInMaintenance = await this.InspectMaintenanceActionAsync().ConfigureAwait(false); + this._isInMaintenance = await this.InspectMaintenanceActionAsync(); } // In maintenance if actions say so or explicitly configured. if (this._serviceOptions.Value.InMaintenance) { - await this._messageRelayHubContext.Clients.All.SendAsync(MaintenanceController.GlobalSiteMaintenance, "Site undergoing maintenance...").ConfigureAwait(false); + await this._messageRelayHubContext.Clients.All.SendAsync(MaintenanceController.GlobalSiteMaintenance, "Site undergoing maintenance..."); } await this._next(ctx); @@ -65,7 +65,7 @@ private async Task InspectMaintenanceActionAsync() foreach (var action in this._actions) { - inMaintenance |= await action.InvokeAsync().ConfigureAwait(false); + inMaintenance |= await action.InvokeAsync(); } return inMaintenance; diff --git a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs index e00bae568..91808a095 100644 --- a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs @@ -55,7 +55,7 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) { try { - await this.InternalMigrateAsync(cancellationToken).ConfigureAwait(false); + await this.InternalMigrateAsync(cancellationToken); } catch (Exception exception) when (!exception.IsCriticalException()) { @@ -65,9 +65,9 @@ public async Task MigrateAsync(CancellationToken cancellationToken = default) private async Task InternalMigrateAsync(CancellationToken cancellationToken = default) { - var collectionNames = (await this._memory.GetCollectionsAsync(cancellationToken).ConfigureAwait(false)).ToHashSet(StringComparer.OrdinalIgnoreCase); + var collectionNames = (await this._memory.GetCollectionsAsync(cancellationToken)).ToHashSet(StringComparer.OrdinalIgnoreCase); - var tokenMemory = await GetTokenMemory(cancellationToken).ConfigureAwait(false); + var tokenMemory = await GetTokenMemory(cancellationToken); if (tokenMemory != null) { // Create memory token already exists @@ -76,9 +76,9 @@ private async Task InternalMigrateAsync(CancellationToken cancellationToken = de // Create memory token var token = Guid.NewGuid().ToString(); - await SetTokenMemory(token, cancellationToken).ConfigureAwait(false); + await SetTokenMemory(token, cancellationToken); - await RemoveMemorySourcesAsync().ConfigureAwait(false); + await RemoveMemorySourcesAsync(); bool needsZombie = true; // Extract and store memories, using the original id to avoid duplication should a retry be required. @@ -93,12 +93,12 @@ private async Task InternalMigrateAsync(CancellationToken cancellationToken = de await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, Guid.Empty.ToString(), "zombie", Guid.NewGuid().ToString(), "Initialized", cancellationToken); } - await SetTokenMemory(ChatMigrationMonitor.MigrationCompletionToken, cancellationToken).ConfigureAwait(false); + await SetTokenMemory(ChatMigrationMonitor.MigrationCompletionToken, cancellationToken); // Inline function to extract all memories for a given chat and memory type. async IAsyncEnumerable<(string chatId, string memoryName, string memoryId, string memoryText)> QueryMemoriesAsync() { - var chats = await this._chatSessionRepository.GetAllChatsAsync().ConfigureAwait(false); + var chats = await this._chatSessionRepository.GetAllChatsAsync(); foreach (var chat in chats) { foreach (var memoryType in this._promptOptions.MemoryMap.Keys) @@ -122,7 +122,7 @@ private async Task InternalMigrateAsync(CancellationToken cancellationToken = de { try { - return await this._memory.GetAsync(this._globalIndex, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken).ConfigureAwait(false); + return await this._memory.GetAsync(this._globalIndex, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken); } catch (Exception ex) when (!ex.IsCriticalException()) { @@ -133,14 +133,14 @@ private async Task InternalMigrateAsync(CancellationToken cancellationToken = de // Inline function to write the token memory async Task SetTokenMemory(string token, CancellationToken cancellationToken) { - await this._memory.SaveInformationAsync(this._globalIndex, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken).ConfigureAwait(false); + await this._memory.SaveInformationAsync(this._globalIndex, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken); } async Task RemoveMemorySourcesAsync() { - var documentMemories = await this._memorySourceRepository.GetAllAsync().ConfigureAwait(false); + var documentMemories = await this._memorySourceRepository.GetAllAsync(); - await Task.WhenAll(documentMemories.Select(memory => this._memorySourceRepository.DeleteAsync(memory))).ConfigureAwait(false); + await Task.WhenAll(documentMemories.Select(memory => this._memorySourceRepository.DeleteAsync(memory))); } } } diff --git a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs index 31433d9b0..c0abd56e5 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs @@ -29,7 +29,7 @@ public ChatMigrationMaintenanceAction( public async Task InvokeAsync(CancellationToken cancellation = default) { - var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(cancellation).ConfigureAwait(false); + var migrationStatus = await this._migrationMonitor.GetCurrentStatusAsync(cancellation); switch (migrationStatus) { diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs index 9be8d5618..af5871de6 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs @@ -59,13 +59,13 @@ public async Task GetCurrentStatusAsync(CancellationToken c // Attempt to determine migration status looking at index existence. (Once) Interlocked.CompareExchange( ref _cachedStatus, - await QueryCollectionAsync().ConfigureAwait(false), + await QueryCollectionAsync(), null); if (_cachedStatus == null) { // Attempt to determine migration status looking at index state. - _cachedStatus = await QueryStatusAsync().ConfigureAwait(false); + _cachedStatus = await QueryStatusAsync(); } } else @@ -74,7 +74,7 @@ await QueryCollectionAsync().ConfigureAwait(false), switch (_cachedStatus) { case ChatMigrationStatus s when s != ChatMigrationStatus.None: - _cachedStatus = await QueryStatusAsync().ConfigureAwait(false); + _cachedStatus = await QueryStatusAsync(); break; default: // ChatVersionStatus.None @@ -92,7 +92,7 @@ await QueryCollectionAsync().ConfigureAwait(false), try { // Cache "found" index state to reduce query count and avoid handling truth mutation. - var collections = await this._memory.GetCollectionsAsync(cancellationToken).ConfigureAwait(false); + var collections = await this._memory.GetCollectionsAsync(cancellationToken); // Does the new "target" index already exist? _hasCurrentIndex = collections.Any(c => c.Equals(this._indexNameAllMemory, StringComparison.OrdinalIgnoreCase)); @@ -122,7 +122,7 @@ await this._memory.SearchAsync( withEmbeddings: false, cancellationToken) .SingleOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); + ; if (result == null) { From d11330804228b5b225b386857a4f491ea2900ce3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 19 Sep 2023 14:19:00 -0700 Subject: [PATCH 26/28] Comment --- webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs index 91808a095..bce9677d6 100644 --- a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs +++ b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs @@ -88,6 +88,7 @@ private async Task InternalMigrateAsync(CancellationToken cancellationToken = de needsZombie = false; } + // Store "Zombie" memory in order to create the index since zero writes have occurred. Won't affect any chats. if (needsZombie) { await this._memoryClient.StoreMemoryAsync(this._promptOptions.MemoryIndexName, Guid.Empty.ToString(), "zombie", Guid.NewGuid().ToString(), "Initialized", cancellationToken); From bf51fc07571d58d20c8fc6024bb209d522871ca5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 19 Sep 2023 14:21:14 -0700 Subject: [PATCH 27/28] Remove try/catch --- .../MemoryMigration/ChatMigrationMaintenanceAction.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs index c0abd56e5..f1210110b 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs @@ -34,15 +34,8 @@ public async Task InvokeAsync(CancellationToken cancellation = default) switch (migrationStatus) { case ChatMigrationStatus s when (s == ChatMigrationStatus.RequiresUpgrade): - try - { - // Migrate all chats to single index (in background) - var task = this._migrationService.MigrateAsync(cancellation); - } - catch (Exception ex) when (!ex.IsCriticalException()) - { - this._logger.LogError(ex, "Error migrating chat memories"); - } + // Migrate all chats to single index (in background) + var task = this._migrationService.MigrateAsync(cancellation); return true; // In maintenance case ChatMigrationStatus s when (s == ChatMigrationStatus.Upgrading): From f3b116c6c164fde36026dc3bf9db096f57a44f33 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 19 Sep 2023 14:25:05 -0700 Subject: [PATCH 28/28] Namespace --- .../Services/MemoryMigration/ChatMigrationMaintenanceAction.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs index f1210110b..798dc06e7 100644 --- a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs +++ b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging;