diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs index 7078e12d4..e22729ec0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs @@ -68,6 +68,8 @@ public class LlmModelSetting /// public LlmCostSetting Cost { get; set; } = new(); + public bool AllowPdfReading => Capabilities?.Contains(LlmModelCapability.PdfReading) == true; + public override string ToString() { return $"[{Type}] {Name} {Endpoint}"; diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs index bee60f1af..93a2c25c9 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.Pdf.cs @@ -1,7 +1,8 @@ -using BotSharp.Abstraction.Instructs.Models; -using BotSharp.Abstraction.Instructs; using BotSharp.Abstraction.Files.Converters; +using BotSharp.Abstraction.Instructs; +using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Options; +using BotSharp.Abstraction.MLTasks; namespace BotSharp.Core.Files.Services; @@ -23,11 +24,16 @@ public async Task ReadPdf(string text, List files, In try { var provider = options?.Provider ?? "openai"; + var model = options?.Model ?? "gpt-5-mini"; + var pdfFiles = await DownloadAndSaveFiles(sessionDir, files); var targetFiles = pdfFiles; + var settingsService = _services.GetRequiredService(); + var modelSettings = settingsService.GetSetting(provider, model); var converter = GetImageConverter(options?.ImageConverter); - if (converter == null && provider == "openai") + + if (converter == null && modelSettings?.AllowPdfReading != true) { var fileCoreSettings = _services.GetRequiredService(); converter = GetImageConverter(fileCoreSettings?.ImageConverter?.Provider); @@ -44,7 +50,7 @@ public async Task ReadPdf(string text, List files, In text = RenderText(text, options?.Data); var completion = CompletionProvider.GetChatCompletion(_services, provider: provider, - model: options?.Model ?? "gpt-5-mini", multiModal: true); + model: model, multiModal: true); var message = await completion.GetChatCompletions(new Agent() { Id = innerAgentId, diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs index 48cf5e83c..fa0156f7d 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Instruct/FileInstructService.cs @@ -101,7 +101,12 @@ private string BuildFileName(string? name, string? extension, string defaultName private IImageConverter? GetImageConverter(string? provider) { - var converter = _services.GetServices().FirstOrDefault(x => x.Provider == (provider ?? "image-handler")); + if (string.IsNullOrEmpty(provider)) + { + return null; + } + + var converter = _services.GetServices().FirstOrDefault(x => x.Provider == provider); return converter; } #endregion diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj index 7309b4570..bd0548553 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -15,7 +15,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Constants/StopReason.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Constants/StopReason.cs new file mode 100644 index 000000000..e82910cef --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Constants/StopReason.cs @@ -0,0 +1,11 @@ +namespace BotSharp.Plugin.AnthropicAI.Constants; + +internal static class StopReason +{ + internal const string EndTurn = "end_turn"; + internal const string MaxTokens = "max_tokens"; + internal const string ToolUse = "tool_use"; + internal const string StopSequence = "stop_sequence"; + internal const string ContentFilter = "content_filter"; + internal const string GuardRail = "guardrail"; +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs index d6b4c4107..f67e86c0d 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs @@ -1,11 +1,8 @@ using Anthropic.SDK.Common; -using BotSharp.Abstraction.Conversations; -using BotSharp.Abstraction.Files; -using BotSharp.Abstraction.Files.Models; -using BotSharp.Abstraction.Files.Utilities; -using BotSharp.Abstraction.Hooks; +using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Core.Infrastructures.Streams; +using BotSharp.Core.MessageHub; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; namespace BotSharp.Plugin.AnthropicAI.Providers; @@ -17,17 +14,22 @@ public class ChatCompletionProvider : IChatCompletion protected readonly AnthropicSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + private readonly IConversationStateService _state; + private List renderedInstructions = []; protected string _model; - public ChatCompletionProvider(AnthropicSettings settings, + public ChatCompletionProvider( + AnthropicSettings settings, + IServiceProvider services, ILogger logger, - IServiceProvider services) + IConversationStateService state) { _settings = settings; - _logger = logger; _services = services; + _logger = logger; + _state = state; } public async Task GetChatCompletions(Agent agent, List conversations) @@ -40,28 +42,23 @@ public async Task GetChatCompletions(Agent agent, List(); - var settings = settingsService.GetSetting(Provider, _model ?? agent.LlmConfig?.Model ?? "claude-haiku-4-5-20251001"); - - var client = new AnthropicClient(new APIAuthentication(settings.ApiKey)); + var client = ProviderHelper.GetAnthropicClient(Provider, _model, _services); var (prompt, parameters) = PrepareOptions(agent, conversations); var response = await client.Messages.GetClaudeMessageAsync(parameters); RoleDialogModel responseMessage; - if (response.StopReason == "tool_use") + if (response.StopReason == StopReason.ToolUse) { - var content = response.Content.OfType().FirstOrDefault(); - var toolResult = response.Content.OfType().First(); - - responseMessage = new RoleDialogModel(AgentRole.Function, content?.Text ?? string.Empty) + var toolCall = response.ToolCalls.FirstOrDefault(); + responseMessage = new RoleDialogModel(AgentRole.Function, string.Empty) { CurrentAgentId = agent.Id, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - ToolCallId = toolResult.Id, - FunctionName = toolResult.Name, - FunctionArgs = JsonSerializer.Serialize(toolResult.Input), + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.Name, + FunctionArgs = toolCall?.Arguments?.ToJsonString(), RenderedInstruction = string.Join("\r\n", renderedInstructions) }; } @@ -94,46 +91,238 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List conversations, + public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) { - throw new NotImplementedException(); + var contentHooks = _services.GetHooks(agent.Id); + + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + var client = ProviderHelper.GetAnthropicClient(Provider, _model, _services); + var (prompt, parameters) = PrepareOptions(agent, conversations); + + var response = await client.Messages.GetClaudeMessageAsync(parameters); + + RoleDialogModel responseMessage; + + if (response.StopReason == StopReason.ToolUse) + { + var toolCall = response.ToolCalls.FirstOrDefault(); + responseMessage = new RoleDialogModel(AgentRole.Function, string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.Name, + FunctionArgs = toolCall?.Arguments?.ToJsonString(), + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + + // Execute functions + await onFunctionExecuting(responseMessage); + } + else + { + var message = response.FirstMessage; + responseMessage = new RoleDialogModel(AgentRole.Assistant, message?.Text ?? string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + + await onMessageReceived(responseMessage); + } + + var tokenUsage = response.Usage; + + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = tokenUsage?.InputTokens ?? 0, + TextOutputTokens = tokenUsage?.OutputTokens ?? 0 + }); + } + + return true; } - public Task GetChatCompletionsStreamingAsync(Agent agent, List conversations) + public async Task GetChatCompletionsStreamingAsync(Agent agent, List conversations) { - throw new NotImplementedException(); + var client = ProviderHelper.GetAnthropicClient(Provider, _model, _services); + var (prompt, parameters) = PrepareOptions(agent, conversations, useStream: true); + + var hub = _services.GetRequiredService>>(); + var conv = _services.GetRequiredService(); + var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; + + var contentHooks = _services.GetHooks(agent.Id); + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + hub.Push(new() + { + EventName = ChatEvent.BeforeReceiveLlmStreamMessage, + RefId = conv.ConversationId, + Data = new RoleDialogModel(AgentRole.Assistant, string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = messageId + } + }); + + using var textStream = new RealtimeTextStream(); + Usage? tokenUsage = null; + + var responseMessage = new RoleDialogModel(AgentRole.Assistant, string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = messageId + }; + + await foreach (var choice in client.Messages.StreamClaudeMessageAsync(parameters)) + { + var startMsg = choice.StreamStartMessage; + var contentBlock = choice.ContentBlock; + var delta = choice.Delta; + + tokenUsage = delta?.Usage ?? startMsg?.Usage ?? choice.Usage; + + if (delta != null) + { + if (delta.StopReason == StopReason.ToolUse) + { + var toolCall = choice.ToolCalls.FirstOrDefault(); + responseMessage = new RoleDialogModel(AgentRole.Function, string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = messageId, + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.Name, + FunctionArgs = toolCall?.Arguments?.ToString()?.IfNullOrEmptyAs("{}") ?? "{}" + }; + +#if DEBUG + _logger.LogDebug($"Tool Call (id: {toolCall?.Id}) => {toolCall?.Name}({toolCall?.Arguments})"); +#endif + } + else if (delta.StopReason == StopReason.EndTurn) + { + var allText = textStream.GetText(); + responseMessage = new RoleDialogModel(AgentRole.Assistant, allText) + { + CurrentAgentId = agent.Id, + MessageId = messageId, + IsStreaming = true + }; + +#if DEBUG + _logger.LogDebug($"Stream text Content: {allText}"); +#endif + } + else if (!string.IsNullOrEmpty(delta.StopReason)) + { + responseMessage = new RoleDialogModel(AgentRole.Assistant, delta.StopReason) + { + CurrentAgentId = agent.Id, + MessageId = messageId, + IsStreaming = true + }; + } + else + { + var deltaText = delta.Text ?? string.Empty; + textStream.Collect(deltaText); + + hub.Push(new() + { + EventName = ChatEvent.OnReceiveLlmStreamMessage, + RefId = conv.ConversationId, + Data = new RoleDialogModel(AgentRole.Assistant, deltaText) + { + CurrentAgentId = agent.Id, + MessageId = messageId + } + }); + } + } + } + + hub.Push(new() + { + EventName = ChatEvent.AfterReceiveLlmStreamMessage, + RefId = conv.ConversationId, + Data = responseMessage + }); + + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = tokenUsage?.InputTokens ?? 0, + TextOutputTokens = tokenUsage?.OutputTokens ?? 0 + }); + } + + return responseMessage; } - private (string, MessageParameters) PrepareOptions(Agent agent, List conversations) + private (string, MessageParameters) PrepareOptions(Agent agent, List conversations, bool useStream = false) { var agentService = _services.GetRequiredService(); - var state = _services.GetRequiredService(); var settingsService = _services.GetRequiredService(); var settings = settingsService.GetSetting(Provider, _model); var allowMultiModal = settings != null && settings.MultiModal; renderedInstructions = []; + var parameters = new MessageParameters() + { + Model = _model, + Stream = useStream, + Tools = new List(), + Thinking = GetThinkingParams(settings) + }; + // Prepare instruction and functions var renderData = agentService.CollectRenderData(agent); var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData); if (!string.IsNullOrWhiteSpace(instruction)) { + parameters.System = new List() + { + new SystemMessage(instruction) + }; renderedInstructions.Add(instruction); } - /*var routing = _services.GetRequiredService(); - var router = routing.Router; - - var render = _services.GetRequiredService(); - var template = router.Templates.FirstOrDefault(x => x.Name == "response_with_function").Content; - - var response_with_function = render.Render(template, new Dictionary + var tools = new List(); + foreach (var function in functions) { - { "functions", agent.Functions } - }); + if (!agentService.RenderFunction(agent, function, renderData)) + { + continue; + } - prompt += "\r\n\r\n" + response_with_function;*/ + var property = agentService.RenderFunctionProperty(agent, function, renderData); + var jsonArgs = property != null ? JsonSerializer.Serialize(property, BotSharpOptions.defaultJsonOptions) : "{}"; + tools.Add(new Function(function.Name, function.Description, JsonNode.Parse(jsonArgs))); + } var messages = new List(); var filteredMessages = conversations.Select(x => x).ToList(); @@ -161,7 +350,17 @@ public Task GetChatCompletionsStreamingAsync(Agent agent, List< } else if (message.Role == AgentRole.Assistant) { - messages.Add(new Message(RoleType.Assistant, message.LlmContent)); + var contentParts = new List(); + if (allowMultiModal && !message.Files.IsNullOrEmpty()) + { + CollectMessageContentParts(contentParts, message.Files); + } + contentParts.Add(new TextContent() { Text = message.LlmContent }); + messages.Add(new Message + { + Role = RoleType.Assistant, + Content = contentParts + }); } else if (message.Role == AgentRole.Function) { @@ -194,63 +393,52 @@ public Task GetChatCompletionsStreamingAsync(Agent agent, List< } } - var temperature = decimal.Parse(state.GetState("temperature", "0.0")); - var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens) + var temperature = decimal.Parse(_state.GetState("temperature", "0.0")); + var maxTokens = int.TryParse(_state.GetState("max_tokens"), out var tokens) ? tokens : agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN; - var parameters = new MessageParameters() - { - Messages = messages, - MaxTokens = maxTokens, - Model = settings.Name, - Stream = false, - Temperature = temperature, - Tools = new List() - }; + parameters.Messages = messages; + parameters.Tools = tools; + parameters.Temperature = temperature; + parameters.MaxTokens = maxTokens; + + var prompt = GetPrompt(parameters); + return (prompt, parameters); + } + + public void SetModelName(string model) + { + _model = model; + } - if (!string.IsNullOrEmpty(instruction)) + private ThinkingParameters? GetThinkingParams(LlmModelSetting? settings) + { + if (settings?.Reasoning?.Parameters == null) { - parameters.System = new List() - { - new SystemMessage(instruction) - }; + return null; } - JsonSerializerOptions? jsonSerializationOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter() }, - ReferenceHandler = ReferenceHandler.IgnoreCycles, - }; + var thinking = new ThinkingParameters(); + var param = settings.Reasoning.Parameters!; - foreach (var fn in functions) + var bt = _state.GetState("budget_tokens"); + if (int.TryParse(bt, out var budgetTokens) + || (param.TryGetValue("BudgetTokens", out var value) + && int.TryParse(value.Default, out budgetTokens))) { - /*var inputschema = new InputSchema() - { - Type = fn.Parameters.Type, - Properties = new Dictionary() - { - { "location", new Property() { Type = "string", Description = "The location of the weather" } }, - { - "tempType", new Property() - { - Type = "string", Enum = Enum.GetNames(typeof(TempType)), - Description = "The unit of temperature, celsius or fahrenheit" - } - } - }, - Required = fn.Parameters.Required - };*/ - - string jsonString = JsonSerializer.Serialize(fn.Parameters, jsonSerializationOptions); - parameters.Tools.Add(new Function(fn.Name, fn.Description, - JsonNode.Parse(jsonString))); + thinking.BudgetTokens = budgetTokens; } - var prompt = GetPrompt(parameters); + var enableInterleavedThinking = _state.GetState("use_interleaved_thinking"); + if (bool.TryParse(enableInterleavedThinking, out var useInterleavedThinking) + || (param.TryGetValue("UseInterleavedThinking", out value) + && bool.TryParse(value.Default, out useInterleavedThinking))) + { + thinking.UseInterleavedThinking = useInterleavedThinking; + } - return (prompt, parameters); + return thinking.BudgetTokens > 0 ? thinking : null; } private string GetPrompt(MessageParameters parameters) @@ -300,8 +488,7 @@ private string GetPrompt(MessageParameters parameters) var functions = string.Join("\r\n", parameters.Tools.Select(x => { - return - $"\r\n{x.Function.Name}: {x.Function.Description}\r\n{JsonSerializer.Serialize(x.Function.Parameters)}"; + return $"\r\n{x.Function.Name}: {x.Function.Description}\r\n{JsonSerializer.Serialize(x.Function.Parameters)}"; })); prompt += $"\r\n[FUNCTIONS]\r\n{functions}\r\n"; } @@ -309,11 +496,6 @@ private string GetPrompt(MessageParameters parameters) return prompt; } - public void SetModelName(string model) - { - _model = model; - } - private void CollectMessageContentParts(List contentParts, List files) { foreach (var file in files) @@ -346,20 +528,6 @@ private void CollectMessageContentParts(List contentParts, List(); + var settings = settingsService.GetSetting(provider, model); + var client = new AnthropicClient(new APIAuthentication(settings.ApiKey)); + return client; + } +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs index b2e4484c7..04f851eb0 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs @@ -4,20 +4,30 @@ global using System.Threading.Tasks; global using System.Linq; global using System.Text.Json; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; + global using Anthropic.SDK; global using Anthropic.SDK.Constants; global using Anthropic.SDK.Messaging; + global using BotSharp.Abstraction.Agents; global using BotSharp.Abstraction.Agents.Constants; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Agents.Models; -global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Functions.Models; global using BotSharp.Abstraction.Loggers; global using BotSharp.Abstraction.MLTasks; -global using BotSharp.Abstraction.Routing; -global using BotSharp.Abstraction.Templating; -global using BotSharp.Plugin.AnthropicAI.Settings; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Logging; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Files; +global using BotSharp.Abstraction.Files.Models; +global using BotSharp.Abstraction.Files.Utilities; +global using BotSharp.Abstraction.Hooks; +global using BotSharp.Abstraction.MessageHub.Models; +global using BotSharp.Abstraction.MLTasks.Settings; +global using BotSharp.Abstraction.Options; global using BotSharp.Abstraction.Utilities; + +global using BotSharp.Plugin.AnthropicAI.Settings; +global using BotSharp.Plugin.AnthropicAI.Constants;