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);
}
}