diff --git a/docs/llm/prompt.md b/docs/llm/prompt.md index da1ae5f2..af101646 100644 --- a/docs/llm/prompt.md +++ b/docs/llm/prompt.md @@ -1,3 +1,3 @@ # Prompt Engineering -LLM uses prompt as input, and the model produces different outputs according to the input. \ No newline at end of file +LLM uses prompt as input, and the model produces different outputs according to the input. diff --git a/docs/llm/template.md b/docs/llm/template.md index 35260c44..f0c197b1 100644 --- a/docs/llm/template.md +++ b/docs/llm/template.md @@ -1,3 +1,9 @@ # Template -We can define the prompt as a template, and the template can be changed according to variables, so that a instruction file can be used to generate a dynamic prompt. \ No newline at end of file +We can define the prompt as a template, and the template can be changed according to variables, so that a instruction file can be used to generate a dynamic prompt. +`BotSharp` uses [liquid](https://shopify.github.io/liquid/) templates to support various complex dynamic prompt engineering. + +`ITemplateRender` +```csharp +bool Render(Agent agent, Dictionary dict) +``` \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs index 8b0769a7..4df491db 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs @@ -4,4 +4,5 @@ public interface IAgentRouting { Task LoadRouter(); Task LoadCurrentAgent(); + RoutingRecord[] GetRoutingRecords(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingTable.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingRecord.cs similarity index 57% rename from src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingTable.cs rename to src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingRecord.cs index 2feedcf8..8f83c74b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingTable.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingRecord.cs @@ -2,19 +2,25 @@ using System.Text.Json.Serialization; namespace BotSharp.Abstraction.Agents.Models; -public class RoutingTable +public class RoutingRecord { [JsonPropertyName("agent_id")] public string AgentId { get; set; } [JsonPropertyName("name")] - public string AgentName { get; set; } + public string Name { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } [JsonPropertyName("required")] public List RequiredFields { get; set; } + [JsonPropertyName("redirect_to")] + public string RedirectTo { get; set; } + public override string ToString() { - return AgentName; + return Name; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs index dfc10822..84ff0274 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs @@ -8,4 +8,5 @@ public class AgentSettings public string RouterId { get; set; } public string DataDir { get; set; } public string TemplateFormat { get; set; } + public int MaxRecursiveDepth { get; set; } = 3; } diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index 48aa9074..802652e9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -4,7 +4,7 @@ netstandard2.1 enable 10.0 - 0.9.0 + 0.9.4 Icon.png diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionExecutionValidationResult.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionExecutionValidationResult.cs deleted file mode 100644 index 423e6563..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionExecutionValidationResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json.Serialization; - -namespace BotSharp.Abstraction.Functions.Models; - -public class FunctionExecutionValidationResult -{ - public FunctionExecutionValidationResult() - { - - } - - public FunctionExecutionValidationResult(string validationStatus, string? validationMessage = null) - { - ValidationStatus = validationStatus; - ValidationMessage = validationMessage; - } - - [JsonPropertyName("validation_status")] - public string ValidationStatus { get; set; } - - [JsonPropertyName("validation_message")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string ValidationMessage { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs b/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs new file mode 100644 index 00000000..61ba9ff7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Templating; + +public interface ITemplateRender +{ + bool Render(Agent agent, Dictionary dict); +} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs index f1b71eb8..eaf28792 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Agents.Models; -using Fluid; namespace BotSharp.Core.Agents.Services; @@ -7,13 +6,14 @@ public abstract class AgentHookBase : IAgentHook { protected Agent _agent; public Agent Agent => _agent; - private static readonly FluidParser _parser = new FluidParser(); - private readonly IServiceProvider _services; + protected readonly IServiceProvider _services; + protected readonly AgentSettings _settings; - public AgentHookBase(IServiceProvider services) + public AgentHookBase(IServiceProvider services, AgentSettings settings) { _services = services; + _settings = settings; } public void SetAget(Agent agent) @@ -28,27 +28,7 @@ public abstract class AgentHookBase : IAgentHook public virtual bool OnInstructionLoaded(string template, Dictionary dict) { - if (_parser.TryParse(template, out var t, out var error)) - { - PopulateStateTokens(dict); - var context = new TemplateContext(dict); - _agent.Instruction = t.Render(context); - return true; - } - else - { - return false; - } - } - - private void PopulateStateTokens(Dictionary dict) - { - var stateService = _services.GetRequiredService(); - var state = stateService.Load(); - foreach (var t in state) - { - dict[t.Key] = t.Value; - } + return true; } public virtual bool OnFunctionsLoaded(ref string functions) diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs index f14ed93c..0aed1531 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs @@ -1,5 +1,5 @@ -using BotSharp.Abstraction.Agents; using BotSharp.Abstraction.Agents.Models; +using System.IO; namespace BotSharp.Core.Agents.Services; @@ -42,4 +42,12 @@ public class AgentRouter : IAgentRouting return agent; } + + public RoutingRecord[] GetRoutingRecords() + { + var agentSettings = _services.GetRequiredService(); + var dbSettings = _services.GetRequiredService(); + var filePath = Path.Combine(dbSettings.FileRepository, agentSettings.DataDir, agentSettings.RouterId, "route.json"); + return JsonSerializer.Deserialize(File.ReadAllText(filePath)); + } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs index f3b91e3e..ec89334f 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Core.Templating; namespace BotSharp.Core.Agents.Services; @@ -15,6 +16,8 @@ public partial class AgentService } var agent = await GetAgent(id); + var templateDict = new Dictionary(); + PopulateState(templateDict); // After agent is loaded foreach (var hook in hooks) @@ -23,7 +26,7 @@ public partial class AgentService if (!string.IsNullOrEmpty(agent.Instruction)) { - hook.OnInstructionLoaded(agent.Instruction, new Dictionary()); + hook.OnInstructionLoaded(agent.Instruction, templateDict); } if (!string.IsNullOrEmpty(agent.Functions)) @@ -41,8 +44,22 @@ public partial class AgentService hook.OnAgentLoaded(agent); } + // render liquid template + var render = _services.GetRequiredService(); + render.Render(agent, templateDict); + _logger.LogInformation($"Loaded agent {agent}."); return agent; } + + private void PopulateState(Dictionary dict) + { + var stateService = _services.GetRequiredService(); + var state = stateService.Load(); + foreach (var t in state) + { + dict[t.Key] = t.Value; + } + } } diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index c6c51419..235ba733 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -4,7 +4,7 @@ netstandard2.1 10.0 false - 0.9.0 + 0.9.4 diff --git a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs index 95e73f6c..376aa7dc 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Repositories; using BotSharp.Core.Functions; +using BotSharp.Core.Hooks; +using BotSharp.Core.Templating; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using DatabaseSettings = BotSharp.Abstraction.Repositories.DatabaseSettings; @@ -37,10 +39,18 @@ public static class BotSharpServiceCollectionExtensions RegisterPlugins(services, config); + // Register template render + services.AddSingleton(); + + // Register router services.AddScoped(); + // Register function callback services.AddScoped(); + // Register Hooks + services.AddScoped(); + return services; } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs index b02f3d03..66f0a25d 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs @@ -29,9 +29,17 @@ public partial class ConversationService await hook.OnFunctionExecuting(msg); } - // Execute function - await fn.Execute(msg); - + try + { + // Execute function + await fn.Execute(msg); + } + catch (Exception ex) + { + msg.ExecutionResult = ex.Message; + _logger.LogError(msg.ExecutionResult); + } + // After functions have been executed foreach (var hook in hooks) { diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs index d4f058f7..40856809 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs @@ -7,13 +7,13 @@ namespace BotSharp.Core.Conversations.Services; public partial class ConversationService { - const int maxRecursiveDepth = 3; int currentRecursiveDepth = 0; private async Task GetChatCompletionsAsyncRecursively(IChatCompletion chatCompletion, string conversationId, Agent agent, List wholeDialogs, + int maxRecursiveDepth, Func onMessageReceived, Func onFunctionExecuting, Func onFunctionExecuted) @@ -83,6 +83,7 @@ public partial class ConversationService conversationId, agent, wholeDialogs, + maxRecursiveDepth, onMessageReceived, onFunctionExecuting, onFunctionExecuted); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 45ede9dc..8d53971a 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -65,11 +65,14 @@ public partial class ConversationService await hook.BeforeCompletion(); } + var agentSettings = _services.GetRequiredService(); + var chatCompletion = GetChatCompletion(); var result = await GetChatCompletionsAsyncRecursively(chatCompletion, conversationId, agent, wholeDialogs, + agentSettings.MaxRecursiveDepth, onMessageReceived, onFunctionExecuting, onFunctionExecuted); diff --git a/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs index 35b2d7a8..f169d5c9 100644 --- a/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs @@ -29,7 +29,12 @@ public class RouteToAgentFn : IFunctionCallback } else { - if (!HasMissingRequiredField(message, out var agentId)) + var missingfield = HasMissingRequiredField(message, out var agentId); + if (missingfield && message.CurrentAgentId != agentId) + { + message.CurrentAgentId = agentId; + } + else { message.CurrentAgentId = agentId; message.ExecutionResult = $"Routed to {args.AgentName}"; @@ -46,23 +51,23 @@ public class RouteToAgentFn : IFunctionCallback private bool HasMissingRequiredField(RoleDialogModel message, out string agentId) { var args = JsonSerializer.Deserialize(message.FunctionArgs); + var router = _services.GetRequiredService(); + var records = router.GetRoutingRecords(); + var routingRule = records.FirstOrDefault(x => x.Name.ToLower() == args.AgentName.ToLower()); - var routes = GetRoutingTable(); - var agent = routes.FirstOrDefault(x => x.AgentName.ToLower() == args.AgentName.ToLower()); - - if (agent == null) + if (routingRule == null) { agentId = message.CurrentAgentId; message.ExecutionResult = $"Can't find agent {args.AgentName}"; return true; } - agentId = agent.AgentId; + agentId = routingRule.AgentId; // Check required fields var jo = JsonSerializer.Deserialize(message.FunctionArgs); bool hasMissingField = false; - foreach (var field in agent.RequiredFields) + foreach (var field in routingRule.RequiredFields) { if (jo is JsonElement root) { @@ -72,17 +77,21 @@ public class RouteToAgentFn : IFunctionCallback hasMissingField = true; break; } + else if (root.EnumerateObject().Any(x => x.Name == field) && + string.IsNullOrEmpty(root.EnumerateObject().FirstOrDefault(x => x.Name == field).Value.ToString())) + { + message.ExecutionResult = $"missing {field}."; + hasMissingField = true; + break; + } } } + if (hasMissingField && !string.IsNullOrEmpty(routingRule.RedirectTo)) + { + agentId = routingRule.RedirectTo; + } + return hasMissingField; } - - private RoutingTable[] GetRoutingTable() - { - var agentSettings = _services.GetRequiredService(); - var dbSettings = _services.GetRequiredService(); - var filePath = Path.Combine(dbSettings.FileRepository, agentSettings.DataDir, agentSettings.RouterId, "route.json"); - return JsonSerializer.Deserialize(File.ReadAllText(filePath)); - } } diff --git a/src/Infrastructure/BotSharp.Core/Hooks/AgentHook.cs b/src/Infrastructure/BotSharp.Core/Hooks/AgentHook.cs new file mode 100644 index 00000000..e030a904 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Hooks/AgentHook.cs @@ -0,0 +1,16 @@ +namespace BotSharp.Core.Hooks; + +public class AgentHook : AgentHookBase +{ + public AgentHook(IServiceProvider services, AgentSettings settings) + : base(services, settings) + { + } + + public override bool OnInstructionLoaded(string template, Dictionary dict) + { + var router = _services.GetRequiredService(); + dict["routing_records"] = router.GetRoutingRecords(); + return true; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs new file mode 100644 index 00000000..ca80356a --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -0,0 +1,39 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Templating; +using Fluid; +using Microsoft.Extensions.Options; + +namespace BotSharp.Core.Templating; + +public class TemplateRender : ITemplateRender +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private static readonly FluidParser _parser = new FluidParser(); + private TemplateOptions _options; + + public TemplateRender(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + _options = new TemplateOptions(); + _options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase; + _options.MemberAccessStrategy.Register(); + } + + public bool Render(Agent agent, Dictionary dict) + { + var template = agent.Instruction; + if (_parser.TryParse(template, out var t, out var error)) + { + var context = new TemplateContext(dict, _options); + agent.Instruction = t.Render(context); + return true; + } + else + { + + return false; + } + } +}