Rename to ReasoningPlanner.

This commit is contained in:
Haiping Chen 2023-10-28 20:54:10 -05:00
parent 513ad5814e
commit c7ff0e4c2e
15 changed files with 133 additions and 124 deletions

View file

@ -7,6 +7,7 @@ namespace BotSharp.Abstraction.Planning;
/// </summary>
public interface IPlaner
{
Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string conversation);
Task<FunctionCallFromLlm> GetNextInstruction(Agent router);
Task<bool> AgentExecuting(FunctionCallFromLlm inst, RoleDialogModel message);
Task<bool> AgentExecuted(FunctionCallFromLlm inst, RoleDialogModel message);
}

View file

@ -24,6 +24,7 @@ public class RoutingContext
public string OriginAgentId
=> _stack.Where(x => x != _setting.RouterId).Last();
public bool IsEmpty => !_stack.Any();
public string GetCurrentAgentId()
{
if (_stack.Count == 0)
@ -44,14 +45,13 @@ public class RoutingContext
/// <summary>
/// Pop current agent
/// </summary>
/// <returns>Return next agent</returns>
public string Pop()
public void Pop()
{
if (_stack.Count > 1)
{
_stack.Pop();
}
_stack.Pop();
}
return _stack.Peek();
public void Empty()
{
_stack.Clear();
}
}

View file

@ -71,14 +71,13 @@ public static class BotSharpServiceCollectionExtensions
services.AddSingleton((IServiceProvider x) => routingSettings);
services.AddScoped<NaivePlanner>();
services.AddScoped<FeedbackReasoningPlanner>();
services.AddScoped<ReasoningPlanner>();
services.AddScoped<IPlaner>(provider =>
{
if (routingSettings.Planner == "NaivePlanner")
if (routingSettings.Planner == nameof(ReasoningPlanner))
return provider.GetRequiredService<ReasoningPlanner>();
else
return provider.GetRequiredService<NaivePlanner>();
else if (routingSettings.Planner == "FeedbackReasoningPlanner")
return provider.GetRequiredService<FeedbackReasoningPlanner>();
throw new NotImplementedException();
});
services.AddScoped<IExecutor, InstructExecutor>();

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Planning;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Templating;
namespace BotSharp.Core.Planning;
@ -16,7 +17,7 @@ public class NaivePlanner : IPlaner
_logger = logger;
}
public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string conversation)
public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router)
{
var next = GetNextStepPrompt(router);
@ -25,7 +26,7 @@ public class NaivePlanner : IPlaner
var agentService = _services.GetRequiredService<IAgentService>();
var instruction = agentService.RenderedInstruction(router);
var content = $"{instruction}\r\n{conversation}\r\n###\r\n{next}";
var content = $"{instruction}\r\n###\r\n{next}";
// text completion
content = content + "\r\nResponse: ";
@ -54,13 +55,22 @@ public class NaivePlanner : IPlaner
retryCount++;
}
}
// Fix LLM malformed response
FixMalformedResponse(inst);
return inst;
}
public async Task<bool> AgentExecuting(FunctionCallFromLlm inst, RoleDialogModel message)
{
return true;
}
public async Task<bool> AgentExecuted(FunctionCallFromLlm inst, RoleDialogModel message)
{
inst.AgentName = null;
var context = _services.GetRequiredService<RoutingContext>();
context.Empty();
return true;
}
@ -73,4 +83,54 @@ public class NaivePlanner : IPlaner
{
});
}
/// <summary>
/// Sometimes LLM hallucinates and fails to set function names correctly.
/// </summary>
/// <param name="args"></param>
private void FixMalformedResponse(FunctionCallFromLlm args)
{
var agentService = _services.GetRequiredService<IAgentService>();
var agents = agentService.GetAgents(allowRouting: true).Result;
var malformed = false;
// Sometimes it populate malformed Function in Agent name
if (!string.IsNullOrEmpty(args.Function) &&
args.Function == args.AgentName)
{
args.Function = "route_to_agent";
malformed = true;
}
// Another case of malformed response
if (string.IsNullOrEmpty(args.AgentName) &&
agents.Select(x => x.Name).Contains(args.Function))
{
args.AgentName = args.Function;
args.Function = "route_to_agent";
malformed = true;
}
// It should be Route to agent, but it is used as Response to user.
if (!string.IsNullOrEmpty(args.AgentName) &&
agents.Select(x => x.Name).Contains(args.AgentName) &&
args.Function != "route_to_agent")
{
args.Function = "route_to_agent";
malformed = true;
}
// Function name shouldn't contain dot symbol
if (!string.IsNullOrEmpty(args.Function) &&
args.Function.Contains('.'))
{
args.Function = args.Function.Split('.').Last();
malformed = true;
}
if (malformed)
{
_logger.LogWarning($"Captured LLM malformed response");
}
}
}

View file

@ -1,30 +1,30 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Planning;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Templating;
namespace BotSharp.Core.Planning;
public class FeedbackReasoningPlanner : IPlaner
public class ReasoningPlanner : IPlaner
{
private readonly IServiceProvider _services;
private readonly ILogger _logger;
public FeedbackReasoningPlanner(IServiceProvider services, ILogger<FeedbackReasoningPlanner> logger)
public ReasoningPlanner(IServiceProvider services, ILogger<ReasoningPlanner> logger)
{
_services = services;
_logger = logger;
}
public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string conversation)
public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router)
{
var next = GetNextStepPrompt(router);
RoleDialogModel response = default;
var inst = new FunctionCallFromLlm();
var content = $"{conversation}\r\n###\r\n{next}";
var completion = CompletionProvider.GetChatCompletion(_services,
model: "llm-gpt4");
@ -35,7 +35,7 @@ public class FeedbackReasoningPlanner : IPlaner
{
response = completion.GetChatCompletions(router, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, content)
new RoleDialogModel(AgentRole.User, next)
});
inst = response.Content.JsonContent<FunctionCallFromLlm>();
@ -57,9 +57,28 @@ public class FeedbackReasoningPlanner : IPlaner
return inst;
}
public async Task<bool> AgentExecuting(FunctionCallFromLlm inst, RoleDialogModel message)
{
message.Content = inst.Question;
message.FunctionArgs = JsonSerializer.Serialize(inst.Arguments);
var db = _services.GetRequiredService<IBotSharpRepository>();
var agent = db.GetAgents(inst.AgentName).FirstOrDefault();
var context = _services.GetRequiredService<RoutingContext>();
context.Push(agent.Id);
return true;
}
public async Task<bool> AgentExecuted(FunctionCallFromLlm inst, RoleDialogModel message)
{
inst.AgentName = null;
var context = _services.GetRequiredService<RoutingContext>();
context.Pop();
// push Router to continue
// Make decision according to last agent's response
return true;
}

View file

@ -3,6 +3,7 @@ using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Planning;
namespace BotSharp.Core.Routing.Handlers;
@ -24,7 +25,7 @@ public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase, IRoutingHan
public List<string> Planers => new List<string>
{
"FeedbackReasoningPlanner"
nameof(ReasoningPlanner)
};
public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger<ContinueExecuteTaskRoutingHandler> logger, RoutingSettings settings)

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Planning;
namespace BotSharp.Core.Routing.Handlers;
@ -18,7 +19,7 @@ public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase, IRouting
public List<string> Planers => new List<string>
{
"FeedbackReasoningPlanner"
nameof(ReasoningPlanner)
};
public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger<InterruptTaskExecutionRoutingHandler> logger, RoutingSettings settings)

View file

@ -1,10 +1,15 @@
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.Core.Planning;
namespace BotSharp.Core.Routing.Handlers;
/// <summary>
/// Retrieve information from specific agent
/// </summary>
public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "retrieve_data_from_agent";
@ -13,9 +18,9 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("agent", "the name of the agent"),
new ParameterPropertyDef("question", "the question you will ask the agent to get the necessary data"),
new ParameterPropertyDef("reason", "why retrieve data"),
new ParameterPropertyDef("question", "the question you will ask the agent to get the necessary data"),
new ParameterPropertyDef("next_action_agent", "agent that can handle the question"),
new ParameterPropertyDef("args", "required parameters extracted from question and hand over to the next agent")
{
Type = "object"
@ -24,7 +29,7 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
public List<string> Planers => new List<string>
{
"FeedbackReasoningPlanner"
nameof(ReasoningPlanner)
};
public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger<RetrieveDataFromAgentRoutingHandler> logger, RoutingSettings settings)
@ -34,33 +39,9 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
// Retrieve information from specific agent
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAgents(inst.AgentName).FirstOrDefault();
var ret = await routing.InvokeAgent(record.Id, message);
var context = _services.GetRequiredService<RoutingContext>();
var ret = await routing.InvokeAgent(context.GetCurrentAgentId(), message);
/*_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)
{
MessageId = inst.MessageId,
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}: {message.Content}";
// Got the response from agent, then send to reasoner again to make the decision
// inst = await GetNextInstructionFromReasoner($"What's the next step based on user's original goal and function result?");
return true;
return ret;
}
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Planning;
namespace BotSharp.Core.Routing.Handlers;
@ -17,7 +18,7 @@ public class TaskEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
public List<string> Planers => new List<string>
{
"FeedbackReasoningPlanner"
nameof(ReasoningPlanner)
};
public TaskEndRoutingHandler(IServiceProvider services, ILogger<TaskEndRoutingHandler> logger, RoutingSettings settings)

View file

@ -1,56 +0,0 @@
using BotSharp.Abstraction.Functions.Models;
namespace BotSharp.Core.Routing;
public partial class RoutingService
{
/// <summary>
/// Sometimes LLM hallucinates and fails to set function names correctly.
/// </summary>
/// <param name="args"></param>
private void FixMalformedResponse(FunctionCallFromLlm args)
{
var agentService = _services.GetRequiredService<IAgentService>();
var agents = agentService.GetAgents(allowRouting: true).Result;
var malformed = false;
// Sometimes it populate malformed Function in Agent name
if (!string.IsNullOrEmpty(args.Function) &&
args.Function == args.AgentName)
{
args.Function = "route_to_agent";
malformed = true;
}
// Another case of malformed response
if (string.IsNullOrEmpty(args.AgentName) &&
agents.Select(x => x.Name).Contains(args.Function))
{
args.AgentName = args.Function;
args.Function = "route_to_agent";
malformed = true;
}
// It should be Route to agent, but it is used as Response to user.
if (!string.IsNullOrEmpty(args.AgentName) &&
agents.Select(x => x.Name).Contains(args.AgentName) &&
args.Function != "route_to_agent")
{
args.Function = "route_to_agent";
malformed = true;
}
// Function name shouldn't contain dot symbol
if (!string.IsNullOrEmpty(args.Function) &&
args.Function.Contains('.'))
{
args.Function = args.Function.Split('.').Last();
malformed = true;
}
if (malformed)
{
_logger.LogWarning($"Captured LLM malformed response");
}
}
}

View file

@ -65,7 +65,7 @@ public partial class RoutingService
}
else
{
await InvokeAgent(message.CurrentAgentId, message);
await InvokeAgent(agent.Id, message);
}
}
else

View file

@ -2,6 +2,7 @@ using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Planning;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Routing.Settings;
using System.Drawing;
@ -71,6 +72,7 @@ public partial class RoutingService : IRoutingService
_routerInstance.Load();
var router = _routerInstance.Router;
var context = _services.GetRequiredService<RoutingContext>();
var planner = _services.GetRequiredService<IPlaner>();
var executor = _services.GetRequiredService<IExecutor>();
@ -81,12 +83,10 @@ public partial class RoutingService : IRoutingService
loopCount++;
var conversation = await GetConversationContent(Dialogs);
router.TemplateDict["conversation"] = conversation;
// Get instruction from Planner
var inst = await planner.GetNextInstruction(router, conversation);
// Fix LLM malformed response
FixMalformedResponse(inst);
var inst = await planner.GetNextInstruction(router);
// Save states
SaveStateByArgs(inst.Arguments);
@ -96,6 +96,7 @@ public partial class RoutingService : IRoutingService
#else
_logger.LogInformation($"*** Next Instruction *** {inst}");
#endif
await planner.AgentExecuting(inst, message);
// Handle instruction by Executor
var executed = await executor.Execute(this, router, inst, Dialogs, message);
@ -103,7 +104,7 @@ public partial class RoutingService : IRoutingService
await planner.AgentExecuted(inst, message);
// There is no need for the agent to continue processing, indicating that the task has been completed.
if (inst.AgentName == null)
if (context.IsEmpty || context.GetCurrentAgentId() == router.Id)
{
break;
}

View file

@ -58,8 +58,6 @@ public class ChatCompletionProvider : IChatCompletion
if (choice.FinishReason == CompletionsFinishReason.FunctionCall)
{
_logger.LogInformation($"[{agent.Name}]: {message.FunctionCall.Name}({message.FunctionCall.Arguments})");
msg = new RoleDialogModel(AgentRole.Function, message.Content)
{
CurrentAgentId = agent.Id,
@ -77,7 +75,9 @@ public class ChatCompletionProvider : IChatCompletion
var setting = _services.GetRequiredService<ConversationSetting>();
if (setting.ShowVerboseLog)
{
_logger.LogInformation(msg.Content);
_logger.LogInformation(msg.Role == AgentRole.Function ?
$"[{agent.Name}]: {msg.FunctionName}({msg.FunctionArgs})" :
$"[{agent.Name}]: {msg.Content}");
}
// After chat completion hook

View file

@ -11,7 +11,7 @@ public class ProviderHelper
{
public static OpenAIClient GetClient(string model, AzureOpenAiSettings settings)
{
if (model == "gpt-4" || model == "llm-gpt4")
if (model.Contains("gpt-4") || model.Contains("gpt4"))
{
var client = new OpenAIClient(new Uri(settings.GPT4.Endpoint), new AzureKeyCredential(settings.GPT4.ApiKey));
return client;

View file

@ -29,4 +29,5 @@ Arguments:
{%- endif %}
{% endfor %}
[CONVERSATION]
[CONVERSATION]
{{ conversation }}