diff --git a/Directory.Packages.props b/Directory.Packages.props index a031cec3..42521480 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,24 +1,24 @@ 8.0.0 + 2.3.0 - - - - - - - + + + + + + + - + - - - + + @@ -33,6 +33,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs index 07258402..ac1efb04 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs @@ -58,17 +58,13 @@ public class DialogElement [JsonPropertyName("payload")] public string? Payload { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("data")] - public object? Data { get; set; } - public DialogElement() { } public DialogElement(DialogMetaData meta, string content, string? richContent = null, - string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null, object? data = null) + string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null) { MetaData = meta; Content = content; @@ -76,7 +72,6 @@ public class DialogElement SecondaryContent = secondaryContent; SecondaryRichContent = secondaryRichContent; Payload = payload; - Data = data; } public override string ToString() diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs index 466893d3..9db7c440 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs @@ -8,8 +8,12 @@ public class StateConst public const string NEXT_ACTION_REASON = "next_action_reason"; public const string USER_GOAL_AGENT = "user_goal_agent"; public const string AGENT_REDIRECTION_REASON = "agent_redirection_reason"; + // lazy or eager + public const string ROUTING_MODE = "routing_mode"; + public const string LAZY_ROUTING_AGENT_ID = "lazy_routing_agent_id"; public const string LANGUAGE = "language"; public const string SUB_CONVERSATION_ID = "sub_conversation_id"; + public const string ORIGIN_CONVERSATION_ID = "origin_conversation_id"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs index 9d0662db..c4f63450 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Functions.Models; + namespace BotSharp.Abstraction.Loggers; /// @@ -37,4 +39,13 @@ public interface IContentGeneratingHook /// /// Task OnRenderingTemplate(Agent agent, string name, string content) => Task.CompletedTask; + + /// + /// Realtime session updated + /// + /// + /// + /// + /// + Task OnSessionUpdated(Agent agent, string instruction, FunctionDef[] functions) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs index 8e1d11d5..be6f8821 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IRealTimeCompletion.cs @@ -24,9 +24,11 @@ public interface IRealTimeCompletion Task Disconnect(); Task CreateSession(Agent agent, List conversations); - Task UpdateInitialSession(RealtimeHubConnection conn); + Task UpdateSession(RealtimeHubConnection conn); Task InsertConversationItem(RoleDialogModel message); + Task RemoveConversationItem(string itemId); Task TriggerModelInference(string? instructions = null); + Task CancelModelResponse(); Task> OnResponsedDone(RealtimeHubConnection conn, string response); Task OnConversationItemCreated(RealtimeHubConnection conn, string response); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs index 90168872..b8609422 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs @@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Options; public class BotSharpOptions { - private readonly static JsonSerializerOptions defaultJsonOptions = new JsonSerializerOptions() + public readonly static JsonSerializerOptions defaultJsonOptions = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs index 60fec1dc..ec521637 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/Models/RealtimeHubConnection.cs @@ -4,7 +4,7 @@ public class RealtimeHubConnection { public string Event { get; set; } = null!; public string StreamId { get; set; } = null!; - public string EntryAgentId { get; set; } = null!; + public string CurrentAgentId { get; set; } = null!; public string ConversationId { get; set; } = null!; public string Data { get; set; } = string.Empty; public string Model { get; set; } = null!; diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs index da7d5c01..337d63c6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs @@ -13,10 +13,10 @@ public interface IRoutingContext bool IsEmpty { get; } string IntentName { get; set; } int AgentCount { get; } - void Push(string agentId, string? reason = null); - void Pop(string? reason = null); - void PopTo(string agentId, string reason); - void Replace(string agentId, string? reason = null); + void Push(string agentId, string? reason = null, bool updateLazyRouting = true); + void Pop(string? reason = null, bool updateLazyRouting = true); + void PopTo(string agentId, string reason, bool updateLazyRouting = true); + void Replace(string agentId, string? reason = null, bool updateLazyRouting = true); void Empty(string? reason = null); diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/FallbackArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/FallbackArgs.cs new file mode 100644 index 00000000..6fc2ce03 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/FallbackArgs.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Routing.Models; + +public class FallbackArgs +{ + [JsonPropertyName("fallback_reason")] + public string Reason { get; set; } = null!; + + [JsonPropertyName("user_question")] + public string Question { get; set; } = null; +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/BotSharp.Core.Crontab.csproj b/src/Infrastructure/BotSharp.Core.Crontab/BotSharp.Core.Crontab.csproj index ae4c9fef..b4bcaf84 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/BotSharp.Core.Crontab.csproj +++ b/src/Infrastructure/BotSharp.Core.Crontab/BotSharp.Core.Crontab.csproj @@ -5,6 +5,7 @@ $(LangVersion) $(BotSharpVersion) $(GeneratePackageOnBuild) + true $(SolutionDir)packages enable enable @@ -29,7 +30,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 6bc7596c..c8ded3cd 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -66,6 +66,8 @@ + + @@ -82,6 +84,7 @@ + @@ -146,6 +149,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index f60d32cf..3967aaaa 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging.Models.RichContent; using BotSharp.Abstraction.Routing.Settings; @@ -36,7 +37,17 @@ public partial class ConversationService // Enqueue receiving agent first in case it stop completion by OnMessageReceived var routing = _services.GetRequiredService(); routing.Context.SetMessageId(_conversationId, message.MessageId); - routing.Context.Push(agent.Id, reason: "request started"); + + // Check the routing mode + var states = _services.GetRequiredService(); + var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard"); + routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false); + + if (routingMode == "lazy") + { + message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId); + routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false); + } // Save payload in order to assign the payload before hook is invoked if (replyMessage != null && !string.IsNullOrEmpty(replyMessage.Payload)) @@ -77,7 +88,7 @@ public partial class ConversationService { agent = await agentService.LoadAgent(message.CurrentAgentId); } - + if (agent.Type == AgentType.Routing) { response = await routing.InstructLoop(message, dialogs); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 4d229866..5d19e2d0 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -220,7 +220,6 @@ public class ConversationStateService : IConversationStateService { if (_conversationId == null || _sidecar?.IsEnabled() == true) { - Reset(); return; } @@ -253,7 +252,6 @@ public class ConversationStateService : IConversationStateService } _db.UpdateConversationStates(_conversationId, states); - Reset(); _logger.LogInformation($"Saved states of conversation {_conversationId}"); } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index 89e9fcaa..75d1a501 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -52,8 +52,7 @@ public class ConversationStorage : IConversationStorage MetaData = meta, Content = dialog.Content, SecondaryContent = dialog.SecondaryContent, - Payload = dialog.Payload, - Data = dialog.Data + Payload = dialog.Payload }); } else @@ -84,8 +83,7 @@ public class ConversationStorage : IConversationStorage SecondaryContent = dialog.SecondaryContent, RichContent = richContent, SecondaryRichContent = secondaryRichContent, - Payload = dialog.Payload, - Data = dialog.Data + Payload = dialog.Payload }); } @@ -123,8 +121,7 @@ public class ConversationStorage : IConversationStorage MetaData = meta, Content = dialog.Content, SecondaryContent = dialog.SecondaryContent, - Payload = dialog.Payload, - Data = dialog.Data + Payload = dialog.Payload }); } else @@ -155,8 +152,7 @@ public class ConversationStorage : IConversationStorage SecondaryContent = dialog.SecondaryContent, RichContent = richContent, SecondaryRichContent = secondaryRichContent, - Payload = dialog.Payload, - Data = dialog.Data + Payload = dialog.Payload }); } } @@ -200,8 +196,7 @@ public class ConversationStorage : IConversationStorage RichContent = richContent, SecondaryContent = secondaryContent, SecondaryRichContent = secondaryRichContent, - Payload = payload, - Data = dialog.Data + Payload = payload }; results.Add(record); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index 9fa5c718..0698c0cb 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -60,12 +60,15 @@ public class TokenStatistics : ITokenStatistics stat.SetState("llm_total_cost", total_cost, isNeedVersion: false, source: StateSource.Application); // Save stats + var metric = StatsMetric.AgentLlmCost; + var dim = "agent"; + var agentId = message.CurrentAgentId ?? string.Empty; var globalStats = _services.GetRequiredService(); var body = new BotSharpStatsInput { - Metric = StatsMetric.AgentLlmCost, - Dimension = "agent", - DimRefVal = message.CurrentAgentId, + Metric = metric, + Dimension = dim, + DimRefVal = agentId, RecordTime = DateTime.UtcNow, IntervalType = StatsInterval.Day, Data = [ @@ -75,7 +78,7 @@ public class TokenStatistics : ITokenStatistics new StatsKeyValuePair("completion_cost_total", deltaCompletionCost) ] }; - globalStats.UpdateStats("global-llm-cost", body); + globalStats.UpdateStats($"global-{metric}-{dim}-{agentId}", body); } public void PrintStatistics() @@ -110,6 +113,10 @@ public class TokenStatistics : ITokenStatistics public void StopTimer() { + if (_timer == null) + { + return; + } _timer.Stop(); } } diff --git a/src/Infrastructure/BotSharp.Core/Realtime/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core/Realtime/RealtimeHub.cs index c44f74d0..7af86a45 100644 --- a/src/Infrastructure/BotSharp.Core/Realtime/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core/Realtime/RealtimeHub.cs @@ -1,9 +1,9 @@ using BotSharp.Abstraction.Realtime; using System.Net.WebSockets; -using System; using BotSharp.Abstraction.Realtime.Models; using BotSharp.Abstraction.MLTasks; -using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Routing.Models; namespace BotSharp.Core.Realtime; @@ -11,6 +11,7 @@ public class RealtimeHub : IRealtimeHub { private readonly IServiceProvider _services; private readonly ILogger _logger; + public RealtimeHub(IServiceProvider services, ILogger logger) { _services = services; @@ -71,18 +72,23 @@ public class RealtimeHub : IRealtimeHub var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(conversation.AgentId); - conn.EntryAgentId = agent.Id; + conn.CurrentAgentId = agent.Id; var routing = _services.GetRequiredService(); + routing.Context.Push(agent.Id); + var dialogs = convService.GetDialogHistory(); + if (dialogs.Count == 0) + { + dialogs.Add(new RoleDialogModel(AgentRole.User, "Hi")); + } routing.Context.SetDialogs(dialogs); await completer.Connect(conn, onModelReady: async () => { // Control initial session - await completer.UpdateInitialSession(conn); - + await completer.UpdateSession(conn); // Add dialog history foreach (var item in dialogs) @@ -92,7 +98,7 @@ public class RealtimeHub : IRealtimeHub if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant) { - // await completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}"); + await completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}"); } else { @@ -118,16 +124,40 @@ public class RealtimeHub : IRealtimeHub foreach (var message in messages) { // Invoke function - if (message.MessageType == "function_call") + if (message.MessageType == MessageTypeName.FunctionCall) { await routing.InvokeFunction(message.FunctionName, message); message.Role = AgentRole.Function; - await completer.InsertConversationItem(message); - await completer.TriggerModelInference("Reply based on the function's output."); + + if (message.FunctionName == "route_to_agent") + { + var inst = JsonSerializer.Deserialize(message.FunctionArgs ?? "{}"); + message.Content = $"Connected to agent of {inst.AgentName}"; + conn.CurrentAgentId = routing.Context.GetCurrentAgentId(); + + await completer.UpdateSession(conn); + await completer.InsertConversationItem(message); + await completer.TriggerModelInference($"Guide the user through the next steps of the process as this Agent ({inst.AgentName}), following its instructions and operational procedures."); + } + else if (message.FunctionName == "util-routing-fallback_to_router") + { + var inst = JsonSerializer.Deserialize(message.FunctionArgs ?? "{}"); + message.Content = $"Returned to Router due to {inst.Reason}"; + conn.CurrentAgentId = routing.Context.GetCurrentAgentId(); + + await completer.UpdateSession(conn); + await completer.InsertConversationItem(message); + await completer.TriggerModelInference($"Check with user whether to proceed the new request: {inst.Reason}"); + } + else + { + await completer.InsertConversationItem(message); + await completer.TriggerModelInference("Reply based on the function's output."); + } } else { - // append transcript to conversation + // append output audio transcript to conversation storage.Append(conn.ConversationId, message); dialogs.Add(message); @@ -136,10 +166,7 @@ public class RealtimeHub : IRealtimeHub hook.SetAgent(agent) .SetConversation(conversation); - if (!string.IsNullOrEmpty(message.Content)) - { - await hook.OnMessageReceived(message); - } + await hook.OnResponseGenerated(message); } } } @@ -150,9 +177,17 @@ public class RealtimeHub : IRealtimeHub }, onInputAudioTranscriptionCompleted: async message => { - // append transcript to conversation + // append input audio transcript to conversation storage.Append(conn.ConversationId, message); dialogs.Add(message); + + foreach (var hook in hookProvider.HooksOrderByPriority) + { + hook.SetAgent(agent) + .SetConversation(conversation); + + await hook.OnMessageReceived(message); + } }, onUserInterrupted: async () => { diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index ec4848d7..7bd1dd15 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -500,16 +500,6 @@ public partial class FileRepository batchSize = batchLimit; } - if (bufferHours <= 0) - { - bufferHours = 12; - } - - if (messageLimit <= 0) - { - messageLimit = 2; - } - foreach (var d in Directory.GetDirectories(dir)) { var convFile = Path.Combine(d, CONVERSATION_FILE); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs index 592a2c29..58e43819 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs @@ -5,8 +5,9 @@ namespace BotSharp.Core.Routing.Functions; public class FallbackToRouterFn : IFunctionCallback { - public string Name => "fallback_to_router"; + public string Name => "util-routing-fallback_to_router"; private readonly IServiceProvider _services; + public FallbackToRouterFn(IServiceProvider services) { _services = services; @@ -14,30 +15,10 @@ public class FallbackToRouterFn : IFunctionCallback public async Task Execute(RoleDialogModel message) { - var args = JsonSerializer.Deserialize(message.FunctionArgs); - var agentService = _services.GetRequiredService(); - var agents = await agentService.GetAgents(new AgentFilter - { - AgentNames = [args.AgentName] - }); - var targetAgent = agents.Items.FirstOrDefault(); - if (targetAgent == null) - { - message.Content = $"Can't find routing agent {args.AgentName}"; - return false; - } - - var conv = _services.GetRequiredService(); - var dialogs = conv.GetDialogHistory(); - + var args = JsonSerializer.Deserialize(message.FunctionArgs); var routing = _services.GetRequiredService(); - routing.Context.Replace(targetAgent.Id); - message.CurrentAgentId = targetAgent.Id; - - var response = await routing.InstructLoop(message, dialogs); - - message.Content = response.Content; - message.StopCompletion = true; + routing.Context.PopTo(routing.Context.EntryAgentId, "pop to entry agent"); + message.Content = args.Question; return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingUtilityHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingUtilityHook.cs new file mode 100644 index 00000000..e6fedc05 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingUtilityHook.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Core.Routing.Hooks; + +public class RoutingUtilityHook : IAgentUtilityHook +{ + private static string PREFIX = "util-routing-"; + private static string REDIRECT_TO_AGENT = $"{PREFIX}redirect_to_agent"; + private static string FALLBACK_TO_ROUTER = $"{PREFIX}fallback_to_router"; + + public void AddUtilities(List utilities) + { + var utility = new AgentUtility + { + Name = "routing.tools", + Functions = [new($"{REDIRECT_TO_AGENT}"), new($"{FALLBACK_TO_ROUTER}")], + Templates = [new($"{REDIRECT_TO_AGENT}.fn"), new($"{FALLBACK_TO_ROUTER}.fn")] + }; + + utilities.Add(utility); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs index 931b53e8..8980246e 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs @@ -73,7 +73,7 @@ public class NaiveReasoner : IRoutingReasoner }; var response = await completion.GetChatCompletions(router, dialogs); - inst = response.Content.JsonContent(); + inst = (response.FunctionArgs ?? response.Content).JsonContent(); break; } catch (Exception ex) diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index cf604c55..47e47631 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Routing; @@ -79,7 +80,7 @@ public class RoutingContext : IRoutingContext /// /// Id or Name /// - public void Push(string agentId, string? reason = null) + public void Push(string agentId, string? reason = null, bool updateLazyRouting = true) { // Convert id to name if (!Guid.TryParse(agentId, out _)) @@ -99,13 +100,15 @@ public class RoutingContext : IRoutingContext HookEmitter.Emit(_services, async hook => await hook.OnAgentEnqueued(agentId, preAgentId, reason: reason) ).Wait(); + + UpdateLazyRoutingAgent(updateLazyRouting); } } /// /// Pop current agent /// - public void Pop(string? reason = null) + public void Pop(string? reason = null, bool updateLazyRouting = true) { if (_stack.Count == 0) { @@ -149,15 +152,17 @@ public class RoutingContext : IRoutingContext _stack.Push(agentId); } } + + UpdateLazyRoutingAgent(updateLazyRouting); } - public void PopTo(string agentId, string reason) + public void PopTo(string agentId, string reason, bool updateLazyRouting = true) { var currentAgentId = GetCurrentAgentId(); while (!string.IsNullOrEmpty(currentAgentId) && currentAgentId != agentId) { - Pop(reason); + Pop(reason, updateLazyRouting: updateLazyRouting); currentAgentId = GetCurrentAgentId(); } } @@ -181,7 +186,7 @@ public class RoutingContext : IRoutingContext return _stack.ToArray().Contains(agentId); } - public void Replace(string agentId, string? reason = null) + public void Replace(string agentId, string? reason = null, bool updateLazyRouting = true) { var fromAgent = agentId; var toAgent = agentId; @@ -200,6 +205,8 @@ public class RoutingContext : IRoutingContext await hook.OnAgentReplaced(fromAgent, toAgent, reason: reason) ).Wait(); } + + UpdateLazyRoutingAgent(updateLazyRouting); } public void Empty(string? reason = null) @@ -275,4 +282,24 @@ public class RoutingContext : IRoutingContext { _dialogs = []; } + + private void UpdateLazyRoutingAgent(bool updateLazyRouting) + { + if (!updateLazyRouting) + { + return; + } + + // Set next handling agent for lazy routing mode + var states = _services.GetRequiredService(); + var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard"); + if (routingMode == "lazy") + { + var agentId = GetCurrentAgentId(); + if (agentId != BuiltInAgentId.Fallback) + { + states.SetState(StateConst.LAZY_ROUTING_AGENT_ID, agentId); + } + } + } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs index c5317ba4..2e409fcf 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs @@ -37,5 +37,7 @@ public class RoutingPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); + + services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index a4d4b37a..bec44bc4 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -53,6 +53,7 @@ public partial class RoutingService // Handle output routing exception. if (agent.Type == AgentType.Routing) { + // Forgot about what situation needs to handle in this way response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?"; } diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/agent.json b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/agent.json index c54a67e0..3200865e 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/agent.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d/agent.json @@ -1,7 +1,7 @@ { "id": "01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d", "name": "Fallback Agent", - "description": "Don't have sufficient confidence to trigger any of existing agent.", + "description": "Handle initiated conversation without specific task given yet or don't have sufficient confidence to handle user task.", "type": "task", "createdDateTime": "2024-05-07T10:00:00Z", "updatedDateTime": "2024-05-07T10:00:00Z", diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/route_to_agent.json b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/route_to_agent.json new file mode 100644 index 00000000..08f938ec --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/route_to_agent.json @@ -0,0 +1,31 @@ +{ + "name": "route_to_agent", + "description": "Route request to appropriate AI agent.", + "visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}", + "parameters": { + "type": "object", + "properties": { + "next_action_agent": { + "type": "string", + "description": "Agent for next action based on user latest response" + }, + "next_action_reason": { + "type": "string", + "description": "The reason why route to this agent." + }, + "user_goal_agent": { + "type": "string", + "description": "Agent who can acheive user initial task." + }, + "conversation_end": { + "type": "boolean", + "description": "User is ending the conversation." + }, + "args": { + "type": "object", + "description": "Required parameters of next action agent" + } + }, + "required": [ "next_action_agent", "user_goal_agent", "next_action_reason", "args" ] + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid index bc614f81..7ee8f56e 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid @@ -5,7 +5,9 @@ Follow these steps to handle user request: 2. Determine which agent is suitable to handle this conversation. Try to minimize the routing of human service. 3. Extract and populate agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments. 4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared. +{% if routing_mode != 'lazy' %} 5. Response must be in JSON format. +{% endif %} {% if routing_requirements and routing_requirements != empty %} [REQUIREMENTS] @@ -14,6 +16,7 @@ Follow these steps to handle user request: {%- endfor %} {% endif %} +{% if routing_mode != 'lazy' %} [FUNCTIONS] {% for handler in routing_handlers -%} # {{ handler.description}} @@ -26,6 +29,7 @@ Parameters: {%- endif %} {{ "\r\n" }} {%- endfor %} +{% endif %} [AGENTS] {% for agent in routing_agents -%} diff --git a/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-routing-fallback_to_router.json b/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-routing-fallback_to_router.json new file mode 100644 index 00000000..3902d8ae --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-routing-fallback_to_router.json @@ -0,0 +1,18 @@ +{ + "name": "util-routing-fallback_to_router", + "description": "Return to the Router to find the appropriate agent who can handle the user's request.", + "parameters": { + "type": "object", + "properties": { + "fallback_reason": { + "type": "string", + "description": "The reason why you need to reach out to other agent." + }, + "user_question": { + "type": "string", + "description": "User question or statement." + } + }, + "required": [ "fallback_reason" ] + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-routing-fallback_to_router.fn.liquid b/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-routing-fallback_to_router.fn.liquid new file mode 100644 index 00000000..a1bd9ceb --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-routing-fallback_to_router.fn.liquid @@ -0,0 +1 @@ +Carefully consider whether the current user request is related to your responsibilities. Only when it is not relevant should you consider Return to the Router. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs index 2cc2c9fb..8ff9be86 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs @@ -25,17 +25,20 @@ public class GlobalStatsConversationHook : IContentGeneratingHook // record agent call var globalStats = _services.GetRequiredService(); + var metric = StatsMetric.AgentCall; + var dim = "agent"; + var agentId = message.CurrentAgentId ?? string.Empty; var body = new BotSharpStatsInput { - Metric = StatsMetric.AgentCall, - Dimension = "agent", - DimRefVal = message.CurrentAgentId ?? string.Empty, + Metric = metric, + Dimension = dim, + DimRefVal = agentId, RecordTime = DateTime.UtcNow, IntervalType = StatsInterval.Day, Data = [ new StatsKeyValuePair("agent_call_count", 1) ] }; - globalStats.UpdateStats("global-agent-call", body); + globalStats.UpdateStats($"global-{metric}-{dim}-{agentId}", body); } } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj b/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj index 284e9b3c..61758e2e 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/BotSharp.Plugin.AzureOpenAI.csproj @@ -8,11 +8,12 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true - - + + diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 9f2c1008..99b734e6 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -7,6 +7,7 @@ public class ChatHubConversationHook : ConversationHookBase { private readonly IServiceProvider _services; private readonly IHubContext _chatHub; + private readonly ILogger _logger; private readonly IUserIdentity _user; private readonly BotSharpOptions _options; private readonly ChatHubSettings _settings; @@ -23,12 +24,14 @@ public class ChatHubConversationHook : ConversationHookBase public ChatHubConversationHook( IServiceProvider services, IHubContext chatHub, + ILogger logger, BotSharpOptions options, ChatHubSettings settings, IUserIdentity user) { _services = services; _chatHub = chatHub; + _logger = logger; _user = user; _options = options; _settings = settings; @@ -177,74 +180,122 @@ public class ChatHubConversationHook : ConversationHookBase private async Task InitClientConversation(string conversationId, ConversationViewModel conversation) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(INIT_CLIENT_CONVERSATION, conversation); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(INIT_CLIENT_CONVERSATION, conversation); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation); + _logger.LogWarning($"Failed to init client conversation in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } private async Task ReceiveClientMessage(string conversationId, ChatResponseModel model) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_CLIENT_MESSAGE, model); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_CLIENT_MESSAGE, model); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model); + _logger.LogWarning($"Failed to receive assistant message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } private async Task ReceiveAssistantMessage(string conversationId, string? json) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); + _logger.LogWarning($"Failed to receive assistant message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } - + } private async Task GenerateSenderAction(string conversationId, ConversationSenderActionModel action) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_SENDER_ACTION, action); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_SENDER_ACTION, action); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action); + _logger.LogWarning($"Failed to generate sender action in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } private async Task DeleteMessage(string conversationId, ChatResponseModel model) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(DELETE_MESSAGE, model); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(DELETE_MESSAGE, model); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model); + _logger.LogWarning($"Failed to delete message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } private async Task GenerateNotification(string conversationId, string? json) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_NOTIFICATION, json); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_NOTIFICATION, json); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json); + _logger.LogWarning($"Failed to generate notification in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } #endregion diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs index a00dae90..f4b4266f 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs @@ -8,6 +8,7 @@ public class ChatHubCrontabHook : ICrontabHook { private readonly IServiceProvider _services; private readonly IHubContext _chatHub; + private readonly ILogger _logger; private readonly IUserIdentity _user; private readonly IConversationStorage _storage; private readonly BotSharpOptions _options; @@ -19,6 +20,7 @@ public class ChatHubCrontabHook : ICrontabHook public ChatHubCrontabHook(IServiceProvider services, IHubContext chatHub, + ILogger logger, IUserIdentity user, IConversationStorage storage, BotSharpOptions options, @@ -26,6 +28,7 @@ public class ChatHubCrontabHook : ICrontabHook { _services = services; _chatHub = chatHub; + _logger = logger; _user = user; _storage = storage; _options = options; @@ -48,13 +51,26 @@ public class ChatHubCrontabHook : ICrontabHook } }, _options.JsonSerializerOptions); - if (_settings.EventDispatchBy == EventDispatchType.Group) + await SendEvent(item, json); + } + + private async Task SendEvent(CrontabItem item, string json) + { + try { - await _chatHub.Clients.Group(item.ConversationId).SendAsync(GENERATE_NOTIFICATION, json); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(item.ConversationId).SendAsync(GENERATE_NOTIFICATION, json); + } + else + { + await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json); + _logger.LogWarning($"Failed to send event in {nameof(ChatHubCrontabHook)} (conversation id: {item.ConversationId})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index a2f527f4..22ec7d4d 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -12,6 +12,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR private readonly ChatHubSettings _settings; private readonly IServiceProvider _services; private readonly IHubContext _chatHub; + private readonly ILogger _logger; private readonly IConversationStateService _state; private readonly IUserIdentity _user; private readonly IAgentService _agentService; @@ -30,6 +31,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR ChatHubSettings settings, IServiceProvider serivces, IHubContext chatHub, + ILogger logger, IConversationStateService state, IUserIdentity user, IAgentService agentService, @@ -40,6 +42,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR _settings = settings; _services = serivces; _chatHub = chatHub; + _logger = logger; _state = state; _user = user; _agentService = agentService; @@ -82,6 +85,33 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR await SendContentLog(conversationId, input); } + public async Task OnSessionUpdated(Agent agent, string instruction, FunctionDef[] functions) + { + var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + + // Agent queue log + var log = $"{instruction}"; + if (functions.Length > 0) + { + log += $"\r\n\r\n[FUNCTIONS]:\r\n\r\n{string.Join("\r\n\r\n", functions.Select(x => JsonSerializer.Serialize(x, BotSharpOptions.defaultJsonOptions)))}"; + } + _logger.LogInformation(log); + + var message = new RoleDialogModel(AgentRole.Assistant, log) + { + MessageId = _routingCtx.MessageId + }; + var input = new ContentLogInputModel(conversationId, message) + { + Name = agent.Name, + AgentId = agent.Id, + Source = ContentLogSource.Prompt, + Log = log + }; + await SendContentLog(conversationId, input); + } + public async Task OnRenderingTemplate(Agent agent, string name, string content) { if (!_convSettings.ShowVerboseLog) return; @@ -439,54 +469,85 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR #region Private methods private async Task SendContentLog(string conversationId, ContentLogInputModel input) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input)); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input)); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input)); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input)); + _logger.LogWarning($"Failed to send content log in {nameof(StreamingLogHook)} (conversation id: {conversationId})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } private async Task SendStateLog(string conversationId, string agentId, Dictionary states, RoleDialogModel message) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message)); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message)); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message)); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message)); + _logger.LogWarning($"Failed to send state log in {nameof(StreamingLogHook)} (conversation id: {conversationId})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } private async Task SendAgentQueueLog(string conversationId, string log) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log)); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log)); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log)); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log)); + _logger.LogWarning($"Failed to send agent queue log in {nameof(StreamingLogHook)} (conversation id: {conversationId})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } private async Task SendStateChange(string conversationId, StateChangeModel stateChange) { - if (_settings.EventDispatchBy == EventDispatchType.Group) + try { - await _chatHub.Clients.Group(conversationId).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange)); + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange)); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange)); + } } - else + catch (Exception ex) { - await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange)); + _logger.LogWarning($"Failed to send state change in {nameof(StreamingLogHook)} (conversation id: {conversationId})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); } } - private string BuildContentLog(ContentLogInputModel input) { var output = new ContentLogOutputModel diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs index c7aedd83..70545305 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/WelcomeHook.cs @@ -6,6 +6,7 @@ public class WelcomeHook : ConversationHookBase { private readonly IServiceProvider _services; private readonly IHubContext _chatHub; + private readonly ILogger _logger; private readonly IUserIdentity _user; private readonly IConversationStorage _storage; private readonly BotSharpOptions _options; @@ -17,6 +18,7 @@ public class WelcomeHook : ConversationHookBase public WelcomeHook(IServiceProvider services, IHubContext chatHub, + ILogger logger, IUserIdentity user, IConversationStorage storage, BotSharpOptions options, @@ -24,6 +26,7 @@ public class WelcomeHook : ConversationHookBase { _services = services; _chatHub = chatHub; + _logger = logger; _user = user; _storage = storage; _options = options; @@ -78,17 +81,30 @@ public class WelcomeHook : ConversationHookBase _storage.Append(conversation.Id, dialog); - if (_settings.EventDispatchBy == EventDispatchType.Group) - { - await _chatHub.Clients.Group(conversation.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); - } - else - { - await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); - } + await SendEvent(conversation.Id, json); } } await base.OnUserAgentConnectedInitially(conversation); } + + private async Task SendEvent(string conversationId, string json) + { + try + { + if (_settings.EventDispatchBy == EventDispatchType.Group) + { + await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); + } + else + { + await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to send event in {nameof(WelcomeHook)} (conversation id: {conversationId})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); + } + } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs b/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs index 240b5993..63ded25a 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs @@ -33,12 +33,7 @@ public class SignalRHub : Hub if (!string.IsNullOrEmpty(conversationId)) { _logger.LogInformation($"Connection {Context.ConnectionId} is with conversation {conversationId}"); - - var settings = _services.GetRequiredService(); - if (settings.EventDispatchBy == EventDispatchType.Group) - { - await Groups.AddToGroupAsync(Context.ConnectionId, conversationId); - } + await AddGroup(conversationId); var conv = await convService.GetConversation(conversationId); if (conv != null) @@ -56,4 +51,21 @@ public class SignalRHub : Hub await base.OnConnectedAsync(); } + + private async Task AddGroup(string conversationId) + { + try + { + var settings = _services.GetRequiredService(); + if (settings.EventDispatchBy == EventDispatchType.Group) + { + await Groups.AddToGroupAsync(Context.ConnectionId, conversationId); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to add chat group in {nameof(SignalRHub)} (conversation id: {conversationId})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); + } + } } diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj b/src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj index 56eb14ef..598e1295 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj @@ -8,10 +8,11 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true - + diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj b/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj index f1a6c28e..a7f4edcf 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj @@ -8,6 +8,7 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true @@ -33,7 +34,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj b/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj index 08cfd79a..3b6db5d3 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true @@ -27,9 +28,9 @@ - - - + + + diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj b/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj index b6e0b24e..9e153d76 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/BotSharp.Plugin.GoogleAI.csproj @@ -8,11 +8,12 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true - - + + diff --git a/src/Plugins/BotSharp.Plugin.HuggingFace/BotSharp.Plugin.HuggingFace.csproj b/src/Plugins/BotSharp.Plugin.HuggingFace/BotSharp.Plugin.HuggingFace.csproj index ae6a05be..2b8854af 100644 --- a/src/Plugins/BotSharp.Plugin.HuggingFace/BotSharp.Plugin.HuggingFace.csproj +++ b/src/Plugins/BotSharp.Plugin.HuggingFace/BotSharp.Plugin.HuggingFace.csproj @@ -8,11 +8,12 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true - - + + diff --git a/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/BotSharp.Plugin.JavaScriptInterpreter.csproj b/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/BotSharp.Plugin.JavaScriptInterpreter.csproj index 4596ce1b..6abacd91 100644 --- a/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/BotSharp.Plugin.JavaScriptInterpreter.csproj +++ b/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/BotSharp.Plugin.JavaScriptInterpreter.csproj @@ -4,10 +4,11 @@ net8.0 enable enable + true - + diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj b/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj index 77a5d076..5cb7342f 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj @@ -8,6 +8,7 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true @@ -23,8 +24,6 @@ - - @@ -46,17 +45,17 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - - + + diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs index de90430d..e4518344 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs @@ -2,5 +2,5 @@ namespace BotSharp.Plugin.KnowledgeBase.Enum; public class UtilityName { - public const string KnowledgeRetrieval = "knowledge.knowledge-retrieval"; + public const string KnowledgeRetrieval = "kg.knowledge-base"; } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs index f3f8cbf1..940c3834 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.KnowledgeBase.Functions; public class KnowledgeRetrievalFn : IFunctionCallback { - public string Name => "util-knowledge-knowledge_retrieval"; + public string Name => "util-kg-knowledge_retrieval"; public string Indication => "searching my brain"; diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs index 700b8457..84254841 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.KnowledgeBase.Hooks; public class KnowledgeBaseUtilityHook : IAgentUtilityHook { - private static string PREFIX = "util-knowledge-"; + private static string PREFIX = "util-kg-"; private static string KNOWLEDGE_RETRIEVAL_FN = $"{PREFIX}knowledge_retrieval"; public void AddUtilities(List utilities) diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeHook.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeHook.cs index da4add53..90aa0114 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeHook.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeHook.cs @@ -40,6 +40,20 @@ public class KnowledgeHook : IKnowledgeHook var result = await _knowledgeService.SearchGraphKnowledge(text, options); results.Add(result.Result); } + else if (knowledgeBase.Type == "document") + { + var options = new VectorSearchOptions + { + Fields = null, + Limit = 5, + Confidence = 0.25f, + WithVector = true + }; + var result = await _knowledgeService.SearchVectorKnowledge(text, knowledgeBase.Name, options); + results.AddRange(result.Where(x => x.Data != null && x.Data.ContainsKey("text")) + .Select(x => x.Data["text"].ToString()) + .Where(x => x != null)!); + } else { var options = new VectorSearchOptions diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-knowledge-knowledge_retrieval.json b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-kg-knowledge_retrieval.json similarity index 86% rename from src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-knowledge-knowledge_retrieval.json rename to src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-kg-knowledge_retrieval.json index 088f4ea6..cb79298a 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-knowledge-knowledge_retrieval.json +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-kg-knowledge_retrieval.json @@ -1,5 +1,5 @@ { - "name": "util-knowledge-knowledge_retrieval", + "name": "util-kg-knowledge_retrieval", "description": "Retrieve related domain knowledge to handle user request", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-knowledge-knowledge_retrieval.fn.liquid b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-kg-knowledge_retrieval.fn.liquid similarity index 63% rename from src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-knowledge-knowledge_retrieval.fn.liquid rename to src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-kg-knowledge_retrieval.fn.liquid index f0bc60c8..e7d61b7f 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-knowledge-knowledge_retrieval.fn.liquid +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-kg-knowledge_retrieval.fn.liquid @@ -1,3 +1,3 @@ -Call function util-knowledge-knowledge_retrieval to retrieve related domain knowledge to handle user request. +Call function util-kg-knowledge_retrieval to retrieve related domain knowledge to handle user request. You must retrieve existing KnowledgeBase to get prerequisite informations before you writing SQL query; You must retrieve existing API specification from KnowledgeBase before calling a Web API; diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj b/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj index d0bea79a..d5581998 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj @@ -8,10 +8,11 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true - + diff --git a/src/Plugins/BotSharp.Plugin.LangChain/BotSharp.Plugin.VertexAI.csproj b/src/Plugins/BotSharp.Plugin.LangChain/BotSharp.Plugin.VertexAI.csproj index 004d7233..92509518 100644 --- a/src/Plugins/BotSharp.Plugin.LangChain/BotSharp.Plugin.VertexAI.csproj +++ b/src/Plugins/BotSharp.Plugin.LangChain/BotSharp.Plugin.VertexAI.csproj @@ -8,10 +8,11 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true - + diff --git a/src/Plugins/BotSharp.Plugin.MetaAI/BotSharp.Plugin.MetaAI.csproj b/src/Plugins/BotSharp.Plugin.MetaAI/BotSharp.Plugin.MetaAI.csproj index 3d390f56..2c247292 100644 --- a/src/Plugins/BotSharp.Plugin.MetaAI/BotSharp.Plugin.MetaAI.csproj +++ b/src/Plugins/BotSharp.Plugin.MetaAI/BotSharp.Plugin.MetaAI.csproj @@ -8,6 +8,7 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true @@ -15,8 +16,8 @@ - - - + + + diff --git a/src/Plugins/BotSharp.Plugin.MetaGLM/BotSharp.Plugin.MetaGLM.csproj b/src/Plugins/BotSharp.Plugin.MetaGLM/BotSharp.Plugin.MetaGLM.csproj index 2e43183c..d1bcb1b4 100644 --- a/src/Plugins/BotSharp.Plugin.MetaGLM/BotSharp.Plugin.MetaGLM.csproj +++ b/src/Plugins/BotSharp.Plugin.MetaGLM/BotSharp.Plugin.MetaGLM.csproj @@ -8,10 +8,11 @@ $(GeneratePackageOnBuild) $(GenerateDocumentationFile) $(SolutionDir)packages + true - + diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs index 87022130..030a93ab 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/DialogMongoElement.cs @@ -11,7 +11,6 @@ public class DialogMongoElement public string? RichContent { get; set; } public string? SecondaryRichContent { get; set; } public string? Payload { get; set; } - public object? Data { get; set; } public static DialogMongoElement ToMongoElement(DialogElement dialog) { @@ -22,8 +21,7 @@ public class DialogMongoElement SecondaryContent = dialog.SecondaryContent, RichContent = dialog.RichContent, SecondaryRichContent = dialog.SecondaryRichContent, - Payload = dialog.Payload, - Data = dialog.Data + Payload = dialog.Payload }; } @@ -36,8 +34,7 @@ public class DialogMongoElement SecondaryContent = dialog.SecondaryContent, RichContent = dialog.RichContent, SecondaryRichContent = dialog.SecondaryRichContent, - Payload = dialog.Payload, - Data = dialog.Data + Payload = dialog.Payload }; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index 06512fb7..a78db60c 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -466,16 +466,6 @@ public partial class MongoRepository batchSize = batchLimit; } - if (bufferHours <= 0) - { - bufferHours = 12; - } - - if (messageLimit <= 0) - { - messageLimit = 2; - } - while (true) { var skip = (page - 1) * batchSize; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj b/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj index 9a9c57fb..0a509b1a 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj +++ b/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj @@ -18,6 +18,7 @@ + \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 9b981cb1..2be07da8 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -1,6 +1,9 @@ +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Realtime.Models; +using BotSharp.Core.Infrastructures; using BotSharp.Plugin.OpenAI.Models.Realtime; using OpenAI.Chat; using System.Net.WebSockets; @@ -99,6 +102,23 @@ public class RealTimeCompletionProvider : IRealTimeCompletion }); } + public async Task CancelModelResponse() + { + await SendEventToModel(new + { + type = "response.cancel" + }); + } + + public async Task RemoveConversationItem(string itemId) + { + await SendEventToModel(new + { + type = "conversation.item.delete", + item_id = itemId + }); + } + private async Task ReceiveMessage(RealtimeHubConnection conn, Action onModelAudioDeltaReceived, Action onModelAudioResponseDone, @@ -166,7 +186,6 @@ public class RealTimeCompletionProvider : IRealTimeCompletion else if (response.Type == "response.done") { _logger.LogInformation($"{response.Type}: {receivedText}"); - await Task.Delay(1000); var messages = await OnResponsedDone(conn, receivedText); onModelResponseDone(messages); } @@ -204,12 +223,18 @@ public class RealTimeCompletionProvider : IRealTimeCompletion public async Task SendEventToModel(object message) { + if (_webSocket.State != WebSocketState.Open) + { + return; + } + if (message is not string data) { - data = JsonSerializer.Serialize(message); + data = JsonSerializer.Serialize(message, BotSharpOptions.defaultJsonOptions); } var buffer = Encoding.UTF8.GetBytes(data); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); } @@ -247,19 +272,29 @@ public class RealTimeCompletionProvider : IRealTimeCompletion return session; } - public async Task UpdateInitialSession(RealtimeHubConnection conn) + public async Task UpdateSession(RealtimeHubConnection conn) { var convService = _services.GetRequiredService(); var conv = await convService.GetConversation(conn.ConversationId); var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(conv.AgentId); + var agent = await agentService.LoadAgent(conn.CurrentAgentId); var client = ProviderHelper.GetClient(Provider, _model, _services); var chatClient = client.GetChatClient(_model); var (prompt, messages, options) = PrepareOptions(agent, []); var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent.Description; + var functions = options.Tools.Select(x => + { + var fn = new FunctionDef + { + Name = x.FunctionName, + Description = x.FunctionDescription + }; + fn.Parameters = JsonSerializer.Deserialize(x.FunctionParameters); + return fn; + }).ToArray(); var sessionUpdate = new { @@ -275,21 +310,23 @@ public class RealTimeCompletionProvider : IRealTimeCompletion Voice = "alloy", Instructions = instruction, ToolChoice = "auto", - Tools = options.Tools.Select(x => - { - var fn = new FunctionDef - { - Name = x.FunctionName, - Description = x.FunctionDescription - }; - fn.Parameters = JsonSerializer.Deserialize(x.FunctionParameters); - return fn; - }).ToArray(), + Tools = functions, Modalities = [ "text", "audio" ], - Temperature = Math.Max(options.Temperature ?? 0f, 0.6f) + Temperature = Math.Max(options.Temperature ?? 0f, 0.6f), + MaxResponseOutputTokens = 512, + TurnDetection = new RealtimeSessionTurnDetection + { + Threshold = 0.8f, + SilenceDuration = 800 + } } }; + await HookEmitter.Emit(_services, async hook => + { + await hook.OnSessionUpdated(agent, instruction, functions); + }); + await SendEventToModel(sessionUpdate); } @@ -550,16 +587,23 @@ public class RealTimeCompletionProvider : IRealTimeCompletion var outputs = new List(); var data = JsonSerializer.Deserialize(response).Body; + if (data.Status != "completed") + { + return []; + } + foreach (var output in data.Outputs) { if (output.Type == "function_call") { outputs.Add(new RoleDialogModel(output.Role, output.Arguments) { - CurrentAgentId = conn.EntryAgentId, + CurrentAgentId = conn.CurrentAgentId, FunctionName = output.Name, FunctionArgs = output.Arguments, - ToolCallId = output.CallId + ToolCallId = output.CallId, + MessageId = output.Id, + MessageType = MessageTypeName.FunctionCall }); } else if (output.Type == "message") @@ -568,11 +612,27 @@ public class RealTimeCompletionProvider : IRealTimeCompletion outputs.Add(new RoleDialogModel(output.Role, content.Transcript) { - CurrentAgentId = conn.EntryAgentId + CurrentAgentId = conn.CurrentAgentId }); } } + var contentHooks = _services.GetServices().ToList(); + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(new RoleDialogModel(AgentRole.Assistant, "response.done") + { + CurrentAgentId = conn.CurrentAgentId + }, new TokenStatsModel + { + Provider = Provider, + Model = _model, + CompletionCount = data.Usage.OutputTokens, + PromptCount = data.Usage.InputTokens + }); + } + return outputs; } @@ -581,7 +641,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion var data = JsonSerializer.Deserialize(response); return new RoleDialogModel(AgentRole.User, data.Transcript) { - CurrentAgentId = conn.EntryAgentId + CurrentAgentId = conn.CurrentAgentId }; } diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs index c2180076..097a32b7 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Text/TextCompletionProvider.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.Net.Mime; using System.Text.Json; using System.Text; +using BotSharp.Abstraction.Options; namespace BotSharp.Plugin.OpenAI.Providers.Text; @@ -13,14 +14,6 @@ public class TextCompletionProvider : ITextCompletion private readonly OpenAiSettings _settings; protected string _model; - protected readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - WriteIndented = true, - AllowTrailingCommas = true, - }; - public virtual string Provider => "openai"; public TextCompletionProvider( @@ -110,7 +103,7 @@ public class TextCompletionProvider : ITextCompletion MaxTokens = maxTokens, Temperature = temperature }; - var data = JsonSerializer.Serialize(request, _jsonOptions); + var data = JsonSerializer.Serialize(request, BotSharpOptions.defaultJsonOptions); var httpRequest = new HttpRequestMessage { Method = HttpMethod.Post, @@ -121,7 +114,7 @@ public class TextCompletionProvider : ITextCompletion var httpResponse = await httpClient.SendAsync(httpRequest); httpResponse.EnsureSuccessStatusCode(); var responseStr = await httpResponse.Content.ReadAsStringAsync(); - var response = JsonSerializer.Deserialize(responseStr, _jsonOptions); + var response = JsonSerializer.Deserialize(responseStr, BotSharpOptions.defaultJsonOptions); return response; } catch (Exception ex) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj b/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj index 91c81e90..ece4e6f6 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj +++ b/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj @@ -9,15 +9,16 @@ - - - - - - + PreserveNewest - + + PreserveNewest + + + PreserveNewest + + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs index e6de9fc5..dcb33959 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs @@ -1,13 +1,12 @@ using BotSharp.Abstraction.Infrastructures; +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Core.Infrastructures; using BotSharp.Plugin.Twilio.Interfaces; using BotSharp.Plugin.Twilio.Models; using BotSharp.Plugin.Twilio.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Twilio.TwiML.Voice; using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation; -using Task = System.Threading.Tasks.Task; namespace BotSharp.Plugin.Twilio.Controllers; @@ -52,10 +51,6 @@ public class TwilioStreamController : TwilioController { request.ConversationId = _context.HttpContext.Request.Query["conversation_id"]; } - else - { - request.ConversationId = request.CallSid; - } await HookEmitter.Emit(_services, async hook => { @@ -65,7 +60,7 @@ public class TwilioStreamController : TwilioController OnlyOnce = true }); - await InitConversation(request); + request.ConversationId = await InitConversation(request); var twilio = _services.GetRequiredService(); @@ -82,7 +77,7 @@ public class TwilioStreamController : TwilioController return TwiML(response); } - private async Task InitConversation(ConversationalVoiceRequest request) + private async Task InitConversation(ConversationalVoiceRequest request) { var convService = _services.GetRequiredService(); var conversation = await convService.GetConversation(request.ConversationId); @@ -90,10 +85,10 @@ public class TwilioStreamController : TwilioController { var conv = new Conversation { - Id = request.CallSid, - AgentId = _settings.AgentId, + AgentId = request.AgentId ?? _settings.AgentId, Channel = ConversationChannel.Phone, - Title = $"Phone call from {request.From}", + ChannelId = request.CallSid, + Title = $"Incoming phone call from {request.From}", Tags = [], }; @@ -103,10 +98,15 @@ public class TwilioStreamController : TwilioController var states = new List { new("channel", ConversationChannel.Phone), - new("calling_phone", request.From) + new("calling_phone", request.From), + new("twilio_call_sid", request.CallSid), + // Enable lazy routing mode to optimize realtime experience + new(StateConst.ROUTING_MODE, "lazy"), }; convService.SetConversationId(conversation.Id, states); convService.SaveStates(); + + return conversation.Id; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index 19144673..7b041ca6 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -108,7 +108,7 @@ public class TwilioVoiceController : TwilioController /// /// /// - // [ValidateRequest] + [ValidateRequest] [HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")] public async Task ReceiveCallerMessage(ConversationalVoiceRequest request) { @@ -202,7 +202,7 @@ public class TwilioVoiceController : TwilioController /// /// /// - // [ValidateRequest] + [ValidateRequest] [HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")] public async Task ReplyCallerMessage(ConversationalVoiceRequest request) { @@ -367,7 +367,7 @@ public class TwilioVoiceController : TwilioController return TwiML(response); } - // [ValidateRequest] + [ValidateRequest] [HttpPost("twilio/voice/init-call")] public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId) { @@ -388,7 +388,7 @@ public class TwilioVoiceController : TwilioController return TwiML(response); } - // [ValidateRequest] + [ValidateRequest] [HttpGet("twilio/voice/speeches/{conversationId}/{fileName}")] public async Task GetSpeechFile([FromRoute] string conversationId, [FromRoute] string fileName) { diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs index 1fe78116..e2b6ee13 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs @@ -4,6 +4,9 @@ namespace BotSharp.Plugin.Twilio.Models; public class ConversationalVoiceRequest : VoiceRequest { + [FromQuery(Name = "agent-id")] + public string AgentId { get; set; } + [FromRoute] public string ConversationId { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs index 63252a57..b7244e7d 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs @@ -2,6 +2,6 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums { public class UtilityName { - public const string OutboundPhoneCall = "twilio.twilio-outbound-phone-call"; + public const string OutboundPhoneCall = "phone.twilio-phone-call"; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HandleOutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HandleOutboundPhoneCallFn.cs deleted file mode 100644 index 12b827af..00000000 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HandleOutboundPhoneCallFn.cs +++ /dev/null @@ -1,106 +0,0 @@ -using BotSharp.Abstraction.Files; -using BotSharp.Abstraction.Infrastructures.Enums; -using BotSharp.Abstraction.Options; -using BotSharp.Abstraction.Routing; -using BotSharp.Core.Infrastructures; -using BotSharp.Plugin.Twilio.Interfaces; -using BotSharp.Plugin.Twilio.Models; -using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; -using Twilio.Rest.Api.V2010.Account; -using Twilio.Types; - -namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions -{ - public class HandleOutboundPhoneCallFn : IFunctionCallback - { - private readonly IServiceProvider _services; - private readonly ILogger _logger; - private readonly BotSharpOptions _options; - private readonly TwilioSetting _twilioSetting; - - public string Name => "util-twilio-twilio_outbound_phone_call"; - public string Indication => "Dialing the number"; - - public HandleOutboundPhoneCallFn( - IServiceProvider services, - ILogger logger, - BotSharpOptions options, - TwilioSetting twilioSetting) - { - _services = services; - _logger = logger; - _options = options; - _twilioSetting = twilioSetting; - } - - public async Task Execute(RoleDialogModel message) - { - var args = JsonSerializer.Deserialize(message.FunctionArgs, _options.JsonSerializerOptions); - if (args.PhoneNumber.Length != 12 || !args.PhoneNumber.StartsWith("+1", StringComparison.OrdinalIgnoreCase)) - { - var error = $"Invalid phone number format: {args.PhoneNumber}"; - _logger.LogError(error); - message.Content = error; - return false; - } - - if (string.IsNullOrWhiteSpace(args.InitialMessage)) - { - _logger.LogError("Initial message is empty."); - message.Content = "There is an error when generating phone message."; - return false; - } - - var convService = _services.GetRequiredService(); - var convStorage = _services.GetRequiredService(); - var routing = _services.GetRequiredService(); - var fileStorage = _services.GetRequiredService(); - var sessionManager = _services.GetRequiredService(); - var states = _services.GetRequiredService(); - - // Fork conversation - var entryAgentId = routing.EntryAgentId; - var newConv = await convService.NewConversation(new Abstraction.Conversations.Models.Conversation - { - AgentId = entryAgentId, - Channel = ConversationChannel.Phone - }); - var conversationId = newConv.Id; - convStorage.Append(conversationId, new List - { - new RoleDialogModel(AgentRole.User, "Hi") - { - CurrentAgentId = entryAgentId - }, - new RoleDialogModel(AgentRole.Assistant, args.InitialMessage) - { - CurrentAgentId = entryAgentId - } - }); - states.SetState(StateConst.SUB_CONVERSATION_ID, conversationId); - - // Generate audio - var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1"); - var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage); - var fileName = $"intial.mp3"; - fileStorage.SaveSpeechFile(conversationId, fileName, data); - - // Call phone number - /*await sessionManager.SetAssistantReplyAsync(conversationId, 0, new AssistantMessage - { - Content = args.InitialMessage, - SpeechFileName = fileName - });*/ - - var call = await CallResource.CreateAsync( - // url: new Uri($"{_twilioSetting.CallbackHost}/twilio/voice/init-call?conversationId={conversationId}"), - url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={conversationId}&init_audio_file={fileName}"), - to: new PhoneNumber(args.PhoneNumber), - from: new PhoneNumber(_twilioSetting.PhoneNumber)); - - message.Content = $"The generated phone message: {args.InitialMessage}." ?? message.Content; - message.StopCompletion = true; - return true; - } - } -} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs new file mode 100644 index 00000000..f54c1b42 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs @@ -0,0 +1,44 @@ +using Twilio.Rest.Api.V2010.Account; + +namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions; + +public class HangupPhoneCallFn : IFunctionCallback +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public string Name => "util-twilio-hangup_phone_call"; + public string Indication => "Hangup"; + + public HangupPhoneCallFn( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Execute(RoleDialogModel message) + { + var states = _services.GetRequiredService(); + var callSid = states.GetState("twilio_call_sid"); + + if (string.IsNullOrEmpty(callSid)) + { + message.Content = "The call has not been initiated."; + _logger.LogError(message.Content); + return false; + } + + // Have to find the SID by the phone number + var call = CallResource.Update( + status: CallResource.UpdateStatusEnum.Completed, + pathSid: callSid + ); + + message.Content = "The call has ended."; + message.StopCompletion = true; + + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs new file mode 100644 index 00000000..df97cd44 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -0,0 +1,125 @@ +using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.Infrastructures.Enums; +using BotSharp.Abstraction.Options; +using BotSharp.Abstraction.Routing; +using BotSharp.Core.Infrastructures; +using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; +using Twilio.Rest.Api.V2010.Account; +using Twilio.Types; +using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation; +using Task = System.Threading.Tasks.Task; + +namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions; + +public class OutboundPhoneCallFn : IFunctionCallback +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly BotSharpOptions _options; + private readonly TwilioSetting _twilioSetting; + + public string Name => "util-twilio-outbound_phone_call"; + public string Indication => "Dialing the phone number"; + + public OutboundPhoneCallFn( + IServiceProvider services, + ILogger logger, + BotSharpOptions options, + TwilioSetting twilioSetting) + { + _services = services; + _logger = logger; + _options = options; + _twilioSetting = twilioSetting; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs, _options.JsonSerializerOptions); + if (args.PhoneNumber.Length != 12 || !args.PhoneNumber.StartsWith("+1", StringComparison.OrdinalIgnoreCase)) + { + var error = $"Invalid phone number format: {args.PhoneNumber}"; + _logger.LogError(error); + message.Content = error; + return false; + } + + if (string.IsNullOrWhiteSpace(args.InitialMessage)) + { + _logger.LogError("Initial message is empty."); + message.Content = "There is an error when generating phone message."; + return false; + } + + var fileStorage = _services.GetRequiredService(); + var states = _services.GetRequiredService(); + + // Fork conversation + var newConversationId = Guid.NewGuid().ToString(); + states.SetState(StateConst.SUB_CONVERSATION_ID, newConversationId); + + // Generate initial assistant audio + var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1"); + var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage); + var fileName = $"intial.mp3"; + fileStorage.SaveSpeechFile(newConversationId, fileName, data); + + // Make outbound call + var call = await CallResource.CreateAsync( + url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={newConversationId}&init_audio_file={fileName}"), + to: new PhoneNumber(args.PhoneNumber), + from: new PhoneNumber(_twilioSetting.PhoneNumber)); + + var convService = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); + var originConversationId = convService.ConversationId; + var entryAgentId = routing.EntryAgentId; + + await ForkConversation(args, entryAgentId, originConversationId, newConversationId, call); + + message.Content = $"The generated phone message: \"{args.InitialMessage}.\" [NEW CONVERSATION ID: {newConversationId}, TWILIO CALL SID: {call.Sid}]"; + message.StopCompletion = true; + return true; + } + + private async Task ForkConversation(LlmContextIn args, + string entryAgentId, + string originConversationId, + string newConversationId, + CallResource resource) + { + // new scope service for isolated conversation + using var scope = _services.CreateScope(); + var services = scope.ServiceProvider; + var convService = services.GetRequiredService(); + var convStorage = services.GetRequiredService(); + + var newConv = await convService.NewConversation(new Conversation + { + Id = newConversationId, + AgentId = entryAgentId, + Channel = ConversationChannel.Phone, + ChannelId = resource.Sid, + Title = args.InitialMessage + }); + + convStorage.Append(newConversationId, new List + { + new RoleDialogModel(AgentRole.User, "Hi") + { + CurrentAgentId = entryAgentId + }, + new RoleDialogModel(AgentRole.Assistant, args.InitialMessage) + { + CurrentAgentId = entryAgentId + } + }); + + convService.SetConversationId(newConversationId, + [ + new MessageState(StateConst.ORIGIN_CONVERSATION_ID, originConversationId), + new MessageState("phone_number", resource.To) + ]); + convService.SaveStates(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs index 32639c9a..692610ff 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs @@ -6,15 +6,24 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks; public class OutboundPhoneCallHandlerUtilityHook : IAgentUtilityHook { private static string PREFIX = "util-twilio-"; - private static string OUTBOUND_PHONE_CALL_FN = $"{PREFIX}twilio_outbound_phone_call"; + private static string OUTBOUND_PHONE_CALL_FN = $"{PREFIX}outbound_phone_call"; + private static string HANGUP_PHONE_CALL_FN = $"{PREFIX}hangup_phone_call"; public void AddUtilities(List utilities) { var utility = new AgentUtility { Name = UtilityName.OutboundPhoneCall, - Functions = [new($"{OUTBOUND_PHONE_CALL_FN}")], - Templates = [new($"{OUTBOUND_PHONE_CALL_FN}.fn")] + Functions = + [ + new($"{OUTBOUND_PHONE_CALL_FN}"), + new($"{HANGUP_PHONE_CALL_FN}") + ], + Templates = + [ + new($"{OUTBOUND_PHONE_CALL_FN}.fn"), + new($"{HANGUP_PHONE_CALL_FN}.fn") + ] }; utilities.Add(utility); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json new file mode 100644 index 00000000..76b0e841 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json @@ -0,0 +1,11 @@ +{ + "name": "util-twilio-hangup_phone_call", + "description": "Call this function if the user wants to end the phone call", + "visibility_expression": "{% if states.channel == 'phone' %}visible{% endif %}", + "parameters": { + "type": "object", + "properties": { + }, + "required": [] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-twilio_outbound_phone_call.json b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-outbound_phone_call.json similarity index 76% rename from src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-twilio_outbound_phone_call.json rename to src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-outbound_phone_call.json index 768c6fde..a93ec3cf 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-twilio_outbound_phone_call.json +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-outbound_phone_call.json @@ -1,15 +1,16 @@ { - "name": "util-twilio-twilio_outbound_phone_call", + "name": "util-twilio-outbound_phone_call", "description": "If the user wants to initiate a phone call, you need to capture the phone number and compose the message the users wants to send. Then call this function to make an outbound call via Twilio.", + "visibility_expression": "{% if states.channel != 'phone' %}visible{% endif %}", "parameters": { "type": "object", "properties": { "phone_number": { - "to_read": "string", + "type": "string", "description": "The phone number which will be dialed. It needs to be a valid phone number starting with +1." }, "initial_message": { - "to_read": "string", + "type": "string", "description": "The initial message which will be sent." } }, diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-hangup_phone_call.fn.liquid b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-hangup_phone_call.fn.liquid new file mode 100644 index 00000000..cc8360be --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-hangup_phone_call.fn.liquid @@ -0,0 +1 @@ +** Please call util-twilio-hangup_phone_call if user wants to end the phone call. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-outbound_phone_call.fn.liquid b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-outbound_phone_call.fn.liquid new file mode 100644 index 00000000..d8fa9131 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-outbound_phone_call.fn.liquid @@ -0,0 +1 @@ +** Please call util-twilio-outbound_phone_call if user wants to make an outbound call. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-twilio_outbound_phone_call.fn.liquid b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-twilio_outbound_phone_call.fn.liquid deleted file mode 100644 index 3bfb7dc2..00000000 --- a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-twilio_outbound_phone_call.fn.liquid +++ /dev/null @@ -1,2 +0,0 @@ -** Please take a look at the conversation and decide whether user wants to make an outbound call. -** Please call util-twilio-twilio_outbound_phone_call if user wants to make an outbound call. \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json index 94746011..3a28b3e0 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json @@ -1,7 +1,7 @@ { "id": "8970b1e5-d260-4e2c-90b1-f1415a257c18", "name": "Pizza Bot", - "description": "AI assistant that can help customer place pizza order.", + "description": "AI assistant that can help customer place pizza order, make payment or inquiry order status.", "type": "routing", "inheritAgentId": "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a", "createdDateTime": "2023-08-18T10:39:32.2349685Z", @@ -10,10 +10,11 @@ "disabled": false, "isPublic": true, "profiles": [ "pizza" ], + "labels": [ "experiment" ], "routingRules": [ { "type": "reasoner", - "field": "NaiveReasoner" + "field": "Naive Reasoner" } ] } \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json index 4f606251..09f7489b 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json @@ -6,5 +6,6 @@ "id": "b284db86-e9c2-4c25-a59e-4649797dd130", "disabled": false, "isPublic": true, - "profiles": [ "pizza" ] + "profiles": [ "pizza" ], + "labels": [ "experiment" ] } \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json index 021fa3c5..7ec18d46 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json @@ -1,6 +1,6 @@ { "name": "Ordering", - "description": "Provide types of pizza available, unit price and total cost. Place the order and returned the order number.", + "description": "Provide types of pizza available, unit price, total cost and place the order.", "createdDateTime": "2023-07-26T02:29:25.123224Z", "updatedDateTime": "2023-07-26T02:29:25.123274Z", "id": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd", @@ -21,4 +21,5 @@ ] } ] + "labels": [ "experiment" ] } \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json index 90ee8f69..f34ffb76 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json @@ -1,6 +1,6 @@ { "name": "Payment", - "description": "Make payment when user wants to pay for the order", + "description": "Make payment when user confirmed the price and going to pay for the order", "createdDateTime": "2023-07-26T02:29:25.123224Z", "updatedDateTime": "2023-07-26T02:29:25.123274Z", "id": "fe8c60aa-b114-4ef3-93cb-a8efeac80f75", @@ -18,6 +18,7 @@ ] } ], + "labels": [ "experiment" ], "routingRules": [ { "field": "order_number", diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/BotSharp.Plugin.SemanticKernel.UnitTests.csproj b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/BotSharp.Plugin.SemanticKernel.UnitTests.csproj index c47fcead..ee159e86 100644 --- a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/BotSharp.Plugin.SemanticKernel.UnitTests.csproj +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/BotSharp.Plugin.SemanticKernel.UnitTests.csproj @@ -10,7 +10,6 @@ -