diff --git a/Directory.Build.props b/Directory.Build.props index 7459be8a..56e1a108 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,9 @@ net8.0 - 12.0 - 1.0.1 - false + 10.0 + 1.2.1 + true false \ No newline at end of file diff --git a/README.md b/README.md index abaad76b..2c072c09 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Discord](https://img.shields.io/discord/1106946823282761851?label=Discord)](https://discord.com/channels/1106946823282761851/1106947212459642991) [![QQ群聊](https://img.shields.io/static/v1?label=QQ&message=群聊&color=brightgreen)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=sN9VVMwbWjs5L0ATpizKKxOcZdEPMrp8&authKey=RLDw41bLTrEyEgZZi%2FzT4pYk%2BwmEFgFcrhs8ZbkiVY7a4JFckzJefaYNW6Lk4yPX&noverify=0&group_code=985366726) -[![Join the chat at https://gitter.im/publiclab/publiclab](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/sci-sharp/community) [![Apache 2.0](https://img.shields.io/hexpm/l/plug.svg)](https://raw.githubusercontent.com/Oceania2018/BotSharp/master/LICENSE) [![NuGet](https://img.shields.io/nuget/dt/BotSharp.Core.svg)](https://www.nuget.org/packages/BotSharp.Core) [![Build status](https://ci.appveyor.com/api/projects/status/qx2dx5ca5hjqodm5?svg=true)](https://ci.appveyor.com/project/Haiping-Chen/botsharp) @@ -88,6 +87,7 @@ BotSharp uses component design, the kernel is kept to a minimum, and business fu - BotSharp.Plugin.HuggingFace - BotSharp.Plugin.LLamaSharp - BotSharp.Plugin.SemanticKernel +- BotSharp.Plugin.SparkDesk #### Messaging / Channel - BotSharp.OpenAPI diff --git a/docs/llm/function.md b/docs/llm/function.md index 07cfeb1e..69c4f20d 100644 --- a/docs/llm/function.md +++ b/docs/llm/function.md @@ -2,4 +2,32 @@ A **calling function** is a function that is passed as an argument to another function and is executed after a specific event or action occurs. In the context of **large language models (LLMs)**, calling functions can be used to hook into various stages of an LLM application. They are useful for tasks such as logging, monitoring, streaming, and more. For example, in the **BotSharp** framework, calling functions can be used to log information, monitor the progress of an LLM application, or perform other tasks. The BotSharp provides a `callbacks` argument that allows developers to interactive with external systems. -The use of calling functions in LLM applications provides flexibility and extensibility. Developers can customize the behavior of their applications by defining callback handlers that implement specific methods. These handlers can be used for tasks like logging, error handling, or interacting with external systems. The function will be triggered by LLM based on the conversation context. \ No newline at end of file +The use of calling functions in LLM applications provides flexibility and extensibility. Developers can customize the behavior of their applications by defining callback handlers that implement specific methods. These handlers can be used for tasks like logging, error handling, or interacting with external systems. The function will be triggered by LLM based on the conversation context. + +## Hide Function + +In order to more flexibly control whether the Agent is allowed to use a certain function, there is a Visibility Expression property in the function definition that can be used to control display or hiding. When we input prompt into LLM, although we can use state variables in the system instruction file to control the rendering content, LLM will still take the definition of the function into consideration. If the related functions are not hidden at the same time, LLM will still be It is possible to call related functions, bringing unexpected results. Because we need to control system instruction and function definition at the same time to make them consistent. + +```json +{ + "name": "make_payment", + "description": "call this function to make payment", + "visibility_expression": "{% if states.order_number != empty %}visible{% endif %}", + "parameters": { + "type": "object", + "properties": { + "order_number": { + "type": "string", + "description": "order number." + }, + "total_amount": { + "type": "string", + "description": "total amount." + } + }, + "required": ["order_number", "total_amount"] + } +} +``` + +The above is an example. The system will parse the liquid template of Visibility Expression `{% if states.order_number != empty %}visible{% endif %}`. When "visible" is returned, the system will allow the Agent to use this function. In liquid In expressions, we can use `states.name` to reference the state value in the conversation. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index 5f3b1b87..c8aee271 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; @@ -23,6 +24,8 @@ public interface IAgentService string RenderedTemplate(Agent agent, string templateName); + bool RenderFunction(Agent agent, FunctionDef def); + /// /// Get agent detail without trigger any hook. /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index ad79ddcf..36f065fd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -29,7 +29,7 @@ public abstract class ConversationHookBase : IConversationHook public virtual Task OnStateLoaded(ConversationState state) => Task.CompletedTask; - public virtual Task OnStateChanged(string name, string preValue, string currentValue) + public virtual Task OnStateChanged(StateChangeModel stateChange) => Task.CompletedTask; public virtual Task OnDialogRecordLoaded(RoleDialogModel dialog) diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index 4d922d87..b9f0f3b8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -43,7 +43,7 @@ public interface IConversationHook Task OnDialogRecordLoaded(RoleDialogModel dialog); Task OnStateLoaded(ConversationState state); - Task OnStateChanged(string name, string preValue, string currentValue); + Task OnStateChanged(StateChangeModel stateChange); Task OnMessageReceived(RoleDialogModel message); Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg); diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateChangeModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateChangeModel.cs new file mode 100644 index 00000000..f98a896d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/StateChangeModel.cs @@ -0,0 +1,25 @@ +namespace BotSharp.Abstraction.Conversations.Models; + +public class StateChangeModel +{ + [JsonPropertyName("conversation_id")] + public string ConversationId { get; set; } + + [JsonPropertyName("message_id")] + public string MessageId { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("before_value")] + public string BeforeValue { get; set; } + + [JsonPropertyName("before_active_rounds")] + public int? BeforeActiveRounds { get; set; } + + [JsonPropertyName("after_value")] + public string AfterValue { get; set; } + + [JsonPropertyName("after_active_rounds")] + public int? AfterActiveRounds { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs index d523cb9c..d5bd5979 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallingResponse.cs @@ -18,4 +18,9 @@ public class FunctionCallingResponse [JsonPropertyName("args")] public JsonDocument? Args { get; set; } + + public override string ToString() + { + return $"{FunctionName}({JsonSerializer.Serialize(Args)}) => {Content}"; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs index 54ef0000..8458adf6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionDef.cs @@ -5,6 +5,9 @@ public class FunctionDef public string Name { get; set; } public string Description { get; set; } + [JsonPropertyName("visibility_expression")] + public string? VisibilityExpression { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Impact { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs new file mode 100644 index 00000000..a4039bf7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/StateConst.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Infrastructures.Enums; + +public class StateConst +{ + public const string EXPECTED_ACTION_AGENT = "expected_next_action_agent"; + public const string EXPECTED_GOAL_AGENT = "expected_user_goal_agent"; + public const string NEXT_ACTION_AGENT = "next_action_agent"; + public const string USER_GOAL_AGENT = "user_goal_agent"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs index 44f229ae..9d0662db 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/IContentGeneratingHook.cs @@ -28,4 +28,13 @@ public interface IContentGeneratingHook /// /// Task AfterGenerated(RoleDialogModel message, TokenStatsModel tokenStats) => Task.CompletedTask; + + /// + /// Rdndering template + /// + /// + /// + /// + /// + Task OnRenderingTemplate(Agent agent, string name, string content) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/AgentQueueChangedLogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/AgentQueueChangedLogModel.cs new file mode 100644 index 00000000..0fca0764 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/AgentQueueChangedLogModel.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Abstraction.Loggers.Models; + +public class AgentQueueChangedLogModel +{ + [JsonPropertyName("conversation_id")] + public string ConversationId { get; set; } + + [JsonPropertyName("log")] + public string Log { get; set; } + + [JsonPropertyName("created_at")] + public DateTime CreateTime { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/StateChangeOutputModel.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/StateChangeOutputModel.cs new file mode 100644 index 00000000..c166bbef --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Models/StateChangeOutputModel.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Loggers.Models; + +public class StateChangeOutputModel : StateChangeModel +{ + [JsonPropertyName("created_at")] + public DateTime CreateTime { get; set; } = DateTime.UtcNow; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs index 20b0d949..502b0785 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/RichContentJsonConverter .cs @@ -10,15 +10,7 @@ public class RichContentJsonConverter : JsonConverter using var jsonDoc = JsonDocument.ParseValue(ref reader); var root = jsonDoc.RootElement; var jsonText = root.GetRawText(); - IRichMessage? res = null; - - var parser = new MessageParser(); - if (root.TryGetProperty("rich_type", out JsonElement element)) - { - var richType = element.GetString(); - res = parser.ParseRichMessage(richType, jsonText, root, options); - } - + var res = MessageParser.ParseRichMessage(root, options); return res; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs index 7729f40f..f0f14730 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/JsonConverters/TemplateMessageJsonConverter.cs @@ -10,15 +10,7 @@ public class TemplateMessageJsonConverter : JsonConverter using var jsonDoc = JsonDocument.ParseValue(ref reader); var root = jsonDoc.RootElement; var jsonText = root.GetRawText(); - ITemplateMessage? res = null; - - var parser = new MessageParser(); - if (root.TryGetProperty("template_type", out JsonElement element)) - { - var templateType = element.GetString(); - res = parser.ParseTemplateMessage(templateType, jsonText, root, options); - } - + var res = MessageParser.ParseTemplateMessage(root, options); return res; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/MessageParser.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/MessageParser.cs index 3707de4a..b739709c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/MessageParser.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/MessageParser.cs @@ -6,44 +6,51 @@ using System.Text.Json; namespace BotSharp.Core.Messaging; -public class MessageParser +public static class MessageParser { - public MessageParser() - { - } - public IRichMessage? ParseRichMessage(string richType, string jsonText, JsonElement root, JsonSerializerOptions options) + public static IRichMessage? ParseRichMessage(JsonElement root, JsonSerializerOptions options) { IRichMessage? res = null; + JsonElement element; + var jsonText = root.GetRawText(); - if (richType == RichTypeEnum.ButtonTemplate) + if (root.TryGetProperty("rich_type", out element)) { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (richType == RichTypeEnum.MultiSelectTemplate) - { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (richType == RichTypeEnum.QuickReply) - { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (richType == RichTypeEnum.CouponTemplate) - { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (richType == RichTypeEnum.Text) - { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (richType == RichTypeEnum.GenericTemplate) - { - if (root.TryGetProperty("element_type", out var element)) + var richType = element.GetString(); + if (richType == RichTypeEnum.ButtonTemplate) { - var elementType = element.GetString(); - if (elementType == typeof(GenericElement).Name) + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (richType == RichTypeEnum.MultiSelectTemplate) + { + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (richType == RichTypeEnum.QuickReply) + { + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (richType == RichTypeEnum.CouponTemplate) + { + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (richType == RichTypeEnum.Text) + { + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (richType == RichTypeEnum.GenericTemplate) + { + if (root.TryGetProperty("element_type", out element)) { - res = JsonSerializer.Deserialize>(jsonText, options); + var elementType = element.GetString(); + if (elementType == typeof(GenericElement).Name) + { + res = JsonSerializer.Deserialize>(jsonText, options); + } + else if (elementType == typeof(ButtonElement).Name) + { + res = JsonSerializer.Deserialize>(jsonText, options); + } } } } @@ -51,34 +58,44 @@ public class MessageParser return res; } - public ITemplateMessage? ParseTemplateMessage(string templateType, string jsonText, JsonElement root, JsonSerializerOptions options) + public static ITemplateMessage? ParseTemplateMessage(JsonElement root, JsonSerializerOptions options) { ITemplateMessage? res = null; + JsonElement element; + var jsonText = root.GetRawText(); - if (templateType == TemplateTypeEnum.Button) + if (root.TryGetProperty("template_type", out element)) { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (templateType == TemplateTypeEnum.MultiSelect) - { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (templateType == TemplateTypeEnum.Coupon) - { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (templateType == TemplateTypeEnum.Product) - { - res = JsonSerializer.Deserialize(jsonText, options); - } - else if (templateType == TemplateTypeEnum.Generic) - { - if (root.TryGetProperty("element_type", out var element)) + var templateType = element.GetString(); + if (templateType == TemplateTypeEnum.Button) { - var elementType = element.GetString(); - if (elementType == typeof(GenericElement).Name) + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (templateType == TemplateTypeEnum.MultiSelect) + { + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (templateType == TemplateTypeEnum.Coupon) + { + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (templateType == TemplateTypeEnum.Product) + { + res = JsonSerializer.Deserialize(jsonText, options); + } + else if (templateType == TemplateTypeEnum.Generic) + { + if (root.TryGetProperty("element_type", out element)) { - res = JsonSerializer.Deserialize>(jsonText, options); + var elementType = element.GetString(); + if (elementType == typeof(GenericElement).Name) + { + res = JsonSerializer.Deserialize>(jsonText, options); + } + else if (elementType == typeof(ButtonElement).Name) + { + res = JsonSerializer.Deserialize>(jsonText, options); + } } } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs index e3a2f2ef..90168872 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Options/BotSharpOptions.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Messaging.JsonConverters; using System.Text.Json; namespace BotSharp.Abstraction.Options; diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs index 686cd0c8..3f13db96 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs @@ -18,6 +18,7 @@ public class RoutingRule /// /// Field type: string, number, object /// + [JsonPropertyName("field_type")] public string FieldType { get; set; } = "string"; public bool Required { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Using.cs b/src/Infrastructure/BotSharp.Abstraction/Using.cs index 639d6fcc..f9b708dc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Using.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Using.cs @@ -9,4 +9,8 @@ global using BotSharp.Abstraction.Agents.Models; global using BotSharp.Abstraction.Utilities; global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Agents.Enums; -global using BotSharp.Abstraction.Models; \ No newline at end of file +global using BotSharp.Abstraction.Infrastructures.Enums; +global using BotSharp.Abstraction.Models; +global using BotSharp.Abstraction.Routing.Models; +global using BotSharp.Abstraction.Routing.Planning; +global using BotSharp.Abstraction.Templating; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs index 5d618611..6c2d7ea2 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs @@ -90,32 +90,6 @@ public partial class AgentService return agent; } - public string RenderedTemplate(Agent agent, string templateName) - { - // render liquid template - var render = _services.GetRequiredService(); - var template = agent.Templates.First(x => x.Name == templateName).Content; - // update states - var conv = _services.GetRequiredService(); - foreach (var t in conv.States.GetStates()) - { - agent.TemplateDict[t.Key] = t.Value; - } - return render.Render(template, agent.TemplateDict); - } - - public string RenderedInstruction(Agent agent) - { - var render = _services.GetRequiredService(); - // update states - var conv = _services.GetRequiredService(); - foreach (var t in conv.States.GetStates()) - { - agent.TemplateDict[t.Key] = t.Value; - } - return render.Render(agent.Instruction, agent.TemplateDict); - } - private void PopulateState(Dictionary dict) { var conv = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index 2831a669..9c3b34f9 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -1,8 +1,4 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Tasks.Models; -using Microsoft.Extensions.Caching.Memory; -using System.Collections.Generic; using System.IO; namespace BotSharp.Core.Agents.Services; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs new file mode 100644 index 00000000..9ce7a7a8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs @@ -0,0 +1,55 @@ +using BotSharp.Abstraction.Loggers; +using BotSharp.Abstraction.Templating; + +namespace BotSharp.Core.Agents.Services; + +public partial class AgentService +{ + public string RenderedInstruction(Agent agent) + { + var render = _services.GetRequiredService(); + // update states + var conv = _services.GetRequiredService(); + foreach (var t in conv.States.GetStates()) + { + agent.TemplateDict[t.Key] = t.Value; + } + return render.Render(agent.Instruction, agent.TemplateDict); + } + + public bool RenderFunction(Agent agent, FunctionDef def) + { + if (!string.IsNullOrEmpty(def.VisibilityExpression)) + { + var render = _services.GetRequiredService(); + var result = render.Render(def.VisibilityExpression, new Dictionary + { + { "states", agent.TemplateDict } + }); + return result == "visible"; + } + + return true; + } + + public string RenderedTemplate(Agent agent, string templateName) + { + // render liquid template + var render = _services.GetRequiredService(); + var template = agent.Templates.First(x => x.Name == templateName).Content; + // update states + var conv = _services.GetRequiredService(); + foreach (var t in conv.States.GetStates()) + { + agent.TemplateDict[t.Key] = t.Value; + } + + var content = render.Render(template, agent.TemplateDict); + + HookEmitter.Emit(_services, async hook => + await hook.OnRenderingTemplate(agent, templateName, content) + ).Wait(); + + return content; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs index 28f9bdd2..bd009db0 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Repositories; using System.IO; namespace BotSharp.Core.Agents.Services; diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index c9ea6f3b..9d01f355 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -148,15 +148,6 @@ public partial class ConversationService }; var hooks = _services.GetServices().ToList(); - foreach (var hook in hooks) - { - await hook.OnResponseGenerated(response); - } - - await onResponseReceived(response); - - // Add to dialog history - _storage.Append(_conversationId, response); if (response.Instruction != null) { @@ -172,5 +163,15 @@ public partial class ConversationService } } } + + foreach (var hook in hooks) + { + await hook.OnResponseGenerated(response); + } + + await onResponseReceived(response); + + // Add to dialog history + _storage.Append(_conversationId, response); } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 92966564..9356e2f5 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -43,45 +43,56 @@ public class ConversationStateService : IConversationStateService, IDisposable var preValue = string.Empty; var currentValue = value.ToString(); var hooks = _services.GetServices(); + var curActiveRounds = activeRounds > 0 ? activeRounds : -1; + int? preActiveRounds = null; if (ContainsState(name) && _states.TryGetValue(name, out var pair)) { - preValue = pair?.Values.LastOrDefault()?.Data ?? string.Empty; + var lastNode = pair?.Values?.LastOrDefault(); + preActiveRounds = lastNode?.ActiveRounds; + preValue = lastNode?.Data ?? string.Empty; } - if (!ContainsState(name) || preValue != currentValue) + _logger.LogInformation($"[STATE] {name} = {value}"); + var routingCtx = _services.GetRequiredService(); + + foreach (var hook in hooks) { - _logger.LogInformation($"[STATE] {name} = {value}"); - foreach (var hook in hooks) + hook.OnStateChanged(new StateChangeModel { - hook.OnStateChanged(name, preValue, currentValue).Wait(); - } - - var routingCtx = _services.GetRequiredService(); - var newPair = new StateKeyValue - { - Key = name, - Versioning = isNeedVersion - }; - - var newValue = new StateValue - { - Data = currentValue, + ConversationId = _conversationId, MessageId = routingCtx.MessageId, - Active = true, - ActiveRounds = activeRounds > 0 ? activeRounds : -1, - UpdateTime = DateTime.UtcNow, - }; + Name = name, + BeforeValue = preValue, + BeforeActiveRounds = preActiveRounds, + AfterValue = currentValue, + AfterActiveRounds = curActiveRounds + }).Wait(); + } - if (!isNeedVersion || !_states.ContainsKey(name)) - { - newPair.Values = new List { newValue }; - _states[name] = newPair; - } - else - { - _states[name].Values.Add(newValue); - } + var newPair = new StateKeyValue + { + Key = name, + Versioning = isNeedVersion + }; + + var newValue = new StateValue + { + Data = currentValue, + MessageId = routingCtx.MessageId, + Active = true, + ActiveRounds = curActiveRounds, + UpdateTime = DateTime.UtcNow, + }; + + if (!isNeedVersion || !_states.ContainsKey(name)) + { + newPair.Values = new List { newValue }; + _states[name] = newPair; + } + else + { + _states[name].Values.Add(newValue); } return this; @@ -118,7 +129,7 @@ public class ConversationStateService : IConversationStateService, IDisposable state.Value.Values.Add(new StateValue { Data = value.Data, - MessageId = !string.IsNullOrEmpty(curMsgId) ? curMsgId : value.MessageId, + MessageId = curMsgId, Active = false, ActiveRounds = value.ActiveRounds, UpdateTime = DateTime.UtcNow @@ -178,7 +189,7 @@ public class ConversationStateService : IConversationStateService, IDisposable value.Values.Add(new StateValue { Data = lastValue.Data, - MessageId = !string.IsNullOrEmpty(curMsgId) ? curMsgId : lastValue.MessageId, + MessageId = curMsgId, Active = false, ActiveRounds = lastValue.ActiveRounds, UpdateTime = utcNow diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index 33717410..e523ce48 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Messaging; -using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Abstraction.Messaging.Models.RichContent; using BotSharp.Abstraction.Options; using System.IO; @@ -10,7 +9,7 @@ public class ConversationStorage : IConversationStorage { private readonly BotSharpDatabaseSettings _dbSettings; private readonly IServiceProvider _services; - private readonly JsonSerializerOptions _options; + private readonly JsonSerializerOptions _jsonOptions; public ConversationStorage( BotSharpDatabaseSettings dbSettings, @@ -19,17 +18,7 @@ public class ConversationStorage : IConversationStorage { _dbSettings = dbSettings; _services = services; - _options = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - AllowTrailingCommas = true, - Converters = - { - new RichContentJsonConverter(), - new TemplateMessageJsonConverter(), - } - }; + _jsonOptions = InitJsonSerilizerOptions(options); } public void Append(string conversationId, RoleDialogModel dialog) @@ -38,6 +27,13 @@ public class ConversationStorage : IConversationStorage var db = _services.GetRequiredService(); var dialogElements = new List(); + // Prevent duplicate record to be inserted + var dialogs = db.GetConversationDialogs(conversationId); + if (dialogs.Any(x => x.MetaData.MessageId == dialog.MessageId && x.Content == dialog.Content)) + { + return; + } + if (dialog.Role == AgentRole.Function) { var meta = new DialogMetaData @@ -73,7 +69,8 @@ public class ConversationStorage : IConversationStorage { return; } - var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _options) : null; + + var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _jsonOptions) : null; dialogElements.Add(new DialogElement(meta, content, richContent)); } @@ -98,7 +95,7 @@ public class ConversationStorage : IConversationStorage var senderId = role == AgentRole.Function ? currentAgentId : meta.SenderId; var createdAt = meta.CreateTime; var richContent = !string.IsNullOrEmpty(dialog.RichContent) ? - JsonSerializer.Deserialize>(dialog.RichContent, _options) : null; + JsonSerializer.Deserialize>(dialog.RichContent, _jsonOptions) : null; var record = new RoleDialogModel(role, content) { @@ -143,4 +140,21 @@ public class ConversationStorage : IConversationStorage } return Path.Combine(dir, "dialogs.txt"); } + + private JsonSerializerOptions InitJsonSerilizerOptions(BotSharpOptions botSharOptions) + { + var options = botSharOptions.JsonSerializerOptions; + var jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive, + PropertyNamingPolicy = options.PropertyNamingPolicy ?? JsonNamingPolicy.CamelCase, + AllowTrailingCommas = options.AllowTrailingCommas, + }; + + foreach (var converter in options.Converters) + { + jsonOptions.Converters.Add(converter); + } + return jsonOptions; + } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs index 7cce46b0..eb92ac51 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs @@ -19,13 +19,19 @@ public class LlmProviderService : ILlmProviderService { var providers = new List(); var services1 = _services.GetServices(); - providers.AddRange(services1.Select(x => x.Provider)); + providers.AddRange(services1 + .Where(x => GetProviderModels(x.Provider).Any()) + .Select(x => x.Provider)); var services2 = _services.GetServices(); - providers.AddRange(services2.Select(x => x.Provider)); + providers.AddRange(services2 + .Where(x => GetProviderModels(x.Provider).Any()) + .Select(x => x.Provider)); var services3 = _services.GetServices(); - providers.AddRange(services3.Select(x => x.Provider)); + providers.AddRange(services3 + .Where(x => GetProviderModels(x.Provider).Any()) + .Select(x => x.Provider)); return providers.Distinct().ToList(); } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 16133fc1..bb477391 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -407,6 +407,11 @@ namespace BotSharp.Core.Repository var utcNow = DateTime.UtcNow; var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + if (batchSize <= 0 || batchSize > batchLimit) { batchSize = batchLimit; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/HumanInterventionNeededHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/HumanInterventionNeededHandler.cs index 39a50a30..f0de3a9c 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/HumanInterventionNeededHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/HumanInterventionNeededHandler.cs @@ -12,7 +12,7 @@ public class HumanInterventionNeededHandler : RoutingHandlerBase, IRoutingHandle { new ParameterPropertyDef("reason", "why need customer service"), new ParameterPropertyDef("summary", "the whole conversation summary with important information"), - new ParameterPropertyDef("response", "tell the user that you are being transferred to customer service") + new ParameterPropertyDef("response", "asking user whether to connect with customer service representative") }; public HumanInterventionNeededHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs index 5283b48d..ad6eaf8d 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs @@ -6,12 +6,13 @@ public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler { public string Name => "response_to_user"; - public string Description => "Response according to the context without asking specific agent."; + public string Description => "When you can handle the conversation without asking specific agent."; public List Parameters => new List { - new ParameterPropertyDef("reason", "why response to user"), - new ParameterPropertyDef("response", "response content") + new ParameterPropertyDef("reason", "why response to user directly without go to other agents"), + new ParameterPropertyDef("response", "response content to user in courteous words. If the user wants to end the conversation, you must set conversation_end to true and response politely."), + new ParameterPropertyDef("conversation_end", "whether to end this conversation, true or false", type: "boolean") }; public ResponseToUserRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs index 1b8cb284..cfbfe31e 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Routing.Handlers; @@ -6,11 +7,11 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler { public string Name => "route_to_agent"; - public string Description => "Route request to appropriate agent."; + public string Description => "Route request to appropriate virtual agent."; public List Parameters => new List { - new ParameterPropertyDef("next_action_reason", "the reason why route to this agent") + new ParameterPropertyDef("next_action_reason", "the reason why route to this virtual agent") { Required = true }, @@ -18,11 +19,11 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler { Required = true }, - new ParameterPropertyDef("user_goal_description", "user original goal") + new ParameterPropertyDef("user_goal_description", "user goal based on user initial task.") { Required = true }, - new ParameterPropertyDef("user_goal_agent", "user original goal") + new ParameterPropertyDef("user_goal_agent", "agent who can acheive user initial task, must align with user_goal_description ") { Required = true }, @@ -39,11 +40,8 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) { - message.FunctionArgs = JsonSerializer.Serialize(inst); - var ret = await routing.InvokeFunction(message.FunctionName, message); - var states = _services.GetRequiredService(); - var goalAgent = states.GetState("user_goal_agent"); + var goalAgent = states.GetState(StateConst.EXPECTED_GOAL_AGENT); if (!string.IsNullOrEmpty(goalAgent) && inst.OriginalAgent != goalAgent) { inst.OriginalAgent = goalAgent; @@ -53,6 +51,9 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler ); } + message.FunctionArgs = JsonSerializer.Serialize(inst); + var ret = await routing.InvokeFunction(message.FunctionName, message); + var agentId = routing.Context.GetCurrentAgentId(); // Update next action agent's name diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs index 5b42f5d8..fa8c5db3 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs @@ -27,6 +27,32 @@ public class RoutingAgentHook : AgentHookBase var routing = _services.GetRequiredService(); var agents = routing.GetRoutableAgents(_agent.Profiles); + + // Postprocess agent required fields, remove it if the states exists + var states = _services.GetRequiredService(); + foreach (var agent in agents) + { + var fields = agent.RequiredFields.ToArray(); + foreach (var field in fields) + { + if (states.ContainsState(field.Name)) + { + var requiredField = agent.RequiredFields.First(x => x.Name == field.Name); + agent.RequiredFields.Remove(requiredField); + } + } + + fields = agent.OptionalFields.ToArray(); + foreach (var field in fields) + { + if (states.ContainsState(field.Name)) + { + var optionalField = agent.OptionalFields.First(x => x.Name == field.Name); + agent.OptionalFields.Remove(optionalField); + } + } + } + dict["routing_agents"] = agents; dict["routing_handlers"] = routing.GetHandlers(_agent); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs index 0b1708c5..eefb88f7 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs @@ -1,7 +1,4 @@ -using Amazon.Runtime.Internal.Transform; -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories.Filters; +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Planning; using BotSharp.Abstraction.Templating; @@ -116,7 +113,8 @@ public class NaivePlanner : IPlaner var render = _services.GetRequiredService(); return render.Render(template, new Dictionary { - { "expected_next_action_agent", states.GetState("expected_next_action_agent")} + { StateConst.EXPECTED_ACTION_AGENT, states.GetState(StateConst.EXPECTED_ACTION_AGENT) }, + { StateConst.EXPECTED_GOAL_AGENT, states.GetState(StateConst.EXPECTED_GOAL_AGENT) } }); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 8518d769..38912ef7 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -38,6 +38,12 @@ public partial class RoutingService } else { + // Handle output routing exception. + if (agent.Type == AgentType.Routing) + { + response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?"; + } + message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content); @@ -74,9 +80,11 @@ public partial class RoutingService else { // Save to memory dialogs - dialogs.Add(RoleDialogModel.From(message, + var msg = RoleDialogModel.From(message, role: AgentRole.Function, - content: message.Content)); + content: message.Content); + + dialogs.Add(msg); // Send to Next LLM var agentId = routing.Context.GetCurrentAgentId(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 690308eb..6ad2ae75 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -4,6 +4,7 @@ namespace BotSharp.Core.Routing; public partial class RoutingService { + private List _functionCallStack = new List(); public async Task InvokeFunction(string name, RoleDialogModel message) { var function = _services.GetServices().FirstOrDefault(x => x.Name == name); @@ -35,6 +36,13 @@ public partial class RoutingService try { result = await function.Execute(message); + _functionCallStack.Add(new FunctionCallingResponse + { + Role = AgentRole.Function, + FunctionName = message.FunctionName, + Args = JsonDocument.Parse(message.FunctionArgs ?? "{}"), + Content = message.Content + }); } catch (JsonException ex) { @@ -68,6 +76,13 @@ public partial class RoutingService message.FunctionName = originalFunctionName; } + // Save to Storage as well + if (!message.StopCompletion && message.FunctionName != "route_to_agent") + { + var storage = _services.GetRequiredService(); + storage.Append(Context.ConversationId, message); + } + return result; } } diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid index d7d0f359..9caf041d 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid @@ -4,18 +4,18 @@ You're {{router.name}} ({{router.description}}). Follow these steps to handle us 3. Determine which agent is suitable to handle this conversation. 4. Re-think on whether the function you chose matches the reason. 5. For agent required arguments, think carefully, leave it as blank object if user doesn't provide specific arguments. -6. Please do not make up any parameters when there is no exact information available, leave it blank. +6. Please do not make up any parameters when there is no exact value provided, you must set the parameter value as null. 7. Response must be in JSON format. {% if routing_requirements and routing_requirements != empty %} [REQUIREMENTS] -{% for requirement in routing_requirements %} -# {{ requirement }} -{% endfor %} +{% for requirement in routing_requirements -%} +# {{ requirement }}{{ "\r\n" }} +{%- endfor %} {% endif %} [FUNCTIONS] -{% for handler in routing_handlers %} +{% for handler in routing_handlers -%} # {{ handler.description}} {% if handler.parameters and handler.parameters != empty -%} Parameters: @@ -24,25 +24,29 @@ Parameters: - {{ p.name }} {% if p.required -%}(required){%- endif %}: {{ p.description }}{{ "\r\n " }} {%- endfor %} {%- endif %} -{% endfor %} +{{ "\r\n" }} +{%- endfor %} [AGENTS] -{% for agent in routing_agents %} +{% for agent in routing_agents -%} * Agent: {{ agent.name }} {{ agent.description}} -{% if agent.required_fields and agent.required_fields != empty -%} +{%- if agent.required_fields and agent.required_fields != empty -%} +{{ "\r\n" }} Required args: {% for f in agent.required_fields -%} - {{ f.name }} (type: {{ f.type }}): {{ f.description }}{{ "\r\n " }} {%- endfor %} -{%- endif %} -{% if agent.optional_fields and agent.optional_fields != empty -%} +{%- endif -%} +{{ "\r\n" }} +{%- if agent.optional_fields and agent.optional_fields != empty -%} Optional args: {% for f in agent.optional_fields -%} - {{ f.name }} (type: {{ f.type }}): {{ f.description }}{{ "\r\n " }} {%- endfor %} -{%- endif %} -{% endfor %} +{%- endif -%} +{{ "\r\n" }} +{%- endfor %} [CONVERSATION] {{ conversation }} diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid index 6502c493..2b42efaa 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid @@ -1,6 +1,14 @@ What is the next step based on the CONVERSATION? Route to the last handling agent in priority. {% if expected_next_action_agent != empty -%} -Expected next action agent is {{ expected_next_action_agent }}. +Expected next action agent is {{ expected_next_action_agent }}. +{%- else -%} +Next action agent is inferred based on user lastest response. {%- endif %} -If user wants to speak to customer service, use function human_intervention_needed. \ No newline at end of file +{% if expected_user_goal_agent != empty -%} +Expected user goal agent is {{ expected_user_goal_agent }}. +{%- else -%} +User goal agent is inferred based on user initial request. +{%- endif %} +If user wants to speak to customer service, use function human_intervention_needed. +If user wants to or is processing with a specific task that can be handled by agents, respond in appropriate output format defined to let proper agent to handle the task. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs index f50884e0..e3f6afca 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs @@ -219,12 +219,15 @@ public class ChatCompletionProvider : IChatCompletion foreach (var function in agent.Functions) { - chatCompletionsOptions.Functions.Add(new FunctionDefinition + if (agentService.RenderFunction(agent, function)) { - Name = function.Name, - Description = function.Description, - Parameters = BinaryData.FromObjectAsJson(function.Parameters) - }); + chatCompletionsOptions.Functions.Add(new FunctionDefinition + { + Name = function.Name, + Description = function.Description, + Parameters = BinaryData.FromObjectAsJson(function.Parameters) + }); + } } foreach (var message in conversations) diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 1f58faa0..2162bcb9 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -72,6 +72,27 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); } + public async Task OnRenderingTemplate(Agent agent, string name, string content) + { + if (!_convSettings.ShowVerboseLog) return; + + var conversationId = _state.GetConversationId(); + + var log = $"{agent.Name} is using template {name}"; + var message = new RoleDialogModel(AgentRole.System, log) + { + MessageId = _routingCtx.MessageId + }; + + var input = new ContentLogInputModel(conversationId, message) + { + Name = agent.Name, + Source = ContentLogSource.HardRule, + Log = log + }; + await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + } + public async Task BeforeGenerating(Agent agent, List conversations) { if (!_convSettings.ShowVerboseLog) return; @@ -213,6 +234,12 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); } + public override async Task OnStateChanged(StateChangeModel stateChange) + { + if (stateChange == null) return; + + await _chatHub.Clients.User(_user.Id).SendAsync("OnStateChangeGenerated", BuildStateChangeLog(stateChange)); + } #endregion #region IRoutingHook @@ -220,9 +247,13 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR { var conversationId = _state.GetConversationId(); var agent = await _agentService.LoadAgent(agentId); - var preAgent = await _agentService.LoadAgent(preAgentId); - var log = $"{agent.Name} is enqueued{(reason != null ? $" ({reason})" : "")}"; + // Agent queue log + var log = $"{agent.Name} is enqueued"; + await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + + // Content log + log = $"{agent.Name} is enqueued{(reason != null ? $" ({reason})" : "")}"; var message = new RoleDialogModel(AgentRole.System, log) { MessageId = _routingCtx.MessageId @@ -243,7 +274,12 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR var agent = await _agentService.LoadAgent(agentId); var currentAgent = await _agentService.LoadAgent(currentAgentId); - var log = $"{agent.Name} is dequeued{(reason != null ? $" ({reason})" : "")}, current agent is {currentAgent?.Name}"; + // Agent queue log + var log = $"{agent.Name} is dequeued"; + await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + + // Content log + log = $"{agent.Name} is dequeued{(reason != null ? $" ({reason})" : "")}, current agent is {currentAgent?.Name}"; var message = new RoleDialogModel(AgentRole.System, log) { MessageId = _routingCtx.MessageId @@ -264,7 +300,12 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR var fromAgent = await _agentService.LoadAgent(fromAgentId); var toAgent = await _agentService.LoadAgent(toAgentId); - var log = $"{fromAgent.Name} is replaced to {toAgent.Name}{(reason != null ? $" ({reason})" : "")}"; + // Agent queue log + var log = $"Agent queue is replaced from {fromAgent.Name} to {toAgent.Name}"; + await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + + // Content log + log = $"{fromAgent.Name} is replaced to {toAgent.Name}{(reason != null ? $" ({reason})" : "")}"; var message = new RoleDialogModel(AgentRole.System, log) { MessageId = _routingCtx.MessageId @@ -282,9 +323,13 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentQueueEmptied(string agentId, string? reason = null) { var conversationId = _state.GetConversationId(); - var agent = await _agentService.LoadAgent(agentId); - var log = reason ?? "Agent queue is cleared"; + // Agent queue log + var log = $"Agent queue is empty"; + await _chatHub.Clients.User(_user.Id).SendAsync("OnAgentQueueChanged", BuildAgentQueueChangedLog(conversationId, log)); + + // Content log + log = reason ?? "Agent queue is cleared"; var message = new RoleDialogModel(AgentRole.System, log) { MessageId = _routingCtx.MessageId @@ -320,7 +365,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR { var conversationId = _state.GetConversationId(); var agent = await _agentService.LoadAgent(message.CurrentAgentId); - var log = $"Revised user goal agent to: {agent?.Name}"; + var log = $"Revised user goal agent to {instruct.OriginalAgent}"; var input = new ContentLogInputModel(conversationId, message) { @@ -379,4 +424,33 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR return JsonSerializer.Serialize(log, _options.JsonSerializerOptions); } + + private string BuildStateChangeLog(StateChangeModel stateChange) + { + var log = new StateChangeOutputModel + { + ConversationId = stateChange.ConversationId, + MessageId = stateChange.MessageId, + Name = stateChange.Name, + BeforeValue = stateChange.BeforeValue, + BeforeActiveRounds = stateChange.BeforeActiveRounds, + AfterValue = stateChange.AfterValue, + AfterActiveRounds = stateChange.AfterActiveRounds, + CreateTime = DateTime.UtcNow + }; + + return JsonSerializer.Serialize(log, _options.JsonSerializerOptions); + } + + private string BuildAgentQueueChangedLog(string conversationId, string log) + { + var model = new AgentQueueChangedLogModel + { + ConversationId = conversationId, + Log = log, + CreateTime = DateTime.UtcNow + }; + + return JsonSerializer.Serialize(model, _options.JsonSerializerOptions); + } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/FunctionDefMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/FunctionDefMongoElement.cs index 5910661f..dac77d69 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/FunctionDefMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/FunctionDefMongoElement.cs @@ -8,6 +8,7 @@ public class FunctionDefMongoElement { public string Name { get; set; } public string Description { get; set; } + public string? VisibilityExpression { get; set; } public string? Impact { get; set; } public FunctionParametersDefMongoElement Parameters { get; set; } = new FunctionParametersDefMongoElement(); @@ -22,6 +23,7 @@ public class FunctionDefMongoElement { Name = function.Name, Description = function.Description, + VisibilityExpression = function.VisibilityExpression, Impact = function.Impact, Parameters = new FunctionParametersDefMongoElement { @@ -38,6 +40,7 @@ public class FunctionDefMongoElement { Name = mongoFunction.Name, Description = mongoFunction.Description, + VisibilityExpression = mongoFunction.VisibilityExpression, Impact = mongoFunction.Impact, Parameters = new FunctionParametersDef { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index 75d496f8..3272930b 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -1,11 +1,7 @@ using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Conversations.Models; -using BotSharp.Abstraction.Evaluations.Settings; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Routing.Models; -using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Abstraction.Tasks.Models; using BotSharp.Plugin.MongoStorage.Collections; using BotSharp.Plugin.MongoStorage.Models;