Merge branch 'master' of https://github.com/SciSharp/BotSharp into features/init-reactive-x

This commit is contained in:
Jicheng Lu 2025-07-08 17:11:25 -05:00
commit d41eb4a7e0
17 changed files with 205 additions and 56 deletions

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion> <LangVersion>12.0</LangVersion>
<BotSharpVersion>5.0.0</BotSharpVersion> <BotSharpVersion>5.1.0</BotSharpVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View file

@ -19,4 +19,9 @@ public class MessageState
Value = value; Value = value;
ActiveRounds = activeRounds; ActiveRounds = activeRounds;
} }
public override string ToString()
{
return $"Key: {Key} => Value: {Value}, ActiveRounds: {ActiveRounds}";
}
} }

View file

@ -54,16 +54,23 @@ public class SideCarAttribute : AsyncMoAttribute
private (IConversationSideCar?, MethodInfo?) GetSideCarMethod(IServiceProvider serviceProvider, string methodName, Type retType, object[] args) private (IConversationSideCar?, MethodInfo?) GetSideCarMethod(IServiceProvider serviceProvider, string methodName, Type retType, object[] args)
{ {
var sidecar = serviceProvider.GetService<IConversationSideCar>(); try
var argTypes = args.Select(x => x.GetType()).ToArray(); {
var sidecarMethod = sidecar?.GetType()?.GetMethods(BindingFlags.Public | BindingFlags.Instance) var sidecar = serviceProvider.GetService<IConversationSideCar>();
.FirstOrDefault(x => x.Name == methodName var argTypes = args.Select(x => x.GetType()).ToArray();
&& x.ReturnType == retType var sidecarMethod = sidecar?.GetType()?.GetMethods(BindingFlags.Public | BindingFlags.Instance)
&& x.GetParameters().Length == argTypes.Length .FirstOrDefault(x => x.Name == methodName
&& x.GetParameters().Select(p => p.ParameterType) && x.ReturnType == retType
.Zip(argTypes, (paramType, argType) => paramType.IsAssignableFrom(argType)).All(y => y)); && 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) private async Task<(bool, object?)> CallAsyncMethod(IConversationSideCar instance, MethodInfo method, Type retType, object[] args)

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.SideCar.Models;
namespace BotSharp.Abstraction.SideCar; namespace BotSharp.Abstraction.SideCar;
public interface IConversationSideCar public interface IConversationSideCar
@ -9,6 +11,10 @@ public interface IConversationSideCar
List<DialogElement> GetConversationDialogs(string conversationId); List<DialogElement> GetConversationDialogs(string conversationId);
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint); void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);
ConversationBreakpoint? GetConversationBreakpoint(string conversationId); ConversationBreakpoint? GetConversationBreakpoint(string conversationId);
void UpdateConversationStates(string conversationId, List<StateKeyValue> states);
Task<RoleDialogModel> SendMessage(string agentId, string text, Task<RoleDialogModel> SendMessage(string agentId, string text,
PostbackMessageModel? postback = null, List<MessageState>? states = null, List<DialogElement>? dialogs = null); PostbackMessageModel? postback = null,
List<MessageState>? states = null,
List<DialogElement>? dialogs = null,
SideCarOptions? options = null);
} }

View file

@ -0,0 +1,12 @@
namespace BotSharp.Abstraction.SideCar.Models;
public class SideCarOptions
{
public bool IsInheritStates { get; set; }
public IEnumerable<string>? InheritStateKeys { get; set; }
public static SideCarOptions Empty()
{
return new SideCarOptions();
}
}

View file

@ -14,6 +14,7 @@
limitations under the License. limitations under the License.
******************************************************************************/ ******************************************************************************/
using BotSharp.Abstraction.SideCar.Models;
using BotSharp.Core.Infrastructures; using BotSharp.Core.Infrastructures;
namespace BotSharp.Core.SideCar.Services; namespace BotSharp.Core.SideCar.Services;
@ -23,9 +24,11 @@ public class BotSharpConversationSideCar : IConversationSideCar
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private readonly ILogger<BotSharpConversationSideCar> _logger; private readonly ILogger<BotSharpConversationSideCar> _logger;
private Stack<ConversationContext> contextStack = new(); private Stack<ConversationContext> _contextStack = new();
private SideCarOptions? _sideCarOptions;
private bool enabled = false; private bool _enabled = false;
private string _conversationId = string.Empty;
public string Provider => "botsharp"; public string Provider => "botsharp";
@ -39,49 +42,71 @@ public class BotSharpConversationSideCar : IConversationSideCar
public bool IsEnabled() public bool IsEnabled()
{ {
return enabled; return _enabled;
} }
public void AppendConversationDialogs(string conversationId, List<DialogElement> messages) public void AppendConversationDialogs(string conversationId, List<DialogElement> messages)
{ {
if (contextStack.IsNullOrEmpty()) return; if (!IsValid(conversationId))
{
return;
}
var top = contextStack.Peek(); var top = _contextStack.Peek();
top.Dialogs.AddRange(messages); top.Dialogs.AddRange(messages);
} }
public List<DialogElement> GetConversationDialogs(string conversationId) public List<DialogElement> GetConversationDialogs(string conversationId)
{ {
if (contextStack.IsNullOrEmpty()) if (!IsValid(conversationId))
{ {
return new List<DialogElement>(); return new List<DialogElement>();
} }
return contextStack.Peek().Dialogs; return _contextStack.Peek().Dialogs;
} }
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) 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); top.Add(breakpoint);
} }
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
{ {
if (contextStack.IsNullOrEmpty()) if (!IsValid(conversationId))
{ {
return null; return null;
} }
var top = contextStack.Peek().Breakpoints; var top = _contextStack.Peek().Breakpoints;
return top.LastOrDefault(); return top.LastOrDefault();
} }
public async Task<RoleDialogModel> SendMessage(string agentId, string text, public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
PostbackMessageModel? postback = null, List<MessageState>? states = null, List<DialogElement>? dialogs = null)
{ {
if (!IsValid(conversationId))
{
return;
}
var top = _contextStack.Peek();
top.State = new ConversationState(states);
}
public async Task<RoleDialogModel> SendMessage(string agentId, string text,
PostbackMessageModel? postback = null,
List<MessageState>? states = null,
List<DialogElement>? dialogs = null,
SideCarOptions? options = null)
{
_sideCarOptions = options;
BeforeExecute(dialogs); BeforeExecute(dialogs);
var response = await InnerExecute(agentId, text, postback, states); var response = await InnerExecute(agentId, text, postback, states);
AfterExecute(); AfterExecute();
@ -94,6 +119,7 @@ public class BotSharpConversationSideCar : IConversationSideCar
var conv = _services.GetRequiredService<IConversationService>(); var conv = _services.GetRequiredService<IConversationService>();
var routing = _services.GetRequiredService<IRoutingService>(); var routing = _services.GetRequiredService<IRoutingService>();
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
_conversationId = conv.ConversationId;
var inputMsg = new RoleDialogModel(AgentRole.User, text); var inputMsg = new RoleDialogModel(AgentRole.User, text);
routing.Context.SetMessageId(conv.ConversationId, inputMsg.MessageId); routing.Context.SetMessageId(conv.ConversationId, inputMsg.MessageId);
@ -116,7 +142,7 @@ public class BotSharpConversationSideCar : IConversationSideCar
private void BeforeExecute(List<DialogElement>? dialogs) private void BeforeExecute(List<DialogElement>? dialogs)
{ {
enabled = true; _enabled = true;
var state = _services.GetRequiredService<IConversationStateService>(); var state = _services.GetRequiredService<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>(); var routing = _services.GetRequiredService<IRoutingService>();
@ -129,7 +155,7 @@ public class BotSharpConversationSideCar : IConversationSideCar
RecursiveCounter = routing.Context.GetRecursiveCounter(), RecursiveCounter = routing.Context.GetRecursiveCounter(),
RoutingStack = routing.Context.GetAgentStack() RoutingStack = routing.Context.GetAgentStack()
}; };
contextStack.Push(node); _contextStack.Push(node);
// Reset // Reset
state.ResetCurrentState(); state.ResetCurrentState();
@ -141,17 +167,62 @@ public class BotSharpConversationSideCar : IConversationSideCar
private void AfterExecute() private void AfterExecute()
{ {
var state = _services.GetRequiredService<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>(); var routing = _services.GetRequiredService<IRoutingService>();
var node = _contextStack.Pop();
var node = contextStack.Pop();
// Recover // Recover
state.SetCurrentState(node.State); RestoreStates(node.State);
routing.Context.SetRecursiveCounter(node.RecursiveCounter); routing.Context.SetRecursiveCounter(node.RecursiveCounter);
routing.Context.SetAgentStack(node.RoutingStack); routing.Context.SetAgentStack(node.RoutingStack);
routing.Context.SetDialogs(node.RoutingDialogs); routing.Context.SetDialogs(node.RoutingDialogs);
Utilities.ClearCache(); 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<IConversationStateService>();
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);
} }
} }

View file

@ -16,5 +16,6 @@ global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Models;
global using BotSharp.Abstraction.Routing; global using BotSharp.Abstraction.Routing;
global using BotSharp.Abstraction.SideCar; global using BotSharp.Abstraction.SideCar;
global using BotSharp.Abstraction.SideCar.Models;
global using BotSharp.Abstraction.Utilities; global using BotSharp.Abstraction.Utilities;
global using BotSharp.Core.SideCar.Settings; global using BotSharp.Core.SideCar.Settings;

View file

@ -443,7 +443,7 @@ public class ConversationStateService : IConversationStateService
public void SetCurrentState(ConversationState state) public void SetCurrentState(ConversationState state)
{ {
var values = _curStates.Values.ToList(); var values = state.Values.ToList();
var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values)); var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values));
_curStates = new ConversationState(copy ?? []); _curStates = new ConversationState(copy ?? []);
} }

View file

@ -307,6 +307,7 @@ public partial class FileRepository
return new ConversationState(states); return new ConversationState(states);
} }
[SideCar]
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states) public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
{ {
if (states.IsNullOrEmpty()) return; if (states.IsNullOrEmpty()) return;

View file

@ -65,6 +65,12 @@ public static class ReasonerHelper
} }
} }
if (args.AgentName == "response_to_user")
{
args.AgentName = "";
malformed = true;
}
if (malformed) if (malformed)
{ {
Console.WriteLine($"Captured LLM malformed response"); Console.WriteLine($"Captured LLM malformed response");

View file

@ -7,6 +7,10 @@
"content": { "content": {
"type": "string", "type": "string",
"description": "Response content" "description": "Response content"
},
"conversation_end": {
"type": "boolean",
"description": "User is ending the conversation."
} }
}, },
"required": [ "content" ] "required": [ "content" ]

View file

@ -367,7 +367,7 @@ public class ChatCompletionProvider : IChatCompletion
{ {
messages.Add(new AssistantChatMessage(new List<ChatToolCall> messages.Add(new AssistantChatMessage(new List<ChatToolCall>
{ {
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)); messages.Add(new ToolChatMessage(message.ToolCallId.IfNullOrEmptyAs(message.FunctionName), message.Content));

View file

@ -35,7 +35,7 @@ public class KnowledgeBasePlugin : IBotSharpPlugin
var section = menu.First(x => x.Label == "Apps"); var section = menu.First(x => x.Label == "Apps");
menu.Add(new PluginMenuDef("Knowledge Base", icon: "bx bx-book-open", weight: section.Weight + 1) menu.Add(new PluginMenuDef("Knowledge Base", icon: "bx bx-book-open", weight: section.Weight + 1)
{ {
Roles = new List<string> { UserRole.Root, UserRole.Admin }, Roles = new List<string> { UserRole.Root, UserRole.Admin, UserRole.Engineer },
SubMenu = new List<PluginMenuDef> SubMenu = new List<PluginMenuDef>
{ {
new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"), new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"),

View file

@ -266,6 +266,7 @@ public partial class MongoRepository
return new ConversationState(savedStates); return new ConversationState(savedStates);
} }
[SideCar]
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states) public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
{ {
if (string.IsNullOrEmpty(conversationId) || states == null) return; if (string.IsNullOrEmpty(conversationId) || states == null) return;

View file

@ -615,10 +615,10 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
{ {
messages.Add(new AssistantChatMessage(new List<ChatToolCall> messages.Add(new AssistantChatMessage(new List<ChatToolCall>
{ {
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) else if (message.Role == AgentRole.User)
{ {

View file

@ -63,6 +63,11 @@ public class TwilioInboundController : TwilioController
instruction.AgentId = request.AgentId; instruction.AgentId = request.AgentId;
instruction.ConversationId = request.ConversationId; instruction.ConversationId = request.ConversationId;
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreated(request);
}, request.AgentId);
if (twilio.MachineDetected(request)) if (twilio.MachineDetected(request))
{ {
response = new VoiceResponse(); response = new VoiceResponse();
@ -114,12 +119,7 @@ public class TwilioInboundController : TwilioController
await Task.Delay(1500); await Task.Delay(1500);
await twilio.StartRecording(request.CallSid, request.AgentId, request.ConversationId); await twilio.StartRecording(request.CallSid, request.AgentId, request.ConversationId);
}); });
} }
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreated(request);
}, request.AgentId);
return TwiML(response); return TwiML(response);
} }

View file

@ -1,7 +1,9 @@
using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Utilities;
using BotSharp.Core.Infrastructures; using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces; using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models; using BotSharp.Plugin.Twilio.Models;
@ -134,9 +136,10 @@ public class OutboundPhoneCallFn : IFunctionCallback
} }
} }
private async Task ForkConversation(LlmContextIn args, private async Task ForkConversation(
string entryAgentId, LlmContextIn args,
string originConversationId, string entryAgentId,
string originConversationId,
string newConversationId, string newConversationId,
CallResource call) CallResource call)
{ {
@ -145,6 +148,8 @@ public class OutboundPhoneCallFn : IFunctionCallback
var services = scope.ServiceProvider; var services = scope.ServiceProvider;
var convService = services.GetRequiredService<IConversationService>(); var convService = services.GetRequiredService<IConversationService>();
var convStorage = services.GetRequiredService<IConversationStorage>(); var convStorage = services.GetRequiredService<IConversationStorage>();
var state = _services.GetRequiredService<IConversationStateService>();
var db = _services.GetRequiredService<IBotSharpRepository>();
var newConv = await convService.NewConversation(new Conversation var newConv = await convService.NewConversation(new Conversation
{ {
@ -170,15 +175,45 @@ public class OutboundPhoneCallFn : IFunctionCallback
} }
}); });
convService.SetConversationId(newConversationId, var utcNow = DateTime.UtcNow;
[ var excludStates = new List<string>
new MessageState(StateConst.ORIGIN_CONVERSATION_ID, originConversationId), {
new MessageState("channel", "phone"), "provider",
new MessageState("phone_from", call.From), "model",
new MessageState("phone_direction", call.Direction), "prompt_total",
new MessageState("phone_number", call.To), "completion_total",
new MessageState("twilio_call_sid", call.Sid) "llm_total_cost"
]); };
convService.SaveStates();
var curStates = state.GetStates().Select(x => new MessageState(x.Key, x.Value)).ToList();
var subConvStates = new List<MessageState>
{
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);
} }
} }