This commit is contained in:
nick.yi 2025-07-03 11:45:21 +08:00
commit 4509b55aca
11 changed files with 126 additions and 39 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

@ -9,6 +9,7 @@ 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);
}

View file

@ -23,9 +23,10 @@ public class BotSharpConversationSideCar : IConversationSideCar
private readonly IServiceProvider _services;
private readonly ILogger<BotSharpConversationSideCar> _logger;
private Stack<ConversationContext> contextStack = new();
private Stack<ConversationContext> _contextStack = new();
private bool enabled = false;
private bool _enabled = false;
private string _conversationId = string.Empty;
public string Provider => "botsharp";
@ -39,46 +40,63 @@ 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 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)
{
@ -94,6 +112,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 +135,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 +148,7 @@ public class BotSharpConversationSideCar : IConversationSideCar
RecursiveCounter = routing.Context.GetRecursiveCounter(),
RoutingStack = routing.Context.GetAgentStack()
};
contextStack.Push(node);
_contextStack.Push(node);
// Reset
state.ResetCurrentState();
@ -144,7 +163,7 @@ public class BotSharpConversationSideCar : IConversationSideCar
var state = _services.GetRequiredService<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>();
var node = contextStack.Pop();
var node = _contextStack.Pop();
// Recover
state.SetCurrentState(node.State);
@ -152,6 +171,14 @@ public class BotSharpConversationSideCar : IConversationSideCar
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);
}
}

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

@ -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

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