diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs index 17f67e085..62b0f2ef5 100644 --- a/webapi/Controllers/MaintenanceController.cs +++ b/webapi/Controllers/MaintenanceController.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Threading; -using System.Threading.Tasks; using CopilotChat.WebApi.Models.Response; using CopilotChat.WebApi.Options; -using CopilotChat.WebApi.Services.MemoryMigration; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -41,25 +39,11 @@ public MaintenanceController( [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> GetMaintenanceStatusAsync( - [FromServices] IChatMigrationMonitor migrationMonitor, + public ActionResult GetMaintenanceStatusAsync( CancellationToken cancellationToken = default) { MaintenanceResult? result = null; - var migrationStatus = await migrationMonitor.GetCurrentStatusAsync(cancellationToken); - - if (migrationStatus != ChatMigrationStatus.None) - { - result = - new MaintenanceResult - { - 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.", - }; - } - if (this._serviceOptions.Value.InMaintenance) { result = new MaintenanceResult(); // Default maintenance message diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index a5e790aa2..26556a524 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -10,7 +10,6 @@ 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; @@ -129,25 +128,6 @@ internal static IServiceCollection AddPlugins(this IServiceCollection services, return services; } - internal static IServiceCollection AddMaintenanceServices(this IServiceCollection services) - { - // Inject migration services - services.AddSingleton(); - services.AddSingleton(); - - // Inject actions so they can be part of the action-list. - services.AddSingleton(); - services.AddSingleton>( - sp => - (IReadOnlyList) - new[] - { - sp.GetRequiredService(), - }); - - return services; - } - /// /// Add CORS settings. /// diff --git a/webapi/Program.cs b/webapi/Program.cs index 051e0ddcb..c33e4552c 100644 --- a/webapi/Program.cs +++ b/webapi/Program.cs @@ -78,7 +78,6 @@ public static async Task Main(string[] args) // Add in the rest of the services. builder.Services - .AddMaintenanceServices() .AddEndpointsApiExplorer() .AddSwaggerGen() .AddCorsPolicy(builder.Configuration) diff --git a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs deleted file mode 100644 index a3f606569..000000000 --- a/webapi/Services/MemoryMigration/ChatMemoryMigrationService.cs +++ /dev/null @@ -1,146 +0,0 @@ -// 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.KernelMemory; -using Microsoft.SemanticKernel.Memory; - -namespace CopilotChat.WebApi.Services.MemoryMigration; - -/// -/// Service implementation of . -/// -public class ChatMemoryMigrationService : IChatMemoryMigrationService -{ - private readonly ILogger _logger; - private readonly ISemanticTextMemory _memory; - private readonly IKernelMemory _memoryClient; - private readonly ChatSessionRepository _chatSessionRepository; - private readonly ChatMemorySourceRepository _memorySourceRepository; - private readonly string _globalIndex; - private readonly PromptsOptions _promptOptions; - - /// - /// Initializes a new instance of the class. - /// - public ChatMemoryMigrationService( - ILogger logger, - IOptions documentMemoryOptions, - IOptions promptOptions, - IKernelMemory memoryClient, - ChatSessionRepository chatSessionRepository, - ChatMemorySourceRepository memorySourceRepository, - SemanticKernelProvider provider) - { - this._logger = logger; - this._promptOptions = promptOptions.Value; - this._memoryClient = memoryClient; - this._chatSessionRepository = chatSessionRepository; - this._memorySourceRepository = memorySourceRepository; - this._globalIndex = documentMemoryOptions.Value.GlobalDocumentCollectionName; - this._memory = provider.GetMigrationMemory(); - } - - /// - public async Task MigrateAsync(CancellationToken cancellationToken = default) - { - try - { - await this.InternalMigrateAsync(cancellationToken); - } - catch (Exception exception) when (!exception.IsCriticalException()) - { - this._logger.LogError(exception, "Error migrating chat memories"); - } - } - - private async Task InternalMigrateAsync(CancellationToken cancellationToken = default) - { - var collectionNames = (await this._memory.GetCollectionsAsync(cancellationToken)).ToHashSet(StringComparer.OrdinalIgnoreCase); - - var tokenMemory = await GetTokenMemory(cancellationToken); - if (tokenMemory != null) - { - // Create memory token already exists - return; - } - - // Create memory token - var token = Guid.NewGuid().ToString(); - await SetTokenMemory(token, cancellationToken); - - await RemoveMemorySourcesAsync(); - - 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; - } - - // 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); - } - - 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(); - foreach (var chat in chats) - { - foreach (var memoryType in this._promptOptions.MemoryMap.Keys) - { - var indexName = $"{chat.Id}-{memoryType}"; - if (collectionNames.Contains(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); - } - } - } - } - } - - // Inline function to read the token memory - async Task GetTokenMemory(CancellationToken cancellationToken) - { - try - { - return await this._memory.GetAsync(this._globalIndex, ChatMigrationMonitor.MigrationKey, withEmbedding: false, cancellationToken); - } - 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._globalIndex, token, ChatMigrationMonitor.MigrationKey, description: null, additionalMetadata: null, cancellationToken); - } - - async Task RemoveMemorySourcesAsync() - { - var documentMemories = await this._memorySourceRepository.GetAllAsync(); - - await Task.WhenAll(documentMemories.Select(memory => this._memorySourceRepository.DeleteAsync(memory))); - } - } -} diff --git a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs b/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs deleted file mode 100644 index 798dc06e7..000000000 --- a/webapi/Services/MemoryMigration/ChatMigrationMaintenanceAction.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace CopilotChat.WebApi.Services.MemoryMigration; - -/// -/// Middleware action to handle memory migration 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); - - switch (migrationStatus) - { - case ChatMigrationStatus s when (s == ChatMigrationStatus.RequiresUpgrade): - // 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): - return true; // In maintenance - - case ChatMigrationStatus s when (s == ChatMigrationStatus.None): - default: - return false; // No maintenance - } - } -} diff --git a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs deleted file mode 100644 index c281a036a..000000000 --- a/webapi/Services/MemoryMigration/ChatMigrationMonitor.cs +++ /dev/null @@ -1,139 +0,0 @@ -// 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.Memory; - -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 processing. -/// If the desire exists to reset/re-execute migration, simply delete the new index. -/// -public class ChatMigrationMonitor : IChatMigrationMonitor -{ - internal const string MigrationCompletionToken = "DONE"; - internal const string MigrationKey = "migrate-00000000-0000-0000-0000-000000000000"; - - private static ChatMigrationStatus? _cachedStatus; - private static bool? _hasCurrentIndex; - - private readonly ILogger _logger; - private readonly string _indexNameGlobalDocs; - private readonly string _indexNameAllMemory; - private readonly ISemanticTextMemory _memory; - - /// - /// Initializes a new instance of the class. - /// - public ChatMigrationMonitor( - ILogger logger, - IOptions docOptions, - IOptions promptOptions, - SemanticKernelProvider provider) - { - this._logger = logger; - this._indexNameGlobalDocs = docOptions.Value.GlobalDocumentCollectionName; - this._indexNameAllMemory = promptOptions.Value.MemoryIndexName; - this._memory = provider.GetMigrationMemory(); - } - - /// - public async Task GetCurrentStatusAsync(CancellationToken cancellationToken = default) - { - if (_cachedStatus == null) - { - // Attempt to determine migration status looking at index existence. (Once) - Interlocked.CompareExchange( - ref _cachedStatus, - await QueryCollectionAsync(), - null); - - if (_cachedStatus == null) - { - // Attempt to determine migration status looking at index state. - _cachedStatus = await QueryStatusAsync(); - } - } - else - { - // Refresh status if we have a cached value for any state other than: ChatVersionStatus.None. - switch (_cachedStatus) - { - case ChatMigrationStatus s when s != ChatMigrationStatus.None: - _cachedStatus = await QueryStatusAsync(); - break; - - default: // ChatVersionStatus.None - break; - } - } - - return _cachedStatus ?? ChatMigrationStatus.None; - - // Reports and caches migration state as either: None or null depending on existence of the target index. - async Task QueryCollectionAsync() - { - if (_hasCurrentIndex == null) - { - try - { - // Cache "found" index state to reduce query count and avoid handling truth mutation. - var collections = await this._memory.GetCollectionsAsync(cancellationToken); - - // Does the new "target" index already exist? - _hasCurrentIndex = collections.Any(c => c.Equals(this._indexNameAllMemory, StringComparison.OrdinalIgnoreCase)); - - return (_hasCurrentIndex ?? false) ? ChatMigrationStatus.None : null; - } - catch (Exception exception) when (!exception.IsCriticalException()) - { - this._logger.LogError(exception, "Unable to search collections"); - } - } - - return (_hasCurrentIndex ?? false) ? ChatMigrationStatus.None : null; - } - - // Note: Only called once determined that target index does not exist. - async Task QueryStatusAsync() - { - try - { - var result = - await this._memory.GetAsync( - this._indexNameGlobalDocs, - MigrationKey, - withEmbedding: false, - cancellationToken); - - if (result == null) - { - // No migration token - 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/Services/MemoryMigration/ChatMigrationStatus.cs b/webapi/Services/MemoryMigration/ChatMigrationStatus.cs deleted file mode 100644 index d51dcd701..000000000 --- a/webapi/Services/MemoryMigration/ChatMigrationStatus.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -namespace CopilotChat.WebApi.Services.MemoryMigration; - -/// -/// Set of migration states/status for chat memory migration. -/// -/// -/// Interlocked.CompareExchange doesn't work with enums. -/// -public sealed class ChatMigrationStatus -{ - /// - /// 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 ChatMigrationStatus(string label) - { - this.Label = label; - } -} diff --git a/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs b/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs deleted file mode 100644 index 95355db7a..000000000 --- a/webapi/Services/MemoryMigration/IChatMemoryMigrationService.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; - -namespace CopilotChat.WebApi.Services.MemoryMigration; - -/// -/// Defines contract for migrating chat memory. -/// -public interface IChatMemoryMigrationService -{ - /// - /// Migrates all non-document memory to the kernel memory index. - /// Subsequent/redunant migration is non-destructive/no-impact to migrated index. - /// - Task MigrateAsync(CancellationToken cancellationToken = default); -} diff --git a/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs b/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs deleted file mode 100644 index 391f7558e..000000000 --- a/webapi/Services/MemoryMigration/IChatMigrationMonitor.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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 cancellationToken = default); -}