resolve conflict.

This commit is contained in:
Haiping Chen 2025-05-06 20:39:52 -05:00
commit cf4ea9cc64
40 changed files with 200 additions and 602 deletions

View file

@ -69,21 +69,6 @@ public interface IConversationHook
Task OnResponseGenerated(RoleDialogModel message);
/// <summary>
/// LLM detected user requested a new task different from previous topic.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
Task OnNewTaskDetected(RoleDialogModel message, string reason);
/// <summary>
/// LLM detected the current task is completed.
/// It's useful for the situation of multiple tasks in the same conversation.
/// </summary>
/// <param name="conversation"></param>
/// <returns></returns>
Task OnTaskCompleted(RoleDialogModel message);
/// <summary>
/// LLM detected the whole conversation is going to be end.
/// </summary>

View file

@ -36,13 +36,6 @@ public class FunctionCallFromLlm : RoutingArgs
{
var route = string.IsNullOrEmpty(AgentName) ? "" : $"<Route to {AgentName.ToUpper()} because {NextActionReason}>";
if (string.IsNullOrEmpty(Response))
{
return $"[{Function} {route} {JsonSerializer.Serialize(Arguments)}]: {Question}";
}
else
{
return $"[{Function} {route} {JsonSerializer.Serialize(Arguments)}]: {Question} => {Response}";
}
return $"[{Function} {route} {JsonSerializer.Serialize(Arguments)}]: {Question}";
}
}

View file

@ -1,20 +0,0 @@
using BotSharp.Abstraction.Functions.Models;
namespace BotSharp.Abstraction.Routing;
/// <summary>
/// The routing handler will be injected to Router's FUNCTIONS section of the system prompt
/// So the handler will be invoked by LLM autonomously.
/// </summary>
public interface IRoutingHandler
{
string Name { get; }
string Description { get; }
List<string> Planers => null;
bool Enabled => true;
List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>();
void SetDialogs(List<RoleDialogModel> dialogs);
Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message);
}

View file

@ -26,15 +26,13 @@ public interface IRoutingService
/// <returns></returns>
RoutingRule[] GetRulesByAgentId(string id);
List<RoutingHandlerDef> GetHandlers(Agent router);
//void ResetRecursiveCounter();
//int GetRecursiveCounter();
//void SetRecursiveCounter(int counter);
Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs);
Task<bool> InvokeFunction(string name, RoleDialogModel messages);
Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs);
Task<RoleDialogModel> InstructLoop(Agent agent, RoleDialogModel message, List<RoleDialogModel> dialogs);
/// <summary>
/// Talk to a specific Agent directly, bypassing the Router
@ -42,7 +40,7 @@ public interface IRoutingService
/// <param name="agent"></param>
/// <param name="message"></param>
/// <returns></returns>
Task<RoleDialogModel> InstructDirect(Agent agent, RoleDialogModel message);
Task<RoleDialogModel> InstructDirect(Agent agent, RoleDialogModel message, List<RoleDialogModel> dialogs);
Task<string> GetConversationContent(List<RoleDialogModel> dialogs, int maxDialogCount = 100);

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Routing.Models;
public class ResponseUserArgs
{
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}

View file

@ -15,19 +15,6 @@ public class RoutingArgs
[JsonPropertyName("conversation_end")]
public bool ConversationEnd { get; set; }
[JsonPropertyName("task_completed")]
public bool TaskCompleted { get; set; }
[JsonPropertyName("is_new_task")]
public bool IsNewTask { get; set; }
/// <summary>
/// The content of replying to user
/// </summary>
[JsonPropertyName("response")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Response { get; set; } = string.Empty;
/// <summary>
/// Agent for next action based on user latest response
/// </summary>
@ -38,10 +25,12 @@ public class RoutingArgs
/// <summary>
/// Agent who can achieve user original goal
/// </summary>
[Obsolete("Will be replaced by dedicate Reasoner")]
[JsonPropertyName("user_goal_agent")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string OriginalAgent { get; set; } = string.Empty;
[Obsolete("Will be replaced by dedicate Reasoner")]
[JsonPropertyName("user_goal_description")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string UserGoal { get; set; } = string.Empty;
@ -50,13 +39,6 @@ public class RoutingArgs
{
var route = string.IsNullOrEmpty(AgentName) ? "" : $"<Route to {AgentName.ToUpper()} because {NextActionReason}>";
if (string.IsNullOrEmpty(Response))
{
return $"[{Function} {route}]";
}
else
{
return $"[{Function} {route}] => {Response}";
}
return $"[{Function} {route}]";
}
}

View file

@ -150,17 +150,6 @@ public static class BotSharpCoreExtensions
var loader = new PluginLoader(services, config, pluginSettings);
loader.Load(assembly =>
{
// Register routing handlers
var handlers = assembly.GetTypes()
.Where(x => x.IsClass)
.Where(x => x.GetInterface(nameof(IRoutingHandler)) != null)
.ToArray();
foreach (var handler in handlers)
{
services.AddScoped(typeof(IRoutingHandler), handler);
}
// Register function callback
var functions = assembly.GetTypes()
.Where(x => x.IsClass

View file

@ -38,17 +38,6 @@ public partial class ConversationService
var routing = _services.GetRequiredService<IRoutingService>();
routing.Context.SetMessageId(_conversationId, message.MessageId);
// Check the routing mode
var states = _services.GetRequiredService<IConversationStateService>();
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false);
if (routingMode == "lazy")
{
message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId);
routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false);
}
// Save payload in order to assign the payload before hook is invoked
if (replyMessage != null && !string.IsNullOrEmpty(replyMessage.Payload))
{
@ -91,11 +80,22 @@ public partial class ConversationService
if (agent.Type == AgentType.Routing)
{
response = await routing.InstructLoop(message, dialogs);
// Check the routing mode
var states = _services.GetRequiredService<IConversationStateService>();
var routingMode = states.GetState(StateConst.ROUTING_MODE, "eager");
routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false);
if (routingMode == "lazy")
{
message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId);
routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false);
}
response = await routing.InstructLoop(agent, message, dialogs);
}
else
{
response = await routing.InstructDirect(agent, message);
response = await routing.InstructDirect(agent, message, dialogs);
}
routing.Context.ResetRecursiveCounter();
@ -155,14 +155,6 @@ public partial class ConversationService
var conversation = _services.GetRequiredService<IConversationService>();
var updatedConversation = await conversation.UpdateConversationTitle(_conversationId, response.Instruction.NextActionReason);
// Emit conversation task completed hook
if (response.Instruction.TaskCompleted)
{
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnTaskCompleted(response)
);
}
// Emit conversation ending hook
if (response.Instruction.ConversationEnd)
{

View file

@ -20,8 +20,8 @@ public class ResponseToUserFn : IFunctionCallback
public Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
message.Content = args.Response;
var args = JsonSerializer.Deserialize<ResponseUserArgs>(message.FunctionArgs);
message.Content = args.Content;
message.Handled = true;
message.StopCompletion = true;
return Task.FromResult(true);

View file

@ -1,50 +0,0 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Routing.Reasoning;
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<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("next_action_agent", "agent for next action based on user latest response"),
new ParameterPropertyDef("user_goal_agent", "agent who can achieve user original goal"),
new ParameterPropertyDef("reason", "why continue to execute current task"),
new ParameterPropertyDef("args", "required parameters extracted from question")
{
Type = "object"
}
};
public List<string> Planers => new List<string>
{
nameof(HFReasoner)
};
public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger<ContinueExecuteTaskRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var filter = new AgentFilter { AgentNames = [inst.AgentName] };
var record = db.GetAgents(filter).FirstOrDefault();
message.FunctionName = inst.Function;
message.CurrentAgentId = record.Id;
message.FunctionArgs = JsonSerializer.Serialize(inst.Arguments);
return true;
}
}

View file

@ -1,37 +0,0 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Routing.Reasoning;
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<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("reason", "the reason why the request is interrupted"),
new ParameterPropertyDef("answer", "the content response to user")
};
public List<string> Planers => new List<string>
{
nameof(HFReasoner)
};
public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger<InterruptTaskExecutionRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
message.FunctionName = inst.Function;
message.StopCompletion = true;
return true;
}
}

View file

@ -1,59 +0,0 @@
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Routing.Reasoning;
namespace BotSharp.Core.Routing.Handlers;
/// <summary>
/// Retrieve information from specific agent
/// </summary>
public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase//, IRoutingHandler
{
public string Name => "retrieve_data_from_agent";
public string Description => "Retrieve data from appropriate agent.";
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("reason", "why choose this function"),
new ParameterPropertyDef("question", "the question you will ask the next action agent to get the necessary information"),
new ParameterPropertyDef("next_action_agent", "agent that can handle the question"),
new ParameterPropertyDef("user_goal_agent", "agent that can achieve user original goal"),
new ParameterPropertyDef("args", "required parameters extracted from question and hand over to the next agent")
{
Type = "object"
}
};
public List<string> Planers => new List<string>
{
nameof(HFReasoner)
};
public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger<RetrieveDataFromAgentRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var context = _services.GetRequiredService<IRoutingContext>();
var agentId = context.GetCurrentAgentId();
var dialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, inst.Question)
{
CurrentAgentId = agentId,
MessageId = message.MessageId
}
};
var ret = await routing.InvokeAgent(agentId, dialogs);
var response = dialogs.Last();
inst.Response = response.Content;
// Add final response to parent dialog
_dialogs.Add(response);
return ret;
}
}

View file

@ -1,99 +0,0 @@
using BotSharp.Abstraction.Infrastructures.Enums;
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 virtual agent.";
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("next_action_reason",
"the reason why route to this virtual agent.",
required: true),
new ParameterPropertyDef("next_action_agent",
"agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent.",
required: true),
new ParameterPropertyDef("args",
"useful parameters of next action agent, format: { }",
type: "object"),
new ParameterPropertyDef("user_goal_description",
"user goal based on user initial task.",
required: true),
new ParameterPropertyDef("user_goal_agent",
"agent who can acheive user initial task.",
required: true),
new ParameterPropertyDef("conversation_end",
"user is ending the conversation.",
type: "boolean",
required: true),
new ParameterPropertyDef("is_new_task",
"whether the user is requesting a new task that is different from the previous topic. Set the first round of conversation to false.",
type: "boolean")
};
public RouteToAgentRoutingHandler(IServiceProvider services, ILogger<RouteToAgentRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var states = _services.GetRequiredService<IConversationStateService>();
var goalAgent = states.GetState(StateConst.EXPECTED_GOAL_AGENT);
if (!string.IsNullOrEmpty(goalAgent) && inst.OriginalAgent != goalAgent)
{
inst.OriginalAgent = goalAgent;
// Emit hook
await HookEmitter.Emit<IRoutingHook>(_services, async hook =>
await hook.OnRoutingInstructionRevised(inst, message)
);
}
if (inst.IsNewTask)
{
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnNewTaskDetected(message, inst.NextActionReason)
);
}
message.FunctionArgs = JsonSerializer.Serialize(inst);
if (message.FunctionName != null)
{
var msg = RoleDialogModel.From(message, role: AgentRole.Function);
await routing.InvokeFunction(message.FunctionName, msg);
}
var agentId = routing.Context.GetCurrentAgentId();
// Update next action agent's name
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.GetAgent(agentId);
inst.AgentName = agent.Name;
if (inst.ExecutingDirectly)
{
message.Content = inst.Question;
}
if (agent.Disabled)
{
var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent.";
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: content);
_dialogs.Add(message);
}
else
{
var ret = await routing.InvokeAgent(agentId, _dialogs);
}
var response = _dialogs.Last();
inst.Response = response.Content;
return true;
}
}

View file

@ -59,7 +59,6 @@ public class RoutingAgentHook : AgentHookBase
}
dict["routing_agents"] = agents;
dict["routing_handlers"] = routing.GetHandlers(_agent);
return base.OnInstructionLoaded(template, dict);
}

View file

@ -38,43 +38,21 @@ public class HFReasoner : IRoutingReasoner
{
var next = GetNextStepPrompt(router);
RoleDialogModel response = default;
var inst = new FunctionCallFromLlm();
var completion = CompletionProvider.GetChatCompletion(_services,
provider: router?.LlmConfig?.Provider,
model: router?.LlmConfig?.Model);
int retryCount = 0;
while (retryCount < 3)
dialogs = new List<RoleDialogModel>
{
try
new RoleDialogModel(AgentRole.User, next)
{
dialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, next)
{
FunctionName = nameof(HFReasoner),
MessageId = messageId
}
};
response = await completion.GetChatCompletions(router, dialogs);
FunctionName = nameof(HFReasoner),
MessageId = messageId
}
};
var response = await completion.GetChatCompletions(router, dialogs);
inst = response.Content.JsonContent<FunctionCallFromLlm>();
break;
}
catch (Exception ex)
{
_logger.LogError($"{ex.Message}: {response.Content}");
inst.Function = "response_to_user";
inst.Response = ex.Message;
inst.AgentName = "Router";
}
finally
{
retryCount++;
}
}
var inst = response.Content.JsonContent<FunctionCallFromLlm>();
// Fix LLM malformed response
ReasonerHelper.FixMalformedResponse(_services, inst);

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Planning;
namespace BotSharp.Core.Routing.Reasoning;
@ -18,17 +19,50 @@ public class InstructExecutor : IExecutor
RoleDialogModel message,
List<RoleDialogModel> dialogs)
{
message.Instruction = inst;
var states = _services.GetRequiredService<IConversationStateService>();
var goalAgent = states.GetState(StateConst.EXPECTED_GOAL_AGENT);
if (!string.IsNullOrEmpty(goalAgent) && inst.OriginalAgent != goalAgent)
{
inst.OriginalAgent = goalAgent;
// Emit hook
await HookEmitter.Emit<IRoutingHook>(_services, async hook =>
await hook.OnRoutingInstructionRevised(inst, message)
);
}
var handlers = _services.GetServices<IRoutingHandler>();
var handler = handlers.FirstOrDefault(x => x.Name == inst.Function);
handler.SetDialogs(dialogs);
message.FunctionArgs = JsonSerializer.Serialize(inst);
if (message.FunctionName != null)
{
var msg = RoleDialogModel.From(message, role: AgentRole.Function);
await routing.InvokeFunction(message.FunctionName, msg);
}
var handled = await handler.Handle(routing, inst, message);
var agentId = routing.Context.GetCurrentAgentId();
// Update next action agent's name
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.GetAgent(agentId);
inst.AgentName = agent.Name;
if (inst.ExecutingDirectly)
{
message.Content = inst.Question;
}
if (agent.Disabled)
{
var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent.";
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: content);
dialogs.Add(message);
}
else
{
var ret = await routing.InvokeAgent(agentId, dialogs);
}
// For client display purpose
var response = dialogs.Last();
response.MessageId = message.MessageId;
response.Instruction = inst;
return response;

View file

@ -41,53 +41,22 @@ public class NaiveReasoner : IRoutingReasoner
{
var next = GetNextStepPrompt(router);
var inst = new FunctionCallFromLlm();
// text completion
/*var agentService = _services.GetRequiredService<IAgentService>();
var instruction = agentService.RenderedInstruction(router);
var content = $"{instruction}\r\n###\r\n{next}";
content = content + "\r\nResponse: ";
var completion = CompletionProvider.GetTextCompletion(_services);*/
// chat completion
var completion = CompletionProvider.GetChatCompletion(_services,
provider: router?.LlmConfig?.Provider,
model: router?.LlmConfig?.Model);
int retryCount = 0;
while (retryCount < 3)
dialogs = new List<RoleDialogModel>
{
string text = string.Empty;
try
new RoleDialogModel(AgentRole.User, next)
{
// text completion
// text = await completion.GetCompletion(content, router.Id, messageId);
dialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, next)
{
FunctionName = nameof(NaiveReasoner),
MessageId = messageId
}
};
var response = await completion.GetChatCompletions(router, dialogs);
FunctionName = nameof(NaiveReasoner),
MessageId = messageId
}
};
var response = await completion.GetChatCompletions(router, dialogs);
inst = (response.FunctionArgs ?? response.Content).JsonContent<FunctionCallFromLlm>();
break;
}
catch (Exception ex)
{
_logger.LogError($"{ex.Message}: {text}");
inst.Function = "response_to_user";
inst.Response = ex.Message;
inst.AgentName = "Router";
}
finally
{
retryCount++;
}
}
var inst = (response.FunctionArgs ?? response.Content).JsonContent<FunctionCallFromLlm>();
// Fix LLM malformed response
ReasonerHelper.FixMalformedResponse(_services, inst);

View file

@ -44,46 +44,26 @@ public class OneStepForwardReasoner : IRoutingReasoner
{
var next = GetNextStepPrompt(router);
var inst = new FunctionCallFromLlm();
// chat completion
var completion = CompletionProvider.GetChatCompletion(_services,
provider: router?.LlmConfig?.Provider,
model: router?.LlmConfig?.Model);
int retryCount = 0;
while (retryCount < 3)
{
string text = string.Empty;
try
{
// text completion
// text = await completion.GetCompletion(content, router.Id, messageId);
dialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, next)
{
FunctionName = Name,
MessageId = messageId
}
};
var response = await completion.GetChatCompletions(router, dialogs);
string text = string.Empty;
inst = response.Content.JsonContent<FunctionCallFromLlm>();
break;
}
catch (Exception ex)
// text completion
// text = await completion.GetCompletion(content, router.Id, messageId);
dialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, next)
{
_logger.LogError($"{ex.Message}: {text}");
inst.Function = "response_to_user";
inst.Response = ex.Message;
inst.AgentName = "Router";
FunctionName = Name,
MessageId = messageId
}
finally
{
retryCount++;
}
}
};
var response = await completion.GetChatCompletions(router, dialogs);
var inst = response.Content.JsonContent<FunctionCallFromLlm>();
// Fix LLM malformed response
ReasonerHelper.FixMalformedResponse(_services, inst);

View file

@ -294,7 +294,7 @@ public class RoutingContext : IRoutingContext
// Set next handling agent for lazy routing mode
var states = _services.GetRequiredService<IConversationStateService>();
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
var routingMode = states.GetState(StateConst.ROUTING_MODE, "eager");
if (routingMode == "lazy")
{
var agentId = GetCurrentAgentId();

View file

@ -7,7 +7,7 @@ namespace BotSharp.Core.Routing;
public partial class RoutingService
{
public async Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs)
public async Task<RoleDialogModel> InstructLoop(Agent agent, RoleDialogModel message, List<RoleDialogModel> dialogs)
{
RoleDialogModel response = default;
@ -15,7 +15,7 @@ public partial class RoutingService
var convService = _services.GetRequiredService<IConversationService>();
var storage = _services.GetRequiredService<IConversationStorage>();
_router = await agentService.LoadAgent(message.CurrentAgentId);
_router = await agentService.GetAgent(message.CurrentAgentId);
var states = _services.GetRequiredService<IConversationStateService>();
var executor = _services.GetRequiredService<IExecutor>();
@ -58,11 +58,8 @@ public partial class RoutingService
// Save states
states.SaveStateByArgs(inst.Arguments);
#if DEBUG
Console.WriteLine($"*** Next Instruction *** {inst}");
#else
_logger.LogInformation($"*** Next Instruction *** {inst}");
#endif
_logger.LogDebug($"*** Next Instruction *** {inst}");
await reasoner.AgentExecuting(_router, inst, message, dialogs);
// Handover to Task Agent
@ -79,7 +76,7 @@ public partial class RoutingService
await reasoner.AgentExecuted(_router, inst, response, dialogs);
if (loopCount >= reasoner.MaxLoopCount || _context.IsEmpty)
if (loopCount >= reasoner.MaxLoopCount || _context.IsEmpty || response.StopCompletion)
{
break;
}

View file

@ -26,54 +26,40 @@ public partial class RoutingService : IRoutingService
_logger = logger;
}
public async Task<RoleDialogModel> InstructDirect(Agent agent, RoleDialogModel message)
public async Task<RoleDialogModel> InstructDirect(Agent agent, RoleDialogModel message, List<RoleDialogModel> dialogs)
{
var handlers = _services.GetServices<IRoutingHandler>();
var handler = handlers.FirstOrDefault(x => x.Name == "route_to_agent");
var conv = _services.GetRequiredService<IConversationService>();
var storage = _services.GetRequiredService<IConversationStorage>();
storage.Append(conv.ConversationId, message);
var dialogs = conv.GetDialogHistory();
dialogs.Add(message);
Context.SetDialogs(dialogs);
handler.SetDialogs(dialogs);
var inst = new FunctionCallFromLlm
var routing = _services.GetRequiredService<IRoutingService>();
routing.Context.Push(agent.Id, "instruct directly");
var agentId = routing.Context.GetCurrentAgentId();
// Update next action agent's name
var agentService = _services.GetRequiredService<IAgentService>();
if (agent.Disabled)
{
Function = "route_to_agent",
Question = message.Content,
NextActionReason = message.Content,
AgentName = agent.Name,
OriginalAgent = agent.Name,
ExecutingDirectly = true
};
var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent.";
var result = await handler.Handle(this, inst, message);
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: content);
dialogs.Add(message);
}
else
{
var ret = await routing.InvokeAgent(agentId, dialogs);
}
var response = dialogs.Last();
response.MessageId = message.MessageId;
response.Instruction = inst;
return response;
}
public List<RoutingHandlerDef> GetHandlers(Agent router)
{
var reasoner = GetReasoner(router);
return _services.GetServices<IRoutingHandler>()
.Where(x => x.Planers == null || x.Planers.Contains(reasoner.GetType().Name))
.Where(x => !string.IsNullOrEmpty(x.Description))
.Select((x, i) => new RoutingHandlerDef
{
Name = x.Name,
Description = x.Description,
Parameters = x.Parameters
}).ToList();
}
#if !DEBUG
[SharpCache(10)]
#endif

View file

@ -1,15 +1,14 @@
{
"name": "response_to_user",
"description": "Response to user without routing to any other agent",
"visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}",
"description": "Response to user without routing to any other agent, user has no specific request.",
"parameters": {
"type": "object",
"properties": {
"response": {
"content": {
"type": "string",
"description": "Response content"
}
},
"required": [ "response" ]
"required": [ "content" ]
}
}

View file

@ -1,7 +1,6 @@
{
"name": "route_to_agent",
"description": "Route request to appropriate AI agent.",
"visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}",
"parameters": {
"type": "object",
"properties": {

View file

@ -5,11 +5,7 @@ Follow these steps to handle user request:
2. Determine which agent is suitable to handle this conversation. Try to minimize the routing of human service.
3. Extract and populate agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments.
4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared.
{% if routing_mode != 'lazy' %}
5. Response must be in JSON format.
{% else %}
5. If user is greeting, you can call function response_to_user with a greeting message.
{% endif %}
{% if routing_requirements and routing_requirements != empty %}
[REQUIREMENTS]
@ -18,21 +14,6 @@ Follow these steps to handle user request:
{%- endfor %}
{% endif %}
{% if routing_mode != 'lazy' %}
[FUNCTIONS]
{% for handler in routing_handlers -%}
# {{ handler.description}}
{% if handler.parameters and handler.parameters != empty -%}
Parameters:
- function: {{ handler.name }}
{% for p in handler.parameters -%}
- {{ p.name }} {% if p.required -%}(required){%- endif %}: {{ p.description }}{{ "\r\n " }}
{%- endfor %}
{%- endif %}
{{ "\r\n" }}
{%- endfor %}
{% endif %}
[AGENTS]
{% for agent in routing_agents -%}
* Agent: {{ agent.name }}

View file

@ -27,7 +27,7 @@ public class VerboseLogHook : IContentGeneratingHook
if (dialog != null)
{
var log = $"{dialog.Role}: {dialog.Content} [msg_id: {dialog.MessageId}] ==>";
_logger.LogInformation(log);
_logger.LogDebug(log);
}
await Task.CompletedTask;
@ -44,7 +44,7 @@ public class VerboseLogHook : IContentGeneratingHook
$"[{agent?.Name}]: {message.Indication} {message.FunctionName}({message.FunctionArgs})" :
$"[{agent?.Name}]: {message.Content}" + $" <== [msg_id: {message.MessageId}]";
_logger.LogInformation(tokenStats.Prompt);
_logger.LogInformation(log);
_logger.LogDebug(tokenStats.Prompt);
_logger.LogDebug(log);
}
}

View file

@ -41,7 +41,7 @@ public class SequentialPlanner : ITaskPlanner
var decomposation = await GetDecomposedStepAsync(router, messageId, dialogs);
if (decomposation.TotalRemainingSteps > 0 && _lastInst != null)
{
_lastInst.Response = decomposation.Description;
// _lastInst.Response = decomposation.Description;
_lastInst.NextActionReason = $"Having {decomposation.TotalRemainingSteps} steps left.";
return _lastInst;
}
@ -62,8 +62,6 @@ public class SequentialPlanner : ITaskPlanner
var next = GetNextStepPrompt(router);
var inst = new FunctionCallFromLlm();
// text completion
/*var agentService = _services.GetRequiredService<IAgentService>();
var instruction = agentService.RenderedInstruction(router);
@ -76,43 +74,26 @@ public class SequentialPlanner : ITaskPlanner
provider: router?.LlmConfig?.Provider,
model: router?.LlmConfig?.Model);
int retryCount = 0;
while (retryCount < 3)
{
string text = string.Empty;
try
{
// text completion
// text = await completion.GetCompletion(content, router.Id, messageId);
dialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, next)
{
FunctionName = nameof(SequentialPlanner),
MessageId = messageId
}
};
var response = await completion.GetChatCompletions(router, dialogs);
inst = response.Content.JsonContent<FunctionCallFromLlm>();
break;
}
catch (Exception ex)
string text = string.Empty;
// text completion
// text = await completion.GetCompletion(content, router.Id, messageId);
dialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, next)
{
_logger.LogError($"{ex.Message}: {text}");
inst.Function = "response_to_user";
inst.Response = ex.Message;
inst.AgentName = "Router";
FunctionName = nameof(SequentialPlanner),
MessageId = messageId
}
finally
{
retryCount++;
}
}
};
var response = await completion.GetChatCompletions(router, dialogs);
var inst = response.Content.JsonContent<FunctionCallFromLlm>();
if (decomposation.TotalRemainingSteps > 0)
{
inst.Response = decomposation.Description;
// inst.Response = decomposation.Description;
inst.NextActionReason = $"{decomposation.TotalRemainingSteps} steps left.";
inst.HandleDialogsByPlanner = true;
}
@ -125,10 +106,10 @@ public class SequentialPlanner : ITaskPlanner
{
var taskAgentDialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, inst.Response)
/*new RoleDialogModel(AgentRole.User, inst.Response)
{
MessageId = message.MessageId,
}
}*/
};
return taskAgentDialogs;

View file

@ -42,14 +42,14 @@ public class SqlGenerationPlanner : ITaskPlanner
public List<RoleDialogModel> BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
{
var question = inst.Response;
// var question = inst.Response;
var taskAgentDialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, question)
/*new RoleDialogModel(AgentRole.User, question)
{
MessageId = message.MessageId,
}
}*/
};
return taskAgentDialogs;

View file

@ -70,14 +70,14 @@ public partial class TwoStageTaskPlanner : ITaskPlanner
public List<RoleDialogModel> BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
{
var question = inst.Response;
// var question = inst.Response;
var taskAgentDialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, question)
/*new RoleDialogModel(AgentRole.User, question)
{
MessageId = message.MessageId,
}
}*/
};
return taskAgentDialogs;

View file

@ -152,7 +152,7 @@ public class TwilioInboundController : TwilioController
AgentId = request.AgentId,
Channel = ConversationChannel.Phone,
ChannelId = request.CallSid,
Title = $"Incoming phone call from {request.From}",
Title = request.Intent ?? $"Incoming phone call from {request.From}",
Tags = [],
};
@ -167,6 +167,15 @@ public class TwilioInboundController : TwilioController
new("twilio_call_sid", request.CallSid),
};
var requestStates = ParseStates(request.States);
foreach (var s in requestStates)
{
if (!states.Any(x => x.Key == s.Key))
{
states.Add(new MessageState(s.Key, s.Value));
}
}
if (request.InitAudioFile != null)
{
states.Add(new("init_audio_file", request.InitAudioFile));
@ -179,7 +188,20 @@ public class TwilioInboundController : TwilioController
{
states.Add(new(StateConst.ROUTING_MODE, agent.Mode));
}
convService.SetConversationId(conversation.Id, states);
if (!string.IsNullOrEmpty(request.Intent))
{
var storage = _services.GetRequiredService<IConversationStorage>();
storage.Append(conversation.Id, new RoleDialogModel(AgentRole.User, request.Intent)
{
CurrentAgentId = conversation.Id,
CreatedAt = DateTime.UtcNow
});
}
convService.SaveStates();
// reload agent rendering with states

View file

@ -17,7 +17,10 @@ public class ConversationalVoiceRequest : VoiceRequest
public int AIResponseWaitTime { get; set; } = 0;
public string? AIResponseErrorMessage { get; set; } = string.Empty;
public string Intent { get; set; } = string.Empty;
/// <summary>
/// Initial intent when incoming call connected
/// </summary>
public string? Intent { get; set; }
[FromQuery(Name = "init-audio-file")]
public string? InitAudioFile { get; set; }

View file

@ -254,8 +254,8 @@
"HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c",
"EnableTranslator": false,
"LlmConfig": {
"Provider": "azure-openai",
"Model": "gpt-4o-mini"
"Provider": "openai",
"Model": "gpt-4.1-nano"
}
},

View file

@ -32,9 +32,7 @@
<None Remove="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\agent.json" />
<None Remove="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\functions\get_pizza_price.json" />
<None Remove="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\functions\get_pizza_types.json" />
<None Remove="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\functions\place_an_order.json" />
<None Remove="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\instructions\instruction.liquid" />
<None Remove="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\responses\func.get_pizza_price.0.liquid" />
<None Remove="data\agents\fe8c60aa-b114-4ef3-93cb-a8efeac80f75\agent.json" />
<None Remove="data\agents\fe8c60aa-b114-4ef3-93cb-a8efeac80f75\functions\make_payment.json" />
<None Remove="data\agents\fe8c60aa-b114-4ef3-93cb-a8efeac80f75\instructions\instruction.liquid" />
@ -63,9 +61,6 @@
<Content Include="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\instructions\instruction.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\responses\func.get_pizza_price.0.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\fe8c60aa-b114-4ef3-93cb-a8efeac80f75\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -93,7 +88,7 @@
<Content Include="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\functions\get_pizza_types.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\functions\place_an_order.json">
<Content Include="data\agents\c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd\functions\place_order.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\fe8c60aa-b114-4ef3-93cb-a8efeac80f75\functions\make_payment.json">

View file

@ -1,11 +1,12 @@
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Infrastructures.Enums;
namespace BotSharp.Plugin.PizzaBot.Functions;
public class PlaceOrderFn : IFunctionCallback
{
public string Name => "place_an_order";
public string Name => "place_order";
private readonly IServiceProvider _service;
public PlaceOrderFn(IServiceProvider service)
@ -19,6 +20,9 @@ public class PlaceOrderFn : IFunctionCallback
var state = _service.GetRequiredService<IConversationStateService>();
state.SetState("order_number", "P123-01");
// Set the next action agent to Payment
state.SetState(StateConst.EXPECTED_ACTION_AGENT, "Payment", activeRounds: 2);
return true;
}
}

View file

@ -1,7 +1,7 @@
{
"id": "8970b1e5-d260-4e2c-90b1-f1415a257c18",
"name": "Pizza Bot",
"description": "AI assistant that can help customer place pizza order, make payment or inquiry order status.",
"description": "AI assistant that can help customer place pizza order, make payment or inquiry existing order.",
"type": "routing",
"inheritAgentId": "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a",
"createdDateTime": "2023-08-18T10:39:32.2349685Z",
@ -11,6 +11,10 @@
"isPublic": true,
"profiles": [ "pizza" ],
"labels": [ "experiment" ],
"llmConfig": {
"provider": "openai",
"model": "gpt-4.1-nano"
},
"routingRules": [
{
"type": "reasoner",

View file

@ -1,6 +1,6 @@
{
"name": "Order Inquiry",
"description": "Check the order status like payment, delivery or baking.",
"description": "Check the existing order status like payment, delivery or baking.",
"createdDateTime": "2023-08-18T14:39:32.2349685Z",
"updatedDateTime": "2023-08-18T14:39:32.2349686Z",
"id": "b284db86-e9c2-4c25-a59e-4649797dd130",

View file

@ -6,7 +6,7 @@
"properties": {
"order_number": {
"type": "string",
"description": "order number."
"description": "order number, value must be provided by user."
}
},
"required": [ "order_number" ]

View file

@ -1,5 +1,5 @@
{
"name": "Ordering",
"name": "Order Placement",
"description": "Provide types of pizza available, pizza unit price, total cost and place the order.",
"createdDateTime": "2023-07-26T02:29:25.123224Z",
"updatedDateTime": "2023-07-26T02:29:25.123274Z",

View file

@ -1,5 +1,5 @@
{
"name": "place_an_order",
"name": "place_order",
"description": "Place an order when user has confirmed the pizza type and quantity.",
"parameters": {
"type": "object",

View file

@ -1,10 +1,9 @@
You are now a Pizza Ordering agent, and you can help customers order a pizza according to the user's preferences.
Follow below step to place order:
1: Ask user preferences, call function get_pizza_types to provide the variety of pizza options.
2: Confirm with user the pizza type and quantity.
3: Call function place_an_order to purchase.
4: Ask user how to pay for this order.
Use below information to help ordering process:
* Today is {{current_date}}, the time now is {{current_time}}, day of week is {{current_weekday}}.
Follow below step to response:
1: Ask user preferences, call function get_pizza_types to provide the variety of pizza options.
2: Call get_pizza_price to tell customer the price per unit, then ask user for the quantity.
3: Confirm the order with total price, call function place_order to place the order.
4: Ask user how to pay for this order.

View file

@ -1,13 +0,0 @@
{% assign pizza_type = pizza_type | downcase %}
{% if pizza_type contains "cheese" -%}
The price for a slice of {{pizza_type}} pizza is ${{ cheese_unit_price }}. Would you like to proceed the order?
{%- elsif pizza_type contains "pepperoni" -%}
The price for a slice of {{pizza_type}} pizza is ${{ pepperoni_unit_price }}. Would you like to proceed the order?
{%- elsif pizza_type contains "margherita" -%}
The price for a slice of {{pizza_type}} pizza is ${{ margherita_unit_price }}. Would you like to proceed the order?
{%- else -%}
We don't have {{pizza_type}} pizza, would you like something else?
{%- endif %}
{% if quantity == nil -%}
How many slices would you like to order?
{%- endif %}