diff --git a/BotSharp.sln b/BotSharp.sln
index 0c99deee..b04cd1c7 100644
--- a/BotSharp.sln
+++ b/BotSharp.sln
@@ -81,7 +81,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.HttpHandler
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SqlDriver", "src\Plugins\BotSharp.Plugin.SqlDriver\BotSharp.Plugin.SqlDriver.csproj", "{D775DB67-A4B4-44E5-9144-522689590057}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Dashboard", "src\Plugins\BotSharp.Plugin.Dashboard\BotSharp.Plugin.Dashboard.csproj", "{267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Dashboard", "src\Plugins\BotSharp.Plugin.Dashboard\BotSharp.Plugin.Dashboard.csproj", "{267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.SparkDesk", "src\Plugins\BotSharp.Plugin.SparkDesk\BotSharp.Plugin.SparkDesk.csproj", "{289E25C8-63F1-4D52-9909-207724DB40CB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -323,6 +325,14 @@ Global
{267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|Any CPU.Build.0 = Release|Any CPU
{267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x64.ActiveCfg = Release|Any CPU
{267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x64.Build.0 = Release|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|x64.Build.0 = Debug|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x64.ActiveCfg = Release|Any CPU
+ {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -364,6 +374,7 @@ Global
{32D9E720-6FE6-4F29-94B1-B10B05BFAD75} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{D775DB67-A4B4-44E5-9144-522689590057} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{267998C1-55C2-4ADC-8361-2CDFA5EA6D6C} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
+ {289E25C8-63F1-4D52-9909-207724DB40CB} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}
diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/BotSharp.Plugin.SparkDesk.csproj b/src/Plugins/BotSharp.Plugin.SparkDesk/BotSharp.Plugin.SparkDesk.csproj
new file mode 100644
index 00000000..b7bd9f09
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.SparkDesk/BotSharp.Plugin.SparkDesk.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netstandard2.1
+ enable
+ $(LangVersion)
+ $(BotSharpVersion)
+ $(GeneratePackageOnBuild)
+ $(GenerateDocumentationFile)
+ $(SolutionDir)packages
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs
new file mode 100644
index 00000000..12549096
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs
@@ -0,0 +1,244 @@
+using BotSharp.Abstraction.Agents;
+using BotSharp.Abstraction.Agents.Enums;
+using BotSharp.Abstraction.Loggers;
+using BotSharp.Abstraction.Routing;
+
+namespace BotSharp.Plugin.SparkDesk.Providers;
+
+public class ChatCompletionProvider : IChatCompletion
+{
+ public string Provider => "sparkdesk";
+
+ private readonly SparkDeskSettings _settings;
+ private readonly IServiceProvider _services;
+ private readonly ILogger _logger;
+ private string _model;
+
+ public ChatCompletionProvider(IServiceProvider services,
+ SparkDeskSettings settings,
+ ILogger logger)
+ {
+ _services = services;
+ _settings = settings;
+ _logger = logger;
+ _model = $"general{settings.ModelVersion.ToString()}";
+ }
+
+
+ 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 = new SparkDeskClient(appId: _settings.AppId, apiKey: _settings.ApiKey, apiSecret: _settings.ApiSecret);
+ var (prompt, messages, funcall) = PrepareOptions(agent, conversations);
+
+ var response = await client.ChatAsync(modelVersion:_settings.ModelVersion, messages,functions:funcall);
+
+ var responseMessage = new RoleDialogModel(AgentRole.Assistant, response.Text)
+ {
+ CurrentAgentId = agent.Id,
+ MessageId = conversations.Last().MessageId
+ };
+
+ if (response.FunctionCall != null)
+ {
+ responseMessage = new RoleDialogModel(AgentRole.Function, response.Text)
+ {
+ CurrentAgentId = agent.Id,
+ MessageId = conversations.Last().MessageId,
+ FunctionName = response.FunctionCall.Name,
+ FunctionArgs = response.FunctionCall.Arguments
+ };
+
+ // Somethings LLM will generate a function name with agent name.
+ if (!string.IsNullOrEmpty(responseMessage.FunctionName))
+ {
+ responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last();
+ }
+ }
+
+ // After chat completion hook
+ foreach (var hook in contentHooks)
+ {
+ await hook.AfterGenerated(responseMessage, new TokenStatsModel
+ {
+ Prompt = prompt,
+ Provider = Provider,
+ Model = _model,
+ PromptCount = response.Usage.PromptTokens,
+ CompletionCount = response.Usage.CompletionTokens
+ });
+ }
+
+ 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 = new SparkDeskClient(appId: _settings.AppId, apiKey: _settings.ApiKey, apiSecret: _settings.ApiSecret);
+ var (prompt, messages, funcall) = PrepareOptions(agent, conversations);
+
+ var response = await client.ChatAsync(modelVersion: _settings.ModelVersion, messages, functions: funcall);
+
+ var msg = new RoleDialogModel(AgentRole.Assistant, response.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.Usage.PromptTokens,
+ CompletionCount = response.Usage.CompletionTokens
+ });
+ }
+
+ if (response.FunctionCall != null)
+ {
+ _logger.LogInformation($"[{agent.Name}]: {response.FunctionCall.Name}({response.FunctionCall.Arguments})");
+
+ var funcContextIn = new RoleDialogModel(AgentRole.Function, response.Text)
+ {
+ CurrentAgentId = agent.Id,
+ FunctionName = response.FunctionCall.Name,
+ FunctionArgs = response.FunctionCall.Arguments
+ };
+
+ // 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 = new SparkDeskClient(appId: _settings.AppId, apiKey: _settings.ApiKey, apiSecret: _settings.ApiSecret);
+ var (prompt, messages, funcall) = PrepareOptions(agent, conversations);
+
+ await foreach (StreamedChatResponse response in client.ChatAsStreamAsync(modelVersion: _settings.ModelVersion, messages, functions: funcall))
+ {
+ if (response.FunctionCall !=null)
+ {
+ //Console.Write(choice.FunctionArgumentsUpdate);
+
+ await onMessageReceived(new RoleDialogModel(AgentRole.Function, response.Text)
+ {
+ CurrentAgentId = agent.Id,
+ FunctionName = response.FunctionCall.Name,
+ FunctionArgs = response.FunctionCall.Arguments
+ });
+ continue;
+ }
+
+ await onMessageReceived(new RoleDialogModel(AgentRole.Assistant, response.Text)
+ {
+ CurrentAgentId = agent.Id
+ });
+
+ }
+
+ return true;
+ }
+
+ public void SetModelName(string model)
+ {
+ _model = model;
+ }
+
+ private (string, ChatMessage[], FunctionDef[]?) PrepareOptions(Agent agent, List conversations)
+ {
+ var prompt = "";
+ var functions = new List();
+ var agentService = _services.GetRequiredService();
+
+ if (!string.IsNullOrEmpty(agent.Instruction))
+ {
+ prompt += agentService.RenderedInstruction(agent);
+ }
+
+ var routing = _services.GetRequiredService();
+ var router = routing.Router;
+
+ var messages = conversations.Select(c => new ChatMessage(c.Role == AgentRole.User ? "user" : "AI", c.Content))
+ .ToList();
+
+ if (agent.Functions != null && agent.Functions.Count > 0)
+ {
+ foreach (var function in agent.Functions)
+ {
+ functions.Add(ConvertToFunctionDef(function));
+ }
+ prompt += "\r\n\r\n[Conversations]\r\n";
+ foreach (var dialog in conversations)
+ {
+ prompt += dialog.Role == AgentRole.Function ?
+ $"{dialog.Role}: {dialog.FunctionName} => {dialog.Content}\r\n" :
+ $"{dialog.Role}: {dialog.Content}\r\n";
+ }
+
+ prompt += "\r\n\r\n" + router.Templates.FirstOrDefault(x => x.Name == "response_with_function").Content;
+
+ return (prompt, new List
+ {
+ new ChatMessage("Which function should be used for the next step based on latest user or function response, output your response in JSON:", AgentRole.User),
+ }.ToArray(), functions.ToArray());
+ }
+
+ return (prompt, messages.ToArray(), null);
+ }
+
+ private FunctionDef ConvertToFunctionDef(BotSharp.Abstraction.Functions.Models.FunctionDef def)
+ {
+ var parameters = def.Parameters;
+ var funcParamsProperties = parameters.Properties;
+ var requiredList = parameters.Required;
+ var fundef = new List();
+ if (funcParamsProperties != null)
+ {
+ var props = funcParamsProperties.RootElement.EnumerateObject();
+ while (props.MoveNext())
+ {
+ var prop = props.Current;
+ var name = prop.Name;
+ bool required = requiredList.Contains(name);
+ FunctionParametersDef parametersDef = new FunctionParametersDef(name, prop.Value.GetProperty("type").GetRawText(), prop.Value.GetProperty("description").GetRawText(), required);
+ fundef.Add(parametersDef);
+ }
+ }
+ FunctionDef functionDef = new FunctionDef(def.Name, def.Description, fundef.ToArray());
+ return functionDef;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/Settings/SparkDeskSettings.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/Settings/SparkDeskSettings.cs
new file mode 100644
index 00000000..d15833b4
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.SparkDesk/Settings/SparkDeskSettings.cs
@@ -0,0 +1,12 @@
+namespace BotSharp.Plugin.SparkDesk;
+
+public class SparkDeskSettings
+{
+ public string AppId { get; set; }
+
+ public string ApiKey { get; set; }
+
+ public string ApiSecret { get; set; }
+
+ public ModelVersion ModelVersion { get; set; }
+}
diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/SparkDeskPlugin.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/SparkDeskPlugin.cs
new file mode 100644
index 00000000..3bcd3fa6
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.SparkDesk/SparkDeskPlugin.cs
@@ -0,0 +1,27 @@
+using BotSharp.Plugin.SparkDesk.Providers;
+
+namespace BotSharp.Plugin.SparkDesk;
+
+public class SparkDeskPlugin : IBotSharpPlugin
+{
+ public string Id => "93ad041b-a99e-5766-946e-6a8cc2d4dd4f";
+
+ public string Name => "sparkdesk";
+ public string Description => "xfyun sparkdesk Service including text generation services.";
+ public SettingsMeta Settings => new SettingsMeta("SparkDesk");
+
+ public object GetNewSettingsInstance()
+ {
+ return new SparkDeskSettings();
+ }
+
+ public void RegisterDI(IServiceCollection services, IConfiguration config)
+ {
+ services.AddScoped(provider =>
+ {
+ var settingService = provider.GetRequiredService();
+ return settingService.Bind("SparkDesk");
+ });
+ services.AddScoped();
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/Using.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/Using.cs
new file mode 100644
index 00000000..b266d1d2
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.SparkDesk/Using.cs
@@ -0,0 +1,17 @@
+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 BotSharp.Abstraction.Conversations.Models;
+global using BotSharp.Abstraction.Agents.Models;
+global using BotSharp.Abstraction.MLTasks;
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Logging;
+global using System.Text.Json.Serialization;
+global using BotSharp.Abstraction.Utilities;
+global using BotSharp.Abstraction.Plugins;
+global using BotSharp.Abstraction.Settings;
+global using Sdcb.SparkDesk;
\ No newline at end of file
diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj
index 85d2d24b..913bd83c 100644
--- a/src/WebStarter/WebStarter.csproj
+++ b/src/WebStarter/WebStarter.csproj
@@ -28,6 +28,7 @@
+