diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs index ce5d03b6..30a58bb4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs @@ -4,5 +4,6 @@ namespace BotSharp.Abstraction.Browsing; public interface IWebDriverHook { - Task> GetUploadFiles(MessageInfo message); + Task> GetUploadFiles(MessageInfo message) => Task.FromResult(new List()); + Task OnLocateElement(MessageInfo message, string content) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs index a8709810..074be848 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs @@ -8,20 +8,24 @@ public class MessageState [JsonPropertyName("active_rounds")] public int ActiveRounds { get; set; } = -1; + [JsonPropertyName("global")] + public bool Global { get; set; } + public MessageState() { } - public MessageState(string key, object value, int activeRounds = -1) + public MessageState(string key, object value, int activeRounds = -1, bool isGlobal = false) { Key = key; Value = value; ActiveRounds = activeRounds; + Global = isGlobal; } public override string ToString() { - return $"Key: {Key} => Value: {Value}, ActiveRounds: {ActiveRounds}"; + return $"Key: {Key} => Value: {Value}, ActiveRounds: {ActiveRounds}, Global: {Global}"; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHub.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHub.cs index cad64038..9d64b678 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHub.cs @@ -13,5 +13,5 @@ public interface IRealtimeHub IRealTimeCompletion Completer { get; } - Task ConnectToModel(Func? responseToUser = null, Func? init = null); + Task ConnectToModel(Func? responseToUser = null, Func? init = null, List? initStates = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs index 9ba98c5d..b94876c3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs @@ -10,6 +10,7 @@ public class ConversationFilter public string? Title { get; set; } public string? TitleAlias { get; set; } public string? AgentId { get; set; } + public List? AgentIds { get; set; } public string? Status { get; set; } public string? Channel { get; set; } public string? ChannelId { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs index 833cb013..4426d71f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs @@ -79,7 +79,7 @@ public class SideCarAttribute : AsyncMoAttribute object? res = null; var isHandled = false; - var enabled = instance != null && instance.IsEnabled() && method != null; + var enabled = instance != null && instance.IsEnabled && method != null; if (!enabled) { return (isHandled, value); @@ -112,7 +112,7 @@ public class SideCarAttribute : AsyncMoAttribute object? value = null; var isHandled = false; - var enabled = instance != null && instance.IsEnabled() && method != null; + var enabled = instance != null && instance.IsEnabled && method != null; if (!enabled) { return (isHandled, value); diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs index 28162a7e..81b0cf9c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs @@ -1,15 +1,20 @@ +using BotSharp.Abstraction.SideCar.Models; + namespace BotSharp.Abstraction.SideCar; public interface IConversationSideCar { string Provider { get; } + bool IsEnabled { get; } - bool IsEnabled(); void AppendConversationDialogs(string conversationId, List messages); List GetConversationDialogs(string conversationId); void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint); ConversationBreakpoint? GetConversationBreakpoint(string conversationId); void UpdateConversationStates(string conversationId, List states); Task SendMessage(string agentId, string text, - PostbackMessageModel? postback = null, List? states = null, List? dialogs = null); + PostbackMessageModel? postback = null, + List? states = null, + List? dialogs = null, + SideCarOptions? options = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs new file mode 100644 index 00000000..f221cab2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs @@ -0,0 +1,21 @@ +namespace BotSharp.Abstraction.SideCar.Models; + +public class SideCarOptions +{ + public bool IsInheritStates { get; set; } + public IEnumerable? InheritStateKeys { get; set; } + + public static SideCarOptions Empty() + { + return new(); + } + + public static SideCarOptions InheritStates(IEnumerable? targetStates = null) + { + return new() + { + IsInheritStates = true, + InheritStateKeys = targetStates + }; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs index baac131c..32b0a112 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Hooks; +using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Options; using BotSharp.Core.Infrastructures; @@ -22,10 +23,10 @@ public class RealtimeHub : IRealtimeHub _logger = logger; } - public async Task ConnectToModel(Func? responseToUser = null, Func? init = null) + public async Task ConnectToModel(Func? responseToUser = null, Func? init = null, List? initStates = null) { var convService = _services.GetRequiredService(); - convService.SetConversationId(_conn.ConversationId, []); + convService.SetConversationId(_conn.ConversationId, initStates ?? []); var conversation = await convService.GetConversation(_conn.ConversationId); var routing = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs index a948e5e2..d636fa71 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs +++ b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs @@ -24,11 +24,13 @@ public class BotSharpConversationSideCar : IConversationSideCar private readonly ILogger _logger; private Stack _contextStack = new(); + private SideCarOptions? _sideCarOptions; private bool _enabled = false; private string _conversationId = string.Empty; public string Provider => "botsharp"; + public bool IsEnabled => _enabled; public BotSharpConversationSideCar( IServiceProvider services, @@ -38,11 +40,6 @@ public class BotSharpConversationSideCar : IConversationSideCar _logger = logger; } - public bool IsEnabled() - { - return _enabled; - } - public void AppendConversationDialogs(string conversationId, List messages) { if (!IsValid(conversationId)) @@ -97,12 +94,22 @@ public class BotSharpConversationSideCar : IConversationSideCar top.State = new ConversationState(states); } - public async Task SendMessage(string agentId, string text, - PostbackMessageModel? postback = null, List? states = null, List? dialogs = null) + public async Task SendMessage( + string agentId, + string text, + PostbackMessageModel? postback = null, + List? states = null, + List? dialogs = null, + SideCarOptions? options = null) { + _sideCarOptions = options; + _logger.LogInformation($"Entering side car conversation..."); + BeforeExecute(dialogs); var response = await InnerExecute(agentId, text, postback, states); AfterExecute(); + + _logger.LogInformation($"Existing side car conversation..."); return response; } @@ -160,13 +167,11 @@ public class BotSharpConversationSideCar : IConversationSideCar private void AfterExecute() { - var state = _services.GetRequiredService(); var routing = _services.GetRequiredService(); - var node = _contextStack.Pop(); // Recover - state.SetCurrentState(node.State); + RestoreStates(node.State); routing.Context.SetRecursiveCounter(node.RecursiveCounter); routing.Context.SetAgentStack(node.RoutingStack); routing.Context.SetDialogs(node.RoutingDialogs); @@ -181,4 +186,43 @@ public class BotSharpConversationSideCar : IConversationSideCar && !string.IsNullOrEmpty(conversationId) && !string.IsNullOrEmpty(_conversationId); } + + private void RestoreStates(ConversationState prevStates) + { + var innerStates = prevStates; + var state = _services.GetRequiredService(); + + if (_sideCarOptions?.IsInheritStates == true) + { + var curStates = state.GetCurrentState(); + foreach (var pair in curStates) + { + var endNode = pair.Value.Values.LastOrDefault(); + if (endNode == null) continue; + + if (_sideCarOptions?.InheritStateKeys?.Any() == true + && !_sideCarOptions.InheritStateKeys.Contains(pair.Key)) + { + continue; + } + + if (innerStates.ContainsKey(pair.Key)) + { + innerStates[pair.Key].Values.Add(endNode); + } + else + { + innerStates[pair.Key] = new StateKeyValue + { + Key = pair.Key, + Versioning = pair.Value.Versioning, + Readonly = pair.Value.Readonly, + Values = [endNode] + }; + } + } + } + + state.SetCurrentState(innerStates); + } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Using.cs b/src/Infrastructure/BotSharp.Core.SideCar/Using.cs index d047ee15..e391d790 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/Using.cs +++ b/src/Infrastructure/BotSharp.Core.SideCar/Using.cs @@ -16,5 +16,6 @@ global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Routing; global using BotSharp.Abstraction.SideCar; +global using BotSharp.Abstraction.SideCar.Models; global using BotSharp.Abstraction.Utilities; -global using BotSharp.Core.SideCar.Settings; \ No newline at end of file +global using BotSharp.Core.SideCar.Settings; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 6eaeb167..676ea362 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -178,7 +178,7 @@ public partial class ConversationService : IConversationService { _conversationId = conversationId; _state.Load(_conversationId, isReadOnly); - states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, isNeedVersion: !x.Global, source: StateSource.External)); } public async Task GetConversationRecordOrCreateNew(string agentId) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index ff61fde2..4c8178d8 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -159,7 +159,7 @@ public class ConversationStateService : IConversationStateService Reset(); var endNodes = new Dictionary(); - if (_sidecar?.IsEnabled() == true) + if (_sidecar?.IsEnabled == true) { return endNodes; } @@ -234,7 +234,7 @@ public class ConversationStateService : IConversationStateService public void Save() { - if (_conversationId == null || _sidecar?.IsEnabled() == true) + if (_conversationId == null || _sidecar?.IsEnabled == true) { return; } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 3d03e816..f772acb2 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -396,6 +396,12 @@ public partial class FileRepository Directory.CreateDirectory(dir); } + if (filter?.AgentId != null) + { + filter.AgentIds ??= []; + filter.AgentIds.Add(filter.AgentId); + } + var totalDirs = Directory.GetDirectories(dir); foreach (var d in totalDirs) { @@ -419,9 +425,9 @@ public partial class FileRepository { matched = matched && record.TitleAlias.Contains(filter.TitleAlias); } - if (filter?.AgentId != null) + if (filter?.AgentIds != null && filter.AgentIds.Any()) { - matched = matched && record.AgentId == filter.AgentId; + matched = matched && filter.AgentIds.Contains(record.AgentId); } if (filter?.Status != null) { diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs index 97308f69..02cafbd0 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs @@ -1,9 +1,6 @@ using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Statistics.Enums; -using BotSharp.Abstraction.Statistics.Models; -using BotSharp.Abstraction.Statistics.Services; using BotSharp.Abstraction.Users; namespace BotSharp.Logger.Hooks; @@ -25,7 +22,10 @@ public class RateLimitConversationHook : ConversationHookBase public override async Task OnMessageReceived(RoleDialogModel message) { var settings = _services.GetRequiredService(); + var states = _services.GetRequiredService(); + var rateLimit = settings.RateLimit; + var channel = states.GetState("channel"); // Check max input length var charCount = message.Content.Length; @@ -45,7 +45,7 @@ public class RateLimitConversationHook : ConversationHookBase var userSents = Dialogs.Where(x => x.Role == AgentRole.User) .TakeLast(2).ToList(); - if (userSents.Count > 1) + if (channel != ConversationChannel.Phone && userSents.Count > 1) { var seconds = (DateTime.UtcNow - userSents.First().CreatedAt).TotalSeconds; if (seconds < rateLimit.MinTimeSecondsBetweenMessages) @@ -56,9 +56,6 @@ public class RateLimitConversationHook : ConversationHookBase } } - var states = _services.GetRequiredService(); - var channel = states.GetState("channel"); - // Check the number of conversations if (channel != ConversationChannel.Phone && channel != ConversationChannel.Email && channel != ConversationChannel.Database) { diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs index cba35a15..138d5084 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Realtime.Models.Session; using BotSharp.Core.Session; using Microsoft.AspNetCore.Http; @@ -62,9 +63,11 @@ public class ChatStreamMiddleware var hub = services.GetRequiredService(); var conn = hub.SetHubConnection(conversationId); conn.CurrentAgentId = agentId; + InitEvents(conn); // load conversation and state var convService = services.GetRequiredService(); + var state = services.GetRequiredService(); convService.SetConversationId(conversationId, []); await convService.GetConversationRecordOrCreateNew(agentId); @@ -79,7 +82,8 @@ public class ChatStreamMiddleware var (eventType, data) = MapEvents(conn, receivedText); if (eventType == "start") { - await ConnectToModel(hub, webSocket); + var request = InitRequest(data); + await ConnectToModel(hub, webSocket, request?.States); } else if (eventType == "media") { @@ -95,25 +99,26 @@ public class ChatStreamMiddleware } } + convService.SaveStates(); await _session.DisconnectAsync(); _session.Dispose(); } - private async Task ConnectToModel(IRealtimeHub hub, WebSocket webSocket) + private async Task ConnectToModel(IRealtimeHub hub, WebSocket webSocket, List? states = null) { - await hub.ConnectToModel(async data => + await hub.ConnectToModel(responseToUser: async data => { if (_session != null) { await _session.SendEventAsync(data); } - }); + }, initStates: states); } private (string, string) MapEvents(RealtimeHubConnection conn, string receivedText) { var response = JsonSerializer.Deserialize(receivedText); - string data = string.Empty; + var data = response?.Body?.Payload ?? string.Empty; switch (response.Event) { @@ -121,13 +126,16 @@ public class ChatStreamMiddleware conn.ResetStreamState(); break; case "media": - var mediaResponse = JsonSerializer.Deserialize(receivedText); - data = mediaResponse?.Body?.Payload ?? string.Empty; break; case "disconnect": break; } + return (response.Event, data); + } + + private void InitEvents(RealtimeHubConnection conn) + { conn.OnModelMessageReceived = message => JsonSerializer.Serialize(new { @@ -147,7 +155,17 @@ public class ChatStreamMiddleware { @event = "clear" }); + } - return (response.Event, data); + private ChatStreamRequest? InitRequest(string data) + { + try + { + return JsonSerializer.Deserialize(data, BotSharpOptions.defaultJsonOptions); + } + catch + { + return null; + } } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index a41abe87..3c7a675d 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -179,7 +179,7 @@ public class ChatHubConversationHook : ConversationHookBase private bool AllowSendingMessage() { var sidecar = _services.GetService(); - return sidecar == null || !sidecar.IsEnabled(); + return sidecar == null || !sidecar.IsEnabled; } private async Task InitClientConversation(string conversationId, ConversationDto conversation) diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamEventResponse.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamEventResponse.cs index aee742df..022ebdf1 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamEventResponse.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamEventResponse.cs @@ -6,15 +6,12 @@ internal class ChatStreamEventResponse { [JsonPropertyName("event")] public string Event { get; set; } -} -internal class ChatStreamMediaEventResponse : ChatStreamEventResponse -{ [JsonPropertyName("body")] - public MediaEventResponseBody Body { get; set; } + public ChatStreamEventResponseBody Body { get; set; } } -internal class MediaEventResponseBody +internal class ChatStreamEventResponseBody { [JsonPropertyName("payload")] public string Payload { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamRequest.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamRequest.cs new file mode 100644 index 00000000..1a72d7dc --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamRequest.cs @@ -0,0 +1,10 @@ +using BotSharp.Abstraction.Models; +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.ChatHub.Models.Stream; + +public class ChatStreamRequest +{ + [JsonPropertyName("states")] + public List States { get; set; } = []; +} diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index 42ce1ac9..eb14ac99 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -270,7 +270,7 @@ public class ChatCompletionProvider : IChatCompletion { messages.Add(new AssistantChatMessage(new List { - ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) })); messages.Add(new ToolChatMessage(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.Content)); diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs index 2e95cfa2..6838c284 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -329,17 +329,13 @@ public class GoogleRealTimeProvider : IRealTimeCompletion var words = new List(); HookEmitter.Emit(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent)), agent.Id); - var functions = request.Tools?.SelectMany(s => s.FunctionDeclarations).Select(x => + var functions = request.Tools?.SelectMany(s => s.FunctionDeclarations).Select(x => new FunctionDef { - var fn = new FunctionDef - { - Name = x.Name ?? string.Empty, - Description = x.Description ?? string.Empty, - Parameters = x.Parameters != null + Name = x.Name ?? string.Empty, + Description = x.Description ?? string.Empty, + Parameters = x.Parameters != null ? JsonSerializer.Deserialize(JsonSerializer.Serialize(x.Parameters)) : null - }; - return fn; }).ToArray(); await HookEmitter.Emit(_services, diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index edcf3a5f..d0da6fbf 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -346,6 +346,12 @@ public partial class MongoRepository var convBuilder = Builders.Filter; var convFilters = new List>() { convBuilder.Empty }; + if (filter?.AgentId != null) + { + filter.AgentIds ??= []; + filter.AgentIds.Add(filter.AgentId); + } + // Filter conversations if (!string.IsNullOrEmpty(filter?.Id)) { @@ -359,9 +365,9 @@ public partial class MongoRepository { convFilters.Add(convBuilder.Regex(x => x.Title, new BsonRegularExpression(filter.TitleAlias, "i"))); } - if (!string.IsNullOrEmpty(filter?.AgentId)) + if (filter?.AgentIds != null && filter.AgentIds.Any()) { - convFilters.Add(convBuilder.Eq(x => x.AgentId, filter.AgentId)); + convFilters.Add(convBuilder.In(x => x.AgentId, filter.AgentIds)); } if (!string.IsNullOrEmpty(filter?.Status)) { diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 8219bf1c..3a0902d6 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -326,15 +326,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion var (prompt, messages, options) = PrepareOptions(agent, []); var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent?.Description ?? string.Empty; - var functions = options.Tools.Select(x => + var functions = options.Tools.Select(x => new FunctionDef { - var fn = new FunctionDef - { - Name = x.FunctionName, - Description = x.FunctionDescription - }; - fn.Parameters = JsonSerializer.Deserialize(x.FunctionParameters); - return fn; + Name = x.FunctionName, + Description = x.FunctionDescription, + Parameters = JsonSerializer.Deserialize(x.FunctionParameters) }).ToArray(); var realtimeModelSettings = _services.GetRequiredService(); @@ -615,10 +611,10 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { messages.Add(new AssistantChatMessage(new List { - ChatToolCall.CreateFunctionToolCall(message.ToolCallId, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) })); - messages.Add(new ToolChatMessage(message.ToolCallId, message.Content)); + messages.Add(new ToolChatMessage(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.Content)); } else if (message.Role == AgentRole.User) { diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs index f571fa6c..c2f0682b 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs @@ -33,7 +33,7 @@ public class ExecuteQueryFn : IFunctionCallback var results = dbType.ToLower() switch { "mysql" => RunQueryInMySql(args.SqlStatements), - "sqlserver" => RunQueryInSqlServer(args.SqlStatements), + "sqlserver" or "mssql" => RunQueryInSqlServer(args.SqlStatements), "redshift" => RunQueryInRedshift(args.SqlStatements), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs index 283620c9..25ef526a 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs @@ -37,7 +37,7 @@ public class GetTableDefinitionFn : IFunctionCallback var tableDdls = dbType switch { "mysql" => GetDdlFromMySql(tables), - "sqlserver" => GetDdlFromSqlServer(tables), + "sqlserver" or "mssql" => GetDdlFromSqlServer(tables), "redshift" => GetDdlFromRedshift(tables), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs index eb9253e4..3e693f80 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs @@ -32,7 +32,7 @@ public class SqlSelect : IFunctionCallback var result = dbType switch { "mysql" => RunQueryInMySql(args), - "sqlserver" => RunQueryInSqlServer(args), + "sqlserver" or "mssql" => RunQueryInSqlServer(args), "redshift" => RunQueryInRedshift(args), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs index 89b38f93..1489c4a8 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs @@ -34,7 +34,7 @@ public class SqlValidateFn : IFunctionCallback var validateSql = dbType.ToLower() switch { "mysql" => $"EXPLAIN\r\n{sql.Replace("SET ", "-- SET ", StringComparison.InvariantCultureIgnoreCase).Replace(";", "; EXPLAIN ").TrimEnd("EXPLAIN ".ToCharArray())}", - "sqlserver" => $"SET PARSEONLY ON;\r\n{sql}\r\nSET PARSEONLY OFF;", + "sqlserver" or "mssql" => $"SET PARSEONLY ON;\r\n{sql}\r\nSET PARSEONLY OFF;", "redshift" => $"explain\r\n{sql}", _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs index 20f35572..43005c68 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs @@ -31,7 +31,7 @@ public class GetTableDefinitionFn : IFunctionCallback var tableDdls = dbType switch { "mysql" => GetDdlFromMySql(tables), - "sqlserver" => GetDdlFromSqlServer(tables), + "sqlserver" or "mssql" => GetDdlFromSqlServer(tables), "redshift" => GetDdlFromRedshift(tables,schema), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs index 1ae27ac7..72bef238 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs @@ -30,7 +30,7 @@ public class SqlSelect : IFunctionCallback var result = dbType switch { "mysql" => RunQueryInMySql(args), - "sqlserver" => RunQueryInSqlServer(args), + "sqlserver" or "mssql" => RunQueryInSqlServer(args), "redshift" => RunQueryInRedshift(args), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs index 511abb45..b5dbfb2e 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs @@ -53,20 +53,29 @@ public class TwilioInboundController : TwilioController instruction.SpeechPaths.Add(request.InitAudioFile); } + // Before creating session await HookEmitter.Emit(_services, async hook => { await hook.OnSessionCreating(request, instruction); }, request.AgentId); + var (agent, conversationId) = await InitConversation(request); request.ConversationId = conversationId.Id; instruction.AgentId = request.AgentId; instruction.ConversationId = request.ConversationId; + + // After creating session + await HookEmitter.Emit(_services, async hook => + { + await hook.OnSessionCreated(request); + }, request.AgentId); + + if (twilio.MachineDetected(request)) { response = new VoiceResponse(); - await HookEmitter.Emit(_services, async hook => await hook.OnVoicemailStarting(request), request.AgentId); @@ -116,11 +125,6 @@ public class TwilioInboundController : TwilioController }); } - await HookEmitter.Emit(_services, async hook => - { - await hook.OnSessionCreated(request); - }, request.AgentId); - return TwiML(response); } @@ -162,16 +166,16 @@ public class TwilioInboundController : TwilioController var states = new List { - new("channel", ConversationChannel.Phone), - new("calling_phone", request.From), - new("phone_direction", request.Direction), - new("twilio_call_sid", request.CallSid), + new("channel", ConversationChannel.Phone, isGlobal: true), + new("calling_phone", request.From, isGlobal: true), + new("phone_direction", request.Direction, isGlobal: true), + new("twilio_call_sid", request.CallSid, isGlobal: true), }; if (request.Direction == "inbound") { - states.Add(new MessageState("calling_phone_from", request.From)); - states.Add(new MessageState("calling_phone_to", request.To)); + states.Add(new MessageState("calling_phone_from", request.From, isGlobal: true)); + states.Add(new MessageState("calling_phone_to", request.To, isGlobal: true)); } var requestStates = ParseStates(request.States); @@ -204,7 +208,7 @@ public class TwilioInboundController : TwilioController storage.Append(conversation.Id, new RoleDialogModel(AgentRole.User, request.Intent) { - CurrentAgentId = conversation.Id, + CurrentAgentId = agent.Id, CreatedAt = DateTime.UtcNow }); } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 8d34dd22..3c5532bd 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -9,6 +9,7 @@ using BotSharp.Plugin.Twilio.Interfaces; using BotSharp.Plugin.Twilio.Models; using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; using Twilio.Rest.Api.V2010.Account; +using Twilio.TwiML.Messaging; using Twilio.Types; using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation; using Task = System.Threading.Tasks.Task; @@ -176,7 +177,7 @@ public class OutboundPhoneCallFn : IFunctionCallback }); var utcNow = DateTime.UtcNow; - var excludStates = new List + var excludeStates = new List { "provider", "model", @@ -185,22 +186,34 @@ public class OutboundPhoneCallFn : IFunctionCallback "llm_total_cost" }; - var curStates = state.GetStates().Select(x => new MessageState(x.Key, x.Value)).ToList(); + var curConvStates = state.GetStates().Select(x => new MessageState(x.Key, x.Value)).ToList(); var subConvStates = new List { - new(StateConst.ORIGIN_CONVERSATION_ID, originConversationId), - new("channel", "phone"), - new("phone_from", call.From), - new("phone_direction", call.Direction), - new("phone_number", call.To), - new("twilio_call_sid", call.Sid) + new(StateConst.ORIGIN_CONVERSATION_ID, originConversationId, isGlobal: true), + new("channel", "phone", isGlobal: true), + new("phone_from", call.From, isGlobal: true), + new("phone_direction", call.Direction, isGlobal: true), + new("phone_number", call.To, isGlobal: true), + new("twilio_call_sid", call.Sid, isGlobal: true) }; var subStateKeys = subConvStates.Select(x => x.Key).ToList(); - var included = curStates.Where(x => !subStateKeys.Contains(x.Key) && !excludStates.Contains(x.Key)); - var newStates = subConvStates.Concat(included).Select(x => new StateKeyValue + var included = curConvStates.Where(x => !subStateKeys.Contains(x.Key) && !excludeStates.Contains(x.Key)); + + var mappedCurConvStates = MapStates(included, messageId, utcNow); + var mappedSubConvStates = MapStates(subConvStates, messageId, utcNow); + var allStates = mappedCurConvStates.Concat(mappedSubConvStates).ToList(); + + db.UpdateConversationStates(newConversationId, allStates); + } + + private IEnumerable MapStates(IEnumerable states, string messageId, DateTime updateTime) + { + if (states.IsNullOrEmpty()) return []; + + return states.Select(x => new StateKeyValue { Key = x.Key, - Versioning = true, + Versioning = !x.Global, Values = [ new StateValue { @@ -209,11 +222,9 @@ public class OutboundPhoneCallFn : IFunctionCallback Active = true, ActiveRounds = x.ActiveRounds, Source = StateSource.Application, - UpdateTime = utcNow + UpdateTime = updateTime } ] }).ToList(); - - db.UpdateConversationStates(newConversationId, newStates); } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs index e70d7f27..b4b2eb2f 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs @@ -114,6 +114,11 @@ public partial class PlaywrightWebDriver // fix if html has & result.Body = HttpUtility.HtmlDecode(html); result.IsSuccess = true; + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + await hook.OnLocateElement(message, result.Body); + } } else if (count > 1) { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs index ff898232..6c7bfefa 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs @@ -28,7 +28,7 @@ public class UtilWebCloseBrowserFn : IFunctionCallback ContextId = webDriverService.GetMessageContext(message) }; - await browser.CloseBrowser(message.CurrentAgentId); + await browser.CloseBrowser(msg.ContextId); message.Content = $"Browser closed."; diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs index d30048c8..e2e3d34f 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs @@ -30,6 +30,7 @@ public class UtilWebLocateElementFn : IFunctionCallback MessageId = message.MessageId, ContextId = webDriverService.GetMessageContext(message) }; + browser.SetServiceProvider(_services); var result = await browser.LocateElement(msg, locatorArgs); message.Content = $"Locating element {(result.IsSuccess ? "success" : "failed")}. ";