diff --git a/Directory.Build.props b/Directory.Build.props index 5ca4cb96..0879abfc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net8.0 12.0 - 5.0.0 + 5.1.0 true false diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs index ff26e981..a8709810 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs @@ -19,4 +19,9 @@ public class MessageState Value = value; ActiveRounds = activeRounds; } + + public override string ToString() + { + return $"Key: {Key} => Value: {Value}, ActiveRounds: {ActiveRounds}"; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs index c10ffd8d..833cb013 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs @@ -54,16 +54,23 @@ public class SideCarAttribute : AsyncMoAttribute private (IConversationSideCar?, MethodInfo?) GetSideCarMethod(IServiceProvider serviceProvider, string methodName, Type retType, object[] args) { - var sidecar = serviceProvider.GetService(); - var argTypes = args.Select(x => x.GetType()).ToArray(); - var sidecarMethod = sidecar?.GetType()?.GetMethods(BindingFlags.Public | BindingFlags.Instance) - .FirstOrDefault(x => x.Name == methodName - && x.ReturnType == retType - && x.GetParameters().Length == argTypes.Length - && x.GetParameters().Select(p => p.ParameterType) - .Zip(argTypes, (paramType, argType) => paramType.IsAssignableFrom(argType)).All(y => y)); + try + { + var sidecar = serviceProvider.GetService(); + var argTypes = args.Select(x => x.GetType()).ToArray(); + var sidecarMethod = sidecar?.GetType()?.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(x => x.Name == methodName + && x.ReturnType == retType + && x.GetParameters().Length == argTypes.Length + && x.GetParameters().Select(p => p.ParameterType) + .Zip(argTypes, (paramType, argType) => paramType.IsAssignableFrom(argType)).All(y => y)); - return (sidecar, sidecarMethod); + return (sidecar, sidecarMethod); + } + catch + { + return (null, null); + } } private async Task<(bool, object?)> CallAsyncMethod(IConversationSideCar instance, MethodInfo method, Type retType, object[] args) diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs index 84b65573..3c9bcfa4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.SideCar.Models; + namespace BotSharp.Abstraction.SideCar; public interface IConversationSideCar @@ -9,6 +11,10 @@ public interface IConversationSideCar List GetConversationDialogs(string conversationId); void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint); ConversationBreakpoint? GetConversationBreakpoint(string conversationId); + void UpdateConversationStates(string conversationId, List states); Task SendMessage(string agentId, string text, - PostbackMessageModel? postback = null, List? states = null, List? dialogs = null); + PostbackMessageModel? postback = null, + List? states = null, + List? dialogs = null, + SideCarOptions? options = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs new file mode 100644 index 00000000..a4858c5c --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Abstraction.SideCar.Models; + +public class SideCarOptions +{ + public bool IsInheritStates { get; set; } + public IEnumerable? InheritStateKeys { get; set; } + + public static SideCarOptions Empty() + { + return new SideCarOptions(); + } +} diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs index 34a98017..e4996b06 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs +++ b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs @@ -14,6 +14,7 @@ limitations under the License. ******************************************************************************/ +using BotSharp.Abstraction.SideCar.Models; using BotSharp.Core.Infrastructures; namespace BotSharp.Core.SideCar.Services; @@ -23,9 +24,11 @@ public class BotSharpConversationSideCar : IConversationSideCar private readonly IServiceProvider _services; private readonly ILogger _logger; - private Stack contextStack = new(); + private Stack _contextStack = new(); + private SideCarOptions? _sideCarOptions; - private bool enabled = false; + private bool _enabled = false; + private string _conversationId = string.Empty; public string Provider => "botsharp"; @@ -39,49 +42,71 @@ public class BotSharpConversationSideCar : IConversationSideCar public bool IsEnabled() { - return enabled; + return _enabled; } public void AppendConversationDialogs(string conversationId, List messages) { - if (contextStack.IsNullOrEmpty()) return; + if (!IsValid(conversationId)) + { + return; + } - var top = contextStack.Peek(); + var top = _contextStack.Peek(); top.Dialogs.AddRange(messages); } public List GetConversationDialogs(string conversationId) { - if (contextStack.IsNullOrEmpty()) + if (!IsValid(conversationId)) { return new List(); } - return contextStack.Peek().Dialogs; + return _contextStack.Peek().Dialogs; } public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) { - if (contextStack.IsNullOrEmpty()) return; + if (!IsValid(conversationId)) + { + return; + } - var top = contextStack.Peek().Breakpoints; + var top = _contextStack.Peek().Breakpoints; top.Add(breakpoint); } public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) { - if (contextStack.IsNullOrEmpty()) + if (!IsValid(conversationId)) { return null; } - var top = contextStack.Peek().Breakpoints; + var top = _contextStack.Peek().Breakpoints; return top.LastOrDefault(); } - public async Task SendMessage(string agentId, string text, - PostbackMessageModel? postback = null, List? states = null, List? dialogs = null) + public void UpdateConversationStates(string conversationId, List states) { + if (!IsValid(conversationId)) + { + return; + } + + var top = _contextStack.Peek(); + top.State = new ConversationState(states); + } + + public async Task SendMessage(string agentId, string text, + PostbackMessageModel? postback = null, + List? states = null, + List? dialogs = null, + SideCarOptions? options = null) + { + _sideCarOptions = options; + BeforeExecute(dialogs); var response = await InnerExecute(agentId, text, postback, states); AfterExecute(); @@ -94,6 +119,7 @@ public class BotSharpConversationSideCar : IConversationSideCar var conv = _services.GetRequiredService(); var routing = _services.GetRequiredService(); var state = _services.GetRequiredService(); + _conversationId = conv.ConversationId; var inputMsg = new RoleDialogModel(AgentRole.User, text); routing.Context.SetMessageId(conv.ConversationId, inputMsg.MessageId); @@ -116,7 +142,7 @@ public class BotSharpConversationSideCar : IConversationSideCar private void BeforeExecute(List? dialogs) { - enabled = true; + _enabled = true; var state = _services.GetRequiredService(); var routing = _services.GetRequiredService(); @@ -129,7 +155,7 @@ public class BotSharpConversationSideCar : IConversationSideCar RecursiveCounter = routing.Context.GetRecursiveCounter(), RoutingStack = routing.Context.GetAgentStack() }; - contextStack.Push(node); + _contextStack.Push(node); // Reset state.ResetCurrentState(); @@ -141,17 +167,62 @@ public class BotSharpConversationSideCar : IConversationSideCar private void AfterExecute() { - var state = _services.GetRequiredService(); var routing = _services.GetRequiredService(); - - var node = contextStack.Pop(); + var node = _contextStack.Pop(); // Recover - state.SetCurrentState(node.State); + RestoreStates(node.State); routing.Context.SetRecursiveCounter(node.RecursiveCounter); routing.Context.SetAgentStack(node.RoutingStack); routing.Context.SetDialogs(node.RoutingDialogs); Utilities.ClearCache(); - enabled = false; + _enabled = false; + } + + private bool IsValid(string conversationId) + { + return !_contextStack.IsNullOrEmpty() + && _conversationId == conversationId + && !string.IsNullOrEmpty(conversationId) + && !string.IsNullOrEmpty(_conversationId); + } + + private void RestoreStates(ConversationState prevStates) + { + var innerStates = prevStates; + var state = _services.GetRequiredService(); + + if (_sideCarOptions?.IsInheritStates == true) + { + var curStates = state.GetCurrentState(); + foreach (var pair in curStates) + { + var endNode = pair.Value.Values.LastOrDefault(); + if (endNode == null) continue; + + if (_sideCarOptions?.InheritStateKeys?.Any() == true + && !_sideCarOptions.InheritStateKeys.Contains(pair.Key)) + { + continue; + } + + if (innerStates.ContainsKey(pair.Key)) + { + innerStates[pair.Key].Values.Add(endNode); + } + else + { + innerStates[pair.Key] = new StateKeyValue + { + Key = pair.Key, + Versioning = pair.Value.Versioning, + Readonly = pair.Value.Readonly, + Values = [endNode] + }; + } + } + } + + state.SetCurrentState(innerStates); } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Using.cs b/src/Infrastructure/BotSharp.Core.SideCar/Using.cs index d047ee15..e391d790 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/Using.cs +++ b/src/Infrastructure/BotSharp.Core.SideCar/Using.cs @@ -16,5 +16,6 @@ global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Routing; global using BotSharp.Abstraction.SideCar; +global using BotSharp.Abstraction.SideCar.Models; global using BotSharp.Abstraction.Utilities; -global using BotSharp.Core.SideCar.Settings; \ No newline at end of file +global using BotSharp.Core.SideCar.Settings; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index c8665179..ff61fde2 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -443,7 +443,7 @@ public class ConversationStateService : IConversationStateService public void SetCurrentState(ConversationState state) { - var values = _curStates.Values.ToList(); + var values = state.Values.ToList(); var copy = JsonSerializer.Deserialize>(JsonSerializer.Serialize(values)); _curStates = new ConversationState(copy ?? []); } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 3b60a61a..3d03e816 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -307,6 +307,7 @@ public partial class FileRepository return new ConversationState(states); } + [SideCar] public void UpdateConversationStates(string conversationId, List states) { if (states.IsNullOrEmpty()) return; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs index 59ff3b9a..a37e2407 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs @@ -65,6 +65,12 @@ public static class ReasonerHelper } } + if (args.AgentName == "response_to_user") + { + args.AgentName = ""; + malformed = true; + } + if (malformed) { Console.WriteLine($"Captured LLM malformed response"); diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json index 9e7f8929..6f6c995a 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json @@ -7,6 +7,10 @@ "content": { "type": "string", "description": "Response content" + }, + "conversation_end": { + "type": "boolean", + "description": "User is ending the conversation." } }, "required": [ "content" ] diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index bcbcc2a4..1335d4dc 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -367,7 +367,7 @@ public class ChatCompletionProvider : IChatCompletion { messages.Add(new AssistantChatMessage(new List { - ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) })); messages.Add(new ToolChatMessage(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.Content)); diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs index 31fa964c..2bbc950a 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs @@ -35,7 +35,7 @@ public class KnowledgeBasePlugin : IBotSharpPlugin var section = menu.First(x => x.Label == "Apps"); menu.Add(new PluginMenuDef("Knowledge Base", icon: "bx bx-book-open", weight: section.Weight + 1) { - Roles = new List { UserRole.Root, UserRole.Admin }, + Roles = new List { UserRole.Root, UserRole.Admin, UserRole.Engineer }, SubMenu = new List { new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"), diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index 02f5a695..edcf3a5f 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -266,6 +266,7 @@ public partial class MongoRepository return new ConversationState(savedStates); } + [SideCar] public void UpdateConversationStates(string conversationId, List states) { if (string.IsNullOrEmpty(conversationId) || states == null) return; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 8219bf1c..92fa6f84 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -615,10 +615,10 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { messages.Add(new AssistantChatMessage(new List { - ChatToolCall.CreateFunctionToolCall(message.ToolCallId, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + ChatToolCall.CreateFunctionToolCall(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? "{}")) })); - messages.Add(new ToolChatMessage(message.ToolCallId, message.Content)); + messages.Add(new ToolChatMessage(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.Content)); } else if (message.Role == AgentRole.User) { diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs index 511abb45..07c4ad87 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs @@ -63,6 +63,11 @@ public class TwilioInboundController : TwilioController instruction.AgentId = request.AgentId; instruction.ConversationId = request.ConversationId; + await HookEmitter.Emit(_services, async hook => + { + await hook.OnSessionCreated(request); + }, request.AgentId); + if (twilio.MachineDetected(request)) { response = new VoiceResponse(); @@ -114,12 +119,7 @@ public class TwilioInboundController : TwilioController await Task.Delay(1500); await twilio.StartRecording(request.CallSid, request.AgentId, request.ConversationId); }); - } - - await HookEmitter.Emit(_services, async hook => - { - await hook.OnSessionCreated(request); - }, request.AgentId); + } return TwiML(response); } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 5ffd692e..8d34dd22 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -1,7 +1,9 @@ using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Options; +using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Utilities; using BotSharp.Core.Infrastructures; using BotSharp.Plugin.Twilio.Interfaces; using BotSharp.Plugin.Twilio.Models; @@ -134,9 +136,10 @@ public class OutboundPhoneCallFn : IFunctionCallback } } - private async Task ForkConversation(LlmContextIn args, - string entryAgentId, - string originConversationId, + private async Task ForkConversation( + LlmContextIn args, + string entryAgentId, + string originConversationId, string newConversationId, CallResource call) { @@ -145,6 +148,8 @@ public class OutboundPhoneCallFn : IFunctionCallback var services = scope.ServiceProvider; var convService = services.GetRequiredService(); var convStorage = services.GetRequiredService(); + var state = _services.GetRequiredService(); + var db = _services.GetRequiredService(); var newConv = await convService.NewConversation(new Conversation { @@ -170,15 +175,45 @@ public class OutboundPhoneCallFn : IFunctionCallback } }); - convService.SetConversationId(newConversationId, - [ - new MessageState(StateConst.ORIGIN_CONVERSATION_ID, originConversationId), - new MessageState("channel", "phone"), - new MessageState("phone_from", call.From), - new MessageState("phone_direction", call.Direction), - new MessageState("phone_number", call.To), - new MessageState("twilio_call_sid", call.Sid) - ]); - convService.SaveStates(); + var utcNow = DateTime.UtcNow; + var excludStates = new List + { + "provider", + "model", + "prompt_total", + "completion_total", + "llm_total_cost" + }; + + var curStates = state.GetStates().Select(x => new MessageState(x.Key, x.Value)).ToList(); + var subConvStates = new List + { + new(StateConst.ORIGIN_CONVERSATION_ID, originConversationId), + new("channel", "phone"), + new("phone_from", call.From), + new("phone_direction", call.Direction), + new("phone_number", call.To), + new("twilio_call_sid", call.Sid) + }; + var subStateKeys = subConvStates.Select(x => x.Key).ToList(); + var included = curStates.Where(x => !subStateKeys.Contains(x.Key) && !excludStates.Contains(x.Key)); + var newStates = subConvStates.Concat(included).Select(x => new StateKeyValue + { + Key = x.Key, + Versioning = true, + Values = [ + new StateValue + { + Data = x.Value.ConvertToString(_options.JsonSerializerOptions), + MessageId = messageId, + Active = true, + ActiveRounds = x.ActiveRounds, + Source = StateSource.Application, + UpdateTime = utcNow + } + ] + }).ToList(); + + db.UpdateConversationStates(newConversationId, newStates); } }