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>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<BotSharpVersion>5.0.0</BotSharpVersion>
<BotSharpVersion>5.1.0</BotSharpVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

View file

@ -19,4 +19,9 @@ public class MessageState
Value = value;
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)
{
var sidecar = serviceProvider.GetService<IConversationSideCar>();
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<IConversationSideCar>();
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)

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.SideCar.Models;
namespace BotSharp.Abstraction.SideCar;
public interface IConversationSideCar
@ -9,6 +11,10 @@ public interface IConversationSideCar
List<DialogElement> GetConversationDialogs(string conversationId);
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);
ConversationBreakpoint? GetConversationBreakpoint(string conversationId);
void UpdateConversationStates(string conversationId, List<StateKeyValue> states);
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.
******************************************************************************/
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<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";
@ -39,49 +42,71 @@ public class BotSharpConversationSideCar : IConversationSideCar
public bool IsEnabled()
{
return enabled;
return _enabled;
}
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);
}
public List<DialogElement> GetConversationDialogs(string conversationId)
{
if (contextStack.IsNullOrEmpty())
if (!IsValid(conversationId))
{
return new List<DialogElement>();
}
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<RoleDialogModel> SendMessage(string agentId, string text,
PostbackMessageModel? postback = null, List<MessageState>? states = null, List<DialogElement>? dialogs = null)
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
{
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);
var response = await InnerExecute(agentId, text, postback, states);
AfterExecute();
@ -94,6 +119,7 @@ public class BotSharpConversationSideCar : IConversationSideCar
var conv = _services.GetRequiredService<IConversationService>();
var routing = _services.GetRequiredService<IRoutingService>();
var state = _services.GetRequiredService<IConversationStateService>();
_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<DialogElement>? dialogs)
{
enabled = true;
_enabled = true;
var state = _services.GetRequiredService<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>();
@ -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<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>();
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<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.Routing;
global using BotSharp.Abstraction.SideCar;
global using BotSharp.Abstraction.SideCar.Models;
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)
{
var values = _curStates.Values.ToList();
var values = state.Values.ToList();
var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values));
_curStates = new ConversationState(copy ?? []);
}

View file

@ -307,6 +307,7 @@ public partial class FileRepository
return new ConversationState(states);
}
[SideCar]
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
{
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)
{
Console.WriteLine($"Captured LLM malformed response");

View file

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

View file

@ -367,7 +367,7 @@ public class ChatCompletionProvider : IChatCompletion
{
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));

View file

@ -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<string> { UserRole.Root, UserRole.Admin },
Roles = new List<string> { UserRole.Root, UserRole.Admin, UserRole.Engineer },
SubMenu = new List<PluginMenuDef>
{
new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"),

View file

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

View file

@ -615,10 +615,10 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
{
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)
{

View file

@ -63,6 +63,11 @@ public class TwilioInboundController : TwilioController
instruction.AgentId = request.AgentId;
instruction.ConversationId = request.ConversationId;
await HookEmitter.Emit<ITwilioSessionHook>(_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<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreated(request);
}, request.AgentId);
}
return TwiML(response);
}

View file

@ -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<IConversationService>();
var convStorage = services.GetRequiredService<IConversationStorage>();
var state = _services.GetRequiredService<IConversationStateService>();
var db = _services.GetRequiredService<IBotSharpRepository>();
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<string>
{
"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<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);
}
}