diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 13d308e1b..b9e2a84be 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -9,8 +9,18 @@ public class ChatHubConversationHook : ConversationHookBase private readonly IServiceProvider _services; private readonly IHubContext _chatHub; private readonly IUserIdentity _user; - private readonly BotSharpOptions _options; - public ChatHubConversationHook(IServiceProvider services, + private readonly BotSharpOptions _options; + + #region Event + private const string INIT_CLIENT_CONVERSATION = "OnConversationInitFromClient"; + private const string RECEIVE_CLIENT_MESSAGE = "OnMessageReceivedFromClient"; + private const string RECEIVE_ASSISTANT_MESSAGE = "OnMessageReceivedFromAssistant"; + private const string GENERATE_SENDER_ACTION = "OnSenderActionGenerated"; + private const string DELETE_MESSAGE = "OnMessageDeleted"; + #endregion + + public ChatHubConversationHook( + IServiceProvider services, IHubContext chatHub, BotSharpOptions options, IUserIdentity user) @@ -29,8 +39,7 @@ public override async Task OnConversationInitialized(Conversation conversation) var user = await userService.GetUser(conv.User.Id); conv.User = UserViewModel.FromUser(user); - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationInitFromClient", conv); - + await InitClientConversation(conv); await base.OnConversationInitialized(conversation); } @@ -41,35 +50,36 @@ public override async Task OnMessageReceived(RoleDialogModel message) var sender = await userService.GetMyProfile(); // Update console conversation UI for CSR - await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromClient", new ChatResponseModel() + + var model = new ChatResponseModel() { ConversationId = conv.ConversationId, MessageId = message.MessageId, Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content, Sender = UserViewModel.FromUser(sender) - }); + }; + await ReceiveClientMessage(model); // Send typing-on to client - await _chatHub.Clients.User(_user.Id).SendAsync("OnSenderActionGenerated", new ConversationSenderActionModel + var action = new ConversationSenderActionModel { ConversationId = conv.ConversationId, SenderAction = SenderActionEnum.TypingOn - }); - + }; + await GenerateSenderAction(action); await base.OnMessageReceived(message); } public override async Task OnFunctionExecuting(RoleDialogModel message) { var conv = _services.GetRequiredService(); - - await _chatHub.Clients.User(_user.Id).SendAsync("OnSenderActionGenerated", new ConversationSenderActionModel + var action = new ConversationSenderActionModel { ConversationId = conv.ConversationId, SenderAction = SenderActionEnum.TypingOn, Indication = message.Indication - }); - + }; + await GenerateSenderAction(action); await base.OnFunctionExecuting(message); } @@ -98,23 +108,52 @@ public override async Task OnResponseGenerated(RoleDialogModel message) }, _options.JsonSerializerOptions); // Send typing-off to client - await _chatHub.Clients.User(_user.Id).SendAsync("OnSenderActionGenerated", new ConversationSenderActionModel + var action = new ConversationSenderActionModel { ConversationId = conv.ConversationId, SenderAction = SenderActionEnum.TypingOff - }); - await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", json); + }; + await GenerateSenderAction(action); + await ReceiveAssistantMessage(json); await base.OnResponseGenerated(message); } public override async Task OnMessageDeleted(string conversationId, string messageId) { - await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageDeleted", new ChatResponseModel + var model = new ChatResponseModel { ConversationId = conversationId, MessageId = messageId - }); + }; + await DeleteMessage(model); await base.OnMessageDeleted(conversationId, messageId); } + + #region Private methods + private async Task InitClientConversation(ConversationViewModel conversation) + { + await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation); + } + + private async Task ReceiveClientMessage(ChatResponseModel model) + { + await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model); + } + + private async Task ReceiveAssistantMessage(string? json) + { + await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); + } + + private async Task GenerateSenderAction(ConversationSenderActionModel action) + { + await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action); + } + + private async Task DeleteMessage(ChatResponseModel model) + { + await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model); + } + #endregion } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 0d578f995..ec3eb8a95 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.Loggers.Enums; @@ -10,6 +9,9 @@ using Microsoft.AspNetCore.SignalR; using System.Text.Encodings.Web; using System.Text.Unicode; +using Newtonsoft.Json.Linq; +using JsonConvert = Newtonsoft.Json.JsonConvert; +using JsonSerializerSettings = Newtonsoft.Json.JsonSerializerSettings; namespace BotSharp.Plugin.ChatHub.Hooks; @@ -25,6 +27,13 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR private readonly IAgentService _agentService; private readonly IRoutingContext _routingCtx; + #region Event + private const string CONTENT_LOG_GENERATED = "OnConversationContentLogGenerated"; + private const string STATE_LOG_GENERATED = "OnConversateStateLogGenerated"; + private const string AGENT_QUEUE_CHANGED = "OnAgentQueueChanged"; + private const string STATE_CHANGED = "OnStateChangeGenerated"; + #endregion + public StreamingLogHook( ConversationSetting convSettings, BotSharpOptions options, @@ -60,7 +69,7 @@ public override async Task OnMessageReceived(RoleDialogModel message) Source = ContentLogSource.UserInput, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) @@ -78,7 +87,7 @@ public override async Task OnPostbackMessageReceived(RoleDialogModel message, Po Source = ContentLogSource.UserInput, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public async Task OnRenderingTemplate(Agent agent, string name, string content) @@ -100,7 +109,7 @@ public async Task OnRenderingTemplate(Agent agent, string name, string content) Source = ContentLogSource.HardRule, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public async Task BeforeGenerating(Agent agent, List conversations) @@ -117,7 +126,7 @@ public override async Task OnFunctionExecuting(RoleDialogModel message) var agent = await _agentService.LoadAgent(message.CurrentAgentId); message.FunctionArgs = message.FunctionArgs ?? "{}"; - var args = JsonSerializer.Serialize(JsonDocument.Parse(message.FunctionArgs), _options.JsonSerializerOptions); + var args = FormatJson(message.FunctionArgs); var log = $"{message.FunctionName} executing\r\n```json\r\n{args}\r\n```"; var input = new ContentLogInputModel(conversationId, message) @@ -127,7 +136,7 @@ public override async Task OnFunctionExecuting(RoleDialogModel message) Source = ContentLogSource.FunctionCall, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public override async Task OnFunctionExecuted(RoleDialogModel message) @@ -139,7 +148,6 @@ public override async Task OnFunctionExecuted(RoleDialogModel message) var agent = await _agentService.LoadAgent(message.CurrentAgentId); message.FunctionArgs = message.FunctionArgs ?? "{}"; - // var args = JsonSerializer.Serialize(JsonDocument.Parse(message.FunctionArgs), _options.JsonSerializerOptions); var log = $"{message.FunctionName} =>\r\n*{message.Content?.Trim()}*"; var input = new ContentLogInputModel(conversationId, message) @@ -149,7 +157,7 @@ public override async Task OnFunctionExecuted(RoleDialogModel message) Source = ContentLogSource.FunctionCall, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } /// @@ -176,7 +184,7 @@ public async Task AfterGenerated(RoleDialogModel message, TokenStatsModel tokenS Source = ContentLogSource.Prompt, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } /// @@ -190,7 +198,7 @@ public override async Task OnResponseGenerated(RoleDialogModel message) if (string.IsNullOrEmpty(conversationId)) return; var conv = _services.GetRequiredService(); - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversateStateLogGenerated", BuildStateLog(conv.ConversationId, _state.GetStates(), message)); + await SendStateLog(conv.ConversationId, _state.GetStates(), message); if (message.Role == AgentRole.Assistant) { @@ -209,7 +217,7 @@ public override async Task OnResponseGenerated(RoleDialogModel message) Source = ContentLogSource.AgentResponse, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } } @@ -227,7 +235,7 @@ public override async Task OnTaskCompleted(RoleDialogModel message) Source = ContentLogSource.FunctionCall, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public override async Task OnConversationEnding(RoleDialogModel message) @@ -244,7 +252,7 @@ public override async Task OnConversationEnding(RoleDialogModel message) Source = ContentLogSource.FunctionCall, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public override async Task OnBreakpointUpdated(string conversationId, bool resetStates) @@ -272,7 +280,7 @@ public override async Task OnBreakpointUpdated(string conversationId, bool reset }, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public override async Task OnStateChanged(StateChangeModel stateChange) @@ -282,7 +290,7 @@ public override async Task OnStateChanged(StateChangeModel stateChange) if (stateChange == null) return; - await _chatHub.Clients.User(_user.Id).SendAsync("OnStateChangeGenerated", BuildStateChangeLog(stateChange)); + await SendStateChange(stateChange); } #endregion @@ -296,7 +304,7 @@ public async Task OnAgentEnqueued(string agentId, string preAgentId, string? rea // Agent queue log var log = $"{agent.Name} is enqueued"; - await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + await SendAgentQueueLog(conversationId, log); // Content log log = $"{agent.Name} is enqueued{(reason != null ? $" ({reason})" : "")}"; @@ -311,7 +319,7 @@ public async Task OnAgentEnqueued(string agentId, string preAgentId, string? rea Source = ContentLogSource.HardRule, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public async Task OnAgentDequeued(string agentId, string currentAgentId, string? reason = null) @@ -324,7 +332,7 @@ public async Task OnAgentDequeued(string agentId, string currentAgentId, string? // Agent queue log var log = $"{agent.Name} is dequeued"; - await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + await SendAgentQueueLog(conversationId, log); // Content log log = $"{agent.Name} is dequeued{(reason != null ? $" ({reason})" : "")}, current agent is {currentAgent?.Name}"; @@ -339,7 +347,7 @@ public async Task OnAgentDequeued(string agentId, string currentAgentId, string? Source = ContentLogSource.HardRule, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public async Task OnAgentReplaced(string fromAgentId, string toAgentId, string? reason = null) @@ -352,7 +360,7 @@ public async Task OnAgentReplaced(string fromAgentId, string toAgentId, string? // Agent queue log var log = $"Agent queue is replaced from {fromAgent.Name} to {toAgent.Name}"; - await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + await SendAgentQueueLog(conversationId, log); // Content log log = $"{fromAgent.Name} is replaced to {toAgent.Name}{(reason != null ? $" ({reason})" : "")}"; @@ -367,7 +375,7 @@ public async Task OnAgentReplaced(string fromAgentId, string toAgentId, string? Source = ContentLogSource.HardRule, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public async Task OnAgentQueueEmptied(string agentId, string? reason = null) @@ -377,7 +385,7 @@ public async Task OnAgentQueueEmptied(string agentId, string? reason = null) // Agent queue log var log = $"Agent queue is empty"; - await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + await SendAgentQueueLog(conversationId, log); // Content log log = reason ?? "Agent queue is cleared"; @@ -392,7 +400,7 @@ public async Task OnAgentQueueEmptied(string agentId, string? reason = null) Source = ContentLogSource.HardRule, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public async Task OnRoutingInstructionReceived(FunctionCallFromLlm instruct, RoleDialogModel message) @@ -411,7 +419,7 @@ public async Task OnRoutingInstructionReceived(FunctionCallFromLlm instruct, Rol Source = ContentLogSource.AgentResponse, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } public async Task OnRoutingInstructionRevised(FunctionCallFromLlm instruct, RoleDialogModel message) @@ -429,11 +437,32 @@ public async Task OnRoutingInstructionRevised(FunctionCallFromLlm instruct, Role Source = ContentLogSource.HardRule, Log = log }; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + await SendContentLog(input); } #endregion + #region Private methods + private async Task SendContentLog(ContentLogInputModel input) + { + await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input)); + } + + private async Task SendStateLog(string conversationId, Dictionary states, RoleDialogModel message) + { + await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, states, message)); + } + + private async Task SendAgentQueueLog(string conversationId, string log) + { + await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log)); + } + + private async Task SendStateChange(StateChangeModel stateChange) + { + await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange)); + } + private string BuildContentLog(ContentLogInputModel input) { var output = new ContentLogOutputModel @@ -538,4 +567,40 @@ private JsonSerializerOptions InitLocalJsonOptions(BotSharpOptions options) return localOptions; } + + private string FormatJson(string? json) + { + var defaultJson = "{}"; + if (string.IsNullOrWhiteSpace(json)) + { + return defaultJson; + } + + try + { + var parsedJson = JObject.Parse(json); + foreach (var item in parsedJson) + { + try + { + var key = item.Key; + var value = parsedJson[key].ToString(); + var parsedValue = JObject.Parse(value); + parsedJson[key] = parsedValue; + } + catch { continue; } + } + + var jsonSettings = new JsonSerializerSettings + { + Formatting = Newtonsoft.Json.Formatting.Indented + }; + return JsonConvert.SerializeObject(parsedJson, jsonSettings) ?? defaultJson; + } + catch + { + return defaultJson; + } + } + #endregion } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index b79953897..1c9f74672 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -1,6 +1,3 @@ -using System.Web; -using System.Xml.Linq; - namespace BotSharp.Plugin.MongoStorage; public class MongoDbContext @@ -22,9 +19,29 @@ public MongoDbContext(BotSharpDatabaseSettings dbSettings) private string GetDatabaseName(string mongoDbConnectionString) { var databaseName = mongoDbConnectionString.Substring(mongoDbConnectionString.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase) + 1); - if (databaseName.Contains("?")) + + var symbol = "?"; + if (databaseName.Contains(symbol)) { - databaseName = databaseName.Substring(0, databaseName.IndexOf("?", StringComparison.InvariantCultureIgnoreCase)); + var markIdx = databaseName.IndexOf(symbol, StringComparison.InvariantCultureIgnoreCase); + var db = databaseName.Substring(0, markIdx); + if (!string.IsNullOrWhiteSpace(db)) + { + return db; + } + + var queryStr = databaseName.Substring(markIdx + 1); + var queries = queryStr.Split("&", StringSplitOptions.RemoveEmptyEntries).Select(x => new + { + Key = x.Split("=")[0], + Value = x.Split("=")[1] + }).ToList(); + + var source = queries.FirstOrDefault(x => x.Key.IsEqualTo(DB_NAME_INDEX)); + if (source != null) + { + databaseName = source.Value; + } } return databaseName; }