Merge branch 'master' of https://github.com/SciSharp/BotSharp into features/init-reactive-x
This commit is contained in:
commit
d41eb4a7e0
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -19,4 +19,9 @@ public class MessageState
|
|||
Value = value;
|
||||
ActiveRounds = activeRounds;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Key: {Key} => Value: {Value}, ActiveRounds: {ActiveRounds}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ?? []);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"content": {
|
||||
"type": "string",
|
||||
"description": "Response content"
|
||||
},
|
||||
"conversation_end": {
|
||||
"type": "boolean",
|
||||
"description": "User is ending the conversation."
|
||||
}
|
||||
},
|
||||
"required": [ "content" ]
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue