From cbc6b9815158abe69c6fcb27a96e64042a5615c4 Mon Sep 17 00:00:00 2001 From: Haiping Chen <101423@smsassist.com> Date: Thu, 22 Feb 2024 13:37:14 -0600 Subject: [PATCH 1/2] lookup_dictionary for sQL Driver. --- .../BotSharp.Plugin.SqlDriver.csproj | 8 +- .../Functions/LookupDictionaryFn.cs | 79 +++++++++++++++++++ .../Functions/SqlInsertFn.cs | 17 ++++ .../Models/LookupDictionary.cs | 15 ++++ .../functions.json | 26 ++++++ .../instruction.liquid | 6 +- .../templates/lookup_dictionary.liquid | 9 +++ 7 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/Plugins/BotSharp.Plugin.SqlDriver/Functions/LookupDictionaryFn.cs create mode 100644 src/Plugins/BotSharp.Plugin.SqlDriver/Models/LookupDictionary.cs create mode 100644 src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/templates/lookup_dictionary.liquid diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj index fcc88874..171ba4cd 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj @@ -14,6 +14,7 @@ + @@ -26,6 +27,9 @@ PreserveNewest + + PreserveNewest + @@ -38,8 +42,4 @@ - - - - diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/LookupDictionaryFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/LookupDictionaryFn.cs new file mode 100644 index 00000000..94607662 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/LookupDictionaryFn.cs @@ -0,0 +1,79 @@ +using Amazon.Runtime.Internal.Transform; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.MLTasks; +using BotSharp.Core.Infrastructures; +using BotSharp.Plugin.SqlDriver.Models; +using MySqlConnector; +using static Dapper.SqlMapper; + +namespace BotSharp.Plugin.SqlDriver.Functions; + +public class LookupDictionaryFn : IFunctionCallback +{ + public string Name => "lookup_dictionary"; + private readonly IServiceProvider _services; + + public LookupDictionaryFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + + var settings = _services.GetRequiredService(); + using var connection = new MySqlConnection(settings.MySqlConnectionString); + var dictionary = new Dictionary(); + var results = connection.Query($"SELECT * FROM {args.Table} LIMIT 10"); + var items = new List(); + foreach(var item in results) + { + items.Add(JsonSerializer.Serialize(item)); + } + + var agentService = _services.GetRequiredService(); + var agent = await agentService.LoadAgent(message.CurrentAgentId); + var prompt = GetPrompt(agent, items, args.Keyword); + + // Ask LLM which one is the best + var llmProviderService = _services.GetRequiredService(); + var model = llmProviderService.GetProviderModel("azure-openai", "gpt-35-turbo"); + + // chat completion + var completion = CompletionProvider.GetChatCompletion(_services, + provider: "azure-openai", + model: model.Name); + + var conversations = new List + { + new RoleDialogModel(AgentRole.User, prompt) + { + CurrentAgentId = message.CurrentAgentId, + MessageId = message.MessageId, + } + }; + + var response = await completion.GetChatCompletions(new Agent + { + Id = message.CurrentAgentId, + Instruction = "" + }, conversations); + + message.Content = response.Content; + + return true; + } + + private string GetPrompt(Agent agent, List task, string keyword) + { + var template = agent.Templates.First(x => x.Name == "lookup_dictionary").Content; + + var render = _services.GetRequiredService(); + return render.Render(template, new Dictionary + { + { "items", task }, + { "keyword", keyword } + }); + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlInsertFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlInsertFn.cs index 42bcd074..e3499b1a 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlInsertFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlInsertFn.cs @@ -16,6 +16,23 @@ public class SqlInsertFn : IFunctionCallback { var args = JsonSerializer.Deserialize(message.FunctionArgs); var sqlDriver = _services.GetRequiredService(); + + // Check duplication + if (sqlDriver.Statements.Exists(x => x.Statement == args.Statement)) + { + var list = sqlDriver.Statements.Where(x => x.Statement == args.Statement).ToList(); + foreach (var statement in list) + { + var p1 = string.Join(", ", statement.Parameters.OrderBy(x => x.Name).Select(x => x.Value)); + var p2 = string.Join(", ", args.Parameters.OrderBy(x => x.Name).Select(x => x.Value)); + if (p1 == p2) + { + message.Content = "Skip duplicated INSERT statement"; + return false; + } + } + } + sqlDriver.Enqueue(args); message.Content = $"Inserted new record successfully."; if (args.Return != null) diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Models/LookupDictionary.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Models/LookupDictionary.cs new file mode 100644 index 00000000..cf7d6345 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Models/LookupDictionary.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.SqlDriver.Models; + +public class LookupDictionary +{ + [JsonPropertyName("table")] + public string Table { get; set; } + + [JsonPropertyName("keyword")] + public string Keyword { get; set; } + + [JsonPropertyName("columns")] + public string[] Columns { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions.json index bcc527ee..85703028 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions.json @@ -124,5 +124,31 @@ }, "required": [ "sql_statement", "reason", "table", "parameters", "return_field" ] } + }, + { + "name": "lookup_dictionary", + "description": "Get id from dictionary table by keyword if tool or solution mentioned this approach", + "parameters": { + "type": "object", + "properties": { + "table": { + "type": "string", + "description": "table name" + }, + "keyword": { + "type": "string", + "description": "table name" + }, + "columns": { + "type": "array", + "description": "columns", + "items": { + "type": "string", + "description": "column" + } + } + }, + "required": [ "table", "columns", "keyword" ] + } } ] \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instruction.liquid b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instruction.liquid index a2aac548..cb0dae8d 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instruction.liquid +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instruction.liquid @@ -4,8 +4,6 @@ Output the next step smartly. Your response must meet below requirements: * Walk through the provided information, don't run query if there is already related information; -* DO NOT generate duplicated sql statements; -* The return field alias should be meaningful, it can be similar name of reference table column; -* Make sure the SELECT and WHERE fields are in corresponding table schema definition; +* Make sure you have the corresponding table columns information before generating SQL; +* The return field alias should be meaningful, you can use the combination of column and value as the alias name; * Use "Unique Index" to help check record existence; -* For INSERT statement with mutliple records, should return in different meaningful alias; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/templates/lookup_dictionary.liquid b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/templates/lookup_dictionary.liquid new file mode 100644 index 00000000..cf5b190a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/templates/lookup_dictionary.liquid @@ -0,0 +1,9 @@ +DICTIONARY: + +{% for item in items %} +* {{ item }} +{% endfor %} + +===== +Which item is the best matching with "{{ keyword }}"? +You must return Id and Name field. \ No newline at end of file From f4b4ca4329a7d63555aaae65d0066ccca2c0da24 Mon Sep 17 00:00:00 2001 From: Haiping Chen <101423@smsassist.com> Date: Thu, 22 Feb 2024 15:14:13 -0600 Subject: [PATCH 2/2] OnConversationRedirected --- .../Conversations/ConversationHookBase.cs | 5 +++ .../Conversations/IConversationHook.cs | 8 +++++ .../Loggers/Enums/ContentLogSource.cs | 1 + .../Routing/Functions/RouteToAgentFn.cs | 5 +++ .../Hooks/StreamingLogHook.cs | 33 ++++++++++++++----- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index 0d0cb67e..cd45a39e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -91,4 +91,9 @@ public abstract class ConversationHookBase : IConversationHook { return Task.CompletedTask; } + + public virtual Task OnConversationRedirected(string toAgentId, RoleDialogModel message) + { + return Task.CompletedTask; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index 4c7168a2..6056718d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -82,4 +82,12 @@ public interface IConversationHook /// /// Task OnHumanInterventionNeeded(RoleDialogModel message); + + /// + /// Conversation is redirected to another agent + /// + /// + /// + /// + Task OnConversationRedirected(string toAgentId, RoleDialogModel message); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs index f6973770..45d74a51 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Enums/ContentLogSource.cs @@ -6,4 +6,5 @@ public static class ContentLogSource public const string Prompt = "prompt"; public const string FunctionCall = "function call"; public const string AgentResponse = "agent response"; + public const string HardRule = "hard rule"; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs index 26f14f45..c367d631 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs @@ -153,6 +153,11 @@ public class RouteToAgentFn : IFunctionCallback #else logger.LogInformation($"*** Routing redirect to {record.Name.ToUpper()} ***"); #endif + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + hook.OnConversationRedirected(routingRule.RedirectTo, message); + } } else { diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index 13997825..0f802b07 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -46,6 +46,18 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook BuildContentLog(conversationId, _user.UserName, log, ContentLogSource.UserInput, message)); } + public override async Task OnConversationRedirected(string toAgentId, RoleDialogModel message) + { + var agentService = _services.GetRequiredService(); + var conversationId = _state.GetConversationId(); + var fromAgent = await agentService.LoadAgent(message.CurrentAgentId); + var toAgent = await agentService.LoadAgent(toAgentId); + + var log = $"{message.Content}\r\n=====\r\nREDIRECTED TO {toAgent.Name}"; + await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", + BuildContentLog(conversationId, fromAgent.Name, log, ContentLogSource.HardRule, message)); + } + public async Task BeforeGenerating(Agent agent, List conversations) { if (!_convSettings.ShowVerboseLog) return; @@ -83,23 +95,26 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook var agent = await agentService.LoadAgent(message.CurrentAgentId); var logSource = string.Empty; + var log = tokenStats.Prompt; + logSource = ContentLogSource.Prompt; + await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", + BuildContentLog(conversationId, agent?.Name, log, logSource, message)); + // Log routing output try { var inst = message.Content.JsonContent(); - logSource = ContentLogSource.AgentResponse; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", - BuildContentLog(conversationId, agent?.Name, message.Content, logSource, message)); + if (!string.IsNullOrEmpty(inst.Function)) + { + logSource = ContentLogSource.AgentResponse; + await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", + BuildContentLog(conversationId, agent?.Name, message.Content, logSource, message)); + } } catch { // ignore } - - var log = tokenStats.Prompt; - logSource = ContentLogSource.Prompt; - await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", - BuildContentLog(conversationId, agent?.Name, log, logSource, message)); } /// @@ -118,7 +133,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook { var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(message.CurrentAgentId); - var log = $"[{agent?.Name}]: {message.Content}"; + var log = $"{message.Content}"; if (message.RichContent != null && message.RichContent.Message.RichType != "text") { var richContent = JsonSerializer.Serialize(message.RichContent, _serializerOptions);