diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs index 9d0662db0..c4f634508 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/Options/BotSharpOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs index 90168872a..b8609422d 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 60fec1dc9..ec521637e 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.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index 9fa5c7185..2be594f5c 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -110,6 +110,10 @@ public void StartTimer() 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 83a251aee..ee59fb62e 100644 --- a/src/Infrastructure/BotSharp.Core/Realtime/RealtimeHub.cs +++ b/src/Infrastructure/BotSharp.Core/Realtime/RealtimeHub.cs @@ -71,9 +71,11 @@ private async Task ConnectToModel(IRealTimeCompletion completer, WebSocket userW 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) { @@ -125,21 +127,19 @@ await completer.Connect(conn, { await routing.InvokeFunction(message.FunctionName, message); message.Role = AgentRole.Function; - if (message.FunctionName == "route_to_agent") + if (message.FunctionName == "route_to_agent" || + message.FunctionName == "util-routing-fallback_to_router") { var routedAgentId = routing.Context.GetCurrentAgentId(); - if (conn.EntryAgentId != routedAgentId) + if (conn.CurrentAgentId != routedAgentId) { - conn.EntryAgentId = routedAgentId; + conn.CurrentAgentId = routedAgentId; await completer.UpdateSession(conn); - await completer.TriggerModelInference("Reply based on the function's output."); } } - else - { - await completer.InsertConversationItem(message); - await completer.TriggerModelInference("Reply based on the function's output."); - } + + await completer.InsertConversationItem(message); + await completer.TriggerModelInference("Reply based on the function's output."); } else { diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 273fc9968..bec44bc4f 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -54,7 +54,7 @@ public async Task InvokeAgent(string agentId, List dialog 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?"; + response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?"; } message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content); 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 index cd9549c35..77f0dbfd6 100644 --- 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 @@ -1,6 +1,6 @@ { "name": "util-routing-fallback_to_router", - "description": "Get the appropriate agent who can handle the user request.", + "description": "Return to the Router to find the appropriate agent who can handle the user's request.", "parameters": { "type": "object", "properties": { 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 index ab3863f89..a1bd9ceb2 100644 --- 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 @@ -1 +1 @@ -"If you're unsure whether you understand the user's request or if the user brings up an unrelated topic, call the function `util-routing-fallback_to_router` to get the appropriate agent from the router." \ No newline at end of file +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/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 351bb3a36..22ec7d4d1 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -85,6 +85,33 @@ public override async Task OnPostbackMessageReceived(RoleDialogModel message, Po 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; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj b/src/Plugins/BotSharp.Plugin.OpenAI/BotSharp.Plugin.OpenAI.csproj index 9a9c57fb7..0a509b1ac 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 d4b747719..722972f8b 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -1,7 +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; @@ -212,14 +214,9 @@ public async Task SendEventToModel(object message) if (message is not string data) { - data = JsonSerializer.Serialize(message, options: new JsonSerializerOptions - { - WriteIndented = true - }); + data = JsonSerializer.Serialize(message, BotSharpOptions.defaultJsonOptions); } - _logger.LogInformation($"SendEventToModel:\r\n{data}"); - var buffer = Encoding.UTF8.GetBytes(data); await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); @@ -265,13 +262,23 @@ public async Task UpdateSession(RealtimeHubConnection conn) var conv = await convService.GetConversation(conn.ConversationId); var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(conn.EntryAgentId); + 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 { @@ -287,21 +294,17 @@ public async Task UpdateSession(RealtimeHubConnection conn) 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) } }; + await HookEmitter.Emit(_services, async hook => + { + await hook.OnSessionUpdated(agent, instruction, functions); + }); + await SendEventToModel(sessionUpdate); } @@ -568,7 +571,7 @@ public async Task> OnResponsedDone(RealtimeHubConnection c { outputs.Add(new RoleDialogModel(output.Role, output.Arguments) { - CurrentAgentId = conn.EntryAgentId, + CurrentAgentId = conn.CurrentAgentId, FunctionName = output.Name, FunctionArgs = output.Arguments, ToolCallId = output.CallId, @@ -581,11 +584,27 @@ public async Task> OnResponsedDone(RealtimeHubConnection c 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; } @@ -594,7 +613,7 @@ public async Task OnInputAudioTranscriptionCompleted(RealtimeHu 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 c21800769..097a32b7e 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.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 @@ private async Task GetTextCompletion(string apiUrl, stri 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 @@ private async Task GetTextCompletion(string apiUrl, stri 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)