From a8fa46eff5034d320622785ede5403e157e017cd Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 27 Jan 2025 18:23:33 -0600 Subject: [PATCH 1/2] add deep seek --- BotSharp.sln | 11 + ...tatService.cs => IBotSharpStatsService.cs} | 2 +- .../BotSharp.Core/Agents/AgentPlugin.cs | 2 +- .../Conversations/Services/TokenStatistics.cs | 2 +- ...StatService.cs => BotSharpStatsService.cs} | 8 +- .../Hooks/GlobalStatsConversationHook.cs | 2 +- .../BotSharp.Plugin.DeepSeekAI.csproj | 21 ++ .../DeepSeekAiPlugin.cs | 18 + .../Providers/Chat/ChatCompletionProvider.cs | 337 ++++++++++++++++++ .../Providers/ProviderHelper.cs | 16 + .../Providers/Text/TextCompletionProvider.cs | 95 +++++ .../BotSharp.Plugin.DeepSeekAI/Using.cs | 19 + .../Providers/Chat/ChatCompletionProvider.cs | 6 +- src/WebStarter/WebStarter.csproj | 1 + src/WebStarter/appsettings.json | 1 + 15 files changed, 529 insertions(+), 12 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Statistics/Services/{IBotSharpStatService.cs => IBotSharpStatsService.cs} (83%) rename src/Infrastructure/BotSharp.Core/Statistics/Services/{BotSharpStatService.cs => BotSharpStatsService.cs} (95%) create mode 100644 src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj create mode 100644 src/Plugins/BotSharp.Plugin.DeepSeekAI/DeepSeekAiPlugin.cs create mode 100644 src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/ProviderHelper.cs create mode 100644 src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs diff --git a/BotSharp.sln b/BotSharp.sln index 303f73208..d4890be14 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Crontab", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Rules", "src\Infrastructure\BotSharp.Core.Rules\BotSharp.Core.Rules.csproj", "{AFD64412-4D6A-452E-82A2-79E5D8842E29}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.DeepSeekAI", "src\Plugins\BotSharp.Plugin.DeepSeekAI\BotSharp.Plugin.DeepSeekAI.csproj", "{AF329442-B48E-4B48-A18A-1C869D1BA6F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -509,6 +511,14 @@ Global {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|Any CPU.Build.0 = Release|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x64.ActiveCfg = Release|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x64.Build.0 = Release|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|x64.Build.0 = Debug|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|Any CPU.Build.0 = Release|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.ActiveCfg = Release|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -569,6 +579,7 @@ Global {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {F812BAAE-5A7D-4DF7-8E71-70696B51C61F} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {AFD64412-4D6A-452E-82A2-79E5D8842E29} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} + {AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatService.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatsService.cs similarity index 83% rename from src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatService.cs rename to src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatsService.cs index 1fa8ba9e5..bd520c9b8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatsService.cs @@ -2,7 +2,7 @@ namespace BotSharp.Abstraction.Statistics.Services; -public interface IBotSharpStatService +public interface IBotSharpStatsService { bool UpdateLlmCost(BotSharpStats stats); bool UpdateAgentCall(BotSharpStats stats); diff --git a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index d34dcd1f9..e6998fe4f 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -33,7 +33,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(provider => { diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index a2b7d5bef..7a8cef670 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -59,7 +59,7 @@ public void AddToken(TokenStatsModel stats, RoleDialogModel message) stat.SetState("llm_total_cost", total_cost, isNeedVersion: false, source: StateSource.Application); - var globalStats = _services.GetRequiredService(); + var globalStats = _services.GetRequiredService(); var body = new BotSharpStats { Category = StatCategory.LlmCost, diff --git a/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs similarity index 95% rename from src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs rename to src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs index d39b94bbe..d70c7c247 100644 --- a/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatService.cs +++ b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs @@ -3,19 +3,19 @@ namespace BotSharp.Core.Statistics.Services; -public class BotSharpStatService : IBotSharpStatService +public class BotSharpStatsService : IBotSharpStatsService { private readonly IServiceProvider _services; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly StatisticsSettings _settings; private const string GLOBAL_LLM_COST = "global-llm-cost"; private const string GLOBAL_AGENT_CALL = "global-agent-call"; private const int TIMEOUT_SECONDS = 5; - public BotSharpStatService( + public BotSharpStatsService( IServiceProvider services, - ILogger logger, + ILogger logger, StatisticsSettings settings) { _services = services; diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs index 79ba4115d..2e7b115b5 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs @@ -27,7 +27,7 @@ public override async Task OnPostbackMessageReceived(RoleDialogModel message, Po private void UpdateAgentCall(RoleDialogModel message) { // record agent call - var globalStats = _services.GetRequiredService(); + var globalStats = _services.GetRequiredService(); var body = new BotSharpStats { diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj b/src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj new file mode 100644 index 000000000..56eb14ef4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/BotSharp.Plugin.DeepSeekAI.csproj @@ -0,0 +1,21 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/DeepSeekAiPlugin.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/DeepSeekAiPlugin.cs new file mode 100644 index 000000000..063d1ad1b --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/DeepSeekAiPlugin.cs @@ -0,0 +1,18 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Plugin.DeepSeek.Providers.Text; +using BotSharp.Plugin.DeepSeekAI.Providers.Chat; + +namespace BotSharp.Plugin.DeepSeek; + +public class DeepSeekAiPlugin : IBotSharpPlugin +{ + public string Id => "1f0e73a5-bcaa-44e9-adde-e46cd94d244b"; + public string Name => "DeepSeek"; + public string Description => "DeepSeek AI"; + public string IconUrl => "https://cdn.deepseek.com/logo.png"; + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs new file mode 100644 index 000000000..a7c6b48dc --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -0,0 +1,337 @@ +using Microsoft.Extensions.Logging; +using OpenAI.Chat; +using BotSharp.Abstraction.Files; +using BotSharp.Plugin.DeepSeek.Providers; + +namespace BotSharp.Plugin.DeepSeekAI.Providers.Chat; + +public class ChatCompletionProvider : IChatCompletion +{ + protected readonly IServiceProvider _services; + protected readonly ILogger _logger; + + protected string _model; + public virtual string Provider => "deepseek-ai"; + + public ChatCompletionProvider( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task GetChatCompletions(Agent agent, List conversations) + { + var contentHooks = _services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + var client = ProviderHelper.GetClient(Provider, _model, _services); + var chatClient = client.GetChatClient(_model); + var (prompt, messages, options) = PrepareOptions(agent, conversations); + + var response = chatClient.CompleteChat(messages, options); + var value = response.Value; + var reason = value.FinishReason; + var content = value.Content; + var text = content.FirstOrDefault()?.Text ?? string.Empty; + + RoleDialogModel responseMessage; + if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + { + var toolCall = value.ToolCalls.FirstOrDefault(); + responseMessage = new RoleDialogModel(AgentRole.Function, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.FunctionName, + FunctionArgs = toolCall?.FunctionArguments?.ToString() + }; + + // Somethings LLM will generate a function name with agent name. + if (!string.IsNullOrEmpty(responseMessage.FunctionName)) + { + responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); + } + } + else + { + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + }; + } + + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + PromptCount = response.Value?.Usage?.InputTokenCount ?? 0, + CompletionCount = response.Value?.Usage?.OutputTokenCount ?? 0 + }); + } + + return responseMessage; + } + + public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) + { + var hooks = _services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in hooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + var client = ProviderHelper.GetClient(Provider, _model, _services); + var chatClient = client.GetChatClient(_model); + var (prompt, messages, options) = PrepareOptions(agent, conversations); + + var response = await chatClient.CompleteChatAsync(messages, options); + var value = response.Value; + var reason = value.FinishReason; + var content = value.Content; + var text = content.FirstOrDefault()?.Text ?? string.Empty; + + var msg = new RoleDialogModel(AgentRole.Assistant, text) + { + CurrentAgentId = agent.Id + }; + + // After chat completion hook + foreach (var hook in hooks) + { + await hook.AfterGenerated(msg, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + PromptCount = response.Value?.Usage?.InputTokenCount ?? 0, + CompletionCount = response.Value?.Usage?.OutputTokenCount ?? 0 + }); + } + + if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + { + var toolCall = value.ToolCalls?.FirstOrDefault(); + _logger.LogInformation($"[{agent.Name}]: {toolCall?.FunctionName}({toolCall?.FunctionArguments})"); + + var funcContextIn = new RoleDialogModel(AgentRole.Function, text) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolCall?.Id, + FunctionName = toolCall?.FunctionName, + FunctionArgs = toolCall?.FunctionArguments?.ToString() + }; + + // Somethings LLM will generate a function name with agent name. + if (!string.IsNullOrEmpty(funcContextIn.FunctionName)) + { + funcContextIn.FunctionName = funcContextIn.FunctionName.Split('.').Last(); + } + + // Execute functions + await onFunctionExecuting(funcContextIn); + } + else + { + // Text response received + await onMessageReceived(msg); + } + + return true; + } + + public async Task GetChatCompletionsStreamingAsync(Agent agent, List conversations, Func onMessageReceived) + { + var client = ProviderHelper.GetClient(Provider, _model, _services); + var chatClient = client.GetChatClient(_model); + var (prompt, messages, options) = PrepareOptions(agent, conversations); + + var response = chatClient.CompleteChatStreamingAsync(messages, options); + + await foreach (var choice in response) + { + if (choice.FinishReason == ChatFinishReason.FunctionCall || choice.FinishReason == ChatFinishReason.ToolCalls) + { + var update = choice.ToolCallUpdates?.FirstOrDefault()?.FunctionArgumentsUpdate?.ToString() ?? string.Empty; + _logger.LogInformation(update); + + await onMessageReceived(new RoleDialogModel(AgentRole.Assistant, update)); + continue; + } + + if (choice.ContentUpdate.IsNullOrEmpty()) continue; + + _logger.LogInformation(choice.ContentUpdate[0]?.Text); + + await onMessageReceived(new RoleDialogModel(choice.Role?.ToString() ?? ChatMessageRole.Assistant.ToString(), choice.ContentUpdate[0]?.Text ?? string.Empty)); + } + + return true; + } + + public void SetModelName(string model) + { + _model = model; + } + + protected (string, IEnumerable, ChatCompletionOptions) PrepareOptions(Agent agent, List conversations) + { + var agentService = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + var fileStorage = _services.GetRequiredService(); + var settingsService = _services.GetRequiredService(); + var settings = settingsService.GetSetting(Provider, _model); + var allowMultiModal = settings != null && settings.MultiModal; + + var messages = new List(); + + var temperature = float.Parse(state.GetState("temperature", "0.0")); + var maxTokens = int.Parse(state.GetState("max_tokens", "1024")); + var options = new ChatCompletionOptions() + { + Temperature = temperature, + MaxOutputTokenCount = maxTokens + }; + + var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []); + foreach (var function in functions) + { + if (!agentService.RenderFunction(agent, function)) continue; + + var property = agentService.RenderFunctionProperty(agent, function); + + options.Tools.Add(ChatTool.CreateFunctionTool( + functionName: function.Name, + functionDescription: function.Description, + functionParameters: BinaryData.FromObjectAsJson(property))); + } + + if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty()) + { + var text = agentService.RenderedInstruction(agent); + messages.Add(new SystemChatMessage(text)); + } + + if (!string.IsNullOrEmpty(agent.Knowledges)) + { + messages.Add(new SystemChatMessage(agent.Knowledges)); + } + + var filteredMessages = conversations.Select(x => x).ToList(); + var firstUserMsgIdx = filteredMessages.FindIndex(x => x.Role == AgentRole.User); + if (firstUserMsgIdx > 0) + { + filteredMessages = filteredMessages.Where((_, idx) => idx >= firstUserMsgIdx).ToList(); + } + + foreach (var message in filteredMessages) + { + if (message.Role == AgentRole.Function) + { + messages.Add(new AssistantChatMessage(new List + { + ChatToolCall.CreateFunctionToolCall(message.ToolCallId, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + })); + + messages.Add(new ToolChatMessage(message.ToolCallId, message.Content)); + } + else if (message.Role == AgentRole.User) + { + var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content; + var textPart = ChatMessageContentPart.CreateTextPart(text); + var contentParts = new List { textPart }; + messages.Add(new UserChatMessage(contentParts)); + } + else if (message.Role == AgentRole.Assistant) + { + messages.Add(new AssistantChatMessage(message.Content)); + } + } + + var prompt = GetPrompt(messages, options); + return (prompt, messages, options); + } + + + private string GetPrompt(IEnumerable messages, ChatCompletionOptions options) + { + var prompt = string.Empty; + + if (!messages.IsNullOrEmpty()) + { + // System instruction + var verbose = string.Join("\r\n", messages + .Select(x => x as SystemChatMessage) + .Where(x => x != null) + .Select(x => + { + if (!string.IsNullOrEmpty(x.ParticipantName)) + { + // To display Agent name in log + return $"[{x.ParticipantName}]: {x.Content.FirstOrDefault()?.Text ?? string.Empty}"; + } + return $"{AgentRole.System}: {x.Content.FirstOrDefault()?.Text ?? string.Empty}"; + })); + prompt += $"{verbose}\r\n"; + + prompt += "\r\n[CONVERSATION]"; + verbose = string.Join("\r\n", messages + .Where(x => x as SystemChatMessage == null) + .Select(x => + { + var fnMessage = x as ToolChatMessage; + if (fnMessage != null) + { + return $"{AgentRole.Function}: {fnMessage.Content.FirstOrDefault()?.Text ?? string.Empty}"; + } + + var userMessage = x as UserChatMessage; + if (userMessage != null) + { + var content = x.Content.FirstOrDefault()?.Text ?? string.Empty; + return !string.IsNullOrEmpty(userMessage.ParticipantName) && userMessage.ParticipantName != "route_to_agent" ? + $"{userMessage.ParticipantName}: {content}" : + $"{AgentRole.User}: {content}"; + } + + var assistMessage = x as AssistantChatMessage; + if (assistMessage != null) + { + var toolCall = assistMessage.ToolCalls?.FirstOrDefault(); + return toolCall != null ? + $"{AgentRole.Assistant}: Call function {toolCall?.FunctionName}({toolCall?.FunctionArguments})" : + $"{AgentRole.Assistant}: {assistMessage.Content.FirstOrDefault()?.Text ?? string.Empty}"; + } + + return string.Empty; + })); + prompt += $"\r\n{verbose}\r\n"; + } + + if (!options.Tools.IsNullOrEmpty()) + { + var functions = string.Join("\r\n", options.Tools.Select(fn => + { + return $"\r\n{fn.FunctionName}: {fn.FunctionDescription}\r\n{fn.FunctionParameters}"; + })); + prompt += $"\r\n[FUNCTIONS]{functions}\r\n"; + } + + return prompt; + } +} diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/ProviderHelper.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/ProviderHelper.cs new file mode 100644 index 000000000..acd6653be --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/ProviderHelper.cs @@ -0,0 +1,16 @@ +using OpenAI; +using System.ClientModel; + +namespace BotSharp.Plugin.DeepSeek.Providers; + +public static class ProviderHelper +{ + public static OpenAIClient GetClient(string provider, string model, IServiceProvider services) + { + var settingsService = services.GetRequiredService(); + var settings = settingsService.GetSetting(provider, model); + var options = !string.IsNullOrEmpty(settings.Endpoint) ? + new OpenAIClientOptions { Endpoint = new Uri(settings.Endpoint) } : null; + return new OpenAIClient(new ApiKeyCredential(settings.ApiKey), options); + } +} diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs new file mode 100644 index 000000000..a1a889782 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Text/TextCompletionProvider.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging; +using OpenAI.Chat; + +namespace BotSharp.Plugin.DeepSeek.Providers.Text; + +public class TextCompletionProvider : ITextCompletion +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + protected string _model; + + public string Provider => "deepseek-ai"; + + public TextCompletionProvider( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task GetCompletion(string text, string agentId, string messageId) + { + var contentHooks = _services.GetServices().ToList(); + var state = _services.GetRequiredService(); + + // Before chat completion hook + var agent = new Agent() + { + Id = agentId, + }; + var message = new RoleDialogModel(AgentRole.User, text) + { + CurrentAgentId = agentId, + MessageId = messageId + }; + + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, new List { message }); + } + + var client = ProviderHelper.GetClient(Provider, _model, _services); + var chatClient = client.GetChatClient(_model); + var options = PrepareOptions(); + var response = chatClient.CompleteChat([ new UserChatMessage(text) ], options); + + // AI response + var content = response.Value?.Content ?? []; + var completion = string.Empty; + foreach (var t in content) + { + completion += t?.Text ?? string.Empty; + }; + + // After chat completion hook + var responseMessage = new RoleDialogModel(AgentRole.Assistant, completion) + { + CurrentAgentId = agentId, + MessageId = messageId + }; + + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = text, + Provider = Provider, + Model = _model, + PromptCount = response?.Value?.Usage?.InputTokenCount ?? default, + CompletionCount = response?.Value?.Usage?.OutputTokenCount ?? default + }); + } + + return completion.Trim(); + } + + public void SetModelName(string model) + { + _model = model; + } + + private ChatCompletionOptions PrepareOptions() + { + var state = _services.GetRequiredService(); + var temperature = float.Parse(state.GetState("temperature", "0.0")); + var maxTokens = int.Parse(state.GetState("max_tokens", "1024")); + + return new ChatCompletionOptions + { + Temperature = temperature, + MaxOutputTokenCount = maxTokens + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs new file mode 100644 index 000000000..a16dcc878 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs @@ -0,0 +1,19 @@ +global using System; +global using System.Collections.Generic; +global using System.Text; +global using System.Threading.Tasks; +global using System.Linq; +global using System.Text.Json; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using DeepSeek.Core; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Loggers; +global using BotSharp.Abstraction.Functions.Models; +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Plugin.DeepSeekAI.Models; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index 21756f5d5..e8f12ec75 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,7 +1,5 @@ using BotSharp.Abstraction.Files.Utilities; -using BotSharp.Abstraction.Templating; using OpenAI.Chat; -using static System.Net.Mime.MediaTypeNames; namespace BotSharp.Plugin.OpenAI.Providers.Chat; @@ -254,10 +252,10 @@ public async Task GetChatCompletionsStreamingAsync(Agent agent, List { - ChatToolCall.CreateFunctionToolCall(message.FunctionName, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + ChatToolCall.CreateFunctionToolCall(message.ToolCallId, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) })); - messages.Add(new ToolChatMessage(message.FunctionName, message.Content)); + messages.Add(new ToolChatMessage(message.ToolCallId, message.Content)); } else if (message.Role == AgentRole.User) { diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 4ecedf9e3..b2fcd8169 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -45,6 +45,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index c5ea7db1b..a85a88a73 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -348,6 +348,7 @@ "BotSharp.Plugin.AnthropicAI", "BotSharp.Plugin.GoogleAI", "BotSharp.Plugin.MetaAI", + "BotSharp.Plugin.DeepSeekAI", "BotSharp.Plugin.MetaMessenger", "BotSharp.Plugin.HuggingFace", "BotSharp.Plugin.KnowledgeBase", From 453d4bc38fefc2f8d1b838f8bc873e899343c22d Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 27 Jan 2025 23:10:37 -0600 Subject: [PATCH 2/2] refine global stats --- .../{StatCategory.cs => StatsCategory.cs} | 4 +- .../Statistics/Enums/StatsOperation.cs | 8 ++ .../Statistics/Models/BotSharpStats.cs | 2 +- .../Statistics/Models/BotSharpStatsInput.cs | 9 ++ .../Statistics/Models/StatsKeyValuePair.cs | 27 +++++ .../Services/IBotSharpStatsService.cs | 3 +- .../Conversations/Services/TokenStatistics.cs | 23 ++-- .../FileRepository/FileRepository.Stats.cs | 14 ++- .../Services/BotSharpStatsService.cs | 114 ++++++------------ .../Hooks/GlobalStatsConversationHook.cs | 16 ++- .../Providers/Chat/ChatCompletionProvider.cs | 4 +- .../BotSharp.Plugin.DeepSeekAI/Using.cs | 4 +- .../Collections/GlobalStatisticsDocument.cs | 2 +- 13 files changed, 118 insertions(+), 112 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/{StatCategory.cs => StatsCategory.cs} (53%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsOperation.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Statistics/Models/StatsKeyValuePair.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatCategory.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsCategory.cs similarity index 53% rename from src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatCategory.cs rename to src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsCategory.cs index f2a255992..5df87cdec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatCategory.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsCategory.cs @@ -1,7 +1,7 @@ namespace BotSharp.Abstraction.Statistics.Enums; -public static class StatCategory +public static class StatsCategory { - public static string LlmCost = "llm-cost"; + public static string AgentLlmCost = "agent-llm-cost"; public static string AgentCall = "agent-call"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsOperation.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsOperation.cs new file mode 100644 index 000000000..c9d1d4524 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Enums/StatsOperation.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Statistics.Enums; + +public enum StatsOperation +{ + Add = 1, + Subtract = 2, + Reset = 3 +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs index 36165defd..d9735d3ad 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStats.cs @@ -9,7 +9,7 @@ public class BotSharpStats public string Group { get; set; } = null!; [JsonPropertyName("data")] - public IDictionary Data { get; set; } = new Dictionary(); + public IDictionary Data { get; set; } = new Dictionary(); private DateTime innerRecordTime; diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs new file mode 100644 index 000000000..d1ccbdbd5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/BotSharpStatsInput.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Statistics.Models; + +public class BotSharpStatsInput +{ + public string Category { get; set; } + public string Group { get; set; } + public List Data { get; set; } = []; + public DateTime RecordTime { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/StatsKeyValuePair.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/StatsKeyValuePair.cs new file mode 100644 index 000000000..252e78a97 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Models/StatsKeyValuePair.cs @@ -0,0 +1,27 @@ +using BotSharp.Abstraction.Statistics.Enums; + +namespace BotSharp.Abstraction.Statistics.Models; + +public class StatsKeyValuePair +{ + public string Key { get; set; } + public double Value { get; set; } + public StatsOperation Operation { get; set; } + + public StatsKeyValuePair() + { + + } + + public StatsKeyValuePair(string key, double value, StatsOperation operation = StatsOperation.Add) + { + Key = key; + Value = value; + Operation = operation; + } + + public override string ToString() + { + return $"[{Key}]: {Value} ({Operation})"; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatsService.cs b/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatsService.cs index bd520c9b8..dd67bfb24 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatsService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Statistics/Services/IBotSharpStatsService.cs @@ -4,6 +4,5 @@ namespace BotSharp.Abstraction.Statistics.Services; public interface IBotSharpStatsService { - bool UpdateLlmCost(BotSharpStats stats); - bool UpdateAgentCall(BotSharpStats stats); + bool UpdateStats(string resourceKey, BotSharpStatsInput input); } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs index 7a8cef670..69462b407 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/TokenStatistics.cs @@ -60,20 +60,19 @@ public void AddToken(TokenStatsModel stats, RoleDialogModel message) var globalStats = _services.GetRequiredService(); - var body = new BotSharpStats + var body = new BotSharpStatsInput { - Category = StatCategory.LlmCost, - Group = $"Agent: {message.CurrentAgentId}", - Data = new Dictionary - { - { "prompt_token_count_total", stats.PromptCount }, - { "completion_token_count_total", stats.CompletionCount }, - { "prompt_cost_total", deltaPromptCost }, - { "completion_cost_total", deltaCompletionCost } - }, - RecordTime = DateTime.UtcNow + Category = StatsCategory.AgentLlmCost, + Group = message.CurrentAgentId, + RecordTime = DateTime.UtcNow, + Data = [ + new StatsKeyValuePair("prompt_token_count_total", stats.PromptCount), + new StatsKeyValuePair("completion_token_count_total", stats.CompletionCount), + new StatsKeyValuePair("prompt_cost_total", deltaPromptCost), + new StatsKeyValuePair("completion_cost_total", deltaCompletionCost) + ] }; - globalStats.UpdateLlmCost(body); + globalStats.UpdateStats("global-llm-cost", body); } public void PrintStatistics() diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs index f4d3f40a7..0735c7f7e 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Stats.cs @@ -13,11 +13,12 @@ public partial class FileRepository var file = Directory.GetFiles(dir).FirstOrDefault(x => Path.GetFileName(x) == STATS_FILE); if (file == null) return null; + var time = BuildRecordTime(recordTime); var text = File.ReadAllText(file); var list = JsonSerializer.Deserialize>(text, _options); var found = list?.FirstOrDefault(x => x.Category.IsEqualTo(category) && x.Group.IsEqualTo(group) - && x.RecordTime == recordTime); + && x.RecordTime == time); return found; } @@ -38,11 +39,12 @@ public bool SaveGlobalStats(BotSharpStats body) } else { + var time = BuildRecordTime(body.RecordTime); var text = File.ReadAllText(file); var list = JsonSerializer.Deserialize>(text, _options); var found = list?.FirstOrDefault(x => x.Category.IsEqualTo(body.Category) && x.Group.IsEqualTo(body.Group) - && x.RecordTime == body.RecordTime); + && x.RecordTime == time); if (found != null) { @@ -65,4 +67,12 @@ public bool SaveGlobalStats(BotSharpStats body) return true; } + + #region Private methods + private DateTime BuildRecordTime(DateTime date) + { + var recordDate = new DateTime(date.Year, date.Month, date.Day, date.Hour, 0, 0); + return DateTime.SpecifyKind(recordDate, DateTimeKind.Utc); + } + #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs index d70c7c247..d30b85dec 100644 --- a/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs +++ b/src/Infrastructure/BotSharp.Core/Statistics/Services/BotSharpStatsService.cs @@ -9,8 +9,6 @@ public class BotSharpStatsService : IBotSharpStatsService private readonly ILogger _logger; private readonly StatisticsSettings _settings; - private const string GLOBAL_LLM_COST = "global-llm-cost"; - private const string GLOBAL_AGENT_CALL = "global-agent-call"; private const int TIMEOUT_SECONDS = 5; public BotSharpStatsService( @@ -23,109 +21,71 @@ public BotSharpStatsService( _settings = settings; } - public bool UpdateLlmCost(BotSharpStats stats) + + public bool UpdateStats(string resourceKey, BotSharpStatsInput input) { try { - if (!_settings.Enabled) return false; - - var db = _services.GetRequiredService(); + if (!_settings.Enabled + || string.IsNullOrEmpty(resourceKey) + || input == null + || string.IsNullOrEmpty(input.Category) + || string.IsNullOrEmpty(input.Group)) + { + return false; + } + var locker = _services.GetRequiredService(); - - var res = locker.Lock(GLOBAL_LLM_COST, () => + var res = locker.Lock(resourceKey, () => { - var body = db.GetGlobalStats(stats.Category, stats.Group, stats.RecordTime); + var db = _services.GetRequiredService(); + var body = db.GetGlobalStats(input.Category, input.Group, input.RecordTime); if (body == null) { + var stats = new BotSharpStats + { + Category = input.Category, + Group = input.Group, + RecordTime = input.RecordTime, + Data = input.Data.ToDictionary(x => x.Key, x => x.Value) + }; db.SaveGlobalStats(stats); return; } - foreach (var item in stats.Data) + foreach (var item in input.Data) { var curValue = item.Value; if (body.Data.TryGetValue(item.Key, out var preValue)) { - var preValStr = preValue?.ToString(); - var curValStr = curValue?.ToString(); - try + switch (item.Operation) { - if (int.TryParse(preValStr, out var count)) - { - curValue = int.Parse(curValStr ?? "0") + count; - } - else if (double.TryParse(preValStr, out var num)) - { - curValue = double.Parse(curValStr ?? "0") + num; - } - } - catch - { - continue; + case StatsOperation.Add: + preValue += curValue; + break; + case StatsOperation.Subtract: + preValue -= curValue; + break; + case StatsOperation.Reset: + preValue = 0; + break; } + body.Data[item.Key] = preValue; } - - body.Data[item.Key] = curValue; - } - - db.SaveGlobalStats(body); - }, TIMEOUT_SECONDS); - return res; - } - catch (Exception ex) - { - _logger.LogError($"Error when updating global llm cost stats {stats}. {ex.Message}\r\n{ex.InnerException}"); - return false; - } - } - - public bool UpdateAgentCall(BotSharpStats stats) - { - try - { - if (!_settings.Enabled) return false; - - var db = _services.GetRequiredService(); - var locker = _services.GetRequiredService(); - - var res = locker.Lock(GLOBAL_AGENT_CALL, () => - { - var body = db.GetGlobalStats(stats.Category, stats.Group, stats.RecordTime); - if (body == null) - { - db.SaveGlobalStats(stats); - return; - } - - foreach (var item in stats.Data) - { - var curValue = item.Value; - if (body.Data.TryGetValue(item.Key, out var preValue)) + else { - var preValStr = preValue?.ToString(); - var curValStr = curValue?.ToString(); - try - { - if (int.TryParse(preValStr, out var count)) - { - curValue = int.Parse(curValStr ?? "0") + count; - } - } - catch - { - continue; - } + body.Data[item.Key] = curValue; } - body.Data[item.Key] = curValue; } db.SaveGlobalStats(body); }, TIMEOUT_SECONDS); + return res; } catch (Exception ex) { - _logger.LogError($"Error when updating global agent call stats {stats}. {ex.Message}\r\n{ex.InnerException}"); + _logger.LogError($"Error when updating global stats {input.Category}-{input.Group}. {ex.Message}\r\n{ex.InnerException}"); return false; } } diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs index 2e7b115b5..d0e6675be 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/GlobalStatsConversationHook.cs @@ -29,17 +29,15 @@ private void UpdateAgentCall(RoleDialogModel message) // record agent call var globalStats = _services.GetRequiredService(); - var body = new BotSharpStats + var body = new BotSharpStatsInput { - Category = StatCategory.AgentCall, - Group = $"Agent: {message.CurrentAgentId}", - Data = new Dictionary - { - { "agent_id", message.CurrentAgentId }, - { "agent_call_count", 1 } - }, + Category = StatsCategory.AgentCall, + Group = message.CurrentAgentId, + Data = [ + new StatsKeyValuePair("agent_call_count", 1) + ], RecordTime = DateTime.UtcNow }; - globalStats.UpdateAgentCall(body); + globalStats.UpdateStats("global-agent-call", body); } } diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index a7c6b48dc..c47f83d17 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -253,9 +253,7 @@ public void SetModelName(string model) else if (message.Role == AgentRole.User) { var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content; - var textPart = ChatMessageContentPart.CreateTextPart(text); - var contentParts = new List { textPart }; - messages.Add(new UserChatMessage(contentParts)); + messages.Add(new UserChatMessage(text)); } else if (message.Role == AgentRole.Assistant) { diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs index a16dcc878..4fa278e60 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Using.cs @@ -6,7 +6,6 @@ global using System.Text.Json; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; -global using DeepSeek.Core; global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Agents.Models; global using BotSharp.Abstraction.MLTasks; @@ -15,5 +14,4 @@ global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Loggers; global using BotSharp.Abstraction.Functions.Models; -global using BotSharp.Abstraction.Utilities; -global using BotSharp.Plugin.DeepSeekAI.Models; \ No newline at end of file +global using BotSharp.Abstraction.Utilities; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs index e90bf4f53..74ca55ef0 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/GlobalStatisticsDocument.cs @@ -4,6 +4,6 @@ public class GlobalStatisticsDocument : MongoBase { public string Category { get; set; } public string Group { get; set; } - public IDictionary Data { get; set; } = new Dictionary(); + public IDictionary Data { get; set; } = new Dictionary(); public DateTime RecordTime { get; set; } }