From e2abe6a5fdd945e2c44f94fe8ae38fbe9f231f35 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Sun, 28 Jan 2024 19:33:40 -0600 Subject: [PATCH] add truncate message --- .../Conversations/IConversationService.cs | 1 + .../Models/TruncateMessageRequest.cs | 6 ++ .../Models/MessageConfig.cs | 2 +- .../Repositories/IBotSharpRepository.cs | 1 + .../ConversationService.TruncateMessage.cs | 13 ++++ .../Repository/BotSharpDbContext.cs | 5 ++ .../FileRepository.Conversation.cs | 76 ++++++++++++++++++- .../Controllers/ConversationController.cs | 16 +++- .../MongoRepository.Conversation.cs | 38 ++++++++++ 9 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TruncateMessageRequest.cs create mode 100644 src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index d41a8831d..0e1d47874 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -13,6 +13,7 @@ public interface IConversationService Task UpdateConversationTitle(string id, string title); Task> GetLastConversations(); Task DeleteConversation(string id); + Task TruncateConversation(string conversationId, string messageId); /// /// Send message to LLM diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TruncateMessageRequest.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TruncateMessageRequest.cs new file mode 100644 index 000000000..088697cdb --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/TruncateMessageRequest.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Conversations.Models; + +public class TruncateMessageRequest +{ + public string? TruncateMessageId { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/MessageConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Models/MessageConfig.cs index 95d72492e..438cc0600 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/MessageConfig.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/MessageConfig.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Models; -public class MessageConfig +public class MessageConfig : TruncateMessageRequest { /// /// Completion Provider diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index cd800cf97..e74ad6a8e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -46,6 +46,7 @@ public interface IBotSharpRepository PagedItems GetConversations(ConversationFilter filter); void UpdateConversationTitle(string conversationId, string title); List GetLastConversations(); + bool TruncateConversation(string conversationId, string messageId); #endregion #region Execution Log diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs new file mode 100644 index 000000000..ca2719d3e --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs @@ -0,0 +1,13 @@ +using BotSharp.Abstraction.Repositories; + +namespace BotSharp.Core.Conversations.Services; + +public partial class ConversationService : IConversationService +{ + public async Task TruncateConversation(string conversationId, string messageId) + { + var db = _services.GetRequiredService(); + var isSaved = db.TruncateConversation(conversationId, messageId); + return await Task.FromResult(isSaved); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index c8b75e7ec..43e44c7a1 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -178,6 +178,11 @@ public void UpdateConversationStatus(string conversationId, string status) { throw new NotImplementedException(); } + + public bool TruncateConversation(string conversationId, string messageId) + { + throw new NotImplementedException(); + } #endregion diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 76122be68..8131bdd2f 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Repositories.Models; +using System.Globalization; using System.IO; namespace BotSharp.Core.Repository @@ -256,6 +257,35 @@ public List GetLastConversations() } + public bool TruncateConversation(string conversationId, string messageId) + { + if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) return false; + + var dialogs = new List(); + var convDir = FindConversationDirectory(conversationId); + if (string.IsNullOrEmpty(convDir)) return false; + + var dialogDir = Path.Combine(convDir, DIALOG_FILE); + dialogs = CollectDialogElements(dialogDir); + if (dialogs.IsNullOrEmpty()) return false; + + var foundIdx = dialogs.FindIndex(x => x.MetaData?.MessageId == messageId); + if (foundIdx < 0) return false; + + // Handle truncated dialogs + var isSaved = HandleTruncatedDialogs(dialogDir, dialogs, foundIdx); + if (!isSaved) return false; + + // Handle truncated states + var refTime = dialogs.ElementAt(foundIdx).MetaData.CreateTime; + var stateDir = Path.Combine(convDir, STATE_FILE); + var states = CollectConversationStates(stateDir); + isSaved = HandleTruncatedStates(stateDir, states, refTime); + + return isSaved; + } + + #region Private methods private string? FindConversationDirectory(string conversationId) { @@ -304,8 +334,9 @@ private List ParseDialogElements(List dialogs) foreach (var element in dialogs) { var meta = element.MetaData; + var createTime = meta.CreateTime.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.InvariantCulture); var source = meta.FunctionName ?? meta.SenderId; - var metaStr = $"{meta.CreateTime}|{meta.Role}|{meta.AgentId}|{meta.MessageId}|{source}"; + var metaStr = $"{createTime}|{meta.Role}|{meta.AgentId}|{meta.MessageId}|{source}"; dialogTexts.Add(metaStr); var content = $" - {element.Content}"; dialogTexts.Add(content); @@ -325,6 +356,49 @@ private List CollectConversationStates(string stateFile) states = JsonSerializer.Deserialize>(stateStr, _options); return states ?? new List(); } + + private bool HandleTruncatedDialogs(string dialogDir, List dialogs, int foundIdx) + { + var truncatedDialogs = dialogs.Where((x, idx) => idx < foundIdx).ToList(); + var isSaved = SaveTruncatedDialogs(dialogDir, truncatedDialogs); + return isSaved; + } + + private bool HandleTruncatedStates(string stateDir, List states, DateTime refTime) + { + var truncatedStates = new List(); + foreach (var state in states) + { + var values = state.Values.Where(x => x.UpdateTime < refTime).ToList(); + if (values.Count == 0) continue; + + state.Values = values; + truncatedStates.Add(state); + } + + var isSaved = SaveTruncatedStates(stateDir, truncatedStates); + return isSaved; + } + + private bool SaveTruncatedDialogs(string dialogDir, List dialogs) + { + if (string.IsNullOrEmpty(dialogDir) || dialogs == null) return false; + if (!File.Exists(dialogDir)) File.Create(dialogDir); + + var texts = ParseDialogElements(dialogs); + File.WriteAllLines(dialogDir, texts); + return true; + } + + private bool SaveTruncatedStates(string stateDir, List states) + { + if (string.IsNullOrEmpty(stateDir) || states == null) return false; + if (!File.Exists(stateDir)) File.Create(stateDir); + + var stateStr = JsonSerializer.Serialize(states, _options); + File.WriteAllText(stateDir, stateStr); + return true; + } #endregion } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index af9216213..ae65fde9b 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -114,14 +114,26 @@ public async Task DeleteConversation([FromRoute] string conversationId) return response; } + [HttpDelete("/conversation/{conversationId}/message/{messageId}")] + public async Task DeleteConversationMessage([FromRoute] string conversationId, [FromRoute] string messageId) + { + var conversationService = _services.GetRequiredService(); + var response = await conversationService.TruncateConversation(conversationId, messageId); + return response; + } + [HttpPost("/conversation/{agentId}/{conversationId}")] public async Task SendMessage([FromRoute] string agentId, [FromRoute] string conversationId, [FromBody] NewMessageModel input) { - var inputMsg = new RoleDialogModel(AgentRole.User, input.Text); - var conv = _services.GetRequiredService(); + if (!string.IsNullOrEmpty(input.TruncateMessageId)) + { + await conv.TruncateConversation(conversationId, input.TruncateMessageId); + } + + var inputMsg = new RoleDialogModel(AgentRole.User, input.Text); conv.SetConversationId(conversationId, input.States); conv.States.SetState("channel", input.Channel) .SetState("provider", input.Provider) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index 15a2406bf..0a1c2530e 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -263,4 +263,42 @@ public List GetLastConversations() UpdatedTime = c.UpdatedTime }).ToList(); } + + public bool TruncateConversation(string conversationId, string messageId) + { + if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) return false; + + var dialogFilter = Builders.Filter.Eq(x => x.ConversationId, conversationId); + var foundDialog = _dc.ConversationDialogs.Find(dialogFilter).FirstOrDefault(); + if (foundDialog == null || foundDialog.Dialogs.IsNullOrEmpty()) return false; + + var foundIdx = foundDialog.Dialogs.FindIndex(x => x.MetaData?.MessageId == messageId); + if (foundIdx < 0) return false; + + // Handle truncated dialogs + var truncatedDialogs = foundDialog.Dialogs.Where((x, idx) => idx < foundIdx).ToList(); + + // Handle truncated states + var refTime = foundDialog.Dialogs.ElementAt(foundIdx).MetaData.CreateTime; + var stateFilter = Builders.Filter.Eq(x => x.ConversationId, conversationId); + var foundStates = _dc.ConversationStates.Find(stateFilter).FirstOrDefault(); + if (foundStates == null || foundStates.States.IsNullOrEmpty()) return false; + + var truncatedStates = new List(); + foreach (var state in foundStates.States) + { + var values = state.Values.Where(x => x.UpdateTime < refTime).ToList(); + if (values.Count == 0) continue; + + state.Values = values; + truncatedStates.Add(state); + } + + // Save + foundDialog.Dialogs = truncatedDialogs; + foundStates.States = truncatedStates; + _dc.ConversationDialogs.ReplaceOne(dialogFilter, foundDialog); + _dc.ConversationStates.ReplaceOne(stateFilter, foundStates); + return true; + } }