Refactor to support agent router.

This commit is contained in:
hchen2020 2023-08-16 23:04:23 -05:00
parent 0fe0f1db79
commit 77960842d9
16 changed files with 332 additions and 120 deletions

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Conversations.Models;
namespace BotSharp.Abstraction.Agents;
public abstract class AgentHookBase : IAgentHook
@ -33,8 +35,12 @@ public abstract class AgentHookBase : IAgentHook
return true;
}
public virtual Agent OnAgentLoaded()
public virtual void OnAgentLoaded(Agent agent)
{
return _agent;
}
public virtual bool OnAgentRouting(RoleDialogModel message, ref string id)
{
return true;
}
}

View file

@ -24,5 +24,5 @@ public interface IAgentHook
/// </summary>
/// <param name="agent"></param>
/// <returns></returns>
Agent OnAgentLoaded();
void OnAgentLoaded(Agent agent);
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Agents;
public interface IAgentRouting
{
Task<Agent> LoadCurrentAgent();
}

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Conversations.Models;
namespace BotSharp.Abstraction.Agents.Models;
public class Agent
@ -29,4 +27,7 @@ public class Agent
/// Domain knowledges
/// </summary>
public string Knowledges { get; set; }
public override string ToString()
=> $"{Name} {Id}";
}

View file

@ -2,5 +2,9 @@ namespace BotSharp.Abstraction.Agents.Settings;
public class AgentSettings
{
/// <summary>
/// Router Agent Id
/// </summary>
public string RouterId { get; set; }
public string DataDir { get; set; }
}

View file

@ -8,25 +8,25 @@ public class RoleDialogModel
public string Role { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Content { get; set; }
public string CurrentAgentId { get; set; }
/// <summary>
/// Function name if LLM response function call
/// </summary>
public string? FunctionName { get; set; }
public string? FunctionArgs { get; set; }
/// <summary>
/// Function execution result
/// </summary>
public string? ExecutionResult { get; set; }
/// <summary>
/// When function callback has been executed, system will pass result to LLM again,
/// Set this property to True to stop calling LLM.
/// </summary>
public bool StopSubsequentInteraction { get; set; }
public bool IsConversationEnd { get; set; }
public bool NeedReloadAgent { get; set; }
public bool StopPropagate { get; set; }
/// <summary>
/// Channel name
/// </summary>

View file

@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.MLTasks;
public interface IChatCompletion
{
string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations);
// string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived);
Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived);
Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived);
}

View file

@ -1,3 +1,6 @@
using System.Linq;
using System.Text.RegularExpressions;
namespace BotSharp.Abstraction.Utilities;
public static class StringExtensions
@ -15,4 +18,19 @@ public static class StringExtensions
else
return str;
}
public static string CleanPhoneNumber(this string phoneNumber)
{
if (phoneNumber != null && !phoneNumber.All(char.IsDigit))
{
phoneNumber = Regex.Replace(phoneNumber, @"[^\d]", "");
}
if (phoneNumber != null && phoneNumber.Length > 10)
{
phoneNumber = phoneNumber.Substring(1);
}
return phoneNumber;
}
}

View file

@ -0,0 +1,37 @@
using BotSharp.Abstraction.Agents.Models;
namespace BotSharp.Core.Agents.Services;
public class AgentRouter : IAgentRouting
{
private readonly IServiceProvider _services;
private readonly ILogger _logger;
private readonly AgentSettings _settings;
public AgentRouter(IServiceProvider services,
ILogger<AgentRouter> logger,
AgentSettings settings)
{
_services = services;
_logger = logger;
_settings = settings;
}
public async Task<Agent> LoadCurrentAgent()
{
// Load current agent from state
var stateService = _services.GetRequiredService<IConversationStateService>();
var currentAgentId = stateService.GetState("agentId");
if (string.IsNullOrEmpty(currentAgentId))
{
currentAgentId = _settings.RouterId;
}
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(currentAgentId);
// Set agent and trigger state changed
stateService.SetState("agentId", currentAgentId);
return agent;
}
}

View file

@ -39,9 +39,11 @@ public partial class AgentService
hook.OnSamplesLoaded(ref samples);
}
hook.OnAgentLoaded();
hook.OnAgentLoaded(agent);
}
_logger.LogInformation($"Loaded agent {agent}.");
return agent;
}
}

View file

@ -33,6 +33,8 @@ public static class BotSharpServiceCollectionExtensions
RegisterPlugins(services, config);
services.AddScoped<IAgentRouting, AgentRouter>();
return services;
}

View file

@ -51,7 +51,7 @@ public class ConversationController : ControllerBase, IApiAdapter
await conv.SendMessage(agentId, conversationId,
new RoleDialogModel("user", input.Text),
async msg =>
response.Text += msg.Content,
response.Text = msg.Content,
async fn
=> await Task.CompletedTask);

View file

@ -1,3 +1,6 @@
using Amazon.SecurityToken.Model.Internal.MarshallTransformations;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.MLTasks;
@ -12,7 +15,7 @@ public class ConversationService : IConversationService
private readonly ConversationSetting _settings;
private readonly IConversationStorage _storage;
public ConversationService(IServiceProvider services,
public ConversationService(IServiceProvider services,
IUserIdentity user,
ConversationSetting settings,
IConversationStorage storage,
@ -69,11 +72,12 @@ public class ConversationService : IConversationService
return record.ToConversation();
}
public async Task<bool> SendMessage(string agentId, string conversationId,
RoleDialogModel lastDalog,
Func<RoleDialogModel, Task> onMessageReceived,
public async Task<bool> SendMessage(string agentId, string conversationId,
RoleDialogModel lastDalog,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
{
lastDalog.CurrentAgentId = agentId;
_storage.Append(conversationId, lastDalog);
var wholeDialogs = GetDialogHistory(conversationId);
@ -95,11 +99,9 @@ public class ConversationService : IConversationService
var stateService = _services.GetRequiredService<IConversationStateService>();
stateService.SetConversation(conversationId);
stateService.Load();
stateService.SetState("agentId", agentId);
// load agent
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(agentId);
var router = _services.GetRequiredService<IAgentRouting>();
var agent = await router.LoadCurrentAgent();
// Get relevant domain knowledge
/*if (_settings.EnableKnowledgeBase)
@ -127,39 +129,100 @@ public class ConversationService : IConversationService
var chatCompletion = GetChatCompletion();
var result = await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg =>
{
if (msg.Role == "function")
await HandleMessage(conversationId, agent, msg, onMessageReceived, onFunctionExecuting);
if (msg.NeedReloadAgent)
{
// Save states
SaveStateByArgs(msg.Content);
// Call functions
await onFunctionExecuting(msg);
await CallFunctions(conversationId, msg);
}
else
{
// Add to dialog history
_storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.Content));
// After chat completion hook
foreach (var hook in hooks)
{
await hook.AfterCompletion(msg);
}
await onMessageReceived(msg);
}
// Clean conversation
if (msg.IsConversationEnd)
{
stateService.CleanState();
await HandleMessageIfAgentReloaded(conversationId, agent, msg, wholeDialogs, onMessageReceived, onFunctionExecuting);
}
});
return result;
}
private async Task HandleMessage(string conversationId, Agent agent, RoleDialogModel msg,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
{
if (msg.Role == "function")
{
// Save states
SaveStateByArgs(msg.FunctionArgs);
// Call functions
await onFunctionExecuting(msg);
await CallFunctions(msg);
// Add to dialog history
if (msg.ExecutionResult != null)
{
if (msg.NeedReloadAgent)
{
_logger.LogInformation($"Skipped append dialog log: {msg.FunctionName}\n{msg.FunctionArgs}\n{msg.ExecutionResult}");
return;
}
_storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.Content)
{
CurrentAgentId = agent.Id,
FunctionName = msg.FunctionName,
FunctionArgs = msg.FunctionArgs,
ExecutionResult = msg.ExecutionResult
});
}
}
else
{
// Add to dialog history
_storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.Content)
{
CurrentAgentId = agent.Id
});
var hooks = _services.GetServices<IConversationHook>().ToList();
// After chat completion hook
foreach (var hook in hooks)
{
await hook.AfterCompletion(msg);
}
await onMessageReceived(msg);
}
}
private async Task HandleMessageIfAgentReloaded(string conversationId, Agent agent,
RoleDialogModel msg,
List<RoleDialogModel> wholeDialogs,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
{
var state = _services.GetRequiredService<IConversationStateService>();
var currentAgentId = state.GetState("agentId");
// Send to LLM to get final response when agent is switched.
var conv = _services.GetRequiredService<IConversationService>();
var chatCompletion = conv.GetChatCompletion();
var agentService = _services.GetRequiredService<IAgentService>();
var newAgent = await agentService.LoadAgent(currentAgentId);
await chatCompletion.GetChatCompletionsAsync(newAgent, wholeDialogs, async newMsg =>
{
if (newMsg.Role == AgentRole.Function)
{
await HandleMessage(conversationId, agent, newMsg, onMessageReceived, onFunctionExecuting);
}
else
{
msg.StopPropagate = true;
await onMessageReceived(newMsg);
_storage.Append(conversationId, new RoleDialogModel(newMsg.Role, newMsg.Content)
{
CurrentAgentId = agent.Id
});
}
});
}
private void SaveStateByArgs(string args)
{
var stateService = _services.GetRequiredService<IConversationStateService>();
@ -173,7 +236,7 @@ public class ConversationService : IConversationService
}
}
private async Task CallFunctions(string conversationId, RoleDialogModel msg)
private async Task CallFunctions(RoleDialogModel msg)
{
var hooks = _services.GetServices<IConversationHook>().ToList();
@ -182,6 +245,12 @@ public class ConversationService : IConversationService
.Where(x => x.Name == msg.FunctionName)
.ToList();
if (functions.Count == 0)
{
_logger.LogError($"Can't find function implementation of {msg.FunctionName}.");
return;
}
foreach (var fn in functions)
{
// Before executing functions
@ -193,12 +262,6 @@ public class ConversationService : IConversationService
// Execute function
await fn.Execute(msg);
// Add to dialog history
_storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.ExecutionResult)
{
FunctionName = msg.FunctionName,
});
// After functions have been executed
foreach (var hook in hooks)
{

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Conversations.Models;
using System.IO;
@ -15,15 +16,40 @@ public class ConversationStorage : IConversationStorage
{
var conversationFile = GetStorageFile(conversationId);
var sb = new StringBuilder();
sb.AppendLine($"{dialog.Role}|{dialog.CreatedAt}|{dialog.FunctionName}");
var content = dialog.Content.Trim().Replace("\r", " ").Replace("\n", " ");
if (string.IsNullOrEmpty(content))
if (dialog.Role == AgentRole.Function)
{
return;
}
var args = dialog.FunctionArgs.Replace("\r", " ").Replace("\n", " ").Trim();
sb.AppendLine($" - {content}");
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{dialog.CurrentAgentId}|{dialog.FunctionName}|{args}");
var content = dialog.ExecutionResult.Replace("\r", " ").Replace("\n", " ").Trim();
if (string.IsNullOrEmpty(content))
{
return;
}
sb.AppendLine($" - {content}");
}
else if (dialog.Role == AgentRole.Assistant)
{
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|||");
var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim();
if (string.IsNullOrEmpty(content))
{
return;
}
sb.AppendLine($" - {content}");
}
else
{
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{dialog.CurrentAgentId}||");
var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim();
if (string.IsNullOrEmpty(content))
{
return;
}
sb.AppendLine($" - {content}");
}
var conversation = sb.ToString();
File.AppendAllText(conversationFile, conversation);
@ -39,13 +65,18 @@ public class ConversationStorage : IConversationStorage
{
var meta = dialogs[i];
var dialog = dialogs[i + 1];
var role = meta.Split('|')[0];
var createdAt = DateTime.Parse(meta.Split('|')[1]);
var createdAt = DateTime.Parse(meta.Split('|')[0]);
var role = meta.Split('|')[1];
var currentAgentId = meta.Split('|')[2];
var funcName = meta.Split('|')[3];
var funcArgs= meta.Split('|')[4];
var text = dialog.Substring(4);
var funcName = meta.Split('|')[2];
results.Add(new RoleDialogModel(role, text)
{
CurrentAgentId = currentAgentId,
FunctionName = funcName,
FunctionArgs = funcArgs,
CreatedAt = createdAt
});
}

View file

@ -14,7 +14,7 @@ public class ChatCompletionProvider : IChatCompletion
_services = services;
}
public string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
public string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
{
throw new NotImplementedException();
}

View file

@ -25,27 +25,41 @@ public class ChatCompletionProvider : IChatCompletion
_logger = logger;
}
public string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
private OpenAIClient GetClient()
{
var client = new OpenAIClient(new Uri(_settings.Endpoint), new AzureKeyCredential(_settings.ApiKey));
return client;
}
/*public string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
{
var client = GetClient();
var chatCompletionsOptions = PrepareOptions(agent, conversations);
var response = client.GetChatCompletions(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions);
var choice = response.Value.Choices[0];
var message = choice.Message;
string output = "";
foreach (var choice in response.Value.Choices)
if (choice.FinishReason == CompletionsFinishReason.FunctionCall)
{
var message = choice.Message;
if (message.Content == null)
continue;
Console.Write(message.Content);
output += message.Content;
response = HandleFunctionCall(message,
onMessageReceived,
chatCompletionsOptions).Result;
}
_logger.LogInformation(output);
choice = response.Value.Choices[0];
message = choice.Message;
return output.Trim();
}
_logger.LogInformation(message.Content);
if (!string.IsNullOrEmpty(message.Content))
{
onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), message.Content))
.Wait();
}
return message.Content.Trim();
}*/
public List<RoleDialogModel> GetChatSamples(string sampleText)
{
@ -95,7 +109,7 @@ public class ChatCompletionProvider : IChatCompletion
public async Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
{
var client = new OpenAIClient(new Uri(_settings.Endpoint), new AzureKeyCredential(_settings.ApiKey));
var client = GetClient();
var chatCompletionsOptions = PrepareOptions(agent, conversations);
var response = await client.GetChatCompletionsAsync(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions);
@ -104,51 +118,24 @@ public class ChatCompletionProvider : IChatCompletion
if (choice.FinishReason == CompletionsFinishReason.FunctionCall)
{
if (message.FunctionCall == null || message.FunctionCall.Arguments == null)
{
return false;
}
_logger.LogInformation($"{message.FunctionCall.Name}: {message.FunctionCall.Arguments}");
var funcContextIn = new RoleDialogModel(ChatRole.Function.ToString(), message.FunctionCall.Arguments)
{
FunctionName = message.FunctionCall.Name
};
// Execute functions
await onMessageReceived(funcContextIn);
if (funcContextIn.StopSubsequentInteraction)
{
// Emit a fake message that should be populated by whom set StopSubsequentInteraction as True.
await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), ""));
return true;
}
if (funcContextIn.IsConversationEnd)
{
await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), funcContextIn.Content)
{
IsConversationEnd = true
});
return true;
}
// After function is executed, pass the result to LLM
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.Function, funcContextIn.ExecutionResult)
{
Name = funcContextIn.FunctionName
});
response = client.GetChatCompletions(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions);
response = await HandleFunctionCall(agent,
message,
onMessageReceived,
chatCompletionsOptions);
}
choice = response.Value.Choices[0];
message = choice.Message;
_logger.LogInformation(message.Content);
if (!string.IsNullOrEmpty(message.Content))
if (response != null)
{
await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), message.Content));
choice = response.Value.Choices[0];
message = choice.Message;
_logger.LogInformation(message.Content);
if (!string.IsNullOrEmpty(message.Content))
{
var msgByLlm = new RoleDialogModel(ChatRole.Assistant.ToString(), message.Content);
await onMessageReceived(msgByLlm);
}
}
return true;
@ -198,10 +185,61 @@ public class ChatCompletionProvider : IChatCompletion
return true;
}
private async Task<Response<ChatCompletions>> HandleFunctionCall(Agent agent,
ChatMessage message,
Func<RoleDialogModel, Task> onMessageReceived,
ChatCompletionsOptions chatCompletionsOptions)
{
Response<ChatCompletions> response = default;
if (message.FunctionCall == null || message.FunctionCall.Arguments == null)
{
return response;
}
_logger.LogInformation($"{message.FunctionCall.Name}: {message.FunctionCall.Arguments}");
var funcContextIn = new RoleDialogModel(ChatRole.Function.ToString(), message.Content)
{
CurrentAgentId = agent.Id,
FunctionName = message.FunctionCall.Name,
FunctionArgs = message.FunctionCall.Arguments
};
// Execute functions
await onMessageReceived(funcContextIn);
if (funcContextIn.StopPropagate)
{
return response;
}
if (funcContextIn.IsConversationEnd)
{
await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), funcContextIn.Content)
{
IsConversationEnd = true
});
return response;
}
// After function is executed, pass the result to LLM
if (funcContextIn.ExecutionResult != null)
{
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.Function, funcContextIn.ExecutionResult)
{
Name = funcContextIn.FunctionName
});
var client = GetClient();
response = client.GetChatCompletions(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions);
}
return response;
}
private ChatCompletionsOptions PrepareOptions(Agent agent, List<RoleDialogModel> conversations)
{
var chatCompletionsOptions = new ChatCompletionsOptions();
if (!string.IsNullOrEmpty(agent.Instruction))
{
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.System, agent.Instruction));
@ -244,6 +282,10 @@ public class ChatCompletionProvider : IChatCompletion
}
}
// https://community.openai.com/t/cheat-sheet-mastering-temperature-and-top-p-in-chatgpt-api-a-few-tips-and-tricks-on-controlling-the-creativity-deterministic-output-of-prompt-responses/172683
chatCompletionsOptions.Temperature = 0.5f;
chatCompletionsOptions.NucleusSamplingFactor = 0.5f;
_logger.LogInformation(string.Join("\n", chatCompletionsOptions.Messages.Select(x => $"{x.Role}: {x.Content}")));
return chatCompletionsOptions;
}