Share one message instance through the conversation.

This commit is contained in:
Haiping Chen 2023-10-27 15:36:26 -05:00
parent 492a1d70ee
commit 933fc3febd
23 changed files with 116 additions and 157 deletions

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
namespace BotSharp.Abstraction.Conversations.Models;
@ -38,6 +39,8 @@ public class RoleDialogModel : ITrackableMessage
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public bool StopCompletion { get; set; }
public FunctionCallFromLlm Instruction { get; set; }
private RoleDialogModel()
{
}

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
namespace BotSharp.Abstraction.Routing;
@ -19,5 +18,5 @@ public interface IRoutingHandler
void SetDialogs(List<RoleDialogModel> dialogs) { }
Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst);
Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message);
}

View file

@ -8,7 +8,7 @@ public interface IRoutingService
void ResetRecursiveCounter();
void RefreshDialogs();
Task<FunctionCallFromLlm> GetNextInstruction();
Task<RoleDialogModel> InvokeAgent(string agentId);
Task<RoleDialogModel> InstructLoop();
Task<RoleDialogModel> ExecuteOnce(Agent agent);
Task<bool> InvokeAgent(string agentId, RoleDialogModel message);
Task<bool> InstructLoop(RoleDialogModel message);
Task<bool> ExecuteOnce(Agent agent, RoleDialogModel message);
}

View file

@ -1,10 +1,7 @@
namespace BotSharp.Abstraction.Routing.Models;
public class RoutingArgs : ITrackableMessage
public class RoutingArgs
{
[JsonPropertyName("message_id")]
public string MessageId { get; set; }
[JsonPropertyName("function")]
public string Function { get; set; }

View file

@ -8,7 +8,7 @@ namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService
{
public async Task<bool> SendMessage(string agentId,
RoleDialogModel incoming,
RoleDialogModel message,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting,
Func<RoleDialogModel, Task> onFunctionExecuted)
@ -18,16 +18,16 @@ public partial class ConversationService
var agentService = _services.GetRequiredService<IAgentService>();
Agent agent = await agentService.LoadAgent(agentId);
var message = $"Received [{agent.Name}] {incoming.Role}: {incoming.Content}";
var content = $"Received [{agent.Name}] {message.Role}: {message.Content}";
#if DEBUG
Console.WriteLine(message, Color.OrangeRed);
Console.WriteLine(content, Color.OrangeRed);
#else
_logger.LogInformation(message);
_logger.LogInformation(content);
#endif
incoming.CurrentAgentId = agent.Id;
message.CurrentAgentId = agent.Id;
_storage.Append(_conversationId, incoming);
_storage.Append(_conversationId, message);
var hooks = _services.GetServices<IConversationHook>().ToList();
@ -37,13 +37,13 @@ public partial class ConversationService
hook.SetAgent(agent)
.SetConversation(conversation);
await hook.OnMessageReceived(incoming);
await hook.OnMessageReceived(message);
// Interrupted by hook
if (incoming.StopCompletion)
if (message.StopCompletion)
{
await onMessageReceived(incoming);
_storage.Append(_conversationId, incoming);
await onMessageReceived(message);
_storage.Append(_conversationId, message);
return true;
}
}
@ -52,11 +52,11 @@ public partial class ConversationService
var routing = _services.GetRequiredService<IRoutingService>();
var settings = _services.GetRequiredService<RoutingSettings>();
var response = agentId == settings.RouterId ?
await routing.InstructLoop() :
await routing.ExecuteOnce(agent);
var ret = agentId == settings.RouterId ?
await routing.InstructLoop(message) :
await routing.ExecuteOnce(agent, message);
await HandleAssistantMessage(response, onMessageReceived);
await HandleAssistantMessage(message, onMessageReceived);
var statistics = _services.GetRequiredService<ITokenStatistics>();
statistics.PrintStatistics();
@ -64,7 +64,7 @@ public partial class ConversationService
routing.ResetRecursiveCounter();
routing.RefreshDialogs();
return true;
return ret;
}
private async Task<Conversation> GetConversationRecord(string agentId)

View file

@ -66,14 +66,11 @@ public class RouteToAgentFn : IFunctionCallback
else
{
message.CurrentAgentId = targetAgent.Id;
message.Content = $"Routing to {args.AgentName}";
}
}
_context.Push(message.CurrentAgentId);
// Set default execution data
message.Data = JsonSerializer.Deserialize<JsonElement>(message.FunctionArgs);
return true;
}

View file

@ -26,19 +26,15 @@ public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase, IRoutingHan
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAgents(inst.AgentName).FirstOrDefault();
var result = new RoleDialogModel(AgentRole.Function, inst.Question)
{
MessageId = inst.MessageId,
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst.Arguments),
CurrentAgentId = record.Id
};
message.FunctionName = inst.Function;
message.CurrentAgentId = record.Id;
message.FunctionArgs = JsonSerializer.Serialize(inst.Arguments);
return result;
return true;
}
}

View file

@ -24,15 +24,11 @@ public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
MessageId = inst.MessageId,
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst
};
message.Content = inst.Response;
message.FunctionName = inst.Function;
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
@ -40,9 +36,9 @@ public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
foreach (var hook in hooks)
{
await hook.OnConversationEnding(result);
await hook.OnConversationEnding(message);
}
return result;
return true;
}
}

View file

@ -11,8 +11,6 @@ public class HumanInterventionNeededHandler : RoutingHandlerBase, IRoutingHandle
public string Description => "Reach out to human being, customer service or customer representative.";
private readonly RoutingSettings _settings;
public List<NameDesc> Parameters => new List<NameDesc>
{
new NameDesc("reason", "why need customer service"),
@ -22,18 +20,13 @@ public class HumanInterventionNeededHandler : RoutingHandlerBase, IRoutingHandle
public HumanInterventionNeededHandler(IServiceProvider services, ILogger<HumanInterventionNeededHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
_settings = settings;
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
MessageId = inst.MessageId,
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst
};
message.Role = AgentRole.Assistant;
message.Content = inst.Response;
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
@ -41,9 +34,9 @@ public class HumanInterventionNeededHandler : RoutingHandlerBase, IRoutingHandle
foreach (var hook in hooks)
{
await hook.OnHumanInterventionNeeded(result);
await hook.OnHumanInterventionNeeded(message);
}
return result;
return true;
}
}

View file

@ -24,15 +24,11 @@ public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase, IRouting
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.User, inst.Reason)
{
MessageId = inst.MessageId,
FunctionName = inst.Function,
StopCompletion = true
};
message.FunctionName = inst.Function;
message.StopCompletion = true;
return result;
return true;
}
}

View file

@ -24,16 +24,11 @@ public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
MessageId = inst.MessageId,
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst,
StopCompletion = true
};
return result;
message.Content = inst.Response;
message.StopCompletion = true;
message.Role = AgentRole.Assistant;
return true;
}
}

View file

@ -27,14 +27,12 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
// Retrieve information from specific agent
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAgents(inst.AgentName).FirstOrDefault();
var response = await routing.InvokeAgent(record.Id);
inst.Response = response.Content;
var ret = await routing.InvokeAgent(record.Id, message);
/*_dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question)
{
@ -53,11 +51,11 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
CurrentAgentId = record.Id
});*/
_router.Instruction += $"\r\n{AgentRole.Function}: {response.Content}";
_router.Instruction += $"\r\n{AgentRole.Function}: {message.Content}";
// Got the response from agent, then send to reasoner again to make the decision
// inst = await GetNextInstructionFromReasoner($"What's the next step based on user's original goal and function result?");
return null;
return true;
}
}

View file

@ -28,26 +28,15 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var context = _services.GetRequiredService<RoutingContext>();
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == inst.Function);
var message = new RoleDialogModel(AgentRole.Function, inst.Question)
{
MessageId = inst.MessageId,
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst),
CurrentAgentId = context.GetCurrentAgentId(),
};
message.FunctionArgs = JsonSerializer.Serialize(inst);
var ret = await function.Execute(message);
var result = await routing.InvokeAgent(context.GetCurrentAgentId());
// Keep last message data for debug
result.Data = result.Data ?? message.Data;
result.FunctionName = result.FunctionName ?? message.FunctionName;
ret = await routing.InvokeAgent(context.GetCurrentAgentId(), message);
return result;
return true;
}
}

View file

@ -23,25 +23,17 @@ public class TaskEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
MessageId = inst.MessageId,
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst
};
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
foreach (var hook in hooks)
{
await hook.OnCurrentTaskEnding(result);
await hook.OnCurrentTaskEnding(message);
}
return result;
return true;
}
}

View file

@ -57,6 +57,7 @@ public partial class RoutingService
int retryCount = 0;
var agentService = _services.GetRequiredService<IAgentService>();
var dialogs = Dialogs;
while (retryCount < 3)
{
@ -64,7 +65,7 @@ public partial class RoutingService
{
var conversation = "";
foreach (var dialog in _dialogs.TakeLast(20))
foreach (var dialog in dialogs.TakeLast(50))
{
var role = dialog.Role;
if (role != AgentRole.User)
@ -120,9 +121,6 @@ public partial class RoutingService
return args;
}
#if !DEBUG
[MemoryCache(10 * 60)]
#endif
private string GetNextStepPrompt()
{
var template = _routerInstance.Router.Templates.First(x => x.Name == "next_step_prompt").Content;

View file

@ -8,13 +8,13 @@ public partial class RoutingService
{
const int MAXIMUM_RECURSION_DEPTH = 3;
private int _currentRecursionDepth = 0;
public async Task<RoleDialogModel> InvokeAgent(string agentId)
public async Task<bool> InvokeAgent(string agentId, RoleDialogModel message)
{
_currentRecursionDepth++;
if (_currentRecursionDepth > MAXIMUM_RECURSION_DEPTH)
{
_logger.LogWarning($"Current recursive call depth greater than {MAXIMUM_RECURSION_DEPTH}, which will cause unexpected result.");
return Dialogs.Last();
return false;
}
var agentService = _services.GetRequiredService<IAgentService>();
@ -23,50 +23,56 @@ public partial class RoutingService
var settings = _services.GetRequiredService<ChatCompletionSetting>();
var chatCompletion = CompletionProvider.GetChatCompletion(_services, provider: settings.Provider, model: settings.Model);
RoleDialogModel response = chatCompletion.GetChatCompletions(agent, Dialogs);
message.Role = response.Role;
if (response.Role == AgentRole.Function)
{
return await InvokeFunction(agent, response);
message.FunctionName = response.FunctionName;
message.FunctionArgs = response.FunctionArgs;
await InvokeFunction(agent, message);
}
else
{
return response;
message.Content = response.Content;
}
return true;
}
private async Task<RoleDialogModel> InvokeFunction(Agent agent, RoleDialogModel response)
private async Task<RoleDialogModel> InvokeFunction(Agent agent, RoleDialogModel message)
{
// execute function
// Save states
SaveStateByArgs(JsonSerializer.Deserialize<JsonDocument>(response.FunctionArgs));
SaveStateByArgs(JsonSerializer.Deserialize<JsonDocument>(message.FunctionArgs));
var conversationService = _services.GetRequiredService<IConversationService>();
// Call functions
await conversationService.CallFunctions(response);
await conversationService.CallFunctions(message);
Dialogs.Add(response);
Dialogs.Add(message);
// Pass execution result to LLM to get response
if (!response.StopCompletion)
if (!message.StopCompletion)
{
// Find response template
var templateService = _services.GetRequiredService<IResponseTemplateService>();
var responseTemplate = await templateService.RenderFunctionResponse(agent.Id, response);
var responseTemplate = await templateService.RenderFunctionResponse(agent.Id, message);
if (!string.IsNullOrEmpty(responseTemplate))
{
response.Role = AgentRole.Assistant;
response.Content = responseTemplate.Trim();
message.Role = AgentRole.Assistant;
message.Content = responseTemplate.Trim();
}
else
{
response = await InvokeAgent(response.CurrentAgentId);
await InvokeAgent(message.CurrentAgentId, message);
}
}
else
{
response.Role = AgentRole.Assistant;
message.Role = AgentRole.Assistant;
}
return response;
return message;
}
}

View file

@ -46,38 +46,29 @@ public partial class RoutingService : IRoutingService
_routerInstance = routerInstance;
}
public async Task<RoleDialogModel> ExecuteOnce(Agent agent)
public async Task<bool> ExecuteOnce(Agent agent, RoleDialogModel message)
{
var message = Dialogs.Last().Content;
var handlers = _services.GetServices<IRoutingHandler>();
var handler = handlers.FirstOrDefault(x => x.Name == "route_to_agent");
handler.SetDialogs(Dialogs);
var result = await handler.Handle(this, new FunctionCallFromLlm
{
Function = "route_to_agent",
Question = message,
Reason = message,
Question = message.Content,
Reason = message.Content,
AgentName = agent.Name
});
}, message);
return result;
}
public async Task<RoleDialogModel> InstructLoop()
public async Task<bool> InstructLoop(RoleDialogModel message)
{
_routerInstance.Load();
var router = _routerInstance.Router;
var result = new RoleDialogModel(AgentRole.Assistant, "Can you repeat your request again?")
{
CurrentAgentId = router.Id
};
var inputMsg = Dialogs.Last();
var handlers = _services.GetServices<IRoutingHandler>();
int loopCount = 0;
@ -87,8 +78,8 @@ public partial class RoutingService : IRoutingService
loopCount++;
var inst = await GetNextInstruction();
inst.MessageId = inputMsg.MessageId;
inst.Question = inst.Question ?? inputMsg.Content;
message.Instruction = inst;
inst.Question = message.Content;
var handler = handlers.FirstOrDefault(x => x.Name == inst.Function);
if (handler == null)
@ -99,13 +90,18 @@ public partial class RoutingService : IRoutingService
handler.SetRouter(router);
handler.SetDialogs(Dialogs);
result = await handler.Handle(this, inst);
result.MessageId = inputMsg.MessageId;
message.FunctionName = inst.Function;
message.Role = AgentRole.Function;
message.FunctionArgs = inst.Arguments == null ? "{}" : JsonSerializer.Serialize(inst.Arguments);
await handler.Handle(this, inst, message);
inst.Response = message.Content;
stop = !_settings.EnableReasoning;
}
return result;
return true;
}
protected void SaveStateByArgs(JsonDocument args)

View file

@ -53,12 +53,11 @@ public class ConversationController : ControllerBase, IApiAdapter
.SetState("sampling_factor", input.SamplingFactor);
var response = new MessageResponseModel();
var stackMsg = new List<RoleDialogModel>();
var inputMsg = new RoleDialogModel("user", input.Text);
await conv.SendMessage(agentId, inputMsg,
async msg =>
{
stackMsg.Add(msg);
},
async fnExecuting =>
{
@ -66,14 +65,14 @@ public class ConversationController : ControllerBase, IApiAdapter
},
async fnExecuted =>
{
response.Function = fnExecuted.FunctionName;
response.Data = fnExecuted.Data;
});
response.Text = string.Join("\r\n", stackMsg.Select(x => x.Content));
response.Data = response.Data ?? stackMsg.Last().Data;
response.Function = stackMsg.Last().FunctionName;
response.MessageId = inputMsg.MessageId;
response.Text = inputMsg.Content;
response.Data = inputMsg.Data;
response.Function = inputMsg.FunctionName;
response.Instruction = inputMsg.Instruction;
return response;
}

View file

@ -1,4 +1,6 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Routing.Models;
namespace BotSharp.OpenAPI.ViewModels.Conversations;
@ -8,4 +10,5 @@ public class MessageResponseModel : ITrackableMessage
public string Text { get; set; }
public string Function { get; set; }
public object Data { get; set; }
public FunctionCallFromLlm Instruction { get; set; }
}

View file

@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>4fb8c9df-7975-4926-ba73-46c8ca440691</UserSecretsId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>

View file

@ -2,7 +2,7 @@ You're {{router.name}} ({{router.description}}). Follow these steps to handle us
1. Read the [CONVERSATION] content.
2. Select a appropriate function from [FUNCTIONS].
3. Determine which agent is suitable to handle this conversation.
4. If user wants to talk with human being, you will transfer to customer representative.
4. Re-think on whether the function you chose matches the reason.
5. For agent required arguments, leave it as blank object if user doesn't provide it.
[FUNCTIONS]

View file

@ -1 +1,4 @@
What is the next step based on the CONVERSATION? Response must be in appropriate JSON format. Route to the Agent that last handled the conversation if necessary.
What is the next step based on the CONVERSATION?
Response must be in appropriate JSON format.
Route to the Agent that last handled the conversation if necessary.
If user wants to speak to customer service, use function human_intervention_needed.

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Conversations.Models;
using System.Text.Json;
namespace BotSharp.Plugin.PizzaBot.Functions;
@ -14,6 +15,7 @@ public class GetPizzaPricesFn : IFunctionCallback
cheese_unit_price = 3.5,
margherita_unit_price = 3.8,
};
message.Content = JsonSerializer.Serialize(message.Data);
return true;
}
}