diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs index 0669a267..f1d17b93 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs @@ -4,5 +4,16 @@ namespace BotSharp.Abstraction.Instructs; public interface IInstructService { + /// + /// Execute completion by using specified instruction or template + /// + /// Agent (static agent) + /// Additional message provided by user + /// Template name + /// System prompt + /// Task Execute(string agentId, RoleDialogModel message, string? templateName = null, string? instruction = null); + + + Task Instruct(string instruction, string agentId, InstructOptions options) where T : class; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructOptions.cs new file mode 100644 index 00000000..46c6c900 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Models/InstructOptions.cs @@ -0,0 +1,29 @@ +namespace BotSharp.Abstraction.Instructs.Models; + +public class InstructOptions +{ + /// + /// Llm provider + /// + public string Provider { get; set; } = null!; + + /// + /// Llm model + /// + public string Model { get; set; } = null!; + + /// + /// Conversation id. When this field is not null, it will get dialogs from conversation. + /// + public string? ConversationId { get; set; } + + /// + /// The single message. It can be append to the whole dialogs or sent alone. + /// + public string? Message { get; set; } + + /// + /// Data to fill in prompt + /// + public Dictionary Data { get; set; } = new(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/GenerateKnowledge.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/GenerateKnowledge.cs deleted file mode 100644 index a5698dc0..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/GenerateKnowledge.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BotSharp.Abstraction.Knowledges.Models; - -public class GenerateKnowledge -{ - [JsonPropertyName("question")] - public string Question { get; set; } = string.Empty; - - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; - - [JsonPropertyName("refined_collection")] - public string RefinedCollection { get; set; } = string.Empty; - - [JsonPropertyName("refine_answer")] - public Boolean RefineAnswer { get; set; } = false; - - [JsonPropertyName("existing_answer")] - public string ExistingAnswer { get; set; } = string.Empty; -} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index d74f71ac..7a3d1d5d 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -372,10 +372,10 @@ public class ConversationStateService : IConversationStateService, IDisposable private bool CheckArgType(string name, string value) { var agentTypes = AgentService.AgentParameterTypes.SelectMany(p => p.Value).ToList(); - var filed = agentTypes.FirstOrDefault(t => t.Key == name); - if (filed.Key != null) + var found = agentTypes.FirstOrDefault(t => t.Key == name); + if (found.Key != null) { - return filed.Value switch + return found.Value switch { "boolean" => bool.TryParse(value, out _), "number" => long.TryParse(value, out _), diff --git a/src/Infrastructure/BotSharp.Core/Instructs/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/InstructService.Execute.cs new file mode 100644 index 00000000..1d9dd1d0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Instructs/InstructService.Execute.cs @@ -0,0 +1,100 @@ +using BotSharp.Abstraction.Instructs; +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.MLTasks; + +namespace BotSharp.Core.Instructs; + +public partial class InstructService +{ + public async Task Execute(string agentId, RoleDialogModel message, string? templateName = null, string? instruction = null) + { + var agentService = _services.GetRequiredService(); + Agent agent = await agentService.LoadAgent(agentId); + + if (agent.Disabled) + { + var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent."; + return new InstructResult + { + MessageId = message.MessageId, + Text = content + }; + } + + // Trigger before completion hooks + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.BeforeCompletion(agent, message); + + // Interrupted by hook + if (message.StopCompletion) + { + return new InstructResult + { + MessageId = message.MessageId, + Text = message.Content + }; + } + } + + // Render prompt + var prompt = string.IsNullOrEmpty(templateName) ? + agentService.RenderedInstruction(agent) : + agentService.RenderedTemplate(agent, templateName); + + var completer = CompletionProvider.GetCompletion(_services, + agentConfig: agent.LlmConfig); + + var response = new InstructResult + { + MessageId = message.MessageId + }; + if (completer is ITextCompletion textCompleter) + { + var result = await textCompleter.GetCompletion(prompt, agentId, message.MessageId); + response.Text = result; + } + else if (completer is IChatCompletion chatCompleter) + { + if (instruction == "#TEMPLATE#") + { + instruction = prompt; + prompt = message.Content; + } + + var result = await chatCompleter.GetChatCompletions(new Agent + { + Id = agentId, + Name = agent.Name, + Instruction = instruction + }, new List + { + new RoleDialogModel(AgentRole.User, prompt) + { + CurrentAgentId = agentId, + MessageId = message.MessageId + } + }); + response.Text = result.Content; + } + + + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) + { + continue; + } + + await hook.AfterCompletion(agent, response); + } + + return response; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Instructs/InstructService.Instruct.cs b/src/Infrastructure/BotSharp.Core/Instructs/InstructService.Instruct.cs new file mode 100644 index 00000000..40297400 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Instructs/InstructService.Instruct.cs @@ -0,0 +1,110 @@ +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Templating; +using System.Collections; +using System.Reflection; + +namespace BotSharp.Core.Instructs; + +public partial class InstructService +{ + public async Task Instruct(string instruction, string agentId, InstructOptions options) where T : class + { + var prompt = GetPrompt(instruction, options.Data); + var response = await GetAiResponse(agentId, prompt, options); + + if (string.IsNullOrWhiteSpace(response.Content)) return null; + + var type = typeof(T); + T? result = null; + + try + { + if (IsStringType(type)) + { + result = response.Content as T; + } + else if (IsListType(type)) + { + var text = response.Content.JsonArrayContent(); + if (!string.IsNullOrWhiteSpace(text)) + { + result = JsonSerializer.Deserialize(text, _options.JsonSerializerOptions); + } + } + else + { + var text = response.Content.JsonContent(); + if (!string.IsNullOrWhiteSpace(text)) + { + result = JsonSerializer.Deserialize(text, _options.JsonSerializerOptions); + } + } + } + catch (Exception ex) + { + _logger.LogWarning($"Error when getting ai response, {ex.Message}\r\n{ex.InnerException}"); + } + + return result; + } + + private string GetPrompt(string instruction, Dictionary data) + { + var render = _services.GetRequiredService(); + + return render.Render(instruction, data ?? new Dictionary()); + } + + private async Task GetAiResponse(string agentId, string prompt, InstructOptions options) + { + var agentService = _services.GetRequiredService(); + var agent = await agentService.LoadAgent(agentId); + + var localAgent = new Agent + { + Id = agentId, + Name = agent.Name, + Instruction = prompt, + TemplateDict = new() + }; + + var messages = BuildDialogs(options); + var completion = CompletionProvider.GetChatCompletion(_services, provider: options.Provider, model: options.Model); + + return await completion.GetChatCompletions(localAgent, messages); + } + + private List BuildDialogs(InstructOptions options) + { + var messages = new List(); + + if (!string.IsNullOrWhiteSpace(options.ConversationId)) + { + var conv = _services.GetRequiredService(); + var dialogs = conv.GetDialogHistory(); + messages.AddRange(dialogs); + } + + if (!string.IsNullOrWhiteSpace(options.Message)) + { + messages.Add(new RoleDialogModel(AgentRole.User, options.Message)); + } + + return messages; + } + + private bool IsStringType(Type? type) + { + if (type == null) return false; + + return type == typeof(string); + } + + private bool IsListType(Type? type) + { + if (type == null) return false; + + var interfaces = type.GetTypeInfo().ImplementedInterfaces; + return type.IsArray || interfaces.Any(x => x.Name == typeof(IEnumerable).Name); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Instructs/InstructService.cs b/src/Infrastructure/BotSharp.Core/Instructs/InstructService.cs index b05889e1..a1a31e0e 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/InstructService.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/InstructService.cs @@ -1,118 +1,21 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Instructs; -using BotSharp.Abstraction.Instructs.Models; -using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Options; namespace BotSharp.Core.Instructs; public partial class InstructService : IInstructService { private readonly IServiceProvider _services; - private readonly ILogger _logger; + private readonly BotSharpOptions _options; + private readonly ILogger _logger; - public InstructService(IServiceProvider services, ILogger logger) + public InstructService( + IServiceProvider services, + BotSharpOptions options, + ILogger logger) { _services = services; + _options = options; _logger = logger; } - - /// - /// Execute completion by using specified instruction or template - /// - /// Agent (static agent) - /// Additional message provided by user - /// Template name - /// System prompt - /// - public async Task Execute(string agentId, RoleDialogModel message, string? templateName = null, string? instruction = null) - { - var agentService = _services.GetRequiredService(); - Agent agent = await agentService.LoadAgent(agentId); - - if (agent.Disabled) - { - var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent."; - return new InstructResult - { - MessageId = message.MessageId, - Text = content - }; - } - - // Trigger before completion hooks - var hooks = _services.GetServices(); - foreach (var hook in hooks) - { - if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) - { - continue; - } - - await hook.BeforeCompletion(agent, message); - - // Interrupted by hook - if (message.StopCompletion) - { - return new InstructResult - { - MessageId = message.MessageId, - Text = message.Content - }; - } - } - - // Render prompt - var prompt = string.IsNullOrEmpty(templateName) ? - agentService.RenderedInstruction(agent) : - agentService.RenderedTemplate(agent, templateName); - - var completer = CompletionProvider.GetCompletion(_services, - agentConfig: agent.LlmConfig); - - var response = new InstructResult - { - MessageId = message.MessageId - }; - if (completer is ITextCompletion textCompleter) - { - var result = await textCompleter.GetCompletion(prompt, agentId, message.MessageId); - response.Text = result; - } - else if (completer is IChatCompletion chatCompleter) - { - if (instruction == "#TEMPLATE#") - { - instruction = prompt; - prompt = message.Content; - } - - var result = await chatCompleter.GetChatCompletions(new Agent - { - Id = agentId, - Name = agent.Name, - Instruction = instruction - }, new List - { - new RoleDialogModel(AgentRole.User, prompt) - { - CurrentAgentId = agentId, - MessageId = message.MessageId - } - }); - response.Text = result.Content; - } - - - foreach (var hook in hooks) - { - if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId) - { - continue; - } - - await hook.AfterCompletion(agent, response); - } - - return response; - } } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj b/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj index 3216564e..283d8b32 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -21,7 +21,7 @@ - + @@ -42,7 +42,7 @@ PreserveNewest - + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/GenerateKnowledgeFn.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/GenerateKnowledgeFn.cs deleted file mode 100644 index 114ce7e4..00000000 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/GenerateKnowledgeFn.cs +++ /dev/null @@ -1,90 +0,0 @@ -using BotSharp.Abstraction.Templating; -using BotSharp.Core.Infrastructures; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace BotSharp.Plugin.KnowledgeBase.Functions; - -public class GenerateKnowledgeFn : IFunctionCallback -{ - public string Name => "generate_knowledge"; - - public string Indication => "generating knowledge"; - - private readonly IServiceProvider _services; - private readonly KnowledgeBaseSettings _settings; - - public GenerateKnowledgeFn(IServiceProvider services, KnowledgeBaseSettings settings) - { - _services = services; - _settings = settings; - } - - public async Task Execute(RoleDialogModel message) - { - var args = JsonSerializer.Deserialize(message.FunctionArgs ?? "{}"); - var agentService = _services.GetRequiredService(); - var llmAgent = await agentService.GetAgent(BuiltInAgentId.Planner); - var refineKnowledge = args.RefinedCollection; - String generateKnowledgePrompt; - if (args.RefineAnswer == true) - { - generateKnowledgePrompt = await GetRefineKnowledgePrompt(args.Question, args.Answer, args.ExistingAnswer); - } - else - { - generateKnowledgePrompt = await GetGenerateKnowledgePrompt(args.Question, args.Answer); - } - var agent = new Agent - { - Id = message.CurrentAgentId ?? string.Empty, - Name = "knowledge_generator", - Instruction = generateKnowledgePrompt, - LlmConfig = llmAgent.LlmConfig - }; - var response = await GetAiResponse(agent); - message.Data = response.Content.JsonArrayContent(); - message.Content = response.Content; - return true; - } - - private async Task GetGenerateKnowledgePrompt(string userQuestions, string sqlAnswer) - { - var agentService = _services.GetRequiredService(); - var render = _services.GetRequiredService(); - - var agent = await agentService.GetAgent(BuiltInAgentId.Learner); - var template = agent.Templates.FirstOrDefault(x => x.Name == "knowledge.generation")?.Content ?? string.Empty; - - return render.Render(template, new Dictionary - { - { "user_questions", userQuestions }, - { "sql_answer", sqlAnswer }, - }); - } - private async Task GetRefineKnowledgePrompt(string userQuestion, string sqlAnswer, string existionAnswer) - { - var agentService = _services.GetRequiredService(); - var render = _services.GetRequiredService(); - - var agent = await agentService.GetAgent(BuiltInAgentId.Learner); - var template = agent.Templates.FirstOrDefault(x => x.Name == "knowledge.generation.refine")?.Content ?? string.Empty; - - return render.Render(template, new Dictionary - { - { "user_question", userQuestion }, - { "new_answer", sqlAnswer }, - { "existing_answer", existionAnswer} - }); - } - private async Task GetAiResponse(Agent agent) - { - var text = "Generate question and answer pair"; - var message = new RoleDialogModel(AgentRole.User, text); - - var completion = CompletionProvider.GetChatCompletion(_services, - provider: agent.LlmConfig.Provider, - model: agent.LlmConfig.Model); - - return await completion.GetChatCompletions(agent, new List { message }); - } -} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/01acc3e5-0af7-49e6-ad7a-a760bd12dc40/templates/knowledge.generation.liquid b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/01acc3e5-0af7-49e6-ad7a-a760bd12dc40/templates/knowledge.generation.plain.liquid similarity index 100% rename from src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/01acc3e5-0af7-49e6-ad7a-a760bd12dc40/templates/knowledge.generation.liquid rename to src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/01acc3e5-0af7-49e6-ad7a-a760bd12dc40/templates/knowledge.generation.plain.liquid