Re-define FunctionCallFromLlm.

This commit is contained in:
hchen2020 2023-09-22 15:38:58 -05:00
parent 8abc002f14
commit 4d449ff7d7
26 changed files with 670 additions and 306 deletions

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Routing.Models;
using System.Text.Json;
namespace BotSharp.Abstraction.Functions.Models;
@ -7,14 +8,27 @@ public class FunctionCallFromLlm
[JsonPropertyName("function")]
public string Function { get; set; } = string.Empty;
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
[JsonPropertyName("route")]
public RoutingArgs Route { get; set; } = new RoutingArgs();
[JsonPropertyName("parameters")]
public RetrievalArgs Parameters { get; set; } = new RetrievalArgs();
[JsonPropertyName("question")]
public string? Question { get; set; }
[JsonPropertyName("answer")]
public string? Answer { get; set; }
[JsonPropertyName("args")]
public JsonDocument Arguments { get; set; } = JsonDocument.Parse("{}");
public override string ToString()
{
return $"{Function} ({Reason}) {Parameters}";
if (string.IsNullOrEmpty(Answer))
{
return $"[{Function} {Route} {JsonSerializer.Serialize(Arguments)}]: {Question}";
}
else
{
return $"[{Function} {Route} {JsonSerializer.Serialize(Arguments)}]: {Question} => {Answer}";
}
}
}

View file

@ -0,0 +1,17 @@
using BotSharp.Abstraction.Functions.Models;
namespace BotSharp.Abstraction.Routing;
public interface IRoutingHandler
{
string Name { get; }
string Description { get; }
bool IsReasoning { get; }
bool RequireAgent { get; }
List<string> Parameters { get; }
void SetRouter(Agent router);
void SetDialogs(List<RoleDialogModel> dialogs);
Task<FunctionCallFromLlm> GetNextInstructionFromReasoner(string prompt);
Task<RoleDialogModel> GetResponseFromReasoner();
Task<RoleDialogModel> Handle(FunctionCallFromLlm inst);
}

View file

@ -1,27 +0,0 @@
using System.Text.Json;
namespace BotSharp.Abstraction.Routing.Models;
public class RetrievalArgs : RoutingArgs
{
[JsonPropertyName("question")]
public string Question { get; set; }
[JsonPropertyName("answer")]
public string Answer { get; set; }
[JsonPropertyName("args")]
public JsonDocument Arguments { get; set; } = JsonDocument.Parse("{}");
public override string ToString()
{
if (string.IsNullOrEmpty(Answer))
{
return $"[{AgentName}]: ({JsonSerializer.Serialize(Arguments)}) {Question}";
}
else
{
return $"[{AgentName}]: ({JsonSerializer.Serialize(Arguments)}) {Question} => {Answer}";
}
}
}

View file

@ -1,14 +1,18 @@
using System.Text.Json.Serialization;
namespace BotSharp.Abstraction.Routing.Models;
public class RoutingArgs
{
[JsonPropertyName("user_goal")]
public string UserGoal { get; set; } = string.Empty;
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
[JsonPropertyName("agent_name")]
public string AgentName { get; set; } = string.Empty;
public override string ToString()
{
return AgentName;
return string.IsNullOrEmpty(AgentName) ? "" : $"<Route to {AgentName.ToUpper()} because {Reason}>";
}
}

View file

@ -0,0 +1,8 @@
namespace BotSharp.Abstraction.Routing.Models;
public class RoutingHandlerDef
{
public string Name { get; set; }
public string Description { get; set; }
public List<string> Parameters { get; set; }
}

View file

@ -87,9 +87,10 @@
<ItemGroup>
<PackageReference Include="Aspects.Cache" Version="2.0.3" />
<PackageReference Include="Colorful.Console" Version="1.2.15" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="6.2.1" />
<PackageReference Include="Fluid.Core" Version="2.4.0" />
<PackageReference Include="TensorFlow.Keras" Version="0.11.2" />
<PackageReference Include="TensorFlow.Keras" Version="0.11.3" />
</ItemGroup>
<ItemGroup>

View file

@ -9,6 +9,7 @@ using BotSharp.Abstraction.Templating;
using BotSharp.Core.Instructs;
using BotSharp.Abstraction.Instructs;
using BotSharp.Abstraction.Routing;
using BotSharp.Core.Routing.Handlers;
namespace BotSharp.Core;
@ -56,7 +57,17 @@ public static class BotSharpServiceCollectionExtensions
// Register function callback
services.AddScoped<IFunctionCallback, RouteToAgentFn>();
// Register routing and handlers
services.AddScoped<IRoutingService, RoutingService>();
services.AddScoped<IRoutingHandler, GetNextInstructionRoutingHandler>();
services.AddScoped<IRoutingHandler, ResponseToUserRoutingHandler>();
services.AddScoped<IRoutingHandler, InterruptTaskExecutionRoutingHandler>();
services.AddScoped<IRoutingHandler, RouteToAgentRoutingHandler>();
services.AddScoped<IRoutingHandler, ContinueExecuteTaskRoutingHandler>();
services.AddScoped<IRoutingHandler, RetrieveDataFromAgentRoutingHandler>();
services.AddScoped<IRoutingHandler, TaskEndRoutingHandler>();
services.AddScoped<IRoutingHandler, ConversationEndRoutingHandler>();
services.AddScoped<IRoutingHandler, TransferToCsrRoutingHandler>();
if (myDatabaseSettings.Default == "FileRepository")
{

View file

@ -27,7 +27,7 @@ public partial class ConversationService
text = latestResponse.Content.Split("=>").Last();
}
await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, text)
await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, text)
{
CurrentAgentId = agent.Id
}, onMessageReceived);
@ -37,7 +37,7 @@ public partial class ConversationService
var result = await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg =>
{
await HandleAssistantMessage(agent, msg, onMessageReceived);
await HandleAssistantMessage(msg, onMessageReceived);
}, async fn =>
{
var preAgentId = agent.Id;
@ -47,7 +47,7 @@ public partial class ConversationService
// Function executed has exception
if (fn.ExecutionResult == null)
{
await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, fn.Content)
await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, fn.Content)
{
CurrentAgentId = fn.CurrentAgentId
}, onMessageReceived);
@ -56,7 +56,7 @@ public partial class ConversationService
}
else if (fn.StopCompletion)
{
await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, fn.Content)
await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, fn.Content)
{
CurrentAgentId = fn.CurrentAgentId,
ExecutionData = fn.ExecutionData,
@ -95,7 +95,7 @@ public partial class ConversationService
var response = await templateService.RenderFunctionResponse(agent.Id, fn);
if (!string.IsNullOrEmpty(response))
{
await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, response)
await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, response)
{
CurrentAgentId = agent.Id
}, onMessageReceived);
@ -124,7 +124,7 @@ public partial class ConversationService
return result;
}
private async Task HandleAssistantMessage(Agent agent, RoleDialogModel message, Func<RoleDialogModel, Task> onMessageReceived)
private async Task HandleAssistantMessage(RoleDialogModel message, Func<RoleDialogModel, Task> onMessageReceived)
{
var hooks = _services.GetServices<IConversationHook>().ToList();
@ -134,7 +134,9 @@ public partial class ConversationService
await hook.AfterCompletion(message);
}
_logger.LogInformation($"[{agent.Name}] {message.Role}: {message.Content}");
var agent = await _services.GetRequiredService<IAgentService>().GetAgent(message.CurrentAgentId);
_logger.LogInformation($"[{agent?.Name ?? "Router"}] {message.Role}: {message.Content}");
await onMessageReceived(message);

View file

@ -57,40 +57,19 @@ public partial class ConversationService
var routing = _services.GetRequiredService<IRoutingService>();
var reasonedContext = await routing.Enter(agent, wholeDialogs);
if (reasonedContext.FunctionName == "interrupt_task_execution")
if (reasonedContext.StopCompletion)
{
await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, reasonedContext.Content)
{
CurrentAgentId = agent.Id
}, onMessageReceived);
await HandleAssistantMessage(reasonedContext, onMessageReceived);
return true;
}
else if (reasonedContext.FunctionName == "response_to_user")
{
await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, reasonedContext.Content)
{
CurrentAgentId = agent.Id
}, onMessageReceived);
return true;
}
else if (reasonedContext.FunctionName == "continue_execute_task")
// Switch agent
if (reasonedContext.CurrentAgentId != agent.Id)
{
if (reasonedContext.CurrentAgentId != agent.Id)
{
agent = await agentService.LoadAgent(reasonedContext.CurrentAgentId);
}
}
else if (reasonedContext.FunctionName == "route_to_agent")
{
if (reasonedContext.CurrentAgentId != agent.Id)
{
agent = await agentService.LoadAgent(reasonedContext.CurrentAgentId);
}
agent = await agentService.LoadAgent(reasonedContext.CurrentAgentId);
}
routing.Dialogs.ForEach(x =>
routing.Dialogs.ForEach(x =>
{
wholeDialogs.Add(x);
if (x.Content != null)

View file

@ -46,7 +46,7 @@ public class ConversationStorage : IConversationStorage
else
{
var routingSetting = _services.GetRequiredService<RoutingSettings>();
var agentName = routingSetting.RouterId == agentId ? "Router" : db.Agents.First(x => x.Id == agentId).Name;
var agentName = routingSetting.RouterId == agentId ? routingSetting.RouterName : db.Agents.First(x => x.Id == agentId).Name;
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agentId}|{agentName}|");
var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim();

View file

@ -0,0 +1,43 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "continue_execute_task";
public string Description => "Continue to execute user's request without further information retrival.";
public List<string> Parameters => new List<string>
{
"1. agent_name: the name of the agent",
"2. args: required parameters extracted from question",
"3. reason: why continue to execute current task"
};
public bool IsReasoning => true;
public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger<ContinueExecuteTaskRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
var routing = _services.GetRequiredService<IAgentRouting>();
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.Agents.First(x => x.Name.ToLower() == inst.Route.AgentName.ToLower());
var result = new RoleDialogModel(AgentRole.Function, inst.Question)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst.Arguments),
CurrentAgentId = record.Id
};
return result;
}
}

View file

@ -0,0 +1,28 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "conversation_end";
public string Description => "Call this function when user wants to end this conversation or all tasks have been completed.";
public List<string> Parameters => new List<string>
{
};
public bool IsReasoning => false;
public ConversationEndRoutingHandler(IServiceProvider services, ILogger<ConversationEndRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,26 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class GetNextInstructionRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "get_next_instruction";
public string Description => "";
public List<string> Parameters => new List<string> { };
public bool IsReasoning => false;
public GetNextInstructionRoutingHandler(IServiceProvider services, ILogger<GetNextInstructionRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
return null;
}
}

View file

@ -0,0 +1,36 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "interrupt_task_execution";
public string Description => "Can't continue user's request becauase the requirements are not met.";
public List<string> Parameters => new List<string>
{
"1. reason: the reason why the request is interrupted",
"2. answer: the content response to user"
};
public bool IsReasoning => true;
public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger<InterruptTaskExecutionRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
var result = new RoleDialogModel(AgentRole.User, inst.Route.Reason)
{
FunctionName = inst.Function,
StopCompletion = true
};
return result;
}
}

View file

@ -0,0 +1,35 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "response_to_user";
public string Description => "You know how to response according to the context, don't need to ask specific agent.";
public List<string> Parameters => new List<string>
{
"1. answer: the content of response",
"2. reason: why response to user"
};
public bool IsReasoning => false;
public ResponseToUserRoutingHandler(IServiceProvider services, ILogger<ResponseToUserRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
var result = new RoleDialogModel(AgentRole.User, inst.Answer)
{
FunctionName = inst.Function,
StopCompletion = true
};
return result;
}
}

View file

@ -0,0 +1,69 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "retrieve_data_from_agent";
public string Description => "Retrieve data from appropriate agent.";
public List<string> Parameters => new List<string>
{
"1. agent_name: the name of the agent",
"2. question: the question you will ask the agent to get the necessary data",
"3. reason: why retrieve data",
"4. args: required parameters extracted from question and hand over to the next agent. The args should be in JSON format"
};
public bool IsReasoning => true;
public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger<RetrieveDataFromAgentRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
if (string.IsNullOrEmpty(inst.Route.AgentName))
{
inst = await GetNextInstructionFromReasoner($"What's the next step? your response must have agent name.");
}
// Retrieve information from specific agent
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.Agents.First(x => x.Name.ToLower() == inst.Route.AgentName.ToLower());
var response = await InvokeAgent(record.Id, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, inst.Question)
});
inst.Answer = response.Content;
/*_dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question)
{
CurrentAgentId = record.Id
});*/
_router.Instruction += $"\r\n{AgentRole.Assistant}: {inst.Question}";
/*_dialogs.Add(new RoleDialogModel(AgentRole.Function, inst.Parameters.Answer)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments),
ExecutionResult = inst.Parameters.Answer,
ExecutionData = response.ExecutionData,
CurrentAgentId = record.Id
});*/
_router.Instruction += $"\r\n{AgentRole.Function}: {response.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;
}
}

View file

@ -0,0 +1,61 @@
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "route_to_agent";
public string Description => "Route request to appropriate agent.";
public List<string> Parameters => new List<string>
{
"1. agent_name: the name of the agent",
"2. reason: why route to this agent",
"3. args: parameters extracted from context",
"4. answer: if you know how to response without asking to other agent",
"5. goal: user's original goal"
};
public bool IsReasoning => false;
public RouteToAgentRoutingHandler(IServiceProvider services, ILogger<RouteToAgentRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
if (string.IsNullOrEmpty(inst.Route.AgentName))
{
inst = await GetNextInstructionFromReasoner($"What's the next step? your response must have agent name.");
}
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == inst.Function);
var message = new RoleDialogModel(AgentRole.Function, inst.Question)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(new RoutingArgs
{
AgentName = inst.Route.AgentName
}),
};
var ret = await function.Execute(message);
var result = await InvokeAgent(message.CurrentAgentId, _dialogs);
result.ExecutionData = result.ExecutionData ?? message.ExecutionData;
if (result.Role == AgentRole.Function && !result.StopCompletion)
{
_dialogs.Add(result);
result = await InvokeAgent(message.CurrentAgentId, _dialogs);
}
return result;
}
}

View file

@ -0,0 +1,155 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Routing.Settings;
using System.Drawing;
namespace BotSharp.Core.Routing.Handlers;
public abstract class RoutingHandlerBase
{
protected Agent _router;
protected readonly IServiceProvider _services;
protected readonly ILogger _logger;
protected RoutingSettings _settings;
protected List<RoleDialogModel> _dialogs;
public virtual bool RequireAgent => true;
public RoutingHandlerBase(IServiceProvider services,
ILogger logger,
RoutingSettings settings)
{
_services = services;
_logger = logger;
_settings = settings;
}
public void SetRouter(Agent router)
{
_router = router;
}
public void SetDialogs(List<RoleDialogModel> dialogs)
{
_dialogs = dialogs;
}
public async Task<FunctionCallFromLlm> GetNextInstructionFromReasoner(string prompt)
{
var responseFormat = JsonSerializer.Serialize(new FunctionCallFromLlm());
var content = $"{prompt} Response must be in JSON format {responseFormat}.";
var chatCompletion = CompletionProvider.GetChatCompletion(_services,
provider: _settings.Provider,
model: _settings.Model);
RoleDialogModel response = null;
await chatCompletion.GetChatCompletionsAsync(_router, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, content)
}, async msg
=> response = msg, fn
=> Task.CompletedTask);
FunctionCallFromLlm args = new FunctionCallFromLlm();
try
{
_logger.LogInformation(response.Content);
args = JsonSerializer.Deserialize<FunctionCallFromLlm>(response.Content);
}
catch (Exception ex)
{
_logger.LogError($"{ex.Message}: {response.Content}");
args.Function = "response_to_user";
args.Answer = ex.Message;
args.Route.AgentName = "";
}
if (args.Arguments != null)
{
SaveStateByArgs(args.Arguments);
}
args.Function = args.Function.Split('.').Last();
#if DEBUG
Console.WriteLine($"*** Next Instruction *** {args}", Color.Green);
#else
_logger.LogInformation($"*** Next Instruction *** {args}");
#endif
return args;
}
public async Task<RoleDialogModel> GetResponseFromReasoner()
{
var wholeDialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, $"How to response to user?")
};
var chatCompletion = CompletionProvider.GetChatCompletion(_services,
provider: _settings.Provider,
model: _settings.Model);
RoleDialogModel response = null;
await chatCompletion.GetChatCompletionsAsync(_router, wholeDialogs, async msg
=> response = msg, fn
=> Task.CompletedTask);
return response;
}
protected async Task<RoleDialogModel> InvokeAgent(string agentId, List<RoleDialogModel> wholeDialogs)
{
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(agentId);
var chatCompletion = CompletionProvider.GetChatCompletion(_services);
RoleDialogModel response = null;
await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs,
async msg =>
{
response = msg;
}, async fn =>
{
// execute function
// Save states
SaveStateByArgs(JsonSerializer.Deserialize<JsonDocument>(fn.FunctionArgs));
var conversationService = _services.GetRequiredService<IConversationService>();
// Call functions
await conversationService.CallFunctions(fn);
response = fn;
if (string.IsNullOrEmpty(response.Content))
{
response.Content = fn.ExecutionResult;
}
});
return response;
}
protected void SaveStateByArgs(JsonDocument args)
{
if (args == null)
{
return;
}
var stateService = _services.GetRequiredService<IConversationStateService>();
if (args.RootElement is JsonElement root)
{
foreach (JsonProperty property in root.EnumerateObject())
{
if (!string.IsNullOrEmpty(property.Value.ToString()))
{
stateService.SetState(property.Name, property.Value);
}
}
}
}
}

View file

@ -0,0 +1,29 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class TaskEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "task_end";
public string Description => "Call this function when current task is completed.";
public List<string> Parameters => new List<string>
{
"1. abandoned_arguments: the arguments next task can't reuse"
};
public bool IsReasoning => true;
public TaskEndRoutingHandler(IServiceProvider services, ILogger<TaskEndRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,34 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class TransferToCsrRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "transfer_to_csr";
public string Description => "Reach out to a real customer representative to help.";
public List<string> Parameters => new List<string>
{
};
public bool IsReasoning => false;
public TransferToCsrRoutingHandler(IServiceProvider services, ILogger<TransferToCsrRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<RoleDialogModel> Handle(FunctionCallFromLlm inst)
{
var result = new RoleDialogModel(AgentRole.User, "I'm transferring to a customer representative, waiting a moment please.")
{
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
StopCompletion = true
};
return result;
}
}

View file

@ -3,70 +3,31 @@ namespace BotSharp.Core.Routing;
public class PromptConst
{
public const string ROUTER_PROMPT = @"
You're a Router with reasoning, you can dispatch request to different agent to complete the task.
You're a Router with reasoning, you can dispatch request to different agent to achieve user's goal.
### Agents:
###
Router can decide which of below agents can handle user's request:
{% for agent in routing_records %}
* {{ agent.name }}
{{ agent.description }}
{% if agent.required_fields != empty -%}Required information: {{ agent.required_fields }}.{%- endif %}
{% if agent.required_fields != empty -%}
Required: {% for field in agent.required_fields %}{{ field }},{% endfor %}
{%- endif %}
{% endfor %}
### Functions
{% if enable_reasoning == false -%}
* route_to_agent
Route request to appropriate agent.
###
Agent can utilize below functions:
{% for fn in routing_handlers %}
* {{ fn.name }}
{{ fn.description }}
{% if fn.parameters != empty %}
Parameters:
1. agent_name: the name of the agent;
2. reason: why route to this agent;
3. args: parameters extracted from context;
{%- endif %}
* task_end
Call this function when current task is completed.
Parameters:
1. abandoned_arguments: the arguments next task can't reuse;
* conversation_end
Call this function when user wants to end this conversation or all tasks have been completed.
* transfer_to_csr
Reach out to a real customer representative to help.
{{ reasoning_functions }}
### Your response must meet below requirements strictly
{% if enable_reasoning == false %}
* If you can find an appropriate Agent, you must call function route_to_agent with required arguments.
{% for arg in fn.parameters -%}
{{ arg }};
{%- endfor %}
{% endif %}
{% endfor %}
### Conversation context:";
public const string REASONING_FUNCTIONS = @"
* retrieve_data_from_agent
Retrieve data from appropriate agent.
Parameters:
1. agent_name: the name of the agent;
2. question: the question you will ask the agent to get the necessary data;
3. reason: why retrieve data;
4. args: required parameters extracted from question and hand over to the next agent. The args should be in JSON format;
* continue_execute_task
Continue to execute user's request without further information retrival.
Parameters:
1. agent_name: the name of the agent;
2. args: required parameters extracted from question;
3. reason: why continue to execute current task;
* interrupt_task_execution
Can't continue user's request becauase the requirements are not met.
Parameters:
1. reason: the reason why the request is interrupted;
2. answer: the content response to user;
* response_to_user
You have already known the answer according the dialogs.
Parameters:
1. answer: the response of user's request;
2. reason: why response to user;";
###
Conversation context:";
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing.Models;
using System.Drawing;
namespace BotSharp.Core.Routing;
@ -115,6 +116,12 @@ public class RouteToAgentFn : IFunctionCallback
// Add redirected agent
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "redirect_to", record.Name);
agentId = routingRule.RedirectTo;
var logger = _services.GetRequiredService<ILogger<RouteToAgentFn>>();
#if DEBUG
Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***", Color.Yellow);
#else
logger.LogInformation($"*** Routing redirect to {record.Name.ToUpper()} ***");
#endif
}
else
{

View file

@ -1,12 +1,10 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Abstraction.Templating;
using System.IO;
using System.Runtime.InteropServices;
namespace BotSharp.Core.Routing;
@ -27,196 +25,56 @@ public class RoutingService : IRoutingService
_logger = logger;
}
public async Task<RoleDialogModel> Enter(Agent router, List<RoleDialogModel> whileDialogs)
public async Task<RoleDialogModel> Enter(Agent router, List<RoleDialogModel> wholeDialogs)
{
_dialogs = new List<RoleDialogModel>();
RoleDialogModel result = new RoleDialogModel(AgentRole.Assistant, "not handled");
var result = new RoleDialogModel(AgentRole.Assistant, "Can you repeat your request again?")
{
CurrentAgentId = router.Id
};
foreach (var dialog in whileDialogs.TakeLast(20))
var message = wholeDialogs.Last().Content;
foreach (var dialog in wholeDialogs.TakeLast(20))
{
router.Instruction += $"\r\n{dialog.Role}: {dialog.Content}";
}
var inst = await GetNextInstructionFromReasoner($"What's the next step to make user's original goal?", router);
var handlers = _services.GetServices<IRoutingHandler>();
var handler = handlers.FirstOrDefault(x => x.Name == "get_next_instruction");
handler.SetRouter(router);
handler.SetDialogs(wholeDialogs);
int loopCount = 0;
while (loopCount < 3)
while (!result.StopCompletion && loopCount < 5)
{
loopCount++;
if (inst.Function == "continue_execute_task")
var inst = await handler.GetNextInstructionFromReasoner($"What's the next step to achieve user's goal?");
inst.Question = inst.Question ?? message;
handler = handlers.FirstOrDefault(x => x.Name == inst.Function);
if (handler == null)
{
var routing = _services.GetRequiredService<IAgentRouting>();
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.Agents.First(x => x.Name.ToLower() == inst.Parameters.AgentName.ToLower());
result = new RoleDialogModel(AgentRole.Function, inst.Parameters.Question)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments),
CurrentAgentId = record.Id,
};
break;
handler = handlers.FirstOrDefault(x => x.Name == "get_next_instruction");
router.Instruction += $"\r\n{AgentRole.System}: the function must be one of [{string.Join(",", GetHandlers().Select(x => x.Name))}].";
continue;
}
// Compatible with previous Router, can be removed in the future.
else if (inst.Function == "route_to_agent")
{
// If the agent name is empty, fallback to router
if (string.IsNullOrEmpty(inst.Parameters.AgentName))
{
result = new RoleDialogModel(AgentRole.Function, inst.Reason)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(new RoutingArgs
{
AgentName = inst.Parameters.AgentName
}),
CurrentAgentId = router.Id
};
break;
}
handler.SetRouter(router);
handler.SetDialogs(wholeDialogs);
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == inst.Function);
result = new RoleDialogModel(AgentRole.Function, inst.Reason)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(new RoutingArgs
{
AgentName = inst.Parameters.AgentName
}),
};
var ret = await function.Execute(result);
break;
}
else if (inst.Function == "interrupt_task_execution")
{
result = new RoleDialogModel(AgentRole.User, inst.Reason)
{
FunctionName = inst.Function
};
break;
}
else if (inst.Function == "response_to_user")
{
result = new RoleDialogModel(AgentRole.User, inst.Parameters.Answer)
{
FunctionName = inst.Function
};
break;
}
else if (inst.Function == "retrieve_data_from_agent")
{
// Retrieve information from specific agent
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.Agents.First(x => x.Name.ToLower() == inst.Parameters.AgentName.ToLower());
var response = await RetrieveDataFromAgent(record.Id, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, inst.Parameters.Question)
});
result = await handler.Handle(inst);
inst.Parameters.Answer = response.Content;
message = result.Content.Replace("\r\n", " ");
router.Instruction += $"\r\n{result.Role}: {message}";
_dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question)
{
CurrentAgentId = record.Id
});
router.Instruction += $"\r\n{AgentRole.Assistant}: {inst.Parameters.Question}";
_dialogs.Add(new RoleDialogModel(AgentRole.Function, inst.Parameters.Answer)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments),
ExecutionResult = inst.Parameters.Answer,
ExecutionData = response.ExecutionData,
CurrentAgentId = record.Id
});
router.Instruction += $"\r\n{AgentRole.Function}: {response.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?", router);
}
result.StopCompletion = !_settings.EnableReasoning;
}
return result;
}
private async Task<FunctionCallFromLlm> GetNextInstructionFromReasoner(string prompt, Agent reasoner)
{
var responseFormat = JsonSerializer.Serialize(new FunctionCallFromLlm());
var wholeDialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, $"{prompt} Response in JSON format {responseFormat}")
};
var chatCompletion = CompletionProvider.GetChatCompletion(_services,
provider: _settings.Provider,
model: _settings.Model);
RoleDialogModel response = null;
await chatCompletion.GetChatCompletionsAsync(reasoner, wholeDialogs, async msg
=> response = msg, fn
=> Task.CompletedTask);
var args = JsonSerializer.Deserialize<FunctionCallFromLlm>(response.Content);
if (args.Parameters.Arguments != null)
{
SaveStateByArgs(args.Parameters.Arguments);
}
args.Function = args.Function.Split('.').Last();
args.Parameters.AgentName = args.Parameters.AgentName.Split(':').Last().Trim();
_logger.LogInformation($"*** Next Instruction *** {args}");
return args;
}
private async Task<RoleDialogModel> RetrieveDataFromAgent(string agentId, List<RoleDialogModel> wholeDialogs)
{
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(agentId);
var chatCompletion = CompletionProvider.GetChatCompletion(_services);
RoleDialogModel response = null;
await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg
=> response = msg, async fn
=>
{
// execute function
// Save states
SaveStateByArgs(JsonSerializer.Deserialize<JsonDocument>(fn.FunctionArgs));
var conversationService = _services.GetRequiredService<IConversationService>();
// Call functions
await conversationService.CallFunctions(fn);
response = fn;
response.Content = fn.ExecutionResult;
});
return response;
}
private void SaveStateByArgs(JsonDocument args)
{
if (args == null)
{
return;
}
var stateService = _services.GetRequiredService<IConversationStateService>();
if (args.RootElement is JsonElement root)
{
foreach (JsonProperty property in root.EnumerateObject())
{
if (!string.IsNullOrEmpty(property.Value.ToString()))
{
stateService.SetState(property.Name, property.Value);
}
}
}
}
public Agent LoadRouter()
{
@ -241,15 +99,24 @@ public class RoutingService : IRoutingService
.ToArray()
}).ToArray();
dict["enable_reasoning"] = _settings.EnableReasoning;
if (_settings.EnableReasoning)
{
dict["reasoning_functions"] = PromptConst.REASONING_FUNCTIONS;
}
dict["routing_handlers"] = GetHandlers();
var render = _services.GetRequiredService<ITemplateRender>();
router.Instruction = render.Render(PromptConst.ROUTER_PROMPT, dict);
return router;
}
private List<RoutingHandlerDef> GetHandlers()
{
return _services.GetServices<IRoutingHandler>()
.Where(x => x.IsReasoning == _settings.EnableReasoning)
.Where(x => !string.IsNullOrEmpty(x.Description))
.Select(x => new RoutingHandlerDef
{
Name = x.Name,
Description = x.Description,
Parameters = x.Parameters
}).ToList();
}
}

View file

@ -18,6 +18,7 @@ public class TemplateRender : ITemplateRender
_options = new TemplateOptions();
_options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.SnakeCase;
_options.MemberAccessStrategy.Register<RoutingItem>();
_options.MemberAccessStrategy.Register<RoutingHandlerDef>();
}
public string Render(string template, Dictionary<string, object> dict)

View file

@ -22,4 +22,5 @@ global using BotSharp.Core.Agents.Services;
global using BotSharp.Core.Conversations.Services;
global using BotSharp.Core.Infrastructures;
global using BotSharp.Core.Users.Services;
global using Aspects.Cache;
global using Aspects.Cache;
global using Console = Colorful.Console;

View file

@ -70,6 +70,8 @@ public class ConversationController : ControllerBase, IApiAdapter
});
response.Text = string.Join("\r\n", stackMsg.Select(x => x.Content));
response.Data = response.Data ?? stackMsg.Last().ExecutionData;
return response;
}
}