Unified InvokeFunction.

This commit is contained in:
Haiping Chen 2024-02-29 23:44:57 -06:00
parent 35f89e9be6
commit fb0adadf6a
15 changed files with 95 additions and 155 deletions

View file

@ -49,7 +49,7 @@ PS D:\> npm run dev
Access http://localhost:5015/
[Online Demo with UI](https://botsharp.azurewebsites.net/)
[Online Demo with UI](https://botsharp.azurewebsites.net/?wt.mc_id=AI-MVP-5005183)
<img src="./docs/static/screenshots/agent-builder-agents.png" height="450px"/>
@ -112,7 +112,7 @@ BotSharp uses component design, the kernel is kept to a minimum, and business fu
### Documents
Read the docs: https://botsharp.readthedocs.io
Read the docs: https://botsharp.readthedocs.io?wt.mc_id=AI-MVP-5005183
If you feel that this project is helpful to you, please Star the project, we would be very grateful.

View file

@ -64,9 +64,9 @@ author = 'Haiping Chen'
# built documents.
#
# The short X.Y version.
version = '0.23'
version = '1.1'
# The full version, including alpha/beta/rc tags.
release = '0.23.0'
release = '1.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -35,6 +35,4 @@ public interface IConversationService
List<RoleDialogModel> GetDialogHistory(int lastCount = 50);
Task CleanHistory(string agentId);
Task CallFunctions(RoleDialogModel msg);
}

View file

@ -1,57 +0,0 @@
using BotSharp.Abstraction.Functions;
namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService
{
public async Task CallFunctions(RoleDialogModel msg)
{
// Invoke functions
var functions = _services.GetServices<IFunctionCallback>()
.Where(x => x.Name == msg.FunctionName)
.ToList();
if (functions.Count == 0)
{
msg.Content = $"Can't find function implementation of {msg.FunctionName}.";
_logger.LogError(msg.Content);
return;
}
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
foreach (var fn in functions)
{
// Before executing functions
foreach (var hook in hooks)
{
await hook.OnFunctionExecuting(msg);
}
try
{
// Execute function
await fn.Execute(msg);
if (string.IsNullOrEmpty(msg.Content))
{
msg.Content = msg.Content ?? JsonSerializer.Serialize(msg.Data);
}
}
catch (Exception ex)
{
msg.Content = ex.Message;
msg.StopCompletion = true;
_logger.LogError(msg.Content);
}
// After functions have been executed
foreach (var hook in hooks)
{
await hook.OnFunctionExecuted(msg);
}
}
}
}

View file

@ -1,7 +1,3 @@
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService : IConversationService

View file

@ -1,51 +0,0 @@
using BotSharp.Abstraction.Functions;
namespace BotSharp.Core.Instructs;
public partial class InstructService
{
private async Task CallFunctions(RoleDialogModel msg)
{
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority).ToList();
// Invoke functions
var functions = _services.GetServices<IFunctionCallback>()
.Where(x => x.Name == msg.FunctionName)
.ToList();
if (functions.Count == 0)
{
msg.Content = $"Can't find function implementation of {msg.FunctionName}.";
_logger.LogError(msg.Content);
return;
}
foreach (var fn in functions)
{
// Before executing functions
foreach (var hook in hooks)
{
await hook.OnFunctionExecuting(msg);
}
try
{
// Execute function
await fn.Execute(msg);
}
catch (Exception ex)
{
msg.Content = ex.Message;
msg.StopCompletion = true;
_logger.LogError(msg.Content);
}
// After functions have been executed
foreach (var hook in hooks)
{
await hook.OnFunctionExecuted(msg);
}
}
}
}

View file

@ -29,6 +29,7 @@ public class RouteToAgentFn : IFunctionCallback
{
// Correct user goal agent to keep orignal task
var goalAgentInState = states.GetState("user_goal_agent", string.Empty);
bool correctToOriginalAgent = false;
if (goalAgentInState == string.Empty)
{
states.SetState("user_goal_agent", args.OriginalAgent, isNeedVersion: true);
@ -37,6 +38,7 @@ public class RouteToAgentFn : IFunctionCallback
{
// Correct to original agent
args.OriginalAgent = goalAgentInState;
correctToOriginalAgent = true;
}
else if (args.OriginalAgent != args.AgentName && args.OriginalAgent != goalAgentInState)
{
@ -49,7 +51,7 @@ public class RouteToAgentFn : IFunctionCallback
var originalAgent = db.GetAgents(filter).FirstOrDefault();
if (originalAgent != null)
{
_context.Push(originalAgent.Id, $"user goal agent");
_context.Push(originalAgent.Id, $"user goal agent{(correctToOriginalAgent ? " & is corrected" : "")}");
}
}
@ -90,8 +92,8 @@ public class RouteToAgentFn : IFunctionCallback
var missingfield = HasMissingRequiredField(message, out var agentId);
if (missingfield && message.CurrentAgentId != agentId)
{
// Stack original Agent
_context.Push(agentId, reason: "redirection rule");
// Stack redirection agent
_context.Push(agentId, reason: $"redirection: {message.Content}");
}
}

View file

@ -1,7 +1,3 @@
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;
@ -39,12 +35,10 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var context = _services.GetRequiredService<IRoutingContext>();
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == inst.Function);
message.FunctionArgs = JsonSerializer.Serialize(inst);
var ret = await function.Execute(message);
var ret = await routing.InvokeFunction(message.FunctionName, message);
var agentId = context.GetCurrentAgentId();
var agentId = routing.Context.GetCurrentAgentId();
// Update next action agent's name
var agentService = _services.GetRequiredService<IAgentService>();

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Templating;
namespace BotSharp.Core.Routing;
@ -57,15 +55,13 @@ public partial class RoutingService
var states = _services.GetRequiredService<IConversationStateService>();
states.SaveStateByArgs(message.FunctionArgs?.JsonContent<JsonDocument>());
var conversationService = _services.GetRequiredService<IConversationService>();
var routing = _services.GetRequiredService<IRoutingService>();
// Call functions
await conversationService.CallFunctions(message);
await routing.InvokeFunction(message.FunctionName, message);
// Pass execution result to LLM to get response
if (!message.StopCompletion)
{
var routing = _services.GetRequiredService<IRoutingContext>();
// Find response template
var templateService = _services.GetRequiredService<IResponseTemplateService>();
var responseTemplate = await templateService.RenderFunctionResponse(message.CurrentAgentId, message);
@ -83,7 +79,7 @@ public partial class RoutingService
content: message.Content));
// Send to Next LLM
var agentId = routing.GetCurrentAgentId();
var agentId = routing.Context.GetCurrentAgentId();
await InvokeAgent(agentId, dialogs);
}
}

View file

@ -7,16 +7,56 @@ public partial class RoutingService
public async Task<bool> InvokeFunction(string name, RoleDialogModel message)
{
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == name);
if (function == null) return false;
if (function == null)
{
message.StopCompletion = true;
message.Content = $"Can't find function implementation of {message.FunctionName}.";
_logger.LogError(message.Content);
return false;
}
var originalFunctionName = message.FunctionName;
message.FunctionName = name;
message.Role = AgentRole.Function;
message.FunctionArgs = message.FunctionArgs;
var result = await function.Execute(message);
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
// Before executing functions
foreach (var hook in hooks)
{
await hook.OnFunctionExecuting(message);
}
bool result = false;
try
{
result = await function.Execute(message);
}
catch (Exception ex)
{
message.StopCompletion = true;
message.Content = ex.Message;
_logger.LogError(ex.ToString());
}
// Make sure content has been populated
if (string.IsNullOrEmpty(message.Content) && message.Data != null)
{
message.Content = JsonSerializer.Serialize(message.Data);
}
// After functions have been executed
foreach (var hook in hooks)
{
await hook.OnFunctionExecuted(message);
}
// restore original function name
if (!message.StopCompletion)
if (!message.StopCompletion && message.FunctionName != originalFunctionName)
{
message.FunctionName = originalFunctionName;
}

View file

@ -1,4 +1,9 @@
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using BotSharp.Abstraction.Messaging.Models.RichContent.Template;
using System.Linq;
namespace BotSharp.Plugin.PizzaBot.Functions;
@ -6,15 +11,40 @@ public class GetPizzaTypesFn : IFunctionCallback
{
public string Name => "get_pizza_types";
private readonly IServiceProvider _services;
public GetPizzaTypesFn(IServiceProvider services)
{
_services = services;
}
public async Task<bool> Execute(RoleDialogModel message)
{
message.Content = "Pepperoni Pizza, Cheese Pizza, Margherita Pizza";
message.Data = new List<string>
var states = _services.GetRequiredService<IConversationStateService>();
var pizzaTypes = new List<string>
{
"Pepperoni Pizza",
"Cheese Pizza",
"Margherita Pizza"
};
message.Data = pizzaTypes;
message.RichContent = new RichContent<IRichMessage>
{
Recipient = new Recipient
{
Id = states.GetConversationId()
},
Message = new ButtonTemplateMessage
{
Text = "Please select a pizza type",
Buttons = pizzaTypes.Select(x => new ButtonElement
{
Type = "text",
Title = x,
Payload = x
}).ToArray()
}
};
return true;
}
}

View file

@ -7,7 +7,7 @@
"createdDateTime": "2023-08-18T10:39:32.2349685Z",
"updatedDateTime": "2023-08-18T14:39:32.2349686Z",
"iconUrl": "https://cdn-icons-png.flaticon.com/512/6978/6978255.png",
"disabled": true,
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ],
"routingRules": [

View file

@ -4,15 +4,7 @@
"createdDateTime": "2023-08-18T14:39:32.2349685Z",
"updatedDateTime": "2023-08-18T14:39:32.2349686Z",
"id": "b284db86-e9c2-4c25-a59e-4649797dd130",
"disabled": true,
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ],
"routingRules": [
{
"field": "order_number",
"description": "order number",
"type": "string",
"redirectTo": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd"
}
]
"profiles": [ "pizza" ]
}

View file

@ -4,7 +4,7 @@
"createdDateTime": "2023-07-26T02:29:25.123224Z",
"updatedDateTime": "2023-07-26T02:29:25.123274Z",
"id": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd",
"disabled": true,
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ]
}

View file

@ -4,7 +4,7 @@
"createdDateTime": "2023-07-26T02:29:25.123224Z",
"updatedDateTime": "2023-07-26T02:29:25.123274Z",
"id": "fe8c60aa-b114-4ef3-93cb-a8efeac80f75",
"disabled": true,
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ],
"routingRules": [
@ -13,7 +13,7 @@
"description": "order number",
"type": "string",
"required": true,
"redirectTo": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd"
"redirectTo": "b284db86-e9c2-4c25-a59e-4649797dd130"
}
]
}