From 5d4dcc3e1325510a7b9139576434b4f3fb9e05c0 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 14 Mar 2025 17:04:54 -0500 Subject: [PATCH] refine state loading --- .../Conversations/IConversationService.cs | 2 +- .../Filters/ConversationFilter.cs | 2 + .../Repositories/IBotSharpRepository.cs | 2 +- .../Services/ConversationService.cs | 7 +- .../Services/ConversationStateService.cs | 39 +++++++---- .../Repository/BotSharpDbContext.cs | 2 +- .../FileRepository.Conversation.cs | 38 +++++++--- .../Routing/RoutingService.InvokeFunction.cs | 69 +++++++++---------- .../Controllers/ConversationController.cs | 6 +- .../View/ConversationViewModel.cs | 5 +- .../Hooks/ChatHubConversationHook.cs | 2 + .../MongoRepository.Conversation.cs | 52 +++++++++----- 12 files changed, 139 insertions(+), 87 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index 15f7e89e..4eb44ae4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -9,7 +9,7 @@ public interface IConversationService string ConversationId { get; } Task NewConversation(Conversation conversation); void SetConversationId(string conversationId, List states, bool isReadOnly = false); - Task GetConversation(string id); + Task GetConversation(string id, bool isLoadStates = false); Task> GetConversations(ConversationFilter filter); Task UpdateConversationTitle(string id, string title); Task UpdateConversationTitleAlias(string id, string titleAlias); diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs index d05c49c6..e5eed361 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs @@ -27,6 +27,8 @@ public class ConversationFilter public List? Tags { get; set; } + public bool IsLoadLatestStates { get; set; } + public static ConversationFilter Empty() { return new ConversationFilter(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 332ca340..b6b58a6b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -124,7 +124,7 @@ public interface IBotSharpRepository : IHaveServiceProvider => throw new NotImplementedException(); void UpdateConversationStatus(string conversationId, string status) => throw new NotImplementedException(); - Conversation GetConversation(string conversationId) + Conversation GetConversation(string conversationId, bool isLoadStates = false) => throw new NotImplementedException(); PagedItems GetConversations(ConversationFilter filter) => throw new NotImplementedException(); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 9fc97a72..20e6a7fd 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -71,7 +71,7 @@ public partial class ConversationService : IConversationService return db.UpdateConversationMessage(conversationId, request); } - public async Task GetConversation(string id) + public async Task GetConversation(string id, bool isLoadStates = false) { var db = _services.GetRequiredService(); var conversation = db.GetConversation(id); @@ -80,6 +80,11 @@ public partial class ConversationService : IConversationService public async Task> GetConversations(ConversationFilter filter) { + if (filter == null) + { + filter = ConversationFilter.Empty(); + } + var db = _services.GetRequiredService(); var conversations = db.GetConversations(filter); return conversations; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 5d19e2d0..37c8c760 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -69,23 +69,34 @@ public class ConversationStateService : IConversationStateService return this; } + var defaultRound = -1; var preValue = string.Empty; var currentValue = value.ToString(); - var hooks = _services.GetServices(); - var curActiveRounds = activeRounds > 0 ? activeRounds : -1; - int? preActiveRounds = null; + var curActive = true; + StateKeyValue? pair = null; + StateValue? prevLeafNode = null; + var curActiveRounds = activeRounds > 0 ? activeRounds : defaultRound; - if (ContainsState(name) && _curStates.TryGetValue(name, out var pair)) + if (ContainsState(name) && _curStates.TryGetValue(name, out pair)) { - var leafNode = pair?.Values?.LastOrDefault(); - preActiveRounds = leafNode?.ActiveRounds; - preValue = leafNode?.Data ?? string.Empty; + prevLeafNode = pair?.Values?.LastOrDefault(); + preValue = prevLeafNode?.Data ?? string.Empty; } _logger.LogInformation($"[STATE] {name} = {value}"); var routingCtx = _services.GetRequiredService(); - if (!ContainsState(name) || preValue != currentValue || preActiveRounds != curActiveRounds) + var isNoChange = ContainsState(name) + && preValue == currentValue + && prevLeafNode?.ActiveRounds == curActiveRounds + && curActiveRounds == defaultRound + && prevLeafNode?.Source == source + && prevLeafNode?.DataType == valueType + && prevLeafNode?.Active == curActive + && pair?.Readonly == readOnly; + + var hooks = _services.GetServices(); + if (!ContainsState(name) || preValue != currentValue || prevLeafNode?.ActiveRounds != curActiveRounds) { foreach (var hook in hooks) { @@ -95,7 +106,7 @@ public class ConversationStateService : IConversationStateService MessageId = routingCtx.MessageId, Name = name, BeforeValue = preValue, - BeforeActiveRounds = preActiveRounds, + BeforeActiveRounds = prevLeafNode?.ActiveRounds, AfterValue = currentValue, AfterActiveRounds = curActiveRounds, DataType = valueType, @@ -116,7 +127,7 @@ public class ConversationStateService : IConversationStateService { Data = currentValue, MessageId = routingCtx.MessageId, - Active = true, + Active = curActive, ActiveRounds = curActiveRounds, DataType = valueType, Source = source, @@ -128,6 +139,10 @@ public class ConversationStateService : IConversationStateService newPair.Values = new List { newValue }; _curStates[name] = newPair; } + else if (isNoChange) + { + // do nothing + } else { _curStates[name].Values.Add(newValue); @@ -415,14 +430,14 @@ public class ConversationStateService : IConversationStateService { var values = _curStates.Values.ToList(); var copy = JsonSerializer.Deserialize>(JsonSerializer.Serialize(values)); - return new ConversationState(copy ?? new()); + return new ConversationState(copy ?? []); } public void SetCurrentState(ConversationState state) { var values = _curStates.Values.ToList(); var copy = JsonSerializer.Deserialize>(JsonSerializer.Serialize(values)); - _curStates = new ConversationState(copy ?? new()); + _curStates = new ConversationState(copy ?? []); } public void ResetCurrentState() diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 587de372..492dcde3 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -80,7 +80,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository public bool DeleteConversations(IEnumerable conversationIds) => throw new NotImplementedException(); - public Conversation GetConversation(string conversationId) + public Conversation GetConversation(string conversationId, bool isLoadStates = false) => throw new NotImplementedException(); public PagedItems GetConversations(ConversationFilter filter) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 647282a4..b8ff420e 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.Loggers.Models; using BotSharp.Abstraction.Users.Models; +using System; using System.IO; namespace BotSharp.Core.Repository; @@ -346,7 +347,7 @@ public partial class FileRepository } } - public Conversation GetConversation(string conversationId) + public Conversation GetConversation(string conversationId, bool isLoadStates = false) { var convDir = FindConversationDirectory(conversationId); if (string.IsNullOrEmpty(convDir)) return null; @@ -361,18 +362,20 @@ public partial class FileRepository record.Dialogs = CollectDialogElements(dialogFile); } - var stateFile = Path.Combine(convDir, STATE_FILE); - if (record != null) + if (isLoadStates) { - var states = CollectConversationStates(stateFile); - var curStates = new Dictionary(); - states.ForEach(x => + var latestStateFile = Path.Combine(convDir, CONV_LATEST_STATE_FILE); + if (record != null && File.Exists(latestStateFile)) { - curStates[x.Key] = x.Values?.LastOrDefault()?.Data ?? string.Empty; - }); - record.States = curStates; + var stateJson = File.ReadAllText(latestStateFile); + var states = JsonSerializer.Deserialize>(stateJson, _options) ?? []; + record.States = states.ToDictionary(x => x.Key, x => + { + var elem = x.Value.RootElement.GetProperty("data"); + return elem.ValueKind != JsonValueKind.Null ? elem.ToString() : null; + }); + } } - return record; } @@ -508,6 +511,21 @@ public partial class FileRepository if (!matched) continue; + if (filter.IsLoadLatestStates) + { + var latestStateFile = Path.Combine(d, CONV_LATEST_STATE_FILE); + if (File.Exists(latestStateFile)) + { + var stateJson = File.ReadAllText(latestStateFile); + var states = JsonSerializer.Deserialize>(stateJson, _options) ?? []; + record.States = states.ToDictionary(x => x.Key, x => + { + var elem = x.Value.RootElement.GetProperty("data"); + return elem.ValueKind != JsonValueKind.Null ? elem.ToString() : null; + }); + } + } + records.Add(record); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index aaa81694..ee403ff8 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Functions; -using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Templating; namespace BotSharp.Core.Routing; @@ -9,18 +8,20 @@ public partial class RoutingService public async Task InvokeFunction(string name, RoleDialogModel message) { var function = _services.GetServices().FirstOrDefault(x => x.Name == name); + + var isFillDummyContent = false; + var dummyFuncResponse = string.Empty; if (function == null) { - var executed = await InvokeDummyFunction(name, message); - if (executed) + dummyFuncResponse = await GetDummyFunctionOutput(name, message); + isFillDummyContent = !string.IsNullOrEmpty(dummyFuncResponse); + if (!isFillDummyContent) { - return true; + message.StopCompletion = true; + message.Content = $"Can't find function implementation of {name}."; + _logger.LogError(message.Content); + return false; } - - message.StopCompletion = true; - message.Content = $"Can't find function implementation of {name}."; - _logger.LogError(message.Content); - return false; } // Clone message @@ -34,7 +35,15 @@ public partial class RoutingService var progressService = _services.GetService(); // Before executing functions - clonedMessage.Indication = await function.GetIndication(message); + if (!isFillDummyContent) + { + clonedMessage.Indication = await function.GetIndication(message); + } + else + { + clonedMessage.Indication = "Running"; + } + if (progressService?.OnFunctionExecuting != null) { await progressService.OnFunctionExecuting(clonedMessage); @@ -49,7 +58,15 @@ public partial class RoutingService try { - result = await function.Execute(clonedMessage); + if (!isFillDummyContent) + { + result = await function.Execute(clonedMessage); + } + else + { + clonedMessage.Content = dummyFuncResponse; + result = true; + } // After functions have been executed foreach (var hook in hooks) @@ -97,11 +114,11 @@ public partial class RoutingService return result; } - private async Task InvokeDummyFunction(string functionName, RoleDialogModel message) + private async Task GetDummyFunctionOutput(string functionName, RoleDialogModel message) { if (string.IsNullOrEmpty(message.CurrentAgentId)) { - return false; + return null; } var agentService = _services.GetRequiredService(); @@ -109,25 +126,11 @@ public partial class RoutingService var found = agent?.Functions?.FirstOrDefault(x => x.Name == functionName); if (string.IsNullOrWhiteSpace(found?.Output)) { - return false; - } - - var clonedMessage = RoleDialogModel.From(message); - clonedMessage.FunctionName = functionName; - clonedMessage.Indication = "Running"; - - var hooks = _services - .GetRequiredService() - .HooksOrderByPriority; - - foreach (var hook in hooks) - { - await hook.OnFunctionExecuting(clonedMessage); + return null; } var render = _services.GetRequiredService(); var state = _services.GetRequiredService(); - var options = _services.GetRequiredService(); var dict = new Dictionary(); foreach (var item in state.GetStates()) @@ -136,14 +139,6 @@ public partial class RoutingService } var text = render.Render(found.Output, dict); - message.Content = text; - clonedMessage.Content = text; - - foreach (var hook in hooks) - { - await hook.OnFunctionExecuted(clonedMessage); - } - - return true; + return text; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index f032f504..dc0f2b0f 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -138,7 +138,7 @@ public class ConversationController : ControllerBase } [HttpGet("/conversation/{conversationId}")] - public async Task GetConversation([FromRoute] string conversationId) + public async Task GetConversation([FromRoute] string conversationId, [FromQuery] bool isLoadStates = false) { var service = _services.GetRequiredService(); var userService = _services.GetRequiredService(); @@ -151,7 +151,8 @@ public class ConversationController : ControllerBase var filter = new ConversationFilter { Id = conversationId, - UserId = !isAdmin ? user.Id : null + UserId = !isAdmin ? user.Id : null, + IsLoadLatestStates = isLoadStates }; var conversations = await service.GetConversations(filter); if (conversations.Items.IsNullOrEmpty()) @@ -161,7 +162,6 @@ public class ConversationController : ControllerBase var result = ConversationViewModel.FromSession(conversations.Items.First()); var state = _services.GetRequiredService(); - result.States = state.Load(conversationId, isReadOnly: true); user = await userService.GetUser(result.User.Id); result.User = UserViewModel.FromUser(user); diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs index a660a525..686974b9 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs @@ -31,7 +31,7 @@ public class ConversationViewModel public string? TaskId { get; set; } public string Status { get; set; } - public Dictionary States { get; set; } + public Dictionary States { get; set; } = []; public List Tags { get; set; } = new(); @@ -55,7 +55,8 @@ public class ConversationViewModel Channel = sess.Channel, Status = sess.Status, TaskId = sess.TaskId, - Tags = sess.Tags ?? new(), + Tags = sess.Tags ?? [], + States = sess.States ?? [], CreatedTime = sess.CreatedTime, UpdatedTime = sess.UpdatedTime }; diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 99b734e6..7d848eb0 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -106,6 +106,7 @@ public class ChatHubConversationHook : ConversationHookBase if (!AllowSendingMessage()) return; var conv = _services.GetRequiredService(); + var state = _services.GetRequiredService(); var json = JsonSerializer.Serialize(new ChatResponseModel() { ConversationId = conv.ConversationId, @@ -114,6 +115,7 @@ public class ChatHubConversationHook : ConversationHookBase Function = message.FunctionName, RichContent = message.SecondaryRichContent ?? message.RichContent, Data = message.Data, + States = state.GetStates(), Sender = new UserViewModel() { FirstName = "AI", diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index ebae075b..f0e89473 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -299,26 +299,25 @@ public partial class MongoRepository _dc.Conversations.UpdateOne(filter, update); } - public Conversation GetConversation(string conversationId) + public Conversation GetConversation(string conversationId, bool isLoadStates = false) { if (string.IsNullOrEmpty(conversationId)) return null; var filterConv = Builders.Filter.Eq(x => x.Id, conversationId); var filterDialog = Builders.Filter.Eq(x => x.ConversationId, conversationId); - var filterState = Builders.Filter.Eq(x => x.ConversationId, conversationId); var conv = _dc.Conversations.Find(filterConv).FirstOrDefault(); var dialog = _dc.ConversationDialogs.Find(filterDialog).FirstOrDefault(); - var states = _dc.ConversationStates.Find(filterState).FirstOrDefault(); if (conv == null) return null; var dialogElements = dialog?.Dialogs?.Select(x => DialogMongoElement.ToDomainElement(x))?.ToList() ?? new List(); - var curStates = new Dictionary(); - states.States.ForEach(x => + var curStates = conv.LatestStates?.ToDictionary(x => x.Key, x => { - curStates[x.Key] = x.Values?.LastOrDefault()?.Data ?? string.Empty; - }); + var jsonDoc = JsonDocument.Parse(x.Value.ToJson()); + var data = jsonDoc.RootElement.GetProperty("data"); + return data.ValueKind != JsonValueKind.Null ? data.ToString() : null; + }) ?? []; return new Conversation { @@ -456,19 +455,34 @@ public partial class MongoRepository var conversationDocs = _dc.Conversations.Find(filterDef).Sort(sortDef).Skip(pager.Offset).Limit(pager.Size).ToList(); var count = _dc.Conversations.CountDocuments(filterDef); - var conversations = conversationDocs.Select(x => new Conversation + var conversations = conversationDocs.Select(x => { - Id = x.Id.ToString(), - AgentId = x.AgentId.ToString(), - UserId = x.UserId.ToString(), - TaskId = x.TaskId, - Title = x.Title, - Channel = x.Channel, - Status = x.Status, - DialogCount = x.DialogCount, - Tags = x.Tags ?? new(), - CreatedTime = x.CreatedTime, - UpdatedTime = x.UpdatedTime + var states = new Dictionary(); + if (filter.IsLoadLatestStates) + { + states = x.LatestStates.ToDictionary(p => p.Key, p => + { + var jsonDoc = JsonDocument.Parse(p.Value.ToJson()); + var data = jsonDoc.RootElement.GetProperty("data"); + return data.ValueKind != JsonValueKind.Null ? data.ToString() : null; + }); + } + + return new Conversation + { + Id = x.Id.ToString(), + AgentId = x.AgentId.ToString(), + UserId = x.UserId.ToString(), + TaskId = x.TaskId, + Title = x.Title, + Channel = x.Channel, + Status = x.Status, + DialogCount = x.DialogCount, + Tags = x.Tags ?? [], + States = states, + CreatedTime = x.CreatedTime, + UpdatedTime = x.UpdatedTime + }; }).ToList(); return new PagedItems