From b97162d5a42a0deabcb63a72c9322b39bbc1f801 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 9 Oct 2025 06:42:15 +0800 Subject: [PATCH 01/18] semantic convention otel System.Diagnostics combined with Microsoft.Extensions.Telemetry integrated into Langfuse via OpenTelemetry (OTEL) --- BotSharp.sln | 11 + Directory.Packages.props | 1 + .../Diagnostics/ActivityExtensions.cs | 119 +++++ .../Diagnostics/AppContextSwitchHelper.cs | 35 ++ .../Diagnostics/ModelDiagnostics.cs | 459 ++++++++++++++++++ .../Executor/FunctionCallbackExecutor.cs | 15 +- .../Routing/Executor/MCPToolExecutor.cs | 49 +- .../Routing/RoutingService.InvokeAgent.cs | 5 +- .../Routing/RoutingService.InvokeFunction.cs | 1 + .../BotSharp.Core/Routing/RoutingService.cs | 1 + .../BotSharp.Langfuse.csproj | 21 + .../BotSharp.Langfuse/LangfusePlugin.cs | 17 + .../LangfuseSeriveExtention.cs | 87 ++++ .../BotSharp.Langfuse/LangfuseSettings.cs | 19 + 14 files changed, 819 insertions(+), 21 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs create mode 100644 src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj create mode 100644 src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs create mode 100644 src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs create mode 100644 src/Infrastructure/BotSharp.Langfuse/LangfuseSettings.cs diff --git a/BotSharp.sln b/BotSharp.sln index f68ce1c60..d08e4cb0c 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -147,6 +147,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandle EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandler", "src\Plugins\BotSharp.Plugin.ExcelHandler\BotSharp.Plugin.ExcelHandler.csproj", "{FC63C875-E880-D8BB-B8B5-978AB7B62983}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Langfuse", "src\Infrastructure\BotSharp.Langfuse\BotSharp.Langfuse.csproj", "{7C73CE98-7610-42F0-B8BA-BA0A671CA355}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -619,6 +621,14 @@ Global {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.Build.0 = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.ActiveCfg = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.Build.0 = Release|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|x64.Build.0 = Debug|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|Any CPU.Build.0 = Release|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|x64.ActiveCfg = Release|Any CPU + {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -690,6 +700,7 @@ Global {B067B126-88CD-4282-BEEF-7369B64423EF} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {FC63C875-E880-D8BB-B8B5-978AB7B62983} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {7C73CE98-7610-42F0-B8BA-BA0A671CA355} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Packages.props b/Directory.Packages.props index de3730f08..17bc01691 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -98,6 +98,7 @@ + diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs new file mode 100644 index 000000000..105d5aae5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace BotSharp.Abstraction.Diagnostics; + +[ExcludeFromCodeCoverage] +public static class ActivityExtensions +{ + /// + /// Starts an activity with the appropriate tags for a kernel function execution. + /// + public static Activity? StartFunctionActivity(this ActivitySource source, string functionName, string functionDescription) + { + const string OperationName = "execute_tool"; + + return source.StartActivityWithTags($"{OperationName} {functionName}", [ + new KeyValuePair("gen_ai.operation.name", OperationName), + new KeyValuePair("gen_ai.tool.name", functionName), + new KeyValuePair("gen_ai.tool.description", functionDescription) + ], ActivityKind.Internal); + } + + /// + /// Starts an activity with the specified name and tags. + /// + public static Activity? StartActivityWithTags(this ActivitySource source, string name, IEnumerable> tags, ActivityKind kind = ActivityKind.Internal) + => source.StartActivity(name, kind, default(ActivityContext), tags); + + /// + /// Adds tags to the activity. + /// + public static Activity SetTags(this Activity activity, ReadOnlySpan> tags) + { + foreach (var tag in tags) + { + activity.SetTag(tag.Key, tag.Value); + } + ; + + return activity; + } + + /// + /// Adds an event to the activity. Should only be used for events that contain sensitive data. + /// + public static Activity AttachSensitiveDataAsEvent(this Activity activity, string name, IEnumerable> tags) + { + activity.AddEvent(new ActivityEvent( + name, + tags: [.. tags] + )); + + return activity; + } + + /// + /// Sets the error status and type on the activity. + /// + public static Activity SetError(this Activity activity, Exception exception) + { + activity.SetTag("error.type", exception.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + return activity; + } + + public static async IAsyncEnumerable RunWithActivityAsync( + Func getActivity, + Func> operation, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + using var activity = getActivity(); + + ConfiguredCancelableAsyncEnumerable result; + + try + { + result = operation().WithCancellation(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + var resultEnumerator = result.ConfigureAwait(false).GetAsyncEnumerator(); + + try + { + while (true) + { + try + { + if (!await resultEnumerator.MoveNextAsync()) + { + break; + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + yield return resultEnumerator.Current; + } + } + finally + { + await resultEnumerator.DisposeAsync(); + } + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs new file mode 100644 index 000000000..64e5806be --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace BotSharp.Abstraction.Diagnostics; + +/// +/// Helper class to get app context switch value +/// +[ExcludeFromCodeCoverage] +internal static class AppContextSwitchHelper +{ + /// + /// Returns the value of the specified app switch or environment variable if it is set. + /// If the switch or environment variable is not set, return false. + /// The app switch value takes precedence over the environment variable. + /// + /// The name of the app switch. + /// The name of the environment variable. + /// The value of the app switch or environment variable if it is set; otherwise, false. + public static bool GetConfigValue(string appContextSwitchName, string envVarName) + { + if (AppContext.TryGetSwitch(appContextSwitchName, out bool value)) + { + return value; + } + + string? envVarValue = Environment.GetEnvironmentVariable(envVarName); + if (envVarValue != null && bool.TryParse(envVarValue, out value)) + { + return value; + } + + return false; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs new file mode 100644 index 000000000..db00df5ba --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs @@ -0,0 +1,459 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Functions.Models; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; +using System.Text.Json; + +namespace BotSharp.Abstraction.Diagnostics; + +/// +/// Model diagnostics helper class that provides a set of methods to trace model activities with the OTel semantic conventions. +/// This class contains experimental features and may change in the future. +/// To enable these features, set one of the following switches to true: +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnostics` +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive` +/// Or set the following environment variables to true: +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS` +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE` +/// +//[System.Diagnostics.CodeAnalysis.Experimental("SKEXP0001")] +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static class ModelDiagnostics +{ + private static readonly string s_namespace = typeof(ModelDiagnostics).Namespace!; + private static readonly ActivitySource s_activitySource = new(s_namespace); + + private const string EnableDiagnosticsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnostics"; + private const string EnableSensitiveEventsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive"; + private const string EnableDiagnosticsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS"; + private const string EnableSensitiveEventsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE"; + + private static readonly bool s_enableDiagnostics = AppContextSwitchHelper.GetConfigValue(EnableDiagnosticsSwitch, EnableDiagnosticsEnvVar); + private static readonly bool s_enableSensitiveEvents = AppContextSwitchHelper.GetConfigValue(EnableSensitiveEventsSwitch, EnableSensitiveEventsEnvVar); + + /// + /// Start a text completion activity for a given model. + /// The activity will be tagged with the a set of attributes specified by the semantic conventions. + /// + public static Activity? StartCompletionActivity( + Uri? endpoint, + string modelName, + string modelProvider, + string prompt, + IConversationStateService services + ) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "text.completions"; + var activity = s_activitySource.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(ModelDiagnosticsTags.Operation, OperationName), + new(ModelDiagnosticsTags.System, modelProvider), + new(ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, services); + + if (s_enableSensitiveEvents) + { + activity?.AttachSensitiveDataAsEvent( + ModelDiagnosticsTags.UserMessage, + [ + new(ModelDiagnosticsTags.EventName, prompt), + new(ModelDiagnosticsTags.System, modelProvider), + ]); + } + + return activity; + } + + /// + /// Start a chat completion activity for a given model. + /// The activity will be tagged with the a set of attributes specified by the semantic conventions. + /// + public static Activity? StartCompletionActivity( + Uri? endpoint, + string modelName, + string modelProvider, + List chatHistory, + IConversationStateService conversationStateService + ) + + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "chat.completions"; + var activity = s_activitySource.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(ModelDiagnosticsTags.Operation, OperationName), + new(ModelDiagnosticsTags.System, modelProvider), + new(ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, conversationStateService); + + if (s_enableSensitiveEvents) + { + foreach (var message in chatHistory) + { + var formattedContent = JsonSerializer.Serialize(ToGenAIConventionsFormat(message)); + activity?.AttachSensitiveDataAsEvent( + ModelDiagnosticsTags.RoleToEventMap[message.Role], + [ + new(ModelDiagnosticsTags.EventName, formattedContent), + new(ModelDiagnosticsTags.System, modelProvider), + ]); + } + } + + return activity; + } + + /// + /// Start an agent invocation activity and return the activity. + /// + public static Activity? StartAgentInvocationActivity( + string agentId, + string agentName, + string? agentDescription, + Agent? agents, + List messages + ) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "invoke_agent"; + + var activity = s_activitySource.StartActivityWithTags( + $"{OperationName} {agentName}", + [ + new(ModelDiagnosticsTags.Operation, OperationName), + new(ModelDiagnosticsTags.AgentId, agentId), + new(ModelDiagnosticsTags.AgentName, agentName) + ], + ActivityKind.Internal); + + if (!string.IsNullOrWhiteSpace(agentDescription)) + { + activity?.SetTag(ModelDiagnosticsTags.AgentDescription, agentDescription); + } + + if (agents is not null && (agents.Functions.Count > 0 || agents.SecondaryFunctions.Count >0)) + { + List allFunctions = []; + allFunctions.AddRange(agents.Functions); + allFunctions.AddRange(agents.SecondaryFunctions); + + activity?.SetTag( + ModelDiagnosticsTags.AgentToolDefinitions, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + if (IsSensitiveEventsEnabled()) + { + activity?.SetTag( + ModelDiagnosticsTags.AgentInvocationInput, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + return activity; + } + + /// + /// Set the agent response for a given activity. + /// + public static void SetAgentResponse(this Activity activity, IEnumerable? responses) + { + if (!IsModelDiagnosticsEnabled() || responses is null) + { + return; + } + + if (s_enableSensitiveEvents) + { + activity?.SetTag( + ModelDiagnosticsTags.AgentInvocationOutput, + JsonSerializer.Serialize(responses.Select(r => ToGenAIConventionsFormat(r)))); + } + } + + ///// + ///// End the agent streaming response for a given activity. + ///// + //internal static void EndAgentStreamingResponse( + // this Activity activity, + // IEnumerable? contents) + //{ + // if (!IsModelDiagnosticsEnabled() || contents is null) + // { + // return; + // } + + // Dictionary> choices = []; + // foreach (var content in contents) + // { + // if (!choices.TryGetValue(content.ChoiceIndex, out var choiceContents)) + // { + // choiceContents = []; + // choices[content.ChoiceIndex] = choiceContents; + // } + + // choiceContents.Add(content); + // } + + // var chatCompletions = choices.Select(choiceContents => + // { + // var lastContent = (StreamingChatMessageContent)choiceContents.Value.Last(); + // var chatMessage = choiceContents.Value.Select(c => c.ToString()).Aggregate((a, b) => a + b); + // return new ChatMessageContent(lastContent.Role ?? AuthorRole.Assistant, chatMessage, metadata: lastContent.Metadata); + // }).ToList(); + + // activity?.SetTag( + // ModelDiagnosticsTags.AgentInvocationOutput, + // JsonSerializer.Serialize(chatCompletions.Select(r => ToGenAIConventionsFormat(r)))); + //} + + ///// + ///// Set the text completion response for a given activity. + ///// The activity will be enriched with the response attributes specified by the semantic conventions. + ///// + //internal static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) + // => SetCompletionResponse(activity, completions, promptTokens, completionTokens, ToGenAIConventionsChoiceFormat); + + ///// + ///// Set the chat completion response for a given activity. + ///// The activity will be enriched with the response attributes specified by the semantic conventions. + ///// + //internal static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) + // => SetCompletionResponse(activity, completions, promptTokens, completionTokens, ToGenAIConventionsChoiceFormat); + + ///// + ///// Notify the end of streaming for a given activity. + ///// + //internal static void EndStreaming( + // this Activity activity, + // IEnumerable? contents, + // IEnumerable? toolCalls = null, + // int? promptTokens = null, + // int? completionTokens = null) + //{ + // if (IsModelDiagnosticsEnabled()) + // { + // var choices = OrganizeStreamingContent(contents); + // SetCompletionResponse(activity, choices, toolCalls, promptTokens, completionTokens); + // } + //} + + /// + /// Set the response id for a given activity. + /// + /// The activity to set the response id + /// The response id + /// The activity with the response id set for chaining + internal static Activity SetResponseId(this Activity activity, string responseId) => activity.SetTag(ModelDiagnosticsTags.ResponseId, responseId); + + /// + /// Set the input tokens usage for a given activity. + /// + /// The activity to set the input tokens usage + /// The number of input tokens used + /// The activity with the input tokens usage set for chaining + internal static Activity SetInputTokensUsage(this Activity activity, int inputTokens) => activity.SetTag(ModelDiagnosticsTags.InputTokens, inputTokens); + + /// + /// Set the output tokens usage for a given activity. + /// + /// The activity to set the output tokens usage + /// The number of output tokens used + /// The activity with the output tokens usage set for chaining + internal static Activity SetOutputTokensUsage(this Activity activity, int outputTokens) => activity.SetTag(ModelDiagnosticsTags.OutputTokens, outputTokens); + + /// + /// Check if model diagnostics is enabled + /// Model diagnostics is enabled if either EnableModelDiagnostics or EnableSensitiveEvents is set to true and there are listeners. + /// + internal static bool IsModelDiagnosticsEnabled() + { + return (s_enableDiagnostics || s_enableSensitiveEvents) && s_activitySource.HasListeners(); + } + + /// + /// Check if sensitive events are enabled. + /// Sensitive events are enabled if EnableSensitiveEvents is set to true and there are listeners. + /// + internal static bool IsSensitiveEventsEnabled() => s_enableSensitiveEvents && s_activitySource.HasListeners(); + + internal static bool HasListeners() => s_activitySource.HasListeners(); + + #region Private + private static void AddOptionalTags(Activity? activity, IConversationStateService conversationStateService) + { + if (activity is null) + { + return; + } + + void TryAddTag(string key, string tag) + { + var value = conversationStateService.GetState(key); + if (!string.IsNullOrEmpty(value)) + { + activity.SetTag(tag, value); + } + } + + TryAddTag("max_tokens", ModelDiagnosticsTags.MaxToken); + TryAddTag("temperature", ModelDiagnosticsTags.Temperature); + TryAddTag("top_p", ModelDiagnosticsTags.TopP); + } + + /// + /// Convert a chat message to a JSON object based on the OTel GenAI Semantic Conventions format + /// + private static object ToGenAIConventionsFormat(RoleDialogModel chatMessage) + { + return new + { + role = chatMessage.Role.ToString(), + name = chatMessage.MessageId, + content = chatMessage.Content, + tool_calls = ToGenAIConventionsToolCallFormat(chatMessage), + }; + } + + /// + /// Helper method to convert tool calls to a list of JSON object based on the OTel GenAI Semantic Conventions format + /// + private static List ToGenAIConventionsToolCallFormat(RoleDialogModel chatMessage) + { + List toolCalls = []; + if (chatMessage.Instruction is not null) + { + toolCalls.Add(new + { + id = chatMessage.ToolCallId, + function = new + { + name = chatMessage.Instruction.Function, + arguments = chatMessage.Instruction.Arguments + }, + type = "function" + }); + } + return toolCalls; + } + + /// + /// Convert a function metadata to a JSON object based on the OTel GenAI Semantic Conventions format + /// + private static object ToGenAIConventionsFormat(FunctionDef metadata) + { + var properties = metadata.Parameters?.Properties; + var required = metadata.Parameters?.Required; + + return new + { + type = "function", + name = metadata.Name, + description = metadata.Description, + parameters = new + { + type = "object", + properties, + required, + } + }; + } + + /// + /// Convert a chat model response to a JSON string based on the OTel GenAI Semantic Conventions format + /// + private static string ToGenAIConventionsChoiceFormat(RoleDialogModel chatMessage, int index) + { + var jsonObject = new + { + index, + message = ToGenAIConventionsFormat(chatMessage), + tool_calls = ToGenAIConventionsToolCallFormat(chatMessage) + }; + + return JsonSerializer.Serialize(jsonObject); + } + + + + /// + /// Tags used in model diagnostics + /// + private static class ModelDiagnosticsTags + { + // Activity tags + public const string System = "gen_ai.system"; + public const string Operation = "gen_ai.operation.name"; + public const string Model = "gen_ai.request.model"; + public const string MaxToken = "gen_ai.request.max_tokens"; + public const string Temperature = "gen_ai.request.temperature"; + public const string TopP = "gen_ai.request.top_p"; + public const string ResponseId = "gen_ai.response.id"; + public const string ResponseModel = "gen_ai.response.model"; + public const string FinishReason = "gen_ai.response.finish_reason"; + public const string InputTokens = "gen_ai.usage.input_tokens"; + public const string OutputTokens = "gen_ai.usage.output_tokens"; + public const string Address = "server.address"; + public const string Port = "server.port"; + public const string AgentId = "gen_ai.agent.id"; + public const string AgentName = "gen_ai.agent.name"; + public const string AgentDescription = "gen_ai.agent.description"; + public const string AgentInvocationInput = "gen_ai.input.messages"; + public const string AgentInvocationOutput = "gen_ai.output.messages"; + public const string AgentToolDefinitions = "gen_ai.tool.definitions"; + + // Activity events + public const string EventName = "gen_ai.event.content"; + public const string SystemMessage = "gen_ai.system.message"; + public const string UserMessage = "gen_ai.user.message"; + public const string AssistantMessage = "gen_ai.assistant.message"; + public const string ToolMessage = "gen_ai.tool.message"; + public const string Choice = "gen_ai.choice"; + public static readonly Dictionary RoleToEventMap = new() + { + { AgentRole.System, SystemMessage }, + { AgentRole.User, UserMessage }, + { AgentRole.Assistant, AssistantMessage }, + { AgentRole.Function, ToolMessage } + }; + } + # endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs index 4b208374f..62a3b6b34 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs @@ -1,10 +1,18 @@ -using BotSharp.Abstraction.Routing.Executor; +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Routing.Executor; +using System.Diagnostics; namespace BotSharp.Core.Routing.Executor; public class FunctionCallbackExecutor : IFunctionExecutor { + /// + /// + /// for function-related activities. + /// + private static readonly ActivitySource s_activitySource = new("BotSharp.Core.Routing.Executor"); + private readonly IFunctionCallback _functionCallback; public FunctionCallbackExecutor(IFunctionCallback functionCallback) @@ -14,7 +22,10 @@ public FunctionCallbackExecutor(IFunctionCallback functionCallback) public async Task ExecuteAsync(RoleDialogModel message) { - return await _functionCallback.Execute(message); + using var activity = s_activitySource.StartFunctionActivity(this._functionCallback.Name, this._functionCallback.Indication); + { + return await _functionCallback.Execute(message); + } } public async Task GetIndicatorAsync(RoleDialogModel message) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs index c452e8066..c86dfe1be 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs @@ -1,6 +1,8 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Executor; using BotSharp.Core.MCP.Managers; using ModelContextProtocol.Client; +using System.Diagnostics; namespace BotSharp.Core.Routing.Executor; @@ -10,6 +12,13 @@ public class McpToolExecutor: IFunctionExecutor private readonly string _mcpServerId; private readonly string _functionName; + /// + /// + /// for function-related activities. + /// + private static readonly ActivitySource s_activitySource = new("BotSharp.Core.Routing.Executor"); + + public McpToolExecutor(IServiceProvider services, string mcpServerId, string functionName) { _services = services; @@ -19,28 +28,32 @@ public McpToolExecutor(IServiceProvider services, string mcpServerId, string fun public async Task ExecuteAsync(RoleDialogModel message) { - try + using var activity = s_activitySource.StartFunctionActivity(this._functionName, $"calling tool {_functionName} of MCP server {_mcpServerId}"); { - // Convert arguments to dictionary format expected by mcpdotnet - Dictionary argDict = JsonToDictionary(message.FunctionArgs); - - var clientManager = _services.GetRequiredService(); - var client = await clientManager.GetMcpClientAsync(_mcpServerId); + try + { + // Convert arguments to dictionary format expected by mcpdotnet + Dictionary argDict = JsonToDictionary(message.FunctionArgs); - // Call the tool through mcpdotnet - var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); + var clientManager = _services.GetRequiredService(); + var client = await clientManager.GetMcpClientAsync(_mcpServerId); + + // Call the tool through mcpdotnet + var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); - // Extract the text content from the result - var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); + // Extract the text content from the result + var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); - message.Content = json; - message.Data = json.JsonContent(); - return true; - } - catch (Exception ex) - { - message.Content = $"Error when calling tool {_functionName} of MCP server {_mcpServerId}. {ex.Message}"; - return false; + message.Content = json; + message.Data = json.JsonContent(); + return true; + } + catch (Exception ex) + { + message.Content = $"Error when calling tool {_functionName} of MCP server {_mcpServerId}. {ex.Message}"; + activity?.SetError(ex); + return false; + } } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index e0175a70d..36a2dbd6a 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; @@ -14,6 +15,8 @@ public async Task InvokeAgent( var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); + using var activity = ModelDiagnostics.StartAgentInvocationActivity(agentId, agent.Name, agent.Description, agent, dialogs); + Context.IncreaseRecursiveCounter(); if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth) { @@ -79,7 +82,7 @@ public async Task InvokeAgent( dialogs.Add(message); Context.AddDialogs([message]); } - + activity?.SetAgentResponse(Context.GetDialogs()); return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 3850dcc13..17cd180a3 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Models; using BotSharp.Core.MessageHub; using BotSharp.Core.Routing.Executor; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 4e43cbd52..4dfc0fe93 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; +using System.Diagnostics; namespace BotSharp.Core.Routing; diff --git a/src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj b/src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj new file mode 100644 index 000000000..05ff0836c --- /dev/null +++ b/src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj @@ -0,0 +1,21 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(SolutionDir)packages + + + + + + + + + + + + diff --git a/src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs b/src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs new file mode 100644 index 000000000..f621c6a46 --- /dev/null +++ b/src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs @@ -0,0 +1,17 @@ +using BotSharp.Abstraction.Plugins; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace BotSharp.Langfuse +{ + public class LangfusePlugin : IBotSharpPlugin + { + public string Id => throw new NotImplementedException(); + + public void RegisterDI(IServiceCollection services,IConfiguration config) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs b/src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs new file mode 100644 index 000000000..7911a31a5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs @@ -0,0 +1,87 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System; +using System.Threading.Tasks; + +namespace BotSharp.Langfuse; + +public static class LangfuseSeriveExtention +{ + public static async Task AddLangfuseOpenTelemetry(this IServiceCollection services) + { + + // 从配置文件中获取 Langfuse 设置 + var configuration = services.BuildServiceProvider().GetRequiredService(); + var langfuseSection = configuration.GetSection("Langfuse"); + var publicKey = langfuseSection.GetValue(nameof(LangfuseSettings.PublicKey)) ?? string.Empty; + var secretKey = langfuseSection.GetValue(nameof(LangfuseSettings.SecretKey)) ?? string.Empty; + var host = langfuseSection.GetValue(nameof(LangfuseSettings.Host)) ?? string.Empty; + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"); + string base64EncodedAuth = Convert.ToBase64String(plainTextBytes); + + // Endpoint to the Aspire Dashboard / Grafana Tempo + var endpoint = host; + + var resourceBuilder = ResourceBuilder + .CreateDefault() + .AddService("TelemetryAspireDashboardQuickstart"); + + // Enable model diagnostics with sensitive data. + AppContext.SetSwitch("Microsoft.SemanticKernel.Experimental.GenAI.EnableOTelDiagnostics", true); + AppContext.SetSwitch("Microsoft.SemanticKernel.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true); + + var traceProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .SetResourceBuilder(resourceBuilder) + .AddSource("BotSharp*") + .AddConsoleExporter(options => { options.Targets = ConsoleExporterOutputTargets.Console; }) + .AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + }) + .Build(); + + // 在应用程序退出前明确刷新遥测数据, + // 对于控制台应用程式非常重要是必須的。 + traceProvider.ForceFlush(); + await Task.Delay(3000); + + var meterProvider = Sdk.CreateMeterProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddMeter("Microsoft.SemanticKernel*") + .AddOtlpExporter(options => options.Endpoint = new Uri(endpoint)) + .Build(); + + services.AddSingleton(traceProvider); + services.AddSingleton(meterProvider); + + services.AddLogging(loggingBuilder => + { + loggingBuilder.AddOpenTelemetry(options => + { + options.SetResourceBuilder(resourceBuilder); + options.AddConsoleExporter(); + options.AddOtlpExporter(options => + { + options.Endpoint = new Uri(endpoint); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + }); + options.IncludeFormattedMessage = true; + options.IncludeScopes = true; + }); + loggingBuilder.SetMinimumLevel(LogLevel.Information); + }); + + return services; + } +} diff --git a/src/Infrastructure/BotSharp.Langfuse/LangfuseSettings.cs b/src/Infrastructure/BotSharp.Langfuse/LangfuseSettings.cs new file mode 100644 index 000000000..4c79832c6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Langfuse/LangfuseSettings.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.Langfuse; + +/// +/// Langfuse Settings +/// +public class LangfuseSettings +{ + public string SecretKey { get; set; } + + public string PublicKey { get; set; } + + public string Host { get; set; } +} From 6462e0354d8f7c92bb1cd11111e94116acc9d672 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 16 Oct 2025 18:09:33 +0800 Subject: [PATCH 02/18] Integrate GiteeAI plugin and enhance OpenTelemetry Removed the BotSharp.Langfuse project and related files, migrating LangfuseSettings to BotSharp.ServiceDefaults. Added the BotSharp.Plugin.GiteeAI plugin with chat and embedding providers. Enhanced OpenTelemetry integration with Langfuse support and improved diagnostics tagging in core executors and controllers. Updated solution and project files to reflect these changes. --- BotSharp.sln | 20 +- src/BotSharp.AppHost/Program.cs | 4 +- src/BotSharp.ServiceDefaults/Extensions.cs | 52 +- .../LangfuseSettings.cs | 0 .../Diagnostics/ModelDiagnostics.cs | 69 +-- .../Executor/FunctionCallbackExecutor.cs | 3 + .../Routing/Executor/MCPToolExecutor.cs | 4 + .../BotSharp.Langfuse.csproj | 21 - .../BotSharp.Langfuse/LangfusePlugin.cs | 17 - .../LangfuseSeriveExtention.cs | 87 --- .../Controllers/ConversationController.cs | 54 +- .../Providers/Chat/ChatCompletionProvider.cs | 144 ++--- .../BotSharp.Plugin.GiteeAI.csproj | 31 ++ .../BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs | 19 + .../Providers/Chat/ChatCompletionProvider.cs | 496 ++++++++++++++++++ .../Embedding/TextEmbeddingProvider.cs | 73 +++ .../Providers/ProviderHelper.cs | 16 + src/Plugins/BotSharp.Plugin.GiteeAI/README.md | 8 + src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs | 15 + .../Providers/Chat/ChatCompletionProvider.cs | 117 +++-- src/WebStarter/Program.cs | 5 +- src/WebStarter/WebStarter.csproj | 1 + src/WebStarter/appsettings.json | 165 +++--- 23 files changed, 1014 insertions(+), 407 deletions(-) rename src/{Infrastructure/BotSharp.Langfuse => BotSharp.ServiceDefaults}/LangfuseSettings.cs (100%) delete mode 100644 src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj delete mode 100644 src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs delete mode 100644 src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/README.md create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs diff --git a/BotSharp.sln b/BotSharp.sln index d08e4cb0c..ccf9b2654 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -147,7 +147,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandle EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandler", "src\Plugins\BotSharp.Plugin.ExcelHandler\BotSharp.Plugin.ExcelHandler.csproj", "{FC63C875-E880-D8BB-B8B5-978AB7B62983}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Langfuse", "src\Infrastructure\BotSharp.Langfuse\BotSharp.Langfuse.csproj", "{7C73CE98-7610-42F0-B8BA-BA0A671CA355}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.GiteeAI", "src\Plugins\BotSharp.Plugin.GiteeAI\BotSharp.Plugin.GiteeAI.csproj", "{50B57066-3267-1D10-0F72-D2F5CC494F2C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -621,14 +621,14 @@ Global {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.Build.0 = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.ActiveCfg = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.Build.0 = Release|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|x64.ActiveCfg = Debug|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Debug|x64.Build.0 = Debug|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|Any CPU.Build.0 = Release|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|x64.ActiveCfg = Release|Any CPU - {7C73CE98-7610-42F0-B8BA-BA0A671CA355}.Release|x64.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -700,7 +700,7 @@ Global {B067B126-88CD-4282-BEEF-7369B64423EF} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {FC63C875-E880-D8BB-B8B5-978AB7B62983} = {51AFE054-AE99-497D-A593-69BAEFB5106F} - {7C73CE98-7610-42F0-B8BA-BA0A671CA355} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} + {50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/src/BotSharp.AppHost/Program.cs b/src/BotSharp.AppHost/Program.cs index 4c54ed11b..444e2ecf3 100644 --- a/src/BotSharp.AppHost/Program.cs +++ b/src/BotSharp.AppHost/Program.cs @@ -2,8 +2,8 @@ var apiService = builder.AddProject("apiservice") .WithExternalHttpEndpoints(); -var mcpService = builder.AddProject("mcpservice") - .WithExternalHttpEndpoints(); +//var mcpService = builder.AddProject("mcpservice") +// .WithExternalHttpEndpoints(); builder.AddNpmApp("BotSharpUI", "../../../BotSharp-UI") .WithReference(apiService) diff --git a/src/BotSharp.ServiceDefaults/Extensions.cs b/src/BotSharp.ServiceDefaults/Extensions.cs index bfc0bb687..caf52b243 100644 --- a/src/BotSharp.ServiceDefaults/Extensions.cs +++ b/src/BotSharp.ServiceDefaults/Extensions.cs @@ -1,12 +1,16 @@ +using BotSharp.Langfuse; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; @@ -45,6 +49,10 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) { + // Enable model diagnostics with sensitive data. + AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnostics", true); + AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true); + builder.Logging.AddOpenTelemetry(logging => { // Use Serilog Log.Logger = new LoggerConfiguration() @@ -87,10 +95,28 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati }) .WithTracing(tracing => { + tracing.SetResourceBuilder( + ResourceBuilder.CreateDefault() + .AddService("apiservice", serviceVersion: "1.0.0") + ) + .AddSource("BotSharp") + .AddSource("BotSharp.Abstraction.Diagnostics") + .AddSource("BotSharp.Core.Routing.Executor"); + tracing.AddAspNetCoreInstrumentation() // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); + .AddHttpClientInstrumentation() + //.AddOtlpExporter(options => + //{ + // //options.Endpoint = new Uri(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? "http://localhost:4317"); + // options.Endpoint = new Uri(host); + // options.Protocol = OtlpExportProtocol.HttpProtobuf; + // options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + //}) + ; + + }); builder.AddOpenTelemetryExporters(); @@ -100,14 +126,34 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) { + var langfuseSection = builder.Configuration.GetSection("Langfuse"); + var useLangfuse = langfuseSection != null; var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); if (useOtlpExporter) { builder.Services.Configure(logging => logging.AddOtlpExporter()); builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); - + if (useLangfuse) + { + var publicKey = langfuseSection.GetValue(nameof(LangfuseSettings.PublicKey)) ?? string.Empty; + var secretKey = langfuseSection.GetValue(nameof(LangfuseSettings.SecretKey)) ?? string.Empty; + var host = langfuseSection.GetValue(nameof(LangfuseSettings.Host)) ?? string.Empty; + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"); + string base64EncodedAuth = Convert.ToBase64String(plainTextBytes); + + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter(options => + { + options.Endpoint = new Uri(host); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + }) + ); + } + else + { + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + } } // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) diff --git a/src/Infrastructure/BotSharp.Langfuse/LangfuseSettings.cs b/src/BotSharp.ServiceDefaults/LangfuseSettings.cs similarity index 100% rename from src/Infrastructure/BotSharp.Langfuse/LangfuseSettings.cs rename to src/BotSharp.ServiceDefaults/LangfuseSettings.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs index db00df5ba..83f6532cb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs @@ -209,72 +209,7 @@ public static void SetAgentResponse(this Activity activity, IEnumerable - ///// End the agent streaming response for a given activity. - ///// - //internal static void EndAgentStreamingResponse( - // this Activity activity, - // IEnumerable? contents) - //{ - // if (!IsModelDiagnosticsEnabled() || contents is null) - // { - // return; - // } - - // Dictionary> choices = []; - // foreach (var content in contents) - // { - // if (!choices.TryGetValue(content.ChoiceIndex, out var choiceContents)) - // { - // choiceContents = []; - // choices[content.ChoiceIndex] = choiceContents; - // } - - // choiceContents.Add(content); - // } - - // var chatCompletions = choices.Select(choiceContents => - // { - // var lastContent = (StreamingChatMessageContent)choiceContents.Value.Last(); - // var chatMessage = choiceContents.Value.Select(c => c.ToString()).Aggregate((a, b) => a + b); - // return new ChatMessageContent(lastContent.Role ?? AuthorRole.Assistant, chatMessage, metadata: lastContent.Metadata); - // }).ToList(); - - // activity?.SetTag( - // ModelDiagnosticsTags.AgentInvocationOutput, - // JsonSerializer.Serialize(chatCompletions.Select(r => ToGenAIConventionsFormat(r)))); - //} - - ///// - ///// Set the text completion response for a given activity. - ///// The activity will be enriched with the response attributes specified by the semantic conventions. - ///// - //internal static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) - // => SetCompletionResponse(activity, completions, promptTokens, completionTokens, ToGenAIConventionsChoiceFormat); - - ///// - ///// Set the chat completion response for a given activity. - ///// The activity will be enriched with the response attributes specified by the semantic conventions. - ///// - //internal static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) - // => SetCompletionResponse(activity, completions, promptTokens, completionTokens, ToGenAIConventionsChoiceFormat); - - ///// - ///// Notify the end of streaming for a given activity. - ///// - //internal static void EndStreaming( - // this Activity activity, - // IEnumerable? contents, - // IEnumerable? toolCalls = null, - // int? promptTokens = null, - // int? completionTokens = null) - //{ - // if (IsModelDiagnosticsEnabled()) - // { - // var choices = OrganizeStreamingContent(contents); - // SetCompletionResponse(activity, choices, toolCalls, promptTokens, completionTokens); - // } - //} + /// /// Set the response id for a given activity. @@ -417,7 +352,7 @@ private static string ToGenAIConventionsChoiceFormat(RoleDialogModel chatMessage /// /// Tags used in model diagnostics /// - private static class ModelDiagnosticsTags + public static class ModelDiagnosticsTags { // Activity tags public const string System = "gen_ai.system"; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs index 62a3b6b34..e49ff3ba3 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Executor; using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Core.Routing.Executor; @@ -24,6 +25,8 @@ public async Task ExecuteAsync(RoleDialogModel message) { using var activity = s_activitySource.StartFunctionActivity(this._functionCallback.Name, this._functionCallback.Indication); { + activity?.SetTag("input", message.FunctionArgs); + activity?.SetTag(ModelDiagnosticsTags.AgentId, message.CurrentAgentId); return await _functionCallback.Execute(message); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs index c86dfe1be..e346ce549 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs @@ -3,6 +3,7 @@ using BotSharp.Core.MCP.Managers; using ModelContextProtocol.Client; using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Core.Routing.Executor; @@ -32,6 +33,9 @@ public async Task ExecuteAsync(RoleDialogModel message) { try { + activity?.SetTag("input", message.FunctionArgs); + activity?.SetTag(ModelDiagnosticsTags.AgentId, message.CurrentAgentId); + // Convert arguments to dictionary format expected by mcpdotnet Dictionary argDict = JsonToDictionary(message.FunctionArgs); diff --git a/src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj b/src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj deleted file mode 100644 index 05ff0836c..000000000 --- a/src/Infrastructure/BotSharp.Langfuse/BotSharp.Langfuse.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - $(TargetFramework) - enable - $(LangVersion) - $(BotSharpVersion) - $(GeneratePackageOnBuild) - $(SolutionDir)packages - - - - - - - - - - - - diff --git a/src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs b/src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs deleted file mode 100644 index f621c6a46..000000000 --- a/src/Infrastructure/BotSharp.Langfuse/LangfusePlugin.cs +++ /dev/null @@ -1,17 +0,0 @@ -using BotSharp.Abstraction.Plugins; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using System; - -namespace BotSharp.Langfuse -{ - public class LangfusePlugin : IBotSharpPlugin - { - public string Id => throw new NotImplementedException(); - - public void RegisterDI(IServiceCollection services,IConfiguration config) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs b/src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs deleted file mode 100644 index 7911a31a5..000000000 --- a/src/Infrastructure/BotSharp.Langfuse/LangfuseSeriveExtention.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OpenTelemetry; -using OpenTelemetry.Exporter; -using OpenTelemetry.Logs; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using System; -using System.Threading.Tasks; - -namespace BotSharp.Langfuse; - -public static class LangfuseSeriveExtention -{ - public static async Task AddLangfuseOpenTelemetry(this IServiceCollection services) - { - - // 从配置文件中获取 Langfuse 设置 - var configuration = services.BuildServiceProvider().GetRequiredService(); - var langfuseSection = configuration.GetSection("Langfuse"); - var publicKey = langfuseSection.GetValue(nameof(LangfuseSettings.PublicKey)) ?? string.Empty; - var secretKey = langfuseSection.GetValue(nameof(LangfuseSettings.SecretKey)) ?? string.Empty; - var host = langfuseSection.GetValue(nameof(LangfuseSettings.Host)) ?? string.Empty; - var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"); - string base64EncodedAuth = Convert.ToBase64String(plainTextBytes); - - // Endpoint to the Aspire Dashboard / Grafana Tempo - var endpoint = host; - - var resourceBuilder = ResourceBuilder - .CreateDefault() - .AddService("TelemetryAspireDashboardQuickstart"); - - // Enable model diagnostics with sensitive data. - AppContext.SetSwitch("Microsoft.SemanticKernel.Experimental.GenAI.EnableOTelDiagnostics", true); - AppContext.SetSwitch("Microsoft.SemanticKernel.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true); - - var traceProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new AlwaysOnSampler()) - .SetResourceBuilder(resourceBuilder) - .AddSource("BotSharp*") - .AddConsoleExporter(options => { options.Targets = ConsoleExporterOutputTargets.Console; }) - .AddOtlpExporter(options => - { - options.Endpoint = new Uri(endpoint); - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.Headers = $"Authorization=Basic {base64EncodedAuth}"; - }) - .Build(); - - // 在应用程序退出前明确刷新遥测数据, - // 对于控制台应用程式非常重要是必須的。 - traceProvider.ForceFlush(); - await Task.Delay(3000); - - var meterProvider = Sdk.CreateMeterProviderBuilder() - .SetResourceBuilder(resourceBuilder) - .AddMeter("Microsoft.SemanticKernel*") - .AddOtlpExporter(options => options.Endpoint = new Uri(endpoint)) - .Build(); - - services.AddSingleton(traceProvider); - services.AddSingleton(meterProvider); - - services.AddLogging(loggingBuilder => - { - loggingBuilder.AddOpenTelemetry(options => - { - options.SetResourceBuilder(resourceBuilder); - options.AddConsoleExporter(); - options.AddOtlpExporter(options => - { - options.Endpoint = new Uri(endpoint); - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.Headers = $"Authorization=Basic {base64EncodedAuth}"; - }); - options.IncludeFormattedMessage = true; - options.IncludeScopes = true; - }); - loggingBuilder.SetMinimumLevel(LogLevel.Information); - }); - - return services; - } -} diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index e45a29dee..a225a17b6 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -8,6 +8,9 @@ using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Dtos; using BotSharp.Core.Infrastructures; +using BotSharp.Core.Users.Services; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.OpenAPI.Controllers; @@ -43,8 +46,12 @@ public async Task NewConversation([FromRoute] string agen }; conv = await service.NewConversation(conv); service.SetConversationId(conv.Id, config.States); - - return ConversationViewModel.FromSession(conv); + using (var trace = new ActivitySource("BotSharp").StartActivity("NewUserSession", ActivityKind.Internal)) + { + trace?.SetTag("user_id", _user.FullName); + trace?.SetTag("conversation_id", conv.Id); + return ConversationViewModel.FromSession(conv); + } } [HttpGet("/conversations")] @@ -364,25 +371,34 @@ public async Task SendMessage( conv.SetConversationId(conversationId, input.States); SetStates(conv, input); - var response = new ChatResponseModel(); - await conv.SendMessage(agentId, inputMsg, - replyMessage: input.Postback, - async msg => - { - response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; - response.Function = msg.FunctionName; - response.MessageLabel = msg.MessageLabel; - response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; - response.Instruction = msg.Instruction; - response.Data = msg.Data; - }); + using (var trace = new ActivitySource("BotSharp").StartActivity("UserSession", ActivityKind.Internal)) + { + trace?.SetTag("user.id", _user.FullName); + trace?.SetTag("session.id", conversationId); + trace?.SetTag("input", inputMsg.Content); + trace?.SetTag(ModelDiagnosticsTags.AgentId, agentId); + + var response = new ChatResponseModel(); + await conv.SendMessage(agentId, inputMsg, + replyMessage: input.Postback, + async msg => + { + response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.Function = msg.FunctionName; + response.MessageLabel = msg.MessageLabel; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; + response.Instruction = msg.Instruction; + response.Data = msg.Data; + }); - var state = _services.GetRequiredService(); - response.States = state.GetStates(); - response.MessageId = inputMsg.MessageId; - response.ConversationId = conversationId; + var state = _services.GetRequiredService(); + response.States = state.GetStates(); + response.MessageId = inputMsg.MessageId; + response.ConversationId = conversationId; - return response; + trace?.SetTag("output", response.Data); + return response; + } } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index 8aaf043a4..80d8709eb 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ #pragma warning disable OPENAI001 using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; @@ -7,6 +8,8 @@ using BotSharp.Core.MessageHub; using OpenAI.Chat; using System.ClientModel; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Plugin.AzureOpenAI.Providers.Chat; @@ -35,6 +38,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -49,91 +53,99 @@ public async Task GetChatCompletions(Agent agent, List? response = null; ChatCompletion value = default; RoleDialogModel responseMessage; - - try + using (var activity = ModelDiagnostics.StartCompletionActivity(null, _model, Provider, prompt, convService)) { - response = chatClient.CompleteChat(messages, options); - value = response.Value; + try + { + response = chatClient.CompleteChat(messages, options); + value = response.Value; - var reason = value.FinishReason; - var content = value.Content; - var text = content.FirstOrDefault()?.Text ?? string.Empty; + var reason = value.FinishReason; + var content = value.Content; + var text = content.FirstOrDefault()?.Text ?? string.Empty; - if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + activity?.SetTag(ModelDiagnosticsTags.FinishReason, reason); + 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(), + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + + // 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, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } + } + catch (ClientResultException ex) { - var toolCall = value.ToolCalls.FirstOrDefault(); - responseMessage = new RoleDialogModel(AgentRole.Function, text) + _logger.LogError(ex, ex.Message); + responseMessage = new RoleDialogModel(AgentRole.Assistant, "The response was filtered due to the prompt triggering our content management policy. Please modify your prompt and retry.") { CurrentAgentId = agent.Id, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - ToolCallId = toolCall?.Id, - FunctionName = toolCall?.FunctionName, - FunctionArgs = toolCall?.FunctionArguments?.ToString(), RenderedInstruction = string.Join("\r\n", renderedInstructions) }; - - // Somethings LLM will generate a function name with agent name. - if (!string.IsNullOrEmpty(responseMessage.FunctionName)) - { - responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); - } } - else + catch (Exception ex) { - responseMessage = new RoleDialogModel(AgentRole.Assistant, text) + _logger.LogError(ex, ex.Message); + responseMessage = new RoleDialogModel(AgentRole.Assistant, ex.Message) { CurrentAgentId = agent.Id, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions), - Annotations = value.Annotations?.Select(x => new ChatAnnotation - { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; } - } - catch (ClientResultException ex) - { - _logger.LogError(ex, ex.Message); - responseMessage = new RoleDialogModel(AgentRole.Assistant, "The response was filtered due to the prompt triggering our content management policy. Please modify your prompt and retry.") - { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - catch (Exception ex) - { - _logger.LogError(ex, ex.Message); - responseMessage = new RoleDialogModel(AgentRole.Assistant, ex.Message) - { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - var tokenUsage = response?.Value?.Usage; - var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); - } + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); - return responseMessage; + return responseMessage; + } } public async Task GetChatCompletionsAsync(Agent agent, @@ -167,7 +179,7 @@ public async Task GetChatCompletionsAsync(Agent agent, var tokenUsage = response?.Value?.Usage; var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - + // After chat completion hook foreach (var hook in hooks) { diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj new file mode 100644 index 000000000..e3a05dd8e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj @@ -0,0 +1,31 @@ + + + $(TargetFramework) + enable + enable + $(LangVersion) + true + $(Ai4cVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + true + $(SolutionDir)packages + + + + + false + runtime + + + + + + PreserveNewest + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs new file mode 100644 index 000000000..ef9686482 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs @@ -0,0 +1,19 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Plugin.GiteeAI.Providers.Chat; +using BotSharp.Plugin.GiteeAI.Providers.Embedding; + +namespace BotSharp.Plugin.GiteeAI; + +public class GiteeAiPlugin : IBotSharpPlugin +{ + public string Id => "59ad4c3c-0b88-3344-ba99-5245ec015938"; + public string Name => "GiteeAI"; + public string Description => "Gitee AI"; + public string IconUrl => "https://ai-assets.gitee.com/_next/static/media/gitee-ai.622edfb0.ico"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs new file mode 100644 index 000000000..2b46e83fc --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs @@ -0,0 +1,496 @@ +using BotSharp.Abstraction.Agents.Constants; +using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Files; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; + +namespace BotSharp.Plugin.GiteeAI.Providers.Chat; + +/// +/// 模力方舟的文本对话 +/// +public class ChatCompletionProvider( + ILogger logger, + IServiceProvider services) : IChatCompletion +{ + protected string _model = string.Empty; + + public virtual string Provider => "gitee-ai"; + + public string Model => _model; + + public async Task GetChatCompletions(Agent agent, List conversations) + { + var contentHooks = services.GetServices().ToList(); + var convService = services.GetService(); + + // 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); + + using (var activity = ModelDiagnostics.StartCompletionActivity(null, _model, Provider, prompt, convService)) + { + 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; + + activity?.SetTag(ModelDiagnosticsTags.FinishReason, reason); + + 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, + }; + } + + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = response.Value?.Usage?.InputTokenCount ?? 0, + TextOutputTokens = response.Value?.Usage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); + return responseMessage; + } + } + + public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onStreamResponseReceived) + { + var contentHooks = services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + + 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) + { + TrackStreamingToolingUpdate(choice.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + if (!choice.ContentUpdate.IsNullOrEmpty() && choice.ContentUpdate[0] != null) + { + foreach (var contentPart in choice.ContentUpdate) + { + if (contentPart.Kind == ChatMessageContentPartKind.Text) + { + (contentBuilder ??= new()).Append(contentPart.Text); + } + } + + logger.LogInformation(choice.ContentUpdate[0]?.Text); + + if (!string.IsNullOrEmpty(choice.ContentUpdate[0]?.Text)) + { + var msg = new RoleDialogModel(choice.Role?.ToString() ?? ChatMessageRole.Assistant.ToString(), choice.ContentUpdate[0]?.Text ?? string.Empty); + + await onStreamResponseReceived(msg); + } + } + } + + // Get any response content that was streamed. + string content = contentBuilder?.ToString() ?? string.Empty; + + RoleDialogModel responseMessage = new(ChatMessageRole.Assistant.ToString(), content); + + var tools = ConvertToolCallUpdatesToFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + foreach (var tool in tools) + { + tool.CurrentAgentId = agent.Id; + tool.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; + await onStreamResponseReceived(tool); + } + + if (tools.Length > 0) + { + responseMessage = tools[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, + TextInputTokens = response.Value?.Usage?.InputTokenCount ?? 0, + TextOutputTokens = 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(); + float? temperature = float.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; + + + state.SetState("temperature", temperature.ToString()); + state.SetState("max_tokens", maxTokens.ToString()); + + 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.RenderInstruction(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.FunctionName, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + })); + + messages.Add(new ToolChatMessage(message.FunctionName, message.Content)); + } + else if (message.Role == AgentRole.User) + { + var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content; + messages.Add(new UserChatMessage(text)); + } + 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; + } + + private static void TrackStreamingToolingUpdate( + IReadOnlyList? updates, + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + if (updates is null) + { + // Nothing to track. + return; + } + + foreach (var update in updates) + { + // If we have an ID, ensure the index is being tracked. Even if it's not a function update, + // we want to keep track of it so we can send back an error. + if (!string.IsNullOrWhiteSpace(update.ToolCallId)) + { + (toolCallIdsByIndex ??= [])[update.Index] = update.ToolCallId; + } + + // Ensure we're tracking the function's name. + if (!string.IsNullOrWhiteSpace(update.FunctionName)) + { + (functionNamesByIndex ??= [])[update.Index] = update.FunctionName; + } + + // Ensure we're tracking the function's arguments. + if (update.FunctionArgumentsUpdate is not null && !update.FunctionArgumentsUpdate.ToMemory().IsEmpty) + { + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.Index, out StringBuilder? arguments)) + { + functionArgumentBuildersByIndex[update.Index] = arguments = new(); + } + + arguments.Append(update.FunctionArgumentsUpdate.ToString()); + } + } + } + + private static RoleDialogModel[] ConvertToolCallUpdatesToFunctionToolCalls( + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + RoleDialogModel[] toolCalls = []; + if (toolCallIdsByIndex is { Count: > 0 }) + { + toolCalls = new RoleDialogModel[toolCallIdsByIndex.Count]; + + int i = 0; + foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) + { + string? functionName = null; + StringBuilder? functionArguments = null; + + functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); + functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); + + toolCalls[i] = new RoleDialogModel(AgentRole.Function, string.Empty) + { + FunctionName = functionName ?? string.Empty, + FunctionArgs = functionArguments?.ToString() ?? string.Empty, + }; + i++; + } + + Debug.Assert(i == toolCalls.Length); + } + + return toolCalls; + } + +} diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs new file mode 100644 index 000000000..80a8dbd71 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Logging; +using OpenAI.Embeddings; + +namespace BotSharp.Plugin.GiteeAI.Providers.Embedding; + +public class TextEmbeddingProvider( + ILogger logger, + IServiceProvider services) : ITextEmbedding +{ + protected readonly IServiceProvider _services = services; + protected readonly ILogger _logger = logger; + + private const int DEFAULT_DIMENSION = 1024; + protected string _model = "bge-m3"; + + public virtual string Provider => "gitee-ai"; + + public string Model => _model; + + protected int _dimension; + + public async Task GetVectorAsync(string text) + { + var client = ProviderHelper.GetClient(Provider, _model, _services); + var embeddingClient = client.GetEmbeddingClient(_model); + var options = PrepareOptions(); + var response = await embeddingClient.GenerateEmbeddingAsync(text, options); + var value = response.Value; + return value.ToFloats().ToArray(); + } + + public async Task> GetVectorsAsync(List texts) + { + var client = ProviderHelper.GetClient(Provider, _model, _services); + var embeddingClient = client.GetEmbeddingClient(_model); + var options = PrepareOptions(); + var response = await embeddingClient.GenerateEmbeddingsAsync(texts, options); + var value = response.Value; + return value.Select(x => x.ToFloats().ToArray()).ToList(); + } + + public void SetModelName(string model) + { + _model = model; + } + + private EmbeddingGenerationOptions PrepareOptions() + { + return new EmbeddingGenerationOptions + { + Dimensions = GetDimension() + }; + } + + public int GetDimension() + { + var state = _services.GetRequiredService(); + var stateDimension = state.GetState("embedding_dimension"); + var defaultDimension = _dimension > 0 ? _dimension : DEFAULT_DIMENSION; + + if (int.TryParse(stateDimension, out var dimension)) + { + return dimension > 0 ? dimension : defaultDimension; + } + return defaultDimension; + } + + public void SetDimension(int dimension) + { + _dimension = dimension > 0 ? dimension : DEFAULT_DIMENSION; + } + +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs new file mode 100644 index 000000000..b532e834c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs @@ -0,0 +1,16 @@ +using OpenAI; +using System.ClientModel; + +namespace BotSharp.Plugin.GiteeAI.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.GiteeAI/README.md b/src/Plugins/BotSharp.Plugin.GiteeAI/README.md new file mode 100644 index 000000000..5b4d00ff4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/README.md @@ -0,0 +1,8 @@ +Model Ark (Gitee AI) , hereinafter referred to as Gitee AI, aggregates the latest and most popular AI models, providing a one-stop service for model experience, inference, fine-tuning, and application deployment . We offer a diverse range of computing power options, aiming to help enterprises and developers build AI applications more easily . +ChatCompletions Interface: + +- https://ai.gitee.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90/post/chat/completions + +Signature Authentication Method: + +- https://ai.gitee.com/docs/organization/access-token \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs new file mode 100644 index 000000000..aa44ad1e2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs @@ -0,0 +1,15 @@ +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Loggers; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Utilities; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text; +global using System.Threading.Tasks; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index a36e32b66..1db3ac96d 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,9 +1,12 @@ #pragma warning disable OPENAI001 +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; using BotSharp.Core.MessageHub; +using Microsoft.AspNetCore.Cors.Infrastructure; using OpenAI.Chat; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Plugin.OpenAI.Providers.Chat; @@ -32,6 +35,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -42,68 +46,77 @@ public async Task GetChatCompletions(Agent agent, List new ChatAnnotation + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() - }; - } + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } - var tokenUsage = response.Value?.Usage; - var inputTokenDetails = response.Value?.Usage?.InputTokenDetails; + var tokenUsage = response.Value?.Usage; + var inputTokenDetails = response.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); - } + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); - return responseMessage; + return responseMessage; + } } public async Task GetChatCompletionsAsync(Agent agent, diff --git a/src/WebStarter/Program.cs b/src/WebStarter/Program.cs index 2c9c073c2..09a7344c5 100644 --- a/src/WebStarter/Program.cs +++ b/src/WebStarter/Program.cs @@ -1,11 +1,10 @@ +using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Core; using BotSharp.Core.MCP; -using BotSharp.OpenAPI; using BotSharp.Logger; +using BotSharp.OpenAPI; using BotSharp.Plugin.ChatHub; using Serilog; -using BotSharp.Abstraction.Messaging.JsonConverters; -using StackExchange.Redis; var builder = WebApplication.CreateBuilder(args); diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 6e0176157..e0317d960 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -37,6 +37,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 59d7d3f55..57426fe28 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -6,6 +6,9 @@ } }, "AllowedHosts": "*", + //"OTEL_EXPORTER_OTLP_ENDPOINT": "https://us.cloud.langfuse.com", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_SERVICE_NAME": "apiservice", "AllowedOrigins": [ "http://localhost:5015", "http://0.0.0.0:5015", @@ -46,26 +49,26 @@ "Provider": "azure-openai", "Models": [ { - "Id": "gpt-3.5-turbo", - "Name": "gpt-35-turbo", - "Version": "1106", - "ApiKey": "", - "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/" + "Id": "gpt-4.1", + "Name": "gpt-4.1", + "ApiKey": "7i8UdCUrqvUuAwvC5ECktLLTmT34cVPHI5WOY3iX9CXSjn0j8p49JQQJ99BBACHYHv6XJ3w3AAAAACOGIfSa", + "Endpoint": "https://ai-east2ai4c450341534958.cognitiveservices.azure.com/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 }, { - "Name": "gpt-35-turbo-instruct", - "Version": "0914", - "ApiKey": "", - "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/", - "Type": "text", - "Cost": { - "TextInputCost": 0.0015, - "CachedTextInputCost": 0, - "AudioInputCost": 0, - "CachedAudioInputCost": 0, - "TextOutputCost": 0.002, - "AudioOutputCost": 0 - } + "Id": "gpt-4.1-mini", + "Name": "gpt-4.1-mini", + "ApiKey": "7i8UdCUrqvUuAwvC5ECktLLTmT34cVPHI5WOY3iX9CXSjn0j8p49JQQJ99BBACHYHv6XJ3w3AAAAACOGIfSa", + "Endpoint": "https://ai-east2ai4c450341534958.cognitiveservices.azure.com/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 } ] }, @@ -240,6 +243,43 @@ } } ] + }, + { + "Provider": "gitee-ai", + "Models": [ + { + "Name": "DeepSeek-V3_1", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": 1024, + "Temperature": 0.6 + }, + { + "Name": "GLM-4_5", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": 1024, + "Temperature": 0.6 + }, + { + "Id": "bge-m3", + "Name": "bge-m3", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/embeddings/", + "Type": "embedding", + "Dimension": 1024, + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 + } + ] } ], @@ -256,8 +296,8 @@ "HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c", "EnableTranslator": false, "LlmConfig": { - "Provider": "openai", - "Model": "gpt-4.1-nano" + "Provider": "azure-openai", + "Model": "gpt-4.1" } }, @@ -583,44 +623,49 @@ "Language": "en" } }, - - "PluginLoader": { - "Assemblies": [ - "BotSharp.Core", - "BotSharp.Core.SideCar", - "BotSharp.Core.Crontab", - "BotSharp.Core.Realtime", - "BotSharp.Logger", - "BotSharp.Plugin.MongoStorage", - "BotSharp.Plugin.Dashboard", - "BotSharp.Plugin.OpenAI", - "BotSharp.Plugin.AzureOpenAI", - "BotSharp.Plugin.AnthropicAI", - "BotSharp.Plugin.GoogleAI", - "BotSharp.Plugin.MetaAI", - "BotSharp.Plugin.DeepSeekAI", - "BotSharp.Plugin.MetaMessenger", - "BotSharp.Plugin.HuggingFace", - "BotSharp.Plugin.KnowledgeBase", - "BotSharp.Plugin.Planner", - "BotSharp.Plugin.Graph", - "BotSharp.Plugin.Qdrant", - "BotSharp.Plugin.ChatHub", - "BotSharp.Plugin.WeChat", - "BotSharp.Plugin.PizzaBot", - "BotSharp.Plugin.WebDriver", - "BotSharp.Plugin.LLamaSharp", - "BotSharp.Plugin.SparkDesk", - "BotSharp.Plugin.MetaGLM", - "BotSharp.Plugin.HttpHandler", - "BotSharp.Plugin.FileHandler", - "BotSharp.Plugin.EmailHandler", - "BotSharp.Plugin.AudioHandler", - "BotSharp.Plugin.ChartHandler", - "BotSharp.Plugin.AudioHandler", - "BotSharp.Plugin.ExcelHandler", - "BotSharp.Plugin.SqlDriver", - "BotSharp.Plugin.TencentCos" - ] + "Langfuse": { + "SecretKey": "sk-lf- ", + "PublicKey": "pk-lf-", + "Host": "https://us.cloud.langfuse.com/api/public/otel/v1/traces" + }, + "PluginLoader": { + "Assemblies": [ + "BotSharp.Core", + "BotSharp.Core.SideCar", + "BotSharp.Core.Crontab", + "BotSharp.Core.Realtime", + "BotSharp.Logger", + "BotSharp.Plugin.MongoStorage", + "BotSharp.Plugin.Dashboard", + "BotSharp.Plugin.OpenAI", + "BotSharp.Plugin.AzureOpenAI", + "BotSharp.Plugin.AnthropicAI", + "BotSharp.Plugin.GoogleAI", + "BotSharp.Plugin.MetaAI", + "BotSharp.Plugin.DeepSeekAI", + "BotSharp.Plugin.GiteeAI", + "BotSharp.Plugin.MetaMessenger", + "BotSharp.Plugin.HuggingFace", + "BotSharp.Plugin.KnowledgeBase", + "BotSharp.Plugin.Planner", + "BotSharp.Plugin.Graph", + "BotSharp.Plugin.Qdrant", + "BotSharp.Plugin.ChatHub", + "BotSharp.Plugin.WeChat", + "BotSharp.Plugin.PizzaBot", + "BotSharp.Plugin.WebDriver", + "BotSharp.Plugin.LLamaSharp", + "BotSharp.Plugin.SparkDesk", + "BotSharp.Plugin.MetaGLM", + "BotSharp.Plugin.HttpHandler", + "BotSharp.Plugin.FileHandler", + "BotSharp.Plugin.EmailHandler", + "BotSharp.Plugin.AudioHandler", + "BotSharp.Plugin.ChartHandler", + "BotSharp.Plugin.AudioHandler", + "BotSharp.Plugin.ExcelHandler", + "BotSharp.Plugin.SqlDriver", + "BotSharp.Plugin.TencentCos" + ] + } } -} From f3faa0b215a472733e8c04c17b12e357e106813b Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 16 Oct 2025 18:12:25 +0800 Subject: [PATCH 03/18] Update Azure OpenAI model configurations Replaces previous GPT-4.1 model entries with updated GPT-3.5-turbo and gpt-35-turbo-instruct configurations, including new endpoints, versioning, and cost structure. Sensitive API keys have been removed from the configuration. --- src/WebStarter/appsettings.json | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 57426fe28..29b89e330 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -49,26 +49,26 @@ "Provider": "azure-openai", "Models": [ { - "Id": "gpt-4.1", - "Name": "gpt-4.1", - "ApiKey": "7i8UdCUrqvUuAwvC5ECktLLTmT34cVPHI5WOY3iX9CXSjn0j8p49JQQJ99BBACHYHv6XJ3w3AAAAACOGIfSa", - "Endpoint": "https://ai-east2ai4c450341534958.cognitiveservices.azure.com/", - "Type": "chat", - "PromptCost": 0.0015, - "CompletionCost": 0.002, - "MaxTokens": null, - "Temperature": 1.0 + "Id": "gpt-3.5-turbo", + "Name": "gpt-35-turbo", + "Version": "1106", + "ApiKey": "", + "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/" }, { - "Id": "gpt-4.1-mini", - "Name": "gpt-4.1-mini", - "ApiKey": "7i8UdCUrqvUuAwvC5ECktLLTmT34cVPHI5WOY3iX9CXSjn0j8p49JQQJ99BBACHYHv6XJ3w3AAAAACOGIfSa", - "Endpoint": "https://ai-east2ai4c450341534958.cognitiveservices.azure.com/", - "Type": "chat", - "PromptCost": 0.0015, - "CompletionCost": 0.002, - "MaxTokens": null, - "Temperature": 1.0 + "Name": "gpt-35-turbo-instruct", + "Version": "0914", + "ApiKey": "", + "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/", + "Type": "text", + "Cost": { + "TextInputCost": 0.0015, + "CachedTextInputCost": 0, + "AudioInputCost": 0, + "CachedAudioInputCost": 0, + "TextOutputCost": 0.002, + "AudioOutputCost": 0 + } } ] }, From 044cc2a9c2fe58419396fecf1604f7bacd9f82a3 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Fri, 17 Oct 2025 08:37:23 +0800 Subject: [PATCH 04/18] Add Langfuse diagnostics and GiteeAI plugin Introduced OpenTelemetry-based model diagnostics with Langfuse integration, including new helper classes and activity tracing for agent and function execution. Added BotSharp.Plugin.GiteeAI with chat and embedding providers, and updated solution/project files to register the new plugin. Enhanced tracing in routing, executor, and controller logic for improved observability. --- BotSharp.sln | 11 + src/BotSharp.AppHost/Program.cs | 4 +- src/BotSharp.ServiceDefaults/Extensions.cs | 52 +- .../LangfuseSettings.cs | 19 + .../Diagnostics/ActivityExtensions.cs | 119 +++++ .../Diagnostics/AppContextSwitchHelper.cs | 35 ++ .../Diagnostics/ModelDiagnostics.cs | 394 ++++++++++++++ .../Executor/FunctionCallbackExecutor.cs | 18 +- .../Routing/Executor/MCPToolExecutor.cs | 51 +- .../Routing/RoutingService.InvokeAgent.cs | 5 +- .../Routing/RoutingService.InvokeFunction.cs | 1 + .../BotSharp.Core/Routing/RoutingService.cs | 1 + .../Controllers/ConversationController.cs | 54 +- .../Providers/Chat/ChatCompletionProvider.cs | 144 ++--- .../BotSharp.Plugin.GiteeAI.csproj | 31 ++ .../BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs | 19 + .../Providers/Chat/ChatCompletionProvider.cs | 496 ++++++++++++++++++ .../Embedding/TextEmbeddingProvider.cs | 73 +++ .../Providers/ProviderHelper.cs | 16 + src/Plugins/BotSharp.Plugin.GiteeAI/README.md | 8 + src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs | 15 + .../Providers/Chat/ChatCompletionProvider.cs | 117 +++-- src/WebStarter/Program.cs | 5 +- src/WebStarter/WebStarter.csproj | 1 + src/WebStarter/appsettings.json | 125 +++-- 25 files changed, 1609 insertions(+), 205 deletions(-) create mode 100644 src/BotSharp.ServiceDefaults/LangfuseSettings.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/README.md create mode 100644 src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs diff --git a/BotSharp.sln b/BotSharp.sln index f68ce1c60..ccf9b2654 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -147,6 +147,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandle EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandler", "src\Plugins\BotSharp.Plugin.ExcelHandler\BotSharp.Plugin.ExcelHandler.csproj", "{FC63C875-E880-D8BB-B8B5-978AB7B62983}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.GiteeAI", "src\Plugins\BotSharp.Plugin.GiteeAI\BotSharp.Plugin.GiteeAI.csproj", "{50B57066-3267-1D10-0F72-D2F5CC494F2C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -619,6 +621,14 @@ Global {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.Build.0 = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.ActiveCfg = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -690,6 +700,7 @@ Global {B067B126-88CD-4282-BEEF-7369B64423EF} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {FC63C875-E880-D8BB-B8B5-978AB7B62983} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/src/BotSharp.AppHost/Program.cs b/src/BotSharp.AppHost/Program.cs index 4c54ed11b..444e2ecf3 100644 --- a/src/BotSharp.AppHost/Program.cs +++ b/src/BotSharp.AppHost/Program.cs @@ -2,8 +2,8 @@ var apiService = builder.AddProject("apiservice") .WithExternalHttpEndpoints(); -var mcpService = builder.AddProject("mcpservice") - .WithExternalHttpEndpoints(); +//var mcpService = builder.AddProject("mcpservice") +// .WithExternalHttpEndpoints(); builder.AddNpmApp("BotSharpUI", "../../../BotSharp-UI") .WithReference(apiService) diff --git a/src/BotSharp.ServiceDefaults/Extensions.cs b/src/BotSharp.ServiceDefaults/Extensions.cs index bfc0bb687..caf52b243 100644 --- a/src/BotSharp.ServiceDefaults/Extensions.cs +++ b/src/BotSharp.ServiceDefaults/Extensions.cs @@ -1,12 +1,16 @@ +using BotSharp.Langfuse; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; @@ -45,6 +49,10 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) { + // Enable model diagnostics with sensitive data. + AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnostics", true); + AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true); + builder.Logging.AddOpenTelemetry(logging => { // Use Serilog Log.Logger = new LoggerConfiguration() @@ -87,10 +95,28 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati }) .WithTracing(tracing => { + tracing.SetResourceBuilder( + ResourceBuilder.CreateDefault() + .AddService("apiservice", serviceVersion: "1.0.0") + ) + .AddSource("BotSharp") + .AddSource("BotSharp.Abstraction.Diagnostics") + .AddSource("BotSharp.Core.Routing.Executor"); + tracing.AddAspNetCoreInstrumentation() // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(); + .AddHttpClientInstrumentation() + //.AddOtlpExporter(options => + //{ + // //options.Endpoint = new Uri(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? "http://localhost:4317"); + // options.Endpoint = new Uri(host); + // options.Protocol = OtlpExportProtocol.HttpProtobuf; + // options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + //}) + ; + + }); builder.AddOpenTelemetryExporters(); @@ -100,14 +126,34 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) { + var langfuseSection = builder.Configuration.GetSection("Langfuse"); + var useLangfuse = langfuseSection != null; var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); if (useOtlpExporter) { builder.Services.Configure(logging => logging.AddOtlpExporter()); builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); - + if (useLangfuse) + { + var publicKey = langfuseSection.GetValue(nameof(LangfuseSettings.PublicKey)) ?? string.Empty; + var secretKey = langfuseSection.GetValue(nameof(LangfuseSettings.SecretKey)) ?? string.Empty; + var host = langfuseSection.GetValue(nameof(LangfuseSettings.Host)) ?? string.Empty; + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"); + string base64EncodedAuth = Convert.ToBase64String(plainTextBytes); + + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter(options => + { + options.Endpoint = new Uri(host); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + }) + ); + } + else + { + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + } } // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) diff --git a/src/BotSharp.ServiceDefaults/LangfuseSettings.cs b/src/BotSharp.ServiceDefaults/LangfuseSettings.cs new file mode 100644 index 000000000..4c79832c6 --- /dev/null +++ b/src/BotSharp.ServiceDefaults/LangfuseSettings.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.Langfuse; + +/// +/// Langfuse Settings +/// +public class LangfuseSettings +{ + public string SecretKey { get; set; } + + public string PublicKey { get; set; } + + public string Host { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs new file mode 100644 index 000000000..105d5aae5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace BotSharp.Abstraction.Diagnostics; + +[ExcludeFromCodeCoverage] +public static class ActivityExtensions +{ + /// + /// Starts an activity with the appropriate tags for a kernel function execution. + /// + public static Activity? StartFunctionActivity(this ActivitySource source, string functionName, string functionDescription) + { + const string OperationName = "execute_tool"; + + return source.StartActivityWithTags($"{OperationName} {functionName}", [ + new KeyValuePair("gen_ai.operation.name", OperationName), + new KeyValuePair("gen_ai.tool.name", functionName), + new KeyValuePair("gen_ai.tool.description", functionDescription) + ], ActivityKind.Internal); + } + + /// + /// Starts an activity with the specified name and tags. + /// + public static Activity? StartActivityWithTags(this ActivitySource source, string name, IEnumerable> tags, ActivityKind kind = ActivityKind.Internal) + => source.StartActivity(name, kind, default(ActivityContext), tags); + + /// + /// Adds tags to the activity. + /// + public static Activity SetTags(this Activity activity, ReadOnlySpan> tags) + { + foreach (var tag in tags) + { + activity.SetTag(tag.Key, tag.Value); + } + ; + + return activity; + } + + /// + /// Adds an event to the activity. Should only be used for events that contain sensitive data. + /// + public static Activity AttachSensitiveDataAsEvent(this Activity activity, string name, IEnumerable> tags) + { + activity.AddEvent(new ActivityEvent( + name, + tags: [.. tags] + )); + + return activity; + } + + /// + /// Sets the error status and type on the activity. + /// + public static Activity SetError(this Activity activity, Exception exception) + { + activity.SetTag("error.type", exception.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error, exception.Message); + return activity; + } + + public static async IAsyncEnumerable RunWithActivityAsync( + Func getActivity, + Func> operation, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + using var activity = getActivity(); + + ConfiguredCancelableAsyncEnumerable result; + + try + { + result = operation().WithCancellation(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + var resultEnumerator = result.ConfigureAwait(false).GetAsyncEnumerator(); + + try + { + while (true) + { + try + { + if (!await resultEnumerator.MoveNextAsync()) + { + break; + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + yield return resultEnumerator.Current; + } + } + finally + { + await resultEnumerator.DisposeAsync(); + } + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs new file mode 100644 index 000000000..64e5806be --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace BotSharp.Abstraction.Diagnostics; + +/// +/// Helper class to get app context switch value +/// +[ExcludeFromCodeCoverage] +internal static class AppContextSwitchHelper +{ + /// + /// Returns the value of the specified app switch or environment variable if it is set. + /// If the switch or environment variable is not set, return false. + /// The app switch value takes precedence over the environment variable. + /// + /// The name of the app switch. + /// The name of the environment variable. + /// The value of the app switch or environment variable if it is set; otherwise, false. + public static bool GetConfigValue(string appContextSwitchName, string envVarName) + { + if (AppContext.TryGetSwitch(appContextSwitchName, out bool value)) + { + return value; + } + + string? envVarValue = Environment.GetEnvironmentVariable(envVarName); + if (envVarValue != null && bool.TryParse(envVarValue, out value)) + { + return value; + } + + return false; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs new file mode 100644 index 000000000..83f6532cb --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs @@ -0,0 +1,394 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Functions.Models; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; +using System.Text.Json; + +namespace BotSharp.Abstraction.Diagnostics; + +/// +/// Model diagnostics helper class that provides a set of methods to trace model activities with the OTel semantic conventions. +/// This class contains experimental features and may change in the future. +/// To enable these features, set one of the following switches to true: +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnostics` +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive` +/// Or set the following environment variables to true: +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS` +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE` +/// +//[System.Diagnostics.CodeAnalysis.Experimental("SKEXP0001")] +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static class ModelDiagnostics +{ + private static readonly string s_namespace = typeof(ModelDiagnostics).Namespace!; + private static readonly ActivitySource s_activitySource = new(s_namespace); + + private const string EnableDiagnosticsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnostics"; + private const string EnableSensitiveEventsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive"; + private const string EnableDiagnosticsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS"; + private const string EnableSensitiveEventsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE"; + + private static readonly bool s_enableDiagnostics = AppContextSwitchHelper.GetConfigValue(EnableDiagnosticsSwitch, EnableDiagnosticsEnvVar); + private static readonly bool s_enableSensitiveEvents = AppContextSwitchHelper.GetConfigValue(EnableSensitiveEventsSwitch, EnableSensitiveEventsEnvVar); + + /// + /// Start a text completion activity for a given model. + /// The activity will be tagged with the a set of attributes specified by the semantic conventions. + /// + public static Activity? StartCompletionActivity( + Uri? endpoint, + string modelName, + string modelProvider, + string prompt, + IConversationStateService services + ) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "text.completions"; + var activity = s_activitySource.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(ModelDiagnosticsTags.Operation, OperationName), + new(ModelDiagnosticsTags.System, modelProvider), + new(ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, services); + + if (s_enableSensitiveEvents) + { + activity?.AttachSensitiveDataAsEvent( + ModelDiagnosticsTags.UserMessage, + [ + new(ModelDiagnosticsTags.EventName, prompt), + new(ModelDiagnosticsTags.System, modelProvider), + ]); + } + + return activity; + } + + /// + /// Start a chat completion activity for a given model. + /// The activity will be tagged with the a set of attributes specified by the semantic conventions. + /// + public static Activity? StartCompletionActivity( + Uri? endpoint, + string modelName, + string modelProvider, + List chatHistory, + IConversationStateService conversationStateService + ) + + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "chat.completions"; + var activity = s_activitySource.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(ModelDiagnosticsTags.Operation, OperationName), + new(ModelDiagnosticsTags.System, modelProvider), + new(ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, conversationStateService); + + if (s_enableSensitiveEvents) + { + foreach (var message in chatHistory) + { + var formattedContent = JsonSerializer.Serialize(ToGenAIConventionsFormat(message)); + activity?.AttachSensitiveDataAsEvent( + ModelDiagnosticsTags.RoleToEventMap[message.Role], + [ + new(ModelDiagnosticsTags.EventName, formattedContent), + new(ModelDiagnosticsTags.System, modelProvider), + ]); + } + } + + return activity; + } + + /// + /// Start an agent invocation activity and return the activity. + /// + public static Activity? StartAgentInvocationActivity( + string agentId, + string agentName, + string? agentDescription, + Agent? agents, + List messages + ) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "invoke_agent"; + + var activity = s_activitySource.StartActivityWithTags( + $"{OperationName} {agentName}", + [ + new(ModelDiagnosticsTags.Operation, OperationName), + new(ModelDiagnosticsTags.AgentId, agentId), + new(ModelDiagnosticsTags.AgentName, agentName) + ], + ActivityKind.Internal); + + if (!string.IsNullOrWhiteSpace(agentDescription)) + { + activity?.SetTag(ModelDiagnosticsTags.AgentDescription, agentDescription); + } + + if (agents is not null && (agents.Functions.Count > 0 || agents.SecondaryFunctions.Count >0)) + { + List allFunctions = []; + allFunctions.AddRange(agents.Functions); + allFunctions.AddRange(agents.SecondaryFunctions); + + activity?.SetTag( + ModelDiagnosticsTags.AgentToolDefinitions, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + if (IsSensitiveEventsEnabled()) + { + activity?.SetTag( + ModelDiagnosticsTags.AgentInvocationInput, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + return activity; + } + + /// + /// Set the agent response for a given activity. + /// + public static void SetAgentResponse(this Activity activity, IEnumerable? responses) + { + if (!IsModelDiagnosticsEnabled() || responses is null) + { + return; + } + + if (s_enableSensitiveEvents) + { + activity?.SetTag( + ModelDiagnosticsTags.AgentInvocationOutput, + JsonSerializer.Serialize(responses.Select(r => ToGenAIConventionsFormat(r)))); + } + } + + + + /// + /// Set the response id for a given activity. + /// + /// The activity to set the response id + /// The response id + /// The activity with the response id set for chaining + internal static Activity SetResponseId(this Activity activity, string responseId) => activity.SetTag(ModelDiagnosticsTags.ResponseId, responseId); + + /// + /// Set the input tokens usage for a given activity. + /// + /// The activity to set the input tokens usage + /// The number of input tokens used + /// The activity with the input tokens usage set for chaining + internal static Activity SetInputTokensUsage(this Activity activity, int inputTokens) => activity.SetTag(ModelDiagnosticsTags.InputTokens, inputTokens); + + /// + /// Set the output tokens usage for a given activity. + /// + /// The activity to set the output tokens usage + /// The number of output tokens used + /// The activity with the output tokens usage set for chaining + internal static Activity SetOutputTokensUsage(this Activity activity, int outputTokens) => activity.SetTag(ModelDiagnosticsTags.OutputTokens, outputTokens); + + /// + /// Check if model diagnostics is enabled + /// Model diagnostics is enabled if either EnableModelDiagnostics or EnableSensitiveEvents is set to true and there are listeners. + /// + internal static bool IsModelDiagnosticsEnabled() + { + return (s_enableDiagnostics || s_enableSensitiveEvents) && s_activitySource.HasListeners(); + } + + /// + /// Check if sensitive events are enabled. + /// Sensitive events are enabled if EnableSensitiveEvents is set to true and there are listeners. + /// + internal static bool IsSensitiveEventsEnabled() => s_enableSensitiveEvents && s_activitySource.HasListeners(); + + internal static bool HasListeners() => s_activitySource.HasListeners(); + + #region Private + private static void AddOptionalTags(Activity? activity, IConversationStateService conversationStateService) + { + if (activity is null) + { + return; + } + + void TryAddTag(string key, string tag) + { + var value = conversationStateService.GetState(key); + if (!string.IsNullOrEmpty(value)) + { + activity.SetTag(tag, value); + } + } + + TryAddTag("max_tokens", ModelDiagnosticsTags.MaxToken); + TryAddTag("temperature", ModelDiagnosticsTags.Temperature); + TryAddTag("top_p", ModelDiagnosticsTags.TopP); + } + + /// + /// Convert a chat message to a JSON object based on the OTel GenAI Semantic Conventions format + /// + private static object ToGenAIConventionsFormat(RoleDialogModel chatMessage) + { + return new + { + role = chatMessage.Role.ToString(), + name = chatMessage.MessageId, + content = chatMessage.Content, + tool_calls = ToGenAIConventionsToolCallFormat(chatMessage), + }; + } + + /// + /// Helper method to convert tool calls to a list of JSON object based on the OTel GenAI Semantic Conventions format + /// + private static List ToGenAIConventionsToolCallFormat(RoleDialogModel chatMessage) + { + List toolCalls = []; + if (chatMessage.Instruction is not null) + { + toolCalls.Add(new + { + id = chatMessage.ToolCallId, + function = new + { + name = chatMessage.Instruction.Function, + arguments = chatMessage.Instruction.Arguments + }, + type = "function" + }); + } + return toolCalls; + } + + /// + /// Convert a function metadata to a JSON object based on the OTel GenAI Semantic Conventions format + /// + private static object ToGenAIConventionsFormat(FunctionDef metadata) + { + var properties = metadata.Parameters?.Properties; + var required = metadata.Parameters?.Required; + + return new + { + type = "function", + name = metadata.Name, + description = metadata.Description, + parameters = new + { + type = "object", + properties, + required, + } + }; + } + + /// + /// Convert a chat model response to a JSON string based on the OTel GenAI Semantic Conventions format + /// + private static string ToGenAIConventionsChoiceFormat(RoleDialogModel chatMessage, int index) + { + var jsonObject = new + { + index, + message = ToGenAIConventionsFormat(chatMessage), + tool_calls = ToGenAIConventionsToolCallFormat(chatMessage) + }; + + return JsonSerializer.Serialize(jsonObject); + } + + + + /// + /// Tags used in model diagnostics + /// + public static class ModelDiagnosticsTags + { + // Activity tags + public const string System = "gen_ai.system"; + public const string Operation = "gen_ai.operation.name"; + public const string Model = "gen_ai.request.model"; + public const string MaxToken = "gen_ai.request.max_tokens"; + public const string Temperature = "gen_ai.request.temperature"; + public const string TopP = "gen_ai.request.top_p"; + public const string ResponseId = "gen_ai.response.id"; + public const string ResponseModel = "gen_ai.response.model"; + public const string FinishReason = "gen_ai.response.finish_reason"; + public const string InputTokens = "gen_ai.usage.input_tokens"; + public const string OutputTokens = "gen_ai.usage.output_tokens"; + public const string Address = "server.address"; + public const string Port = "server.port"; + public const string AgentId = "gen_ai.agent.id"; + public const string AgentName = "gen_ai.agent.name"; + public const string AgentDescription = "gen_ai.agent.description"; + public const string AgentInvocationInput = "gen_ai.input.messages"; + public const string AgentInvocationOutput = "gen_ai.output.messages"; + public const string AgentToolDefinitions = "gen_ai.tool.definitions"; + + // Activity events + public const string EventName = "gen_ai.event.content"; + public const string SystemMessage = "gen_ai.system.message"; + public const string UserMessage = "gen_ai.user.message"; + public const string AssistantMessage = "gen_ai.assistant.message"; + public const string ToolMessage = "gen_ai.tool.message"; + public const string Choice = "gen_ai.choice"; + public static readonly Dictionary RoleToEventMap = new() + { + { AgentRole.System, SystemMessage }, + { AgentRole.User, UserMessage }, + { AgentRole.Assistant, AssistantMessage }, + { AgentRole.Function, ToolMessage } + }; + } + # endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs index 4b208374f..e49ff3ba3 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs @@ -1,10 +1,19 @@ -using BotSharp.Abstraction.Routing.Executor; +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Routing.Executor; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Core.Routing.Executor; public class FunctionCallbackExecutor : IFunctionExecutor { + /// + /// + /// for function-related activities. + /// + private static readonly ActivitySource s_activitySource = new("BotSharp.Core.Routing.Executor"); + private readonly IFunctionCallback _functionCallback; public FunctionCallbackExecutor(IFunctionCallback functionCallback) @@ -14,7 +23,12 @@ public FunctionCallbackExecutor(IFunctionCallback functionCallback) public async Task ExecuteAsync(RoleDialogModel message) { - return await _functionCallback.Execute(message); + using var activity = s_activitySource.StartFunctionActivity(this._functionCallback.Name, this._functionCallback.Indication); + { + activity?.SetTag("input", message.FunctionArgs); + activity?.SetTag(ModelDiagnosticsTags.AgentId, message.CurrentAgentId); + return await _functionCallback.Execute(message); + } } public async Task GetIndicatorAsync(RoleDialogModel message) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs index c452e8066..e346ce549 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs @@ -1,6 +1,9 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Executor; using BotSharp.Core.MCP.Managers; using ModelContextProtocol.Client; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Core.Routing.Executor; @@ -10,6 +13,13 @@ public class McpToolExecutor: IFunctionExecutor private readonly string _mcpServerId; private readonly string _functionName; + /// + /// + /// for function-related activities. + /// + private static readonly ActivitySource s_activitySource = new("BotSharp.Core.Routing.Executor"); + + public McpToolExecutor(IServiceProvider services, string mcpServerId, string functionName) { _services = services; @@ -19,28 +29,35 @@ public McpToolExecutor(IServiceProvider services, string mcpServerId, string fun public async Task ExecuteAsync(RoleDialogModel message) { - try + using var activity = s_activitySource.StartFunctionActivity(this._functionName, $"calling tool {_functionName} of MCP server {_mcpServerId}"); { - // Convert arguments to dictionary format expected by mcpdotnet - Dictionary argDict = JsonToDictionary(message.FunctionArgs); + try + { + activity?.SetTag("input", message.FunctionArgs); + activity?.SetTag(ModelDiagnosticsTags.AgentId, message.CurrentAgentId); - var clientManager = _services.GetRequiredService(); - var client = await clientManager.GetMcpClientAsync(_mcpServerId); + // Convert arguments to dictionary format expected by mcpdotnet + Dictionary argDict = JsonToDictionary(message.FunctionArgs); - // Call the tool through mcpdotnet - var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); + var clientManager = _services.GetRequiredService(); + var client = await clientManager.GetMcpClientAsync(_mcpServerId); + + // Call the tool through mcpdotnet + var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); - // Extract the text content from the result - var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); + // Extract the text content from the result + var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); - message.Content = json; - message.Data = json.JsonContent(); - return true; - } - catch (Exception ex) - { - message.Content = $"Error when calling tool {_functionName} of MCP server {_mcpServerId}. {ex.Message}"; - return false; + message.Content = json; + message.Data = json.JsonContent(); + return true; + } + catch (Exception ex) + { + message.Content = $"Error when calling tool {_functionName} of MCP server {_mcpServerId}. {ex.Message}"; + activity?.SetError(ex); + return false; + } } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index e0175a70d..36a2dbd6a 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; @@ -14,6 +15,8 @@ public async Task InvokeAgent( var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); + using var activity = ModelDiagnostics.StartAgentInvocationActivity(agentId, agent.Name, agent.Description, agent, dialogs); + Context.IncreaseRecursiveCounter(); if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth) { @@ -79,7 +82,7 @@ public async Task InvokeAgent( dialogs.Add(message); Context.AddDialogs([message]); } - + activity?.SetAgentResponse(Context.GetDialogs()); return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 3850dcc13..17cd180a3 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Models; using BotSharp.Core.MessageHub; using BotSharp.Core.Routing.Executor; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 4e43cbd52..4dfc0fe93 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; +using System.Diagnostics; namespace BotSharp.Core.Routing; diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index e45a29dee..a225a17b6 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -8,6 +8,9 @@ using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Dtos; using BotSharp.Core.Infrastructures; +using BotSharp.Core.Users.Services; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.OpenAPI.Controllers; @@ -43,8 +46,12 @@ public async Task NewConversation([FromRoute] string agen }; conv = await service.NewConversation(conv); service.SetConversationId(conv.Id, config.States); - - return ConversationViewModel.FromSession(conv); + using (var trace = new ActivitySource("BotSharp").StartActivity("NewUserSession", ActivityKind.Internal)) + { + trace?.SetTag("user_id", _user.FullName); + trace?.SetTag("conversation_id", conv.Id); + return ConversationViewModel.FromSession(conv); + } } [HttpGet("/conversations")] @@ -364,25 +371,34 @@ public async Task SendMessage( conv.SetConversationId(conversationId, input.States); SetStates(conv, input); - var response = new ChatResponseModel(); - await conv.SendMessage(agentId, inputMsg, - replyMessage: input.Postback, - async msg => - { - response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; - response.Function = msg.FunctionName; - response.MessageLabel = msg.MessageLabel; - response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; - response.Instruction = msg.Instruction; - response.Data = msg.Data; - }); + using (var trace = new ActivitySource("BotSharp").StartActivity("UserSession", ActivityKind.Internal)) + { + trace?.SetTag("user.id", _user.FullName); + trace?.SetTag("session.id", conversationId); + trace?.SetTag("input", inputMsg.Content); + trace?.SetTag(ModelDiagnosticsTags.AgentId, agentId); + + var response = new ChatResponseModel(); + await conv.SendMessage(agentId, inputMsg, + replyMessage: input.Postback, + async msg => + { + response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.Function = msg.FunctionName; + response.MessageLabel = msg.MessageLabel; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; + response.Instruction = msg.Instruction; + response.Data = msg.Data; + }); - var state = _services.GetRequiredService(); - response.States = state.GetStates(); - response.MessageId = inputMsg.MessageId; - response.ConversationId = conversationId; + var state = _services.GetRequiredService(); + response.States = state.GetStates(); + response.MessageId = inputMsg.MessageId; + response.ConversationId = conversationId; - return response; + trace?.SetTag("output", response.Data); + return response; + } } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index 8aaf043a4..80d8709eb 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ #pragma warning disable OPENAI001 using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; @@ -7,6 +8,8 @@ using BotSharp.Core.MessageHub; using OpenAI.Chat; using System.ClientModel; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Plugin.AzureOpenAI.Providers.Chat; @@ -35,6 +38,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -49,91 +53,99 @@ public async Task GetChatCompletions(Agent agent, List? response = null; ChatCompletion value = default; RoleDialogModel responseMessage; - - try + using (var activity = ModelDiagnostics.StartCompletionActivity(null, _model, Provider, prompt, convService)) { - response = chatClient.CompleteChat(messages, options); - value = response.Value; + try + { + response = chatClient.CompleteChat(messages, options); + value = response.Value; - var reason = value.FinishReason; - var content = value.Content; - var text = content.FirstOrDefault()?.Text ?? string.Empty; + var reason = value.FinishReason; + var content = value.Content; + var text = content.FirstOrDefault()?.Text ?? string.Empty; - if (reason == ChatFinishReason.FunctionCall || reason == ChatFinishReason.ToolCalls) + activity?.SetTag(ModelDiagnosticsTags.FinishReason, reason); + 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(), + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + + // 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, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } + } + catch (ClientResultException ex) { - var toolCall = value.ToolCalls.FirstOrDefault(); - responseMessage = new RoleDialogModel(AgentRole.Function, text) + _logger.LogError(ex, ex.Message); + responseMessage = new RoleDialogModel(AgentRole.Assistant, "The response was filtered due to the prompt triggering our content management policy. Please modify your prompt and retry.") { CurrentAgentId = agent.Id, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - ToolCallId = toolCall?.Id, - FunctionName = toolCall?.FunctionName, - FunctionArgs = toolCall?.FunctionArguments?.ToString(), RenderedInstruction = string.Join("\r\n", renderedInstructions) }; - - // Somethings LLM will generate a function name with agent name. - if (!string.IsNullOrEmpty(responseMessage.FunctionName)) - { - responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); - } } - else + catch (Exception ex) { - responseMessage = new RoleDialogModel(AgentRole.Assistant, text) + _logger.LogError(ex, ex.Message); + responseMessage = new RoleDialogModel(AgentRole.Assistant, ex.Message) { CurrentAgentId = agent.Id, MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions), - Annotations = value.Annotations?.Select(x => new ChatAnnotation - { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() + RenderedInstruction = string.Join("\r\n", renderedInstructions) }; } - } - catch (ClientResultException ex) - { - _logger.LogError(ex, ex.Message); - responseMessage = new RoleDialogModel(AgentRole.Assistant, "The response was filtered due to the prompt triggering our content management policy. Please modify your prompt and retry.") - { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - catch (Exception ex) - { - _logger.LogError(ex, ex.Message); - responseMessage = new RoleDialogModel(AgentRole.Assistant, ex.Message) - { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - var tokenUsage = response?.Value?.Usage; - var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); - } + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); - return responseMessage; + return responseMessage; + } } public async Task GetChatCompletionsAsync(Agent agent, @@ -167,7 +179,7 @@ public async Task GetChatCompletionsAsync(Agent agent, var tokenUsage = response?.Value?.Usage; var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - + // After chat completion hook foreach (var hook in hooks) { diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj new file mode 100644 index 000000000..e3a05dd8e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj @@ -0,0 +1,31 @@ + + + $(TargetFramework) + enable + enable + $(LangVersion) + true + $(Ai4cVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + true + $(SolutionDir)packages + + + + + false + runtime + + + + + + PreserveNewest + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs new file mode 100644 index 000000000..ef9686482 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/GiteeAiPlugin.cs @@ -0,0 +1,19 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Plugin.GiteeAI.Providers.Chat; +using BotSharp.Plugin.GiteeAI.Providers.Embedding; + +namespace BotSharp.Plugin.GiteeAI; + +public class GiteeAiPlugin : IBotSharpPlugin +{ + public string Id => "59ad4c3c-0b88-3344-ba99-5245ec015938"; + public string Name => "GiteeAI"; + public string Description => "Gitee AI"; + public string IconUrl => "https://ai-assets.gitee.com/_next/static/media/gitee-ai.622edfb0.ico"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs new file mode 100644 index 000000000..2b46e83fc --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs @@ -0,0 +1,496 @@ +using BotSharp.Abstraction.Agents.Constants; +using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Files; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Logging; +using OpenAI.Chat; +using System.Diagnostics; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; + +namespace BotSharp.Plugin.GiteeAI.Providers.Chat; + +/// +/// 模力方舟的文本对话 +/// +public class ChatCompletionProvider( + ILogger logger, + IServiceProvider services) : IChatCompletion +{ + protected string _model = string.Empty; + + public virtual string Provider => "gitee-ai"; + + public string Model => _model; + + public async Task GetChatCompletions(Agent agent, List conversations) + { + var contentHooks = services.GetServices().ToList(); + var convService = services.GetService(); + + // 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); + + using (var activity = ModelDiagnostics.StartCompletionActivity(null, _model, Provider, prompt, convService)) + { + 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; + + activity?.SetTag(ModelDiagnosticsTags.FinishReason, reason); + + 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, + }; + } + + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) + { + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = response.Value?.Usage?.InputTokenCount ?? 0, + TextOutputTokens = response.Value?.Usage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); + return responseMessage; + } + } + + public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onStreamResponseReceived) + { + var contentHooks = services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in contentHooks) + { + await hook.BeforeGenerating(agent, conversations); + } + + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + + 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) + { + TrackStreamingToolingUpdate(choice.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + if (!choice.ContentUpdate.IsNullOrEmpty() && choice.ContentUpdate[0] != null) + { + foreach (var contentPart in choice.ContentUpdate) + { + if (contentPart.Kind == ChatMessageContentPartKind.Text) + { + (contentBuilder ??= new()).Append(contentPart.Text); + } + } + + logger.LogInformation(choice.ContentUpdate[0]?.Text); + + if (!string.IsNullOrEmpty(choice.ContentUpdate[0]?.Text)) + { + var msg = new RoleDialogModel(choice.Role?.ToString() ?? ChatMessageRole.Assistant.ToString(), choice.ContentUpdate[0]?.Text ?? string.Empty); + + await onStreamResponseReceived(msg); + } + } + } + + // Get any response content that was streamed. + string content = contentBuilder?.ToString() ?? string.Empty; + + RoleDialogModel responseMessage = new(ChatMessageRole.Assistant.ToString(), content); + + var tools = ConvertToolCallUpdatesToFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + foreach (var tool in tools) + { + tool.CurrentAgentId = agent.Id; + tool.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty; + await onStreamResponseReceived(tool); + } + + if (tools.Length > 0) + { + responseMessage = tools[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, + TextInputTokens = response.Value?.Usage?.InputTokenCount ?? 0, + TextOutputTokens = 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(); + float? temperature = float.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; + + + state.SetState("temperature", temperature.ToString()); + state.SetState("max_tokens", maxTokens.ToString()); + + 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.RenderInstruction(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.FunctionName, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty)) + })); + + messages.Add(new ToolChatMessage(message.FunctionName, message.Content)); + } + else if (message.Role == AgentRole.User) + { + var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content; + messages.Add(new UserChatMessage(text)); + } + 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; + } + + private static void TrackStreamingToolingUpdate( + IReadOnlyList? updates, + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + if (updates is null) + { + // Nothing to track. + return; + } + + foreach (var update in updates) + { + // If we have an ID, ensure the index is being tracked. Even if it's not a function update, + // we want to keep track of it so we can send back an error. + if (!string.IsNullOrWhiteSpace(update.ToolCallId)) + { + (toolCallIdsByIndex ??= [])[update.Index] = update.ToolCallId; + } + + // Ensure we're tracking the function's name. + if (!string.IsNullOrWhiteSpace(update.FunctionName)) + { + (functionNamesByIndex ??= [])[update.Index] = update.FunctionName; + } + + // Ensure we're tracking the function's arguments. + if (update.FunctionArgumentsUpdate is not null && !update.FunctionArgumentsUpdate.ToMemory().IsEmpty) + { + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.Index, out StringBuilder? arguments)) + { + functionArgumentBuildersByIndex[update.Index] = arguments = new(); + } + + arguments.Append(update.FunctionArgumentsUpdate.ToString()); + } + } + } + + private static RoleDialogModel[] ConvertToolCallUpdatesToFunctionToolCalls( + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + RoleDialogModel[] toolCalls = []; + if (toolCallIdsByIndex is { Count: > 0 }) + { + toolCalls = new RoleDialogModel[toolCallIdsByIndex.Count]; + + int i = 0; + foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) + { + string? functionName = null; + StringBuilder? functionArguments = null; + + functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); + functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); + + toolCalls[i] = new RoleDialogModel(AgentRole.Function, string.Empty) + { + FunctionName = functionName ?? string.Empty, + FunctionArgs = functionArguments?.ToString() ?? string.Empty, + }; + i++; + } + + Debug.Assert(i == toolCalls.Length); + } + + return toolCalls; + } + +} diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs new file mode 100644 index 000000000..80a8dbd71 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Embedding/TextEmbeddingProvider.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Logging; +using OpenAI.Embeddings; + +namespace BotSharp.Plugin.GiteeAI.Providers.Embedding; + +public class TextEmbeddingProvider( + ILogger logger, + IServiceProvider services) : ITextEmbedding +{ + protected readonly IServiceProvider _services = services; + protected readonly ILogger _logger = logger; + + private const int DEFAULT_DIMENSION = 1024; + protected string _model = "bge-m3"; + + public virtual string Provider => "gitee-ai"; + + public string Model => _model; + + protected int _dimension; + + public async Task GetVectorAsync(string text) + { + var client = ProviderHelper.GetClient(Provider, _model, _services); + var embeddingClient = client.GetEmbeddingClient(_model); + var options = PrepareOptions(); + var response = await embeddingClient.GenerateEmbeddingAsync(text, options); + var value = response.Value; + return value.ToFloats().ToArray(); + } + + public async Task> GetVectorsAsync(List texts) + { + var client = ProviderHelper.GetClient(Provider, _model, _services); + var embeddingClient = client.GetEmbeddingClient(_model); + var options = PrepareOptions(); + var response = await embeddingClient.GenerateEmbeddingsAsync(texts, options); + var value = response.Value; + return value.Select(x => x.ToFloats().ToArray()).ToList(); + } + + public void SetModelName(string model) + { + _model = model; + } + + private EmbeddingGenerationOptions PrepareOptions() + { + return new EmbeddingGenerationOptions + { + Dimensions = GetDimension() + }; + } + + public int GetDimension() + { + var state = _services.GetRequiredService(); + var stateDimension = state.GetState("embedding_dimension"); + var defaultDimension = _dimension > 0 ? _dimension : DEFAULT_DIMENSION; + + if (int.TryParse(stateDimension, out var dimension)) + { + return dimension > 0 ? dimension : defaultDimension; + } + return defaultDimension; + } + + public void SetDimension(int dimension) + { + _dimension = dimension > 0 ? dimension : DEFAULT_DIMENSION; + } + +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs new file mode 100644 index 000000000..b532e834c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/ProviderHelper.cs @@ -0,0 +1,16 @@ +using OpenAI; +using System.ClientModel; + +namespace BotSharp.Plugin.GiteeAI.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.GiteeAI/README.md b/src/Plugins/BotSharp.Plugin.GiteeAI/README.md new file mode 100644 index 000000000..5b4d00ff4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/README.md @@ -0,0 +1,8 @@ +Model Ark (Gitee AI) , hereinafter referred to as Gitee AI, aggregates the latest and most popular AI models, providing a one-stop service for model experience, inference, fine-tuning, and application deployment . We offer a diverse range of computing power options, aiming to help enterprises and developers build AI applications more easily . +ChatCompletions Interface: + +- https://ai.gitee.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90/post/chat/completions + +Signature Authentication Method: + +- https://ai.gitee.com/docs/organization/access-token \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs new file mode 100644 index 000000000..aa44ad1e2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Using.cs @@ -0,0 +1,15 @@ +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Loggers; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Utilities; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text; +global using System.Threading.Tasks; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index a36e32b66..1db3ac96d 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,9 +1,12 @@ #pragma warning disable OPENAI001 +using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Core.Infrastructures.Streams; using BotSharp.Core.MessageHub; +using Microsoft.AspNetCore.Cors.Infrastructure; using OpenAI.Chat; +using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.Plugin.OpenAI.Providers.Chat; @@ -32,6 +35,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -42,68 +46,77 @@ public async Task GetChatCompletions(Agent agent, List new ChatAnnotation + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() - }; - } + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } - var tokenUsage = response.Value?.Usage; - var inputTokenDetails = response.Value?.Usage?.InputTokenDetails; + var tokenUsage = response.Value?.Usage; + var inputTokenDetails = response.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); - } + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); - return responseMessage; + return responseMessage; + } } public async Task GetChatCompletionsAsync(Agent agent, diff --git a/src/WebStarter/Program.cs b/src/WebStarter/Program.cs index 2c9c073c2..09a7344c5 100644 --- a/src/WebStarter/Program.cs +++ b/src/WebStarter/Program.cs @@ -1,11 +1,10 @@ +using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Core; using BotSharp.Core.MCP; -using BotSharp.OpenAPI; using BotSharp.Logger; +using BotSharp.OpenAPI; using BotSharp.Plugin.ChatHub; using Serilog; -using BotSharp.Abstraction.Messaging.JsonConverters; -using StackExchange.Redis; var builder = WebApplication.CreateBuilder(args); diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 6e0176157..e0317d960 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -37,6 +37,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 59d7d3f55..6ddd21fb0 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -6,6 +6,9 @@ } }, "AllowedHosts": "*", + //"OTEL_EXPORTER_OTLP_ENDPOINT": "https://us.cloud.langfuse.com", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317", + "OTEL_SERVICE_NAME": "apiservice", "AllowedOrigins": [ "http://localhost:5015", "http://0.0.0.0:5015", @@ -240,6 +243,43 @@ } } ] + }, + { + "Provider": "gitee-ai", + "Models": [ + { + "Name": "DeepSeek-V3_1", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": 1024, + "Temperature": 0.6 + }, + { + "Name": "GLM-4_5", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": 1024, + "Temperature": 0.6 + }, + { + "Id": "bge-m3", + "Name": "bge-m3", + "ApiKey": " ", + "Endpoint": "https://ai.gitee.com/v1/embeddings/", + "Type": "embedding", + "Dimension": 1024, + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 + } + ] } ], @@ -583,44 +623,49 @@ "Language": "en" } }, - - "PluginLoader": { - "Assemblies": [ - "BotSharp.Core", - "BotSharp.Core.SideCar", - "BotSharp.Core.Crontab", - "BotSharp.Core.Realtime", - "BotSharp.Logger", - "BotSharp.Plugin.MongoStorage", - "BotSharp.Plugin.Dashboard", - "BotSharp.Plugin.OpenAI", - "BotSharp.Plugin.AzureOpenAI", - "BotSharp.Plugin.AnthropicAI", - "BotSharp.Plugin.GoogleAI", - "BotSharp.Plugin.MetaAI", - "BotSharp.Plugin.DeepSeekAI", - "BotSharp.Plugin.MetaMessenger", - "BotSharp.Plugin.HuggingFace", - "BotSharp.Plugin.KnowledgeBase", - "BotSharp.Plugin.Planner", - "BotSharp.Plugin.Graph", - "BotSharp.Plugin.Qdrant", - "BotSharp.Plugin.ChatHub", - "BotSharp.Plugin.WeChat", - "BotSharp.Plugin.PizzaBot", - "BotSharp.Plugin.WebDriver", - "BotSharp.Plugin.LLamaSharp", - "BotSharp.Plugin.SparkDesk", - "BotSharp.Plugin.MetaGLM", - "BotSharp.Plugin.HttpHandler", - "BotSharp.Plugin.FileHandler", - "BotSharp.Plugin.EmailHandler", - "BotSharp.Plugin.AudioHandler", - "BotSharp.Plugin.ChartHandler", - "BotSharp.Plugin.AudioHandler", - "BotSharp.Plugin.ExcelHandler", - "BotSharp.Plugin.SqlDriver", - "BotSharp.Plugin.TencentCos" - ] + "Langfuse": { + "SecretKey": "sk-lf- ", + "PublicKey": "pk-lf-", + "Host": "https://us.cloud.langfuse.com/api/public/otel/v1/traces" + }, + "PluginLoader": { + "Assemblies": [ + "BotSharp.Core", + "BotSharp.Core.SideCar", + "BotSharp.Core.Crontab", + "BotSharp.Core.Realtime", + "BotSharp.Logger", + "BotSharp.Plugin.MongoStorage", + "BotSharp.Plugin.Dashboard", + "BotSharp.Plugin.OpenAI", + "BotSharp.Plugin.AzureOpenAI", + "BotSharp.Plugin.AnthropicAI", + "BotSharp.Plugin.GoogleAI", + "BotSharp.Plugin.MetaAI", + "BotSharp.Plugin.DeepSeekAI", + "BotSharp.Plugin.GiteeAI", + "BotSharp.Plugin.MetaMessenger", + "BotSharp.Plugin.HuggingFace", + "BotSharp.Plugin.KnowledgeBase", + "BotSharp.Plugin.Planner", + "BotSharp.Plugin.Graph", + "BotSharp.Plugin.Qdrant", + "BotSharp.Plugin.ChatHub", + "BotSharp.Plugin.WeChat", + "BotSharp.Plugin.PizzaBot", + "BotSharp.Plugin.WebDriver", + "BotSharp.Plugin.LLamaSharp", + "BotSharp.Plugin.SparkDesk", + "BotSharp.Plugin.MetaGLM", + "BotSharp.Plugin.HttpHandler", + "BotSharp.Plugin.FileHandler", + "BotSharp.Plugin.EmailHandler", + "BotSharp.Plugin.AudioHandler", + "BotSharp.Plugin.ChartHandler", + "BotSharp.Plugin.AudioHandler", + "BotSharp.Plugin.ExcelHandler", + "BotSharp.Plugin.SqlDriver", + "BotSharp.Plugin.TencentCos" + ] + } } -} From 6bf68d910fb82559a6d45603545db8d0089f960e Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 16 Oct 2025 18:12:25 +0800 Subject: [PATCH 05/18] Update Azure OpenAI model configurations Replaces previous GPT-4.1 model entries with updated GPT-3.5-turbo and gpt-35-turbo-instruct configurations, including new endpoints, versioning, and cost structure. Sensitive API keys have been removed from the configuration. --- Directory.Packages.props | 33 +- .../Properties/launchSettings.json | 2 + .../BotSharp.ServiceDefaults.csproj | 3 +- src/BotSharp.ServiceDefaults/Extensions.cs | 51 +-- .../BotSharp.Abstraction.csproj | 7 +- .../Diagnostics/ActivityExtensions.cs | 82 ++-- .../Diagnostics/AppContextSwitchHelper.cs | 4 +- .../Diagnostics/BotSharpOTelOptions.cs | 12 + .../Diagnostics/EnvironmentConfigLoader.cs | 56 +++ .../Diagnostics/ModelDiagnostics.cs | 394 ------------------ .../Diagnostics/OpenTelemetryExtensions.cs | 55 +++ .../Telemetry/IMachineInformationProvider.cs | 15 + .../Telemetry/ITelemetryService.cs | 66 +++ .../Telemetry/MachineInformationProvider.cs | 83 ++++ .../Telemetry/TelemetryConstants.cs | 98 +++++ .../Diagnostics/Telemetry/TelemetryService.cs | 378 +++++++++++++++++ .../BotSharp.Core/BotSharpCoreExtensions.cs | 3 +- .../MCP/Managers/McpClientManager.cs | 23 +- .../Executor/FunctionCallbackExecutor.cs | 3 +- .../Routing/Executor/MCPToolExecutor.cs | 10 +- .../Routing/RoutingService.InvokeAgent.cs | 5 +- .../BotSharp.Core/Routing/RoutingService.cs | 4 + .../BotSharpOpenApiExtensions.cs | 20 +- .../Controllers/ConversationController.cs | 3 +- .../Providers/Chat/ChatCompletionProvider.cs | 10 +- .../BotSharp.Plugin.GiteeAI.csproj | 2 +- .../Providers/Chat/ChatCompletionProvider.cs | 12 +- .../Providers/Chat/ChatCompletionProvider.cs | 10 +- src/WebStarter/Program.cs | 2 +- src/WebStarter/appsettings.json | 48 +-- tests/BotSharp.PizzaBot.MCPServer/Program.cs | 1 + 31 files changed, 939 insertions(+), 556 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/BotSharpOTelOptions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/EnvironmentConfigLoader.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/OpenTelemetryExtensions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/IMachineInformationProvider.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/ITelemetryService.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/MachineInformationProvider.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryConstants.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryService.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 17bc01691..32ce5d6af 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,11 @@ true + + + + + @@ -25,8 +30,8 @@ - - + + @@ -61,8 +66,8 @@ - - + + @@ -97,12 +102,12 @@ - - - - - - + + + + + + @@ -115,9 +120,11 @@ - - - + + + + + diff --git a/src/BotSharp.AppHost/Properties/launchSettings.json b/src/BotSharp.AppHost/Properties/launchSettings.json index c315179c6..e4b685d27 100644 --- a/src/BotSharp.AppHost/Properties/launchSettings.json +++ b/src/BotSharp.AppHost/Properties/launchSettings.json @@ -8,6 +8,7 @@ "applicationUrl": "https://localhost:17248;http://localhost:15140", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21247", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22140" @@ -20,6 +21,7 @@ "applicationUrl": "http://localhost:15140", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", "DOTNET_ENVIRONMENT": "Development", "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19185", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20069" diff --git a/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj b/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj index d19f2eea5..5d5baa03d 100644 --- a/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj +++ b/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -20,6 +20,7 @@ + diff --git a/src/BotSharp.ServiceDefaults/Extensions.cs b/src/BotSharp.ServiceDefaults/Extensions.cs index caf52b243..f595ae9c2 100644 --- a/src/BotSharp.ServiceDefaults/Extensions.cs +++ b/src/BotSharp.ServiceDefaults/Extensions.cs @@ -1,4 +1,5 @@ using BotSharp.Langfuse; +using Langfuse.OpenTelemetry; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Configuration; @@ -100,10 +101,11 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati .AddService("apiservice", serviceVersion: "1.0.0") ) .AddSource("BotSharp") + .AddSource("BotSharp.Server") .AddSource("BotSharp.Abstraction.Diagnostics") - .AddSource("BotSharp.Core.Routing.Executor"); - - tracing.AddAspNetCoreInstrumentation() + .AddSource("BotSharp.Core.Routing.Executor") + .AddLangfuseExporter(builder.Configuration) + .AddAspNetCoreInstrumentation() // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation() @@ -115,7 +117,7 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati // options.Headers = $"Authorization=Basic {base64EncodedAuth}"; //}) ; - + }); @@ -126,34 +128,33 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) { - var langfuseSection = builder.Configuration.GetSection("Langfuse"); - var useLangfuse = langfuseSection != null; var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); if (useOtlpExporter) { builder.Services.Configure(logging => logging.AddOtlpExporter()); builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - if (useLangfuse) - { - var publicKey = langfuseSection.GetValue(nameof(LangfuseSettings.PublicKey)) ?? string.Empty; - var secretKey = langfuseSection.GetValue(nameof(LangfuseSettings.SecretKey)) ?? string.Empty; - var host = langfuseSection.GetValue(nameof(LangfuseSettings.Host)) ?? string.Empty; - var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"); - string base64EncodedAuth = Convert.ToBase64String(plainTextBytes); - - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter(options => - { - options.Endpoint = new Uri(host); - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.Headers = $"Authorization=Basic {base64EncodedAuth}"; - }) - ); - } - else - { + //if (useLangfuse) + //{ + // var publicKey = langfuseSection.GetValue(nameof(LangfuseSettings.PublicKey)) ?? string.Empty; + // var secretKey = langfuseSection.GetValue(nameof(LangfuseSettings.SecretKey)) ?? string.Empty; + // var host = langfuseSection.GetValue(nameof(LangfuseSettings.Host)) ?? string.Empty; + // var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"); + // string base64EncodedAuth = Convert.ToBase64String(plainTextBytes); + + // builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter(options => + // { + // options.Endpoint = new Uri(host); + // options.Protocol = OtlpExportProtocol.HttpProtobuf; + // options.Headers = $"Authorization=Basic {base64EncodedAuth}"; + // }) + // ); + + //} + //else + //{ builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); - } + //} } // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index 2008c6a2e..cf56a7527 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -40,6 +40,11 @@ + + + + + + - diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs index 105d5aae5..11c44a987 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs @@ -1,18 +1,33 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; +using BotSharp.Abstraction.Diagnostics.Telemetry; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; namespace BotSharp.Abstraction.Diagnostics; +/// +/// Model diagnostics helper class that provides a set of methods to trace model activities with the OTel semantic conventions. +/// This class contains experimental features and may change in the future. +/// To enable these features, set one of the following switches to true: +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnostics` +/// `BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive` +/// Or set the following environment variables to true: +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS` +/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE` +/// [ExcludeFromCodeCoverage] -public static class ActivityExtensions +public static class ActivityExtensions { + private const string EnableDiagnosticsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnostics"; + private const string EnableSensitiveEventsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive"; + private const string EnableDiagnosticsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS"; + private const string EnableSensitiveEventsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE"; + + public static readonly bool s_enableDiagnostics = AppContextSwitchHelper.GetConfigValue(EnableDiagnosticsSwitch, EnableDiagnosticsEnvVar); + public static readonly bool s_enableSensitiveEvents = AppContextSwitchHelper.GetConfigValue(EnableSensitiveEventsSwitch, EnableSensitiveEventsEnvVar); + + /// /// Starts an activity with the appropriate tags for a kernel function execution. /// @@ -21,9 +36,9 @@ public static class ActivityExtensions const string OperationName = "execute_tool"; return source.StartActivityWithTags($"{OperationName} {functionName}", [ - new KeyValuePair("gen_ai.operation.name", OperationName), - new KeyValuePair("gen_ai.tool.name", functionName), - new KeyValuePair("gen_ai.tool.description", functionDescription) + new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.ToolName, functionName), + new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.ToolDescription, functionDescription) ], ActivityKind.Internal); } @@ -42,8 +57,6 @@ public static Activity SetTags(this Activity activity, ReadOnlySpan RunWithActivityAsync( - Func getActivity, - Func> operation, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - using var activity = getActivity(); - - ConfiguredCancelableAsyncEnumerable result; - - try - { - result = operation().WithCancellation(cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - var resultEnumerator = result.ConfigureAwait(false).GetAsyncEnumerator(); - - try - { - while (true) - { - try - { - if (!await resultEnumerator.MoveNextAsync()) - { - break; - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - yield return resultEnumerator.Current; - } - } - finally - { - await resultEnumerator.DisposeAsync(); - } - } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs index 64e5806be..2add23728 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs @@ -1,7 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + using System; using System.Diagnostics.CodeAnalysis; -namespace BotSharp.Abstraction.Diagnostics; +namespace BotSharp.Abstraction.Diagnostics; /// /// Helper class to get app context switch value diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/BotSharpOTelOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/BotSharpOTelOptions.cs new file mode 100644 index 000000000..72c1a03d2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/BotSharpOTelOptions.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Abstraction.Diagnostics; + +public class BotSharpOTelOptions +{ + public const string DefaultName = "BotSharp.Server"; + + public string Name { get; set; } = DefaultName; + + public string Version { get; set; } = "4.0.0"; + + public bool IsTelemetryEnabled { get; set; } = true; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/EnvironmentConfigLoader.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/EnvironmentConfigLoader.cs new file mode 100644 index 000000000..39fba4d40 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/EnvironmentConfigLoader.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.Abstraction.Diagnostics; + +internal static class EnvironmentConfigLoader +{ + private const string DefaultBaseUrl = "https://cloud.langfuse.com"; + + private const string EnvTelemetry = "BOTSHARP_COLLECT_TELEMETRY"; + + + /// + /// Loads configuration from environment variables and applies defaults. + /// + public static BotSharpOTelOptions LoadFromEnvironment(IConfiguration? configuration = null) + { + var options = new BotSharpOTelOptions(); + + // Try configuration first (appsettings.json, etc.) + if (configuration != null) + { + if (bool.TryParse(configuration["Otel:IsTelemetryEnabled"], out bool istelemetryEnabled)) + { + options.IsTelemetryEnabled = istelemetryEnabled; + } + } + + var collectTelemetry = Environment.GetEnvironmentVariable(EnvTelemetry); + if (!string.IsNullOrWhiteSpace(collectTelemetry)) + { + options.IsTelemetryEnabled = bool.TryParse(collectTelemetry, out var shouldCollect) && shouldCollect; + } + + + return options; + } + + /// + /// Validates that required options are set. + /// + public static void Validate(BotSharpOTelOptions options) + { + if (string.IsNullOrWhiteSpace(options.Name)) + { + throw new InvalidOperationException( + $"Otel name is required. Set it via code or configuration."); + } + + } + +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs deleted file mode 100644 index 83f6532cb..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs +++ /dev/null @@ -1,394 +0,0 @@ -using BotSharp.Abstraction.Conversations; -using BotSharp.Abstraction.Functions.Models; -using Microsoft.Extensions.DependencyInjection; -using System.Diagnostics; -using System.Text.Json; - -namespace BotSharp.Abstraction.Diagnostics; - -/// -/// Model diagnostics helper class that provides a set of methods to trace model activities with the OTel semantic conventions. -/// This class contains experimental features and may change in the future. -/// To enable these features, set one of the following switches to true: -/// `BotSharp.Experimental.GenAI.EnableOTelDiagnostics` -/// `BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive` -/// Or set the following environment variables to true: -/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS` -/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE` -/// -//[System.Diagnostics.CodeAnalysis.Experimental("SKEXP0001")] -[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -public static class ModelDiagnostics -{ - private static readonly string s_namespace = typeof(ModelDiagnostics).Namespace!; - private static readonly ActivitySource s_activitySource = new(s_namespace); - - private const string EnableDiagnosticsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnostics"; - private const string EnableSensitiveEventsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive"; - private const string EnableDiagnosticsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS"; - private const string EnableSensitiveEventsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE"; - - private static readonly bool s_enableDiagnostics = AppContextSwitchHelper.GetConfigValue(EnableDiagnosticsSwitch, EnableDiagnosticsEnvVar); - private static readonly bool s_enableSensitiveEvents = AppContextSwitchHelper.GetConfigValue(EnableSensitiveEventsSwitch, EnableSensitiveEventsEnvVar); - - /// - /// Start a text completion activity for a given model. - /// The activity will be tagged with the a set of attributes specified by the semantic conventions. - /// - public static Activity? StartCompletionActivity( - Uri? endpoint, - string modelName, - string modelProvider, - string prompt, - IConversationStateService services - ) - { - if (!IsModelDiagnosticsEnabled()) - { - return null; - } - - const string OperationName = "text.completions"; - var activity = s_activitySource.StartActivityWithTags( - $"{OperationName} {modelName}", - [ - new(ModelDiagnosticsTags.Operation, OperationName), - new(ModelDiagnosticsTags.System, modelProvider), - new(ModelDiagnosticsTags.Model, modelName), - ], - ActivityKind.Client); - - if (endpoint is not null) - { - activity?.SetTags([ - // Skip the query string in the uri as it may contain keys - new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), - new(ModelDiagnosticsTags.Port, endpoint.Port), - ]); - } - - AddOptionalTags(activity, services); - - if (s_enableSensitiveEvents) - { - activity?.AttachSensitiveDataAsEvent( - ModelDiagnosticsTags.UserMessage, - [ - new(ModelDiagnosticsTags.EventName, prompt), - new(ModelDiagnosticsTags.System, modelProvider), - ]); - } - - return activity; - } - - /// - /// Start a chat completion activity for a given model. - /// The activity will be tagged with the a set of attributes specified by the semantic conventions. - /// - public static Activity? StartCompletionActivity( - Uri? endpoint, - string modelName, - string modelProvider, - List chatHistory, - IConversationStateService conversationStateService - ) - - { - if (!IsModelDiagnosticsEnabled()) - { - return null; - } - - const string OperationName = "chat.completions"; - var activity = s_activitySource.StartActivityWithTags( - $"{OperationName} {modelName}", - [ - new(ModelDiagnosticsTags.Operation, OperationName), - new(ModelDiagnosticsTags.System, modelProvider), - new(ModelDiagnosticsTags.Model, modelName), - ], - ActivityKind.Client); - - if (endpoint is not null) - { - activity?.SetTags([ - // Skip the query string in the uri as it may contain keys - new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), - new(ModelDiagnosticsTags.Port, endpoint.Port), - ]); - } - - AddOptionalTags(activity, conversationStateService); - - if (s_enableSensitiveEvents) - { - foreach (var message in chatHistory) - { - var formattedContent = JsonSerializer.Serialize(ToGenAIConventionsFormat(message)); - activity?.AttachSensitiveDataAsEvent( - ModelDiagnosticsTags.RoleToEventMap[message.Role], - [ - new(ModelDiagnosticsTags.EventName, formattedContent), - new(ModelDiagnosticsTags.System, modelProvider), - ]); - } - } - - return activity; - } - - /// - /// Start an agent invocation activity and return the activity. - /// - public static Activity? StartAgentInvocationActivity( - string agentId, - string agentName, - string? agentDescription, - Agent? agents, - List messages - ) - { - if (!IsModelDiagnosticsEnabled()) - { - return null; - } - - const string OperationName = "invoke_agent"; - - var activity = s_activitySource.StartActivityWithTags( - $"{OperationName} {agentName}", - [ - new(ModelDiagnosticsTags.Operation, OperationName), - new(ModelDiagnosticsTags.AgentId, agentId), - new(ModelDiagnosticsTags.AgentName, agentName) - ], - ActivityKind.Internal); - - if (!string.IsNullOrWhiteSpace(agentDescription)) - { - activity?.SetTag(ModelDiagnosticsTags.AgentDescription, agentDescription); - } - - if (agents is not null && (agents.Functions.Count > 0 || agents.SecondaryFunctions.Count >0)) - { - List allFunctions = []; - allFunctions.AddRange(agents.Functions); - allFunctions.AddRange(agents.SecondaryFunctions); - - activity?.SetTag( - ModelDiagnosticsTags.AgentToolDefinitions, - JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); - } - - if (IsSensitiveEventsEnabled()) - { - activity?.SetTag( - ModelDiagnosticsTags.AgentInvocationInput, - JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); - } - - return activity; - } - - /// - /// Set the agent response for a given activity. - /// - public static void SetAgentResponse(this Activity activity, IEnumerable? responses) - { - if (!IsModelDiagnosticsEnabled() || responses is null) - { - return; - } - - if (s_enableSensitiveEvents) - { - activity?.SetTag( - ModelDiagnosticsTags.AgentInvocationOutput, - JsonSerializer.Serialize(responses.Select(r => ToGenAIConventionsFormat(r)))); - } - } - - - - /// - /// Set the response id for a given activity. - /// - /// The activity to set the response id - /// The response id - /// The activity with the response id set for chaining - internal static Activity SetResponseId(this Activity activity, string responseId) => activity.SetTag(ModelDiagnosticsTags.ResponseId, responseId); - - /// - /// Set the input tokens usage for a given activity. - /// - /// The activity to set the input tokens usage - /// The number of input tokens used - /// The activity with the input tokens usage set for chaining - internal static Activity SetInputTokensUsage(this Activity activity, int inputTokens) => activity.SetTag(ModelDiagnosticsTags.InputTokens, inputTokens); - - /// - /// Set the output tokens usage for a given activity. - /// - /// The activity to set the output tokens usage - /// The number of output tokens used - /// The activity with the output tokens usage set for chaining - internal static Activity SetOutputTokensUsage(this Activity activity, int outputTokens) => activity.SetTag(ModelDiagnosticsTags.OutputTokens, outputTokens); - - /// - /// Check if model diagnostics is enabled - /// Model diagnostics is enabled if either EnableModelDiagnostics or EnableSensitiveEvents is set to true and there are listeners. - /// - internal static bool IsModelDiagnosticsEnabled() - { - return (s_enableDiagnostics || s_enableSensitiveEvents) && s_activitySource.HasListeners(); - } - - /// - /// Check if sensitive events are enabled. - /// Sensitive events are enabled if EnableSensitiveEvents is set to true and there are listeners. - /// - internal static bool IsSensitiveEventsEnabled() => s_enableSensitiveEvents && s_activitySource.HasListeners(); - - internal static bool HasListeners() => s_activitySource.HasListeners(); - - #region Private - private static void AddOptionalTags(Activity? activity, IConversationStateService conversationStateService) - { - if (activity is null) - { - return; - } - - void TryAddTag(string key, string tag) - { - var value = conversationStateService.GetState(key); - if (!string.IsNullOrEmpty(value)) - { - activity.SetTag(tag, value); - } - } - - TryAddTag("max_tokens", ModelDiagnosticsTags.MaxToken); - TryAddTag("temperature", ModelDiagnosticsTags.Temperature); - TryAddTag("top_p", ModelDiagnosticsTags.TopP); - } - - /// - /// Convert a chat message to a JSON object based on the OTel GenAI Semantic Conventions format - /// - private static object ToGenAIConventionsFormat(RoleDialogModel chatMessage) - { - return new - { - role = chatMessage.Role.ToString(), - name = chatMessage.MessageId, - content = chatMessage.Content, - tool_calls = ToGenAIConventionsToolCallFormat(chatMessage), - }; - } - - /// - /// Helper method to convert tool calls to a list of JSON object based on the OTel GenAI Semantic Conventions format - /// - private static List ToGenAIConventionsToolCallFormat(RoleDialogModel chatMessage) - { - List toolCalls = []; - if (chatMessage.Instruction is not null) - { - toolCalls.Add(new - { - id = chatMessage.ToolCallId, - function = new - { - name = chatMessage.Instruction.Function, - arguments = chatMessage.Instruction.Arguments - }, - type = "function" - }); - } - return toolCalls; - } - - /// - /// Convert a function metadata to a JSON object based on the OTel GenAI Semantic Conventions format - /// - private static object ToGenAIConventionsFormat(FunctionDef metadata) - { - var properties = metadata.Parameters?.Properties; - var required = metadata.Parameters?.Required; - - return new - { - type = "function", - name = metadata.Name, - description = metadata.Description, - parameters = new - { - type = "object", - properties, - required, - } - }; - } - - /// - /// Convert a chat model response to a JSON string based on the OTel GenAI Semantic Conventions format - /// - private static string ToGenAIConventionsChoiceFormat(RoleDialogModel chatMessage, int index) - { - var jsonObject = new - { - index, - message = ToGenAIConventionsFormat(chatMessage), - tool_calls = ToGenAIConventionsToolCallFormat(chatMessage) - }; - - return JsonSerializer.Serialize(jsonObject); - } - - - - /// - /// Tags used in model diagnostics - /// - public static class ModelDiagnosticsTags - { - // Activity tags - public const string System = "gen_ai.system"; - public const string Operation = "gen_ai.operation.name"; - public const string Model = "gen_ai.request.model"; - public const string MaxToken = "gen_ai.request.max_tokens"; - public const string Temperature = "gen_ai.request.temperature"; - public const string TopP = "gen_ai.request.top_p"; - public const string ResponseId = "gen_ai.response.id"; - public const string ResponseModel = "gen_ai.response.model"; - public const string FinishReason = "gen_ai.response.finish_reason"; - public const string InputTokens = "gen_ai.usage.input_tokens"; - public const string OutputTokens = "gen_ai.usage.output_tokens"; - public const string Address = "server.address"; - public const string Port = "server.port"; - public const string AgentId = "gen_ai.agent.id"; - public const string AgentName = "gen_ai.agent.name"; - public const string AgentDescription = "gen_ai.agent.description"; - public const string AgentInvocationInput = "gen_ai.input.messages"; - public const string AgentInvocationOutput = "gen_ai.output.messages"; - public const string AgentToolDefinitions = "gen_ai.tool.definitions"; - - // Activity events - public const string EventName = "gen_ai.event.content"; - public const string SystemMessage = "gen_ai.system.message"; - public const string UserMessage = "gen_ai.user.message"; - public const string AssistantMessage = "gen_ai.assistant.message"; - public const string ToolMessage = "gen_ai.tool.message"; - public const string Choice = "gen_ai.choice"; - public static readonly Dictionary RoleToEventMap = new() - { - { AgentRole.System, SystemMessage }, - { AgentRole.User, UserMessage }, - { AgentRole.Assistant, AssistantMessage }, - { AgentRole.Function, ToolMessage } - }; - } - # endregion -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/OpenTelemetryExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/OpenTelemetryExtensions.cs new file mode 100644 index 000000000..390c0e1ae --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/OpenTelemetryExtensions.cs @@ -0,0 +1,55 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace BotSharp.Abstraction.Diagnostics; + +public static class OpenTelemetryExtensions +{ + public static void AddOpenTelemetry(this IServiceCollection services, + IConfiguration configure) + { + // Load from environment first + var options = EnvironmentConfigLoader.LoadFromEnvironment(configure); + + // Validate configuration + EnvironmentConfigLoader.Validate(options); + + services.Configure(cfg => + { + cfg.Name = options.Name; + cfg.Version = _assemblyVersion.Value; + cfg.IsTelemetryEnabled = options.IsTelemetryEnabled; + }); + + services.AddSingleton(); + services.AddSingleton(); + + } + + /// + /// Align with --version command. + /// https://github.com/dotnet/command-line-api/blob/bcdd4b9b424f0ff6ec855d08665569061a5d741f/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs#L23-L39 + /// + private static readonly Lazy _assemblyVersion = new(() => + { + var assembly = Assembly.GetEntryAssembly(); + + if (assembly == null) + { + throw new InvalidOperationException("Should be able to get entry assembly."); + } + + var assemblyVersionAttribute = assembly.GetCustomAttribute(); + + if (assemblyVersionAttribute is null) + { + return assembly.GetName().Version?.ToString() ?? ""; + } + else + { + return assemblyVersionAttribute.InformationalVersion; + } + }); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/IMachineInformationProvider.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/IMachineInformationProvider.cs new file mode 100644 index 000000000..74ee63621 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/IMachineInformationProvider.cs @@ -0,0 +1,15 @@ +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public interface IMachineInformationProvider +{ + /// + /// Gets existing or creates the device id. In case the cached id cannot be retrieved, or the + /// newly generated id cannot be cached, a value of null is returned. + /// + Task GetOrCreateDeviceId(); + + /// + /// Gets a hash of the machine's MAC address. + /// + Task GetMacAddressHash(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/ITelemetryService.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/ITelemetryService.cs new file mode 100644 index 000000000..89e3f5966 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/ITelemetryService.cs @@ -0,0 +1,66 @@ +using BotSharp.Abstraction.Conversations; +using ModelContextProtocol.Protocol; +using System.Diagnostics; + +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public interface ITelemetryService : IDisposable +{ + ActivitySource Parent { get; } + + /// + /// Creates and starts a new telemetry activity. + /// + /// Name of the activity. + /// An Activity object or null if there are no active listeners or telemetry is disabled. + /// If the service is not in an operational state or was not invoked. + Activity? StartActivity(string activityName); + + /// + /// Creates and starts a new telemetry activity. + /// + /// Name of the activity. + /// MCP client information to add to the activity. + /// An Activity object or null if there are no active listeners or telemetry is disabled. + /// If the service is not in an operational state or was not invoked. + Activity? StartActivity(string activityName, Implementation? clientInfo); + + /// + /// Creates and starts a new telemetry activity + /// + /// + /// + /// + /// + /// + /// + Activity? StartTextCompletionActivity(Uri? endpoint, string modelName, string modelProvider, string prompt, IConversationStateService services); + + /// + /// Creates and starts a new telemetry activity + /// + /// + /// + /// + /// + /// + /// + Activity? StartCompletionActivity(Uri? endpoint, string modelName, string modelProvider, List chatHistory, IConversationStateService conversationStateService); + + /// + /// Creates and starts a new telemetry activity + /// + /// + /// + /// + /// + /// + /// + Activity? StartAgentInvocationActivity(string agentId, string agentName, string? agentDescription, Agent? agents, List messages); + + /// + /// Performs any initialization operations before telemetry service is ready. + /// + /// A task that completes when initialization is complete. + Task InitializeAsync(); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/MachineInformationProvider.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/MachineInformationProvider.cs new file mode 100644 index 000000000..9cd954f56 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/MachineInformationProvider.cs @@ -0,0 +1,83 @@ +using DeviceId; +using Microsoft.Extensions.Logging; +using System.Net.NetworkInformation; +using System.Security.Cryptography; + +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public class MachineInformationProvider(ILogger logger) + : IMachineInformationProvider +{ + protected const string NotAvailable = "N/A"; + + private static readonly SHA256 s_sHA256 = SHA256.Create(); + + private readonly ILogger _logger = logger; + + /// + /// + /// + public async Task GetOrCreateDeviceId() + { + string deviceId = new DeviceIdBuilder() + .AddMachineName() + .AddOsVersion() + .OnWindows(windows => windows + .AddProcessorId() + .AddMotherboardSerialNumber() + .AddSystemDriveSerialNumber()) + .OnLinux(linux => linux + .AddMotherboardSerialNumber() + .AddSystemDriveSerialNumber()) + .OnMac(mac => mac + .AddSystemDriveSerialNumber() + .AddPlatformSerialNumber()) + .ToString(); + + return deviceId; + } + + /// + /// + /// + public virtual Task GetMacAddressHash() + { + return Task.Run(() => + { + try + { + var address = GetMacAddress(); + + return address != null + ? HashValue(address) + : NotAvailable; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to calculate MAC address hash."); + return NotAvailable; + } + }); + } + + /// + /// Searches for first network interface card that is up and has a physical address. + /// + /// Hash of the MAC address or if none can be found. + protected virtual string? GetMacAddress() + { + return NetworkInterface.GetAllNetworkInterfaces() + .Where(x => x.OperationalStatus == OperationalStatus.Up && x.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .Select(x => x.GetPhysicalAddress().ToString()) + .FirstOrDefault(x => !string.IsNullOrEmpty(x)); + } + + /// + /// Generates a SHA-256 of the given value. + /// + protected string HashValue(string value) + { + var hashInput = s_sHA256.ComputeHash(Encoding.UTF8.GetBytes(value)); + return BitConverter.ToString(hashInput).Replace("-", string.Empty).ToLowerInvariant(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryConstants.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryConstants.cs new file mode 100644 index 000000000..88433d8ce --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryConstants.cs @@ -0,0 +1,98 @@ +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public static class TelemetryConstants +{ + /// + /// Name of tags published. + /// + public static class TagName + { + public const string BotSharpVersion = "Version"; + public const string ClientName = "ClientName"; + public const string ClientVersion = "ClientVersion"; + public const string DevDeviceId = "DevDeviceId"; + public const string ErrorDetails = "ErrorDetails"; + public const string EventId = "EventId"; + public const string MacAddressHash = "MacAddressHash"; + public const string ToolName = "ToolName"; + public const string ToolArea = "ToolArea"; + public const string ServerMode = "ServerMode"; + public const string IsServerCommandInvoked = "IsServerCommandInvoked"; + public const string Transport = "Transport"; + public const string IsReadOnly = "IsReadOnly"; + public const string Namespace = "Namespace"; + public const string ToolCount = "ToolCount"; + public const string InsecureDisableElicitation = "InsecureDisableElicitation"; + public const string IsDebug = "IsDebug"; + public const string EnableInsecureTransports = "EnableInsecureTransports"; + public const string Tool = "Tool"; + } + + public static class ActivityName + { + public const string ListToolsHandler = "ListToolsHandler"; + public const string ToolExecuted = "ToolExecuted"; + public const string ServerStarted = "ServerStarted"; + } + + /// + /// 工具输入输出参数键常量类 + /// + public static class ToolParameterKeys + { + /// + /// 输入参数键 + /// + public const string Input = "input"; + + /// + /// 输出参数键 + /// + public const string Output = "output"; + } + + /// + /// Tags used in model diagnostics + /// + public static class ModelDiagnosticsTags + { + // Activity tags + public const string System = "gen_ai.system"; + public const string Operation = "gen_ai.operation.name"; + public const string Model = "gen_ai.request.model"; + public const string MaxToken = "gen_ai.request.max_tokens"; + public const string Temperature = "gen_ai.request.temperature"; + public const string TopP = "gen_ai.request.top_p"; + public const string ResponseId = "gen_ai.response.id"; + public const string ResponseModel = "gen_ai.response.model"; + public const string FinishReason = "gen_ai.response.finish_reason"; + public const string InputTokens = "gen_ai.usage.input_tokens"; + public const string OutputTokens = "gen_ai.usage.output_tokens"; + public const string Address = "server.address"; + public const string Port = "server.port"; + public const string AgentId = "gen_ai.agent.id"; + public const string AgentName = "gen_ai.agent.name"; + public const string AgentDescription = "gen_ai.agent.description"; + public const string AgentInvocationInput = "gen_ai.input.messages"; + public const string AgentInvocationOutput = "gen_ai.output.messages"; + public const string AgentToolDefinitions = "gen_ai.tool.definitions"; + + // Activity events + public const string EventName = "gen_ai.event.content"; + public const string SystemMessage = "gen_ai.system.message"; + public const string UserMessage = "gen_ai.user.message"; + public const string AssistantMessage = "gen_ai.assistant.message"; + public const string ToolName = "gen_ai.tool.name"; + public const string ToolMessage = "gen_ai.tool.message"; + public const string ToolDescription = "gen_ai.tool.description"; + public const string Choice = "gen_ai.choice"; + + public static readonly Dictionary RoleToEventMap = new() + { + { AgentRole.System, SystemMessage }, + { AgentRole.User, UserMessage }, + { AgentRole.Assistant, AssistantMessage }, + { AgentRole.Function, ToolMessage } + }; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryService.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryService.cs new file mode 100644 index 000000000..f1414daef --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/Telemetry/TelemetryService.cs @@ -0,0 +1,378 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Functions.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; + +namespace BotSharp.Abstraction.Diagnostics.Telemetry; + +public class TelemetryService : ITelemetryService +{ + private readonly IMachineInformationProvider _informationProvider; + private readonly bool _isEnabled; + private readonly ILogger _logger; + private readonly List> _tagsList; + private readonly SemaphoreSlim _initalizeLock = new(1); + + /// + /// Task created on the first invocation of . + /// This is saved so that repeated invocations will see the same exception + /// as the first invocation. + /// + private Task? _initalizationTask = null; + + private bool _initializationSuccessful; + private bool _isInitialized; + + public ActivitySource Parent { get; } + + public TelemetryService(IMachineInformationProvider informationProvider, + IOptions options, + ILogger logger) + { + _isEnabled = options.Value.IsTelemetryEnabled; + _tagsList = + [ + new(TelemetryConstants.TagName.BotSharpVersion, options.Value.Version), + ]; + + + Parent = new ActivitySource(options.Value.Name, options.Value.Version); + _informationProvider = informationProvider; + _logger = logger; + } + + /// + /// TESTING PURPOSES ONLY: Gets the default tags used for telemetry. + /// + internal IReadOnlyList> GetDefaultTags() + { + if (!_isEnabled) + { + return []; + } + + CheckInitialization(); + return [.. _tagsList]; + } + + /// + /// + /// + public Activity? StartActivity(string activityId) => StartActivity(activityId, null); + + /// + /// + /// + public Activity? StartActivity(string activityId, Implementation? clientInfo) + { + if (!_isEnabled) + { + return null; + } + + CheckInitialization(); + + var activity = Parent.StartActivity(activityId); + + if (activity == null) + { + return activity; + } + + if (clientInfo != null) + { + activity.AddTag(TelemetryConstants.TagName.ClientName, clientInfo.Name) + .AddTag(TelemetryConstants.TagName.ClientVersion, clientInfo.Version); + } + + activity.AddTag(TelemetryConstants.TagName.EventId, Guid.NewGuid().ToString()); + + _tagsList.ForEach(kvp => activity.AddTag(kvp.Key, kvp.Value)); + + return activity; + } + + public Activity? StartTextCompletionActivity(Uri? endpoint, string modelName, string modelProvider, string prompt, IConversationStateService services) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "text.completions"; + var activity = Parent.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + new(TelemetryConstants.ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(TelemetryConstants.ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(TelemetryConstants.ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, services); + + if (ActivityExtensions.s_enableSensitiveEvents) + { + activity?.AttachSensitiveDataAsEvent( + TelemetryConstants.ModelDiagnosticsTags.UserMessage, + [ + new(TelemetryConstants.ModelDiagnosticsTags.EventName, prompt), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + ]); + } + + return activity; + } + + public Activity? StartCompletionActivity(Uri? endpoint, string modelName, string modelProvider, List chatHistory, IConversationStateService conversationStateService) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "chat.completions"; + var activity = Parent.StartActivityWithTags( + $"{OperationName} {modelName}", + [ + new(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + new(TelemetryConstants.ModelDiagnosticsTags.Model, modelName), + ], + ActivityKind.Client); + + if (endpoint is not null) + { + activity?.SetTags([ + // Skip the query string in the uri as it may contain keys + new(TelemetryConstants.ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), + new(TelemetryConstants.ModelDiagnosticsTags.Port, endpoint.Port), + ]); + } + + AddOptionalTags(activity, conversationStateService); + + if (ActivityExtensions.s_enableSensitiveEvents) + { + foreach (var message in chatHistory) + { + var formattedContent = JsonSerializer.Serialize(ToGenAIConventionsFormat(message)); + activity?.AttachSensitiveDataAsEvent( + TelemetryConstants.ModelDiagnosticsTags.RoleToEventMap[message.Role], + [ + new(TelemetryConstants.ModelDiagnosticsTags.EventName, formattedContent), + new(TelemetryConstants.ModelDiagnosticsTags.System, modelProvider), + ]); + } + } + + return activity; + } + + public Activity? StartAgentInvocationActivity(string agentId, string agentName, string? agentDescription, Agent? agents, List messages) + { + if (!IsModelDiagnosticsEnabled()) + { + return null; + } + + const string OperationName = "invoke_agent"; + + var activity = Parent.StartActivityWithTags( + $"{OperationName} {agentName}", + [ + new(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName), + new(TelemetryConstants.ModelDiagnosticsTags.AgentId, agentId), + new(TelemetryConstants.ModelDiagnosticsTags.AgentName, agentName) + ], + ActivityKind.Internal); + + if (!string.IsNullOrWhiteSpace(agentDescription)) + { + activity?.SetTag(TelemetryConstants.ModelDiagnosticsTags.AgentDescription, agentDescription); + } + + if (agents is not null && (agents.Functions.Count > 0 || agents.SecondaryFunctions.Count > 0)) + { + List allFunctions = []; + allFunctions.AddRange(agents.Functions); + allFunctions.AddRange(agents.SecondaryFunctions); + + activity?.SetTag( + TelemetryConstants.ModelDiagnosticsTags.AgentToolDefinitions, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + if (IsSensitiveEventsEnabled()) + { + activity?.SetTag( + TelemetryConstants.ModelDiagnosticsTags.AgentInvocationInput, + JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); + } + + return activity; + } + + + public void Dispose() + { + + } + + /// + /// + /// + public async Task InitializeAsync() + { + if (!_isEnabled) + { + return; + } + + // Quick check if initialization already happened. Avoids + // trying to get the lock. + if (_initalizationTask == null) + { + // Get async lock for starting initialization + await _initalizeLock.WaitAsync(); + + try + { + // Check after acquiring lock to ensure we honor work + // started while we were waiting. + if (_initalizationTask == null) + { + _initalizationTask = InnerInitializeAsync(); + } + } + finally + { + _initalizeLock.Release(); + } + } + + // Await the response of the initialization work regardless of if + // we or another invocation created the Task representing it. All + // awaiting on this will give the same result to ensure idempotency. + await _initalizationTask; + + async Task InnerInitializeAsync() + { + try + { + var macAddressHash = await _informationProvider.GetMacAddressHash(); + var deviceId = await _informationProvider.GetOrCreateDeviceId(); + + _tagsList.Add(new(TelemetryConstants.TagName.MacAddressHash, macAddressHash)); + _tagsList.Add(new(TelemetryConstants.TagName.DevDeviceId, deviceId)); + + _initializationSuccessful = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred initializing telemetry service."); + throw; + } + finally + { + _isInitialized = true; + } + } + } + + private void CheckInitialization() + { + if (!_isInitialized) + { + throw new InvalidOperationException( + $"Telemetry service has not been initialized. Use {nameof(InitializeAsync)}() before any other operations."); + } + + if (!_initializationSuccessful) + { + throw new InvalidOperationException("Telemetry service was not successfully initialized. Check logs for initialization errors."); + } + + } + + internal bool IsModelDiagnosticsEnabled() + { + return (ActivityExtensions.s_enableDiagnostics || ActivityExtensions.s_enableSensitiveEvents) && Parent.HasListeners(); + } + + /// + /// Check if sensitive events are enabled. + /// Sensitive events are enabled if EnableSensitiveEvents is set to true and there are listeners. + /// + internal bool IsSensitiveEventsEnabled() => ActivityExtensions.s_enableSensitiveEvents && Parent.HasListeners(); + + private static void AddOptionalTags(Activity? activity, IConversationStateService conversationStateService) + { + if (activity is null) + { + return; + } + + void TryAddTag(string key, string tag) + { + var value = conversationStateService.GetState(key); + if (!string.IsNullOrEmpty(value)) + { + activity.SetTag(tag, value); + } + } + + TryAddTag("max_tokens", TelemetryConstants.ModelDiagnosticsTags.MaxToken); + TryAddTag("temperature", TelemetryConstants.ModelDiagnosticsTags.Temperature); + TryAddTag("top_p", TelemetryConstants.ModelDiagnosticsTags.TopP); + } + + /// + /// Convert a chat message to a JSON object based on the OTel GenAI Semantic Conventions format + /// + private static object ToGenAIConventionsFormat(RoleDialogModel chatMessage) + { + return new + { + role = chatMessage.Role.ToString(), + name = chatMessage.MessageId, + content = chatMessage.Content, + tool_calls = ToGenAIConventionsToolCallFormat(chatMessage), + }; + } + + /// + /// Helper method to convert tool calls to a list of JSON object based on the OTel GenAI Semantic Conventions format + /// + private static List ToGenAIConventionsToolCallFormat(RoleDialogModel chatMessage) + { + List toolCalls = []; + if (chatMessage.Instruction is not null) + { + toolCalls.Add(new + { + id = chatMessage.ToolCallId, + function = new + { + name = chatMessage.Instruction.Function, + arguments = chatMessage.Instruction.Arguments + }, + type = "function" + }); + } + return toolCalls; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index bfae45bac..ece3b5ae1 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -17,6 +17,7 @@ using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Realtime; using BotSharp.Abstraction.Repositories.Settings; +using BotSharp.Abstraction.Diagnostics; namespace BotSharp.Core; @@ -36,7 +37,7 @@ public static IServiceCollection AddBotSharpCore(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); - + services.AddOpenTelemetry(config); AddRedisEvents(services, config); // Register cache service AddCacheServices(services, config); diff --git a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs index 50b798eb4..c0a46c15d 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs @@ -1,6 +1,5 @@ using BotSharp.Core.MCP.Settings; using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol.Transport; namespace BotSharp.Core.MCP.Managers; @@ -17,7 +16,7 @@ public McpClientManager( _logger = logger; } - public async Task GetMcpClientAsync(string serverId) + public async Task GetMcpClientAsync(string serverId) { try { @@ -31,13 +30,15 @@ public McpClientManager( IClientTransport? transport = null; if (config.SseConfig != null) { - transport = new SseClientTransport(new SseClientTransportOptions - { - Name = config.Name, - Endpoint = new Uri(config.SseConfig.EndPoint), - AdditionalHeaders = config.SseConfig.AdditionalHeaders, - ConnectionTimeout = config.SseConfig.ConnectionTimeout - }); + transport = new HttpClientTransport( + new HttpClientTransportOptions + { + Endpoint = new Uri(config.SseConfig.EndPoint), + TransportMode = HttpTransportMode.AutoDetect, + Name = config.Name, + ConnectionTimeout = config.SseConfig.ConnectionTimeout, + AdditionalHeaders = config.SseConfig.AdditionalHeaders + }); } else if (config.StdioConfig != null) { @@ -56,14 +57,14 @@ public McpClientManager( return null; } - return await McpClientFactory.CreateAsync(transport, settings.McpClientOptions); + return await McpClient.CreateAsync(transport, settings.McpClientOptions); } catch (Exception ex) { _logger.LogWarning(ex, $"Error when loading mcp client {serverId}"); return null; } - } + } public void Dispose() { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs index e49ff3ba3..773b03183 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs @@ -1,8 +1,9 @@ using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Executor; using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Core.Routing.Executor; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs index e346ce549..2d44348af 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs @@ -1,9 +1,10 @@ using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Executor; using BotSharp.Core.MCP.Managers; -using ModelContextProtocol.Client; +using ModelContextProtocol; +using ModelContextProtocol.Protocol; using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Core.Routing.Executor; @@ -46,7 +47,10 @@ public async Task ExecuteAsync(RoleDialogModel message) var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); // Extract the text content from the result - var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); + var json = string.Join("\n", result.Content + .OfType() + .Where(c => c.Type == "text") + .Select(c => c.Text)); message.Content = json; message.Data = json.JsonContent(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 36a2dbd6a..fbf30069d 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Diagnostics; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; @@ -15,7 +14,7 @@ public async Task InvokeAgent( var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); - using var activity = ModelDiagnostics.StartAgentInvocationActivity(agentId, agent.Name, agent.Description, agent, dialogs); + using var activity = _telemetryService.StartAgentInvocationActivity(agentId, agent.Name, agent.Description, agent, dialogs); Context.IncreaseRecursiveCounter(); if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth) @@ -82,7 +81,7 @@ public async Task InvokeAgent( dialogs.Add(message); Context.AddDialogs([message]); } - activity?.SetAgentResponse(Context.GetDialogs()); + return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 4dfc0fe93..b327daff1 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; using System.Diagnostics; @@ -9,6 +10,7 @@ public partial class RoutingService : IRoutingService private readonly IServiceProvider _services; private readonly RoutingSettings _settings; private readonly IRoutingContext _context; + private readonly ITelemetryService _telemetryService; private readonly ILogger _logger; private Agent _router; @@ -19,11 +21,13 @@ public RoutingService( IServiceProvider services, RoutingSettings settings, IRoutingContext context, + ITelemetryService telemetryService, ILogger logger) { _services = services; _settings = settings; _context = context; + _telemetryService = telemetryService; _logger = logger; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs index 6136fc6c1..db5352b72 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs @@ -1,18 +1,19 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Core.Users.Services; +using BotSharp.OpenAPI.BackgroundServices; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; -using Microsoft.IdentityModel.JsonWebTokens; -using BotSharp.OpenAPI.BackgroundServices; using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Authentication; namespace BotSharp.OpenAPI; @@ -234,6 +235,7 @@ public static IApplicationBuilder UseBotSharpOpenAPI(this IApplicationBuilder ap app.UseAuthorization(); + app.UseOtelInitialize(); app.UseEndpoints( endpoints => { @@ -277,5 +279,17 @@ public static IApplicationBuilder UseBotSharpUI(this IApplicationBuilder app, bo return app; } + + internal static void UseOtelInitialize(this IApplicationBuilder app) + { + // Perform any initialization before starting the service. + // If the initialization operation fails, do not continue because we do not want + // invalid telemetry published. + var telemetryService = app.ApplicationServices.GetRequiredService(); + telemetryService.InitializeAsync() + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index a225a17b6..d7ac0b025 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -8,9 +8,8 @@ using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Dtos; using BotSharp.Core.Infrastructures; -using BotSharp.Core.Users.Services; using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.OpenAPI.Controllers; diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index 80d8709eb..76d304a89 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,6 +1,6 @@ #pragma warning disable OPENAI001 using BotSharp.Abstraction.Conversations.Enums; -using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; @@ -8,8 +8,7 @@ using BotSharp.Core.MessageHub; using OpenAI.Chat; using System.ClientModel; -using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.AzureOpenAI.Providers.Chat; @@ -18,6 +17,7 @@ public class ChatCompletionProvider : IChatCompletion protected readonly AzureOpenAiSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; protected string _model; @@ -28,11 +28,13 @@ public class ChatCompletionProvider : IChatCompletion public ChatCompletionProvider( AzureOpenAiSettings settings, ILogger logger, + ITelemetryService telemetryService, IServiceProvider services) { _settings = settings; _logger = logger; _services = services; + _telemetryService = telemetryService; } public async Task GetChatCompletions(Agent agent, List conversations) @@ -53,7 +55,7 @@ public async Task GetChatCompletions(Agent agent, List? response = null; ChatCompletion value = default; RoleDialogModel responseMessage; - using (var activity = ModelDiagnostics.StartCompletionActivity(null, _model, Provider, prompt, convService)) + using (var activity = _telemetryService.StartCompletionActivity(null, _model, Provider, conversations, convService)) { try { diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj index e3a05dd8e..8d73c6489 100644 --- a/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/BotSharp.Plugin.GiteeAI.csproj @@ -5,7 +5,7 @@ enable $(LangVersion) true - $(Ai4cVersion) + $(BotSharpVersion) $(GeneratePackageOnBuild) $(GenerateDocumentationFile) true diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs index 2b46e83fc..766fa4450 100644 --- a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,11 +1,11 @@ using BotSharp.Abstraction.Agents.Constants; -using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files; -using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.Extensions.Logging; using OpenAI.Chat; using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; +using System.Runtime; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.GiteeAI.Providers.Chat; @@ -14,13 +14,15 @@ namespace BotSharp.Plugin.GiteeAI.Providers.Chat; /// public class ChatCompletionProvider( ILogger logger, + ITelemetryService telemetryService, IServiceProvider services) : IChatCompletion { - protected string _model = string.Empty; + protected string _model = string.Empty; public virtual string Provider => "gitee-ai"; public string Model => _model; + public async Task GetChatCompletions(Agent agent, List conversations) { @@ -37,7 +39,7 @@ public async Task GetChatCompletions(Agent agent, List _logger; + protected readonly ITelemetryService _telemetryService; protected string _model; private List renderedInstructions = []; @@ -25,11 +25,13 @@ public class ChatCompletionProvider : IChatCompletion public ChatCompletionProvider( OpenAiSettings settings, ILogger logger, + ITelemetryService telemetryService, IServiceProvider services) { _settings = settings; _logger = logger; _services = services; + _telemetryService = telemetryService; } public async Task GetChatCompletions(Agent agent, List conversations) @@ -46,7 +48,7 @@ public async Task GetChatCompletions(Agent agent, List Date: Mon, 17 Nov 2025 15:26:37 +0800 Subject: [PATCH 06/18] refactor: spilt ConversationController --- .../Controllers/ConversationController.cs | 240 ++---------------- 1 file changed, 23 insertions(+), 217 deletions(-) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 6351457a9..18d17d14f 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,28 +1,24 @@ -using BotSharp.Abstraction.Chart; using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Files.Enums; -using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.MessageHub.Models; using BotSharp.Abstraction.MessageHub.Services; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Dtos; using BotSharp.Core.Infrastructures; -using BotSharp.Core.Users.Services; -using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; namespace BotSharp.OpenAPI.Controllers; [Authorize] [ApiController] -public class ConversationController : ControllerBase +public partial class ConversationController : ControllerBase { private readonly IServiceProvider _services; private readonly IUserIdentity _user; private readonly JsonSerializerOptions _jsonOptions; - public ConversationController(IServiceProvider services, + public ConversationController( + IServiceProvider services, IUserIdentity user, BotSharpOptions options) { @@ -46,12 +42,8 @@ public async Task NewConversation([FromRoute] string agen }; conv = await service.NewConversation(conv); service.SetConversationId(conv.Id, config.States); - using (var trace = new ActivitySource("BotSharp").StartActivity("NewUserSession", ActivityKind.Internal)) - { - trace?.SetTag("user_id", _user.FullName); - trace?.SetTag("conversation_id", conv.Id); - return ConversationViewModel.FromSession(conv); - } + + return ConversationViewModel.FromSession(conv); } [HttpGet("/conversations")] @@ -371,34 +363,25 @@ public async Task SendMessage( conv.SetConversationId(conversationId, input.States); SetStates(conv, input); - using (var trace = new ActivitySource("BotSharp").StartActivity("UserSession", ActivityKind.Internal)) - { - trace?.SetTag("user.id", _user.FullName); - trace?.SetTag("session.id", conversationId); - trace?.SetTag("input", inputMsg.Content); - trace?.SetTag(ModelDiagnosticsTags.AgentId, agentId); - - var response = new ChatResponseModel(); - await conv.SendMessage(agentId, inputMsg, - replyMessage: input.Postback, - async msg => - { - response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; - response.Function = msg.FunctionName; - response.MessageLabel = msg.MessageLabel; - response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; - response.Instruction = msg.Instruction; - response.Data = msg.Data; - }); + var response = new ChatResponseModel(); + await conv.SendMessage(agentId, inputMsg, + replyMessage: input.Postback, + async msg => + { + response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.Function = msg.FunctionName; + response.MessageLabel = msg.MessageLabel; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; + response.Instruction = msg.Instruction; + response.Data = msg.Data; + }); - var state = _services.GetRequiredService(); - response.States = state.GetStates(); - response.MessageId = inputMsg.MessageId; - response.ConversationId = conversationId; + var state = _services.GetRequiredService(); + response.States = state.GetStates(); + response.MessageId = inputMsg.MessageId; + response.ConversationId = conversationId; - trace?.SetTag("output", response.Data); - return response; - } + return response; } @@ -449,7 +432,7 @@ await conv.SendMessage(agentId, inputMsg, response.Instruction = msg.Instruction; response.Data = msg.Data; response.States = state.GetStates(); - + await OnChunkReceived(Response, response); }); @@ -475,183 +458,6 @@ private async Task OnReceiveToolCallIndication(string conversationId, RoleDialog } #endregion - #region Files and attachments - [HttpGet("/conversation/{conversationId}/attachments")] - public List ListAttachments([FromRoute] string conversationId) - { - var fileStorage = _services.GetRequiredService(); - var dir = fileStorage.GetDirectory(conversationId); - - // List files in the directory - var files = Directory.Exists(dir) - ? Directory.GetFiles(dir).Select(f => new MessageFileViewModel - { - FileName = Path.GetFileName(f), - FileExtension = Path.GetExtension(f).TrimStart('.').ToLower(), - ContentType = FileUtility.GetFileContentType(f), - FileDownloadUrl = $"/conversation/{conversationId}/attachments/file/{Path.GetFileName(f)}", - }).ToList() - : new List(); - - return files; - } - - [AllowAnonymous] - [HttpGet("/conversation/{conversationId}/attachments/file/{fileName}")] - public IActionResult GetAttachment([FromRoute] string conversationId, [FromRoute] string fileName) - { - var fileStorage = _services.GetRequiredService(); - var dir = fileStorage.GetDirectory(conversationId); - var filePath = Path.Combine(dir, fileName); - if (!System.IO.File.Exists(filePath)) - { - return NotFound(); - } - return BuildFileResult(filePath); - } - - [HttpPost("/conversation/{conversationId}/attachments")] - public IActionResult UploadAttachments([FromRoute] string conversationId, IFormFile[] files) - { - if (files != null && files.Length > 0) - { - var fileStorage = _services.GetRequiredService(); - var dir = fileStorage.GetDirectory(conversationId); - foreach (var file in files) - { - // Save the file, process it, etc. - var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"'); - var filePath = Path.Combine(dir, fileName); - - fileStorage.SaveFileStreamToPath(filePath, file.OpenReadStream()); - } - - return Ok(new { message = "File uploaded successfully." }); - } - - return BadRequest(new { message = "Invalid file." }); - } - - [HttpPost("/agent/{agentId}/conversation/{conversationId}/upload")] - public async Task UploadConversationMessageFiles([FromRoute] string agentId, [FromRoute] string conversationId, [FromBody] InputMessageFiles input) - { - var convService = _services.GetRequiredService(); - convService.SetConversationId(conversationId, input.States); - var conv = await convService.GetConversationRecordOrCreateNew(agentId); - var fileStorage = _services.GetRequiredService(); - var messageId = Guid.NewGuid().ToString(); - var isSaved = fileStorage.SaveMessageFiles(conv.Id, messageId, FileSource.User, input.Files); - return Ok(new { messageId = isSaved ? messageId : string.Empty }); - } - - [HttpGet("/conversation/{conversationId}/files/{messageId}/{source}")] - public IEnumerable GetConversationMessageFiles([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source) - { - var fileStorage = _services.GetRequiredService(); - var files = fileStorage.GetMessageFiles(conversationId, [messageId], options: new() { Sources = [source] }); - return files?.Select(x => MessageFileViewModel.Transform(x))?.ToList() ?? []; - } - - [HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}")] - public IActionResult GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName) - { - var fileStorage = _services.GetRequiredService(); - var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName); - if (string.IsNullOrEmpty(file)) - { - return NotFound(); - } - return BuildFileResult(file); - } - - [HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}/download")] - public IActionResult DownloadMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName) - { - var fileStorage = _services.GetRequiredService(); - var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName); - if (string.IsNullOrEmpty(file)) - { - return NotFound(); - } - - var fName = file.Split(Path.DirectorySeparatorChar).Last(); - var contentType = FileUtility.GetFileContentType(fName); - var stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); - var bytes = new byte[stream.Length]; - stream.Read(bytes, 0, (int)stream.Length); - stream.Position = 0; - - return new FileStreamResult(stream, contentType) { FileDownloadName = fName }; - } - #endregion - - #region Chart - [AllowAnonymous] - [HttpGet("/conversation/{conversationId}/message/{messageId}/user/chart/data")] - public async Task GetConversationChartData( - [FromRoute] string conversationId, - [FromRoute] string messageId, - [FromQuery] ConversationChartDataRequest request) - { - var chart = _services.GetServices().FirstOrDefault(x => x.Provider == request?.ChartProvider); - if (chart == null) return null; - - var result = await chart.GetConversationChartData(conversationId, messageId, request); - return ConversationChartDataResponse.From(result); - } - - [HttpPost("/conversation/{conversationId}/message/{messageId}/user/chart/code")] - public async Task GetConversationChartCode( - [FromRoute] string conversationId, - [FromRoute] string messageId, - [FromBody] ConversationChartCodeRequest request) - { - var chart = _services.GetServices().FirstOrDefault(x => x.Provider == request?.ChartProvider); - if (chart == null) return null; - - var result = await chart.GetConversationChartCode(conversationId, messageId, request); - return ConversationChartCodeResponse.From(result); - } - #endregion - - #region Dashboard - [HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")] - public async Task PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId) - { - var userService = _services.GetRequiredService(); - var pinned = await userService.AddDashboardConversation(conversationId); - return pinned; - } - - [HttpDelete("/agent/{agentId}/conversation/{conversationId}/dashboard")] - public async Task UnpinConversationFromDashboard([FromRoute] string agentId, [FromRoute] string conversationId) - { - var userService = _services.GetRequiredService(); - var unpinned = await userService.RemoveDashboardConversation(conversationId); - return unpinned; - } - #endregion - - #region Search state keys - [HttpGet("/conversation/state/keys")] - public async Task> GetConversationStateKeys([FromQuery] ConversationStateKeysFilter request) - { - var convService = _services.GetRequiredService(); - var keys = await convService.GetConversationStateSearhKeys(request); - return keys; - } - #endregion - - #region Migrate Latest States - [HttpPost("/conversation/latest-state/migrate")] - public async Task MigrateConversationLatestStates([FromBody] MigrateLatestStateRequest request) - { - var convService = _services.GetRequiredService(); - var res = await convService.MigrateLatestStates(request.BatchSize, request.ErrorLimit); - return res; - } - #endregion - #region Private methods private void SetStates(IConversationService conv, NewMessageModel input) { From c4965c0f5047e9ae4481f6042ebdacb8ab6ed852 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Mon, 17 Nov 2025 16:00:34 +0800 Subject: [PATCH 07/18] =?UTF-8?q?refactor=EF=BC=9A=20update=20with=20ITele?= =?UTF-8?q?metryService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Routing/Executor/FunctionCallbackExecutor.cs | 12 ++++-------- .../Routing/Executor/MCPToolExecutor.cs | 15 ++++++--------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs index 773b03183..63490d12f 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs @@ -9,22 +9,18 @@ namespace BotSharp.Core.Routing.Executor; public class FunctionCallbackExecutor : IFunctionExecutor { - /// - /// - /// for function-related activities. - /// - private static readonly ActivitySource s_activitySource = new("BotSharp.Core.Routing.Executor"); - private readonly IFunctionCallback _functionCallback; + private readonly ITelemetryService _telemetryService; - public FunctionCallbackExecutor(IFunctionCallback functionCallback) + public FunctionCallbackExecutor(ITelemetryService telemetryService, IFunctionCallback functionCallback) { _functionCallback = functionCallback; + _telemetryService = telemetryService; } public async Task ExecuteAsync(RoleDialogModel message) { - using var activity = s_activitySource.StartFunctionActivity(this._functionCallback.Name, this._functionCallback.Indication); + using var activity = _telemetryService.Parent.StartFunctionActivity(this._functionCallback.Name, this._functionCallback.Indication); { activity?.SetTag("input", message.FunctionArgs); activity?.SetTag(ModelDiagnosticsTags.AgentId, message.CurrentAgentId); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs index 2d44348af..17ba6afa1 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Routing.Executor; using BotSharp.Core.MCP.Managers; using ModelContextProtocol; @@ -13,24 +14,20 @@ public class McpToolExecutor: IFunctionExecutor private readonly IServiceProvider _services; private readonly string _mcpServerId; private readonly string _functionName; + private readonly ITelemetryService _telemetryService; + - /// - /// - /// for function-related activities. - /// - private static readonly ActivitySource s_activitySource = new("BotSharp.Core.Routing.Executor"); - - - public McpToolExecutor(IServiceProvider services, string mcpServerId, string functionName) + public McpToolExecutor(IServiceProvider services, ITelemetryService telemetryService, string mcpServerId, string functionName) { _services = services; + _telemetryService = telemetryService; _mcpServerId = mcpServerId; _functionName = functionName; } public async Task ExecuteAsync(RoleDialogModel message) { - using var activity = s_activitySource.StartFunctionActivity(this._functionName, $"calling tool {_functionName} of MCP server {_mcpServerId}"); + using var activity = _telemetryService.Parent.StartFunctionActivity(this._functionName, $"calling tool {_functionName} of MCP server {_mcpServerId}"); { try { From d9ee1868a569ed35e6f7c77cd14060c02f86e985 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Mon, 17 Nov 2025 16:11:11 +0800 Subject: [PATCH 08/18] =?UTF-8?q?feat=EF=BC=9A=20update=20With=20ITelemetr?= =?UTF-8?q?yService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Routing/Executor/FunctionExecutorFactory.cs | 7 +++++-- .../Providers/Chat/ChatCompletionProvider.cs | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs index 8a4a54865..516317c2f 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Executor; @@ -7,10 +8,12 @@ internal class FunctionExecutorFactory { public static IFunctionExecutor? Create(IServiceProvider services, string functionName, Agent agent) { + ITelemetryService telemetryService = services.GetRequiredService(); + var functionCall = services.GetServices().FirstOrDefault(x => x.Name == functionName); if (functionCall != null) { - return new FunctionCallbackExecutor(functionCall); + return new FunctionCallbackExecutor( telemetryService,functionCall); } var functions = (agent?.Functions ?? []).Concat(agent?.SecondaryFunctions ?? []); @@ -23,7 +26,7 @@ internal class FunctionExecutorFactory var mcpServerId = agent?.McpTools?.Where(x => x.Functions.Any(y => y.Name == funcDef?.Name))?.FirstOrDefault()?.ServerId; if (!string.IsNullOrWhiteSpace(mcpServerId)) { - return new McpToolExecutor(services, mcpServerId, functionName); + return new McpToolExecutor(services, telemetryService, mcpServerId, functionName); } return null; diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs index e36693b0f..a90c20001 100644 --- a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,7 +1,6 @@ using BotSharp.Abstraction.Agents.Constants; -using BotSharp.Abstraction.Diagnostics; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files; -using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.Extensions.Logging; using OpenAI.Chat; using System.Diagnostics; From 3d2cd05c7042d71336fe7c1f603fe198bb493b51 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Fri, 21 Nov 2025 17:23:49 +0800 Subject: [PATCH 09/18] feat: activity set error --- Directory.Packages.props | 53 +++++++++---------- .../Providers/Chat/ChatCompletionProvider.cs | 3 ++ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f94b3d655..8ab5b9184 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -34,9 +34,9 @@ - - - + + + @@ -99,30 +99,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index a1d35e1b9..5e50e96da 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -8,6 +8,7 @@ using BotSharp.Core.MessageHub; using OpenAI.Chat; using System.ClientModel; +using BotSharp.Abstraction.Diagnostics; using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.AzureOpenAI.Providers.Chat; @@ -112,6 +113,7 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletions(Agent agent, List Date: Sat, 22 Nov 2025 10:14:18 +0800 Subject: [PATCH 10/18] update vs project https://github.com/Azure/azure-sdk-for-net/issues/54080 --- BotSharp.sln | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/BotSharp.sln b/BotSharp.sln index ccf9b2654..8f05574e2 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.6.33712.159 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{E29DC6C4-5E57-48C5-BCB0-6B8F84782749}" EndProject @@ -149,6 +149,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandle EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.GiteeAI", "src\Plugins\BotSharp.Plugin.GiteeAI\BotSharp.Plugin.GiteeAI.csproj", "{50B57066-3267-1D10-0F72-D2F5CC494F2C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.FuzzySharp", "src\Plugins\BotSharp.Plugin.FuzzySharp\BotSharp.Plugin.FuzzySharp.csproj", "{E7C243B9-E751-B3B4-8F16-95C76CA90D31}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -629,6 +631,14 @@ Global {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.Build.0 = Release|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.ActiveCfg = Release|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.Build.0 = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.Build.0 = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.Build.0 = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.ActiveCfg = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -701,6 +711,7 @@ Global {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {FC63C875-E880-D8BB-B8B5-978AB7B62983} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} + {E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} From 2fe8b0d4d95bf98efa4ae170d1a20aa541bd50b8 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sat, 22 Nov 2025 10:35:39 +0800 Subject: [PATCH 11/18] =?UTF-8?q?fix=EF=BC=9ASerializedAdditionalRawData?= =?UTF-8?q?=20MissingMethodException?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8ab5b9184..3d3a990a0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -53,7 +53,7 @@ - + From 8116799225320400cee141c146acb21adb664b1a Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sat, 22 Nov 2025 11:28:45 +0800 Subject: [PATCH 12/18] =?UTF-8?q?feat=EF=BC=9A=20add=20chatcompletion=20ac?= =?UTF-8?q?tivity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Diagnostics/ModelDiagnostics.cs | 394 ------------------ .../Providers/ChatCompletionProvider.cs | 92 ++-- .../Providers/Chat/ChatCompletionProvider.cs | 3 +- .../Providers/Chat/ChatCompletionProvider.cs | 117 +++--- .../Providers/Chat/ChatCompletionProvider.cs | 2 +- .../Providers/Chat/ChatCompletionProvider.cs | 90 ++-- .../Providers/Chat/ChatCompletionProvider.cs | 2 +- 7 files changed, 174 insertions(+), 526 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs deleted file mode 100644 index 83f6532cb..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ModelDiagnostics.cs +++ /dev/null @@ -1,394 +0,0 @@ -using BotSharp.Abstraction.Conversations; -using BotSharp.Abstraction.Functions.Models; -using Microsoft.Extensions.DependencyInjection; -using System.Diagnostics; -using System.Text.Json; - -namespace BotSharp.Abstraction.Diagnostics; - -/// -/// Model diagnostics helper class that provides a set of methods to trace model activities with the OTel semantic conventions. -/// This class contains experimental features and may change in the future. -/// To enable these features, set one of the following switches to true: -/// `BotSharp.Experimental.GenAI.EnableOTelDiagnostics` -/// `BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive` -/// Or set the following environment variables to true: -/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS` -/// `BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE` -/// -//[System.Diagnostics.CodeAnalysis.Experimental("SKEXP0001")] -[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -public static class ModelDiagnostics -{ - private static readonly string s_namespace = typeof(ModelDiagnostics).Namespace!; - private static readonly ActivitySource s_activitySource = new(s_namespace); - - private const string EnableDiagnosticsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnostics"; - private const string EnableSensitiveEventsSwitch = "BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive"; - private const string EnableDiagnosticsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS"; - private const string EnableSensitiveEventsEnvVar = "BOTSHARP_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE"; - - private static readonly bool s_enableDiagnostics = AppContextSwitchHelper.GetConfigValue(EnableDiagnosticsSwitch, EnableDiagnosticsEnvVar); - private static readonly bool s_enableSensitiveEvents = AppContextSwitchHelper.GetConfigValue(EnableSensitiveEventsSwitch, EnableSensitiveEventsEnvVar); - - /// - /// Start a text completion activity for a given model. - /// The activity will be tagged with the a set of attributes specified by the semantic conventions. - /// - public static Activity? StartCompletionActivity( - Uri? endpoint, - string modelName, - string modelProvider, - string prompt, - IConversationStateService services - ) - { - if (!IsModelDiagnosticsEnabled()) - { - return null; - } - - const string OperationName = "text.completions"; - var activity = s_activitySource.StartActivityWithTags( - $"{OperationName} {modelName}", - [ - new(ModelDiagnosticsTags.Operation, OperationName), - new(ModelDiagnosticsTags.System, modelProvider), - new(ModelDiagnosticsTags.Model, modelName), - ], - ActivityKind.Client); - - if (endpoint is not null) - { - activity?.SetTags([ - // Skip the query string in the uri as it may contain keys - new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), - new(ModelDiagnosticsTags.Port, endpoint.Port), - ]); - } - - AddOptionalTags(activity, services); - - if (s_enableSensitiveEvents) - { - activity?.AttachSensitiveDataAsEvent( - ModelDiagnosticsTags.UserMessage, - [ - new(ModelDiagnosticsTags.EventName, prompt), - new(ModelDiagnosticsTags.System, modelProvider), - ]); - } - - return activity; - } - - /// - /// Start a chat completion activity for a given model. - /// The activity will be tagged with the a set of attributes specified by the semantic conventions. - /// - public static Activity? StartCompletionActivity( - Uri? endpoint, - string modelName, - string modelProvider, - List chatHistory, - IConversationStateService conversationStateService - ) - - { - if (!IsModelDiagnosticsEnabled()) - { - return null; - } - - const string OperationName = "chat.completions"; - var activity = s_activitySource.StartActivityWithTags( - $"{OperationName} {modelName}", - [ - new(ModelDiagnosticsTags.Operation, OperationName), - new(ModelDiagnosticsTags.System, modelProvider), - new(ModelDiagnosticsTags.Model, modelName), - ], - ActivityKind.Client); - - if (endpoint is not null) - { - activity?.SetTags([ - // Skip the query string in the uri as it may contain keys - new(ModelDiagnosticsTags.Address, endpoint.GetLeftPart(UriPartial.Path)), - new(ModelDiagnosticsTags.Port, endpoint.Port), - ]); - } - - AddOptionalTags(activity, conversationStateService); - - if (s_enableSensitiveEvents) - { - foreach (var message in chatHistory) - { - var formattedContent = JsonSerializer.Serialize(ToGenAIConventionsFormat(message)); - activity?.AttachSensitiveDataAsEvent( - ModelDiagnosticsTags.RoleToEventMap[message.Role], - [ - new(ModelDiagnosticsTags.EventName, formattedContent), - new(ModelDiagnosticsTags.System, modelProvider), - ]); - } - } - - return activity; - } - - /// - /// Start an agent invocation activity and return the activity. - /// - public static Activity? StartAgentInvocationActivity( - string agentId, - string agentName, - string? agentDescription, - Agent? agents, - List messages - ) - { - if (!IsModelDiagnosticsEnabled()) - { - return null; - } - - const string OperationName = "invoke_agent"; - - var activity = s_activitySource.StartActivityWithTags( - $"{OperationName} {agentName}", - [ - new(ModelDiagnosticsTags.Operation, OperationName), - new(ModelDiagnosticsTags.AgentId, agentId), - new(ModelDiagnosticsTags.AgentName, agentName) - ], - ActivityKind.Internal); - - if (!string.IsNullOrWhiteSpace(agentDescription)) - { - activity?.SetTag(ModelDiagnosticsTags.AgentDescription, agentDescription); - } - - if (agents is not null && (agents.Functions.Count > 0 || agents.SecondaryFunctions.Count >0)) - { - List allFunctions = []; - allFunctions.AddRange(agents.Functions); - allFunctions.AddRange(agents.SecondaryFunctions); - - activity?.SetTag( - ModelDiagnosticsTags.AgentToolDefinitions, - JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); - } - - if (IsSensitiveEventsEnabled()) - { - activity?.SetTag( - ModelDiagnosticsTags.AgentInvocationInput, - JsonSerializer.Serialize(messages.Select(m => ToGenAIConventionsFormat(m)))); - } - - return activity; - } - - /// - /// Set the agent response for a given activity. - /// - public static void SetAgentResponse(this Activity activity, IEnumerable? responses) - { - if (!IsModelDiagnosticsEnabled() || responses is null) - { - return; - } - - if (s_enableSensitiveEvents) - { - activity?.SetTag( - ModelDiagnosticsTags.AgentInvocationOutput, - JsonSerializer.Serialize(responses.Select(r => ToGenAIConventionsFormat(r)))); - } - } - - - - /// - /// Set the response id for a given activity. - /// - /// The activity to set the response id - /// The response id - /// The activity with the response id set for chaining - internal static Activity SetResponseId(this Activity activity, string responseId) => activity.SetTag(ModelDiagnosticsTags.ResponseId, responseId); - - /// - /// Set the input tokens usage for a given activity. - /// - /// The activity to set the input tokens usage - /// The number of input tokens used - /// The activity with the input tokens usage set for chaining - internal static Activity SetInputTokensUsage(this Activity activity, int inputTokens) => activity.SetTag(ModelDiagnosticsTags.InputTokens, inputTokens); - - /// - /// Set the output tokens usage for a given activity. - /// - /// The activity to set the output tokens usage - /// The number of output tokens used - /// The activity with the output tokens usage set for chaining - internal static Activity SetOutputTokensUsage(this Activity activity, int outputTokens) => activity.SetTag(ModelDiagnosticsTags.OutputTokens, outputTokens); - - /// - /// Check if model diagnostics is enabled - /// Model diagnostics is enabled if either EnableModelDiagnostics or EnableSensitiveEvents is set to true and there are listeners. - /// - internal static bool IsModelDiagnosticsEnabled() - { - return (s_enableDiagnostics || s_enableSensitiveEvents) && s_activitySource.HasListeners(); - } - - /// - /// Check if sensitive events are enabled. - /// Sensitive events are enabled if EnableSensitiveEvents is set to true and there are listeners. - /// - internal static bool IsSensitiveEventsEnabled() => s_enableSensitiveEvents && s_activitySource.HasListeners(); - - internal static bool HasListeners() => s_activitySource.HasListeners(); - - #region Private - private static void AddOptionalTags(Activity? activity, IConversationStateService conversationStateService) - { - if (activity is null) - { - return; - } - - void TryAddTag(string key, string tag) - { - var value = conversationStateService.GetState(key); - if (!string.IsNullOrEmpty(value)) - { - activity.SetTag(tag, value); - } - } - - TryAddTag("max_tokens", ModelDiagnosticsTags.MaxToken); - TryAddTag("temperature", ModelDiagnosticsTags.Temperature); - TryAddTag("top_p", ModelDiagnosticsTags.TopP); - } - - /// - /// Convert a chat message to a JSON object based on the OTel GenAI Semantic Conventions format - /// - private static object ToGenAIConventionsFormat(RoleDialogModel chatMessage) - { - return new - { - role = chatMessage.Role.ToString(), - name = chatMessage.MessageId, - content = chatMessage.Content, - tool_calls = ToGenAIConventionsToolCallFormat(chatMessage), - }; - } - - /// - /// Helper method to convert tool calls to a list of JSON object based on the OTel GenAI Semantic Conventions format - /// - private static List ToGenAIConventionsToolCallFormat(RoleDialogModel chatMessage) - { - List toolCalls = []; - if (chatMessage.Instruction is not null) - { - toolCalls.Add(new - { - id = chatMessage.ToolCallId, - function = new - { - name = chatMessage.Instruction.Function, - arguments = chatMessage.Instruction.Arguments - }, - type = "function" - }); - } - return toolCalls; - } - - /// - /// Convert a function metadata to a JSON object based on the OTel GenAI Semantic Conventions format - /// - private static object ToGenAIConventionsFormat(FunctionDef metadata) - { - var properties = metadata.Parameters?.Properties; - var required = metadata.Parameters?.Required; - - return new - { - type = "function", - name = metadata.Name, - description = metadata.Description, - parameters = new - { - type = "object", - properties, - required, - } - }; - } - - /// - /// Convert a chat model response to a JSON string based on the OTel GenAI Semantic Conventions format - /// - private static string ToGenAIConventionsChoiceFormat(RoleDialogModel chatMessage, int index) - { - var jsonObject = new - { - index, - message = ToGenAIConventionsFormat(chatMessage), - tool_calls = ToGenAIConventionsToolCallFormat(chatMessage) - }; - - return JsonSerializer.Serialize(jsonObject); - } - - - - /// - /// Tags used in model diagnostics - /// - public static class ModelDiagnosticsTags - { - // Activity tags - public const string System = "gen_ai.system"; - public const string Operation = "gen_ai.operation.name"; - public const string Model = "gen_ai.request.model"; - public const string MaxToken = "gen_ai.request.max_tokens"; - public const string Temperature = "gen_ai.request.temperature"; - public const string TopP = "gen_ai.request.top_p"; - public const string ResponseId = "gen_ai.response.id"; - public const string ResponseModel = "gen_ai.response.model"; - public const string FinishReason = "gen_ai.response.finish_reason"; - public const string InputTokens = "gen_ai.usage.input_tokens"; - public const string OutputTokens = "gen_ai.usage.output_tokens"; - public const string Address = "server.address"; - public const string Port = "server.port"; - public const string AgentId = "gen_ai.agent.id"; - public const string AgentName = "gen_ai.agent.name"; - public const string AgentDescription = "gen_ai.agent.description"; - public const string AgentInvocationInput = "gen_ai.input.messages"; - public const string AgentInvocationOutput = "gen_ai.output.messages"; - public const string AgentToolDefinitions = "gen_ai.tool.definitions"; - - // Activity events - public const string EventName = "gen_ai.event.content"; - public const string SystemMessage = "gen_ai.system.message"; - public const string UserMessage = "gen_ai.user.message"; - public const string AssistantMessage = "gen_ai.assistant.message"; - public const string ToolMessage = "gen_ai.tool.message"; - public const string Choice = "gen_ai.choice"; - public static readonly Dictionary RoleToEventMap = new() - { - { AgentRole.System, SystemMessage }, - { AgentRole.User, UserMessage }, - { AgentRole.Assistant, AssistantMessage }, - { AgentRole.Function, ToolMessage } - }; - } - # endregion -} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs index d6b4c4107..571f01232 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Providers/ChatCompletionProvider.cs @@ -1,11 +1,13 @@ using Anthropic.SDK.Common; using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.AnthropicAI.Providers; @@ -17,22 +19,26 @@ public class ChatCompletionProvider : IChatCompletion protected readonly AnthropicSettings _settings; protected readonly IServiceProvider _services; protected readonly ILogger _logger; + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; protected string _model; public ChatCompletionProvider(AnthropicSettings settings, ILogger logger, + ITelemetryService telemetryService, IServiceProvider services) { _settings = settings; _logger = logger; _services = services; + _telemetryService = telemetryService; } public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -45,53 +51,61 @@ public async Task GetChatCompletions(Agent agent, List().FirstOrDefault(); - var toolResult = response.Content.OfType().First(); - responseMessage = new RoleDialogModel(AgentRole.Function, content?.Text ?? string.Empty) + var response = await client.Messages.GetClaudeMessageAsync(parameters); + + RoleDialogModel responseMessage; + activity?.SetTag(ModelDiagnosticsTags.FinishReason, response.StopReason); + if (response.StopReason == "tool_use") { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - ToolCallId = toolResult.Id, - FunctionName = toolResult.Name, - FunctionArgs = JsonSerializer.Serialize(toolResult.Input), - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } - else - { - var message = response.FirstMessage; - responseMessage = new RoleDialogModel(AgentRole.Assistant, message?.Text ?? string.Empty) + var content = response.Content.OfType().FirstOrDefault(); + var toolResult = response.Content.OfType().First(); + + responseMessage = new RoleDialogModel(AgentRole.Function, content?.Text ?? string.Empty) + { + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + ToolCallId = toolResult.Id, + FunctionName = toolResult.Name, + FunctionArgs = JsonSerializer.Serialize(toolResult.Input), + RenderedInstruction = string.Join("\r\n", renderedInstructions) + }; + } + else { - CurrentAgentId = agent.Id, - MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, - RenderedInstruction = string.Join("\r\n", renderedInstructions) - }; - } + 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) + }; + } - var tokenUsage = response.Usage; + var tokenUsage = response.Usage; + var inputTokenDetails = tokenUsage?.InputTokens ?? 0; + var outputTokenDetails = tokenUsage?.OutputTokens ?? 0; + var cachedInputTokens = tokenUsage?.CacheReadInputTokens ; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (inputTokenDetails - cachedInputTokens)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, outputTokenDetails); + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = tokenUsage?.InputTokens ?? 0, - TextOutputTokens = tokenUsage?.OutputTokens ?? 0 - }); + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = tokenUsage?.InputTokens ?? 0, + TextOutputTokens = tokenUsage?.OutputTokens ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); + return responseMessage; } - - return responseMessage; } public Task GetChatCompletionsAsync(Agent agent, List conversations, diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index 5e50e96da..8c1c4d7d9 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -131,8 +131,7 @@ public async Task GetChatCompletions(Agent agent, List _logger; + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; protected string _model; @@ -25,15 +28,18 @@ public class ChatCompletionProvider : IChatCompletion public ChatCompletionProvider( IServiceProvider services, + ITelemetryService telemetryService, ILogger logger) { _services = services; + _telemetryService = telemetryService; _logger = logger; } public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -44,68 +50,75 @@ public async Task GetChatCompletions(Agent agent, List new ChatAnnotation + responseMessage = new RoleDialogModel(AgentRole.Assistant, text) { - Title = x.WebResourceTitle, - Url = x.WebResourceUri.AbsoluteUri, - StartIndex = x.StartIndex, - EndIndex = x.EndIndex - })?.ToList() - }; - } + CurrentAgentId = agent.Id, + MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty, + RenderedInstruction = string.Join("\r\n", renderedInstructions), + Annotations = value.Annotations?.Select(x => new ChatAnnotation + { + Title = x.WebResourceTitle, + Url = x.WebResourceUri.AbsoluteUri, + StartIndex = x.StartIndex, + EndIndex = x.EndIndex + })?.ToList() + }; + } - var tokenUsage = response?.Value?.Usage; - var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; + var tokenUsage = response?.Value?.Usage; + var inputTokenDetails = response?.Value?.Usage?.InputTokenDetails; - // After chat completion hook - foreach (var hook in contentHooks) - { - await hook.AfterGenerated(responseMessage, new TokenStatsModel + activity?.SetTag(ModelDiagnosticsTags.InputTokens, (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0)); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + activity?.SetTag(ModelDiagnosticsTags.OutputTokens, tokenUsage?.OutputTokenCount ?? 0); + + // After chat completion hook + foreach (var hook in contentHooks) { - Prompt = prompt, - Provider = Provider, - Model = _model, - TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), - CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, - TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 - }); + await hook.AfterGenerated(responseMessage, new TokenStatsModel + { + Prompt = prompt, + Provider = Provider, + Model = _model, + TextInputTokens = (tokenUsage?.InputTokenCount ?? 0) - (inputTokenDetails?.CachedTokenCount ?? 0), + CachedTextInputTokens = inputTokenDetails?.CachedTokenCount ?? 0, + TextOutputTokens = tokenUsage?.OutputTokenCount ?? 0 + }); + } + activity?.SetTag("output", responseMessage.Content); + return responseMessage; } - - return responseMessage; } public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) diff --git a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs index a90c20001..96152c053 100644 --- a/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GiteeAI/Providers/Chat/ChatCompletionProvider.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; using OpenAI.Chat; using System.Diagnostics; -using static BotSharp.Abstraction.Diagnostics.ModelDiagnostics; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.GiteeAI.Providers.Chat; diff --git a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs index d224fb122..44563fa44 100644 --- a/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Diagnostics.Telemetry; using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Files.Utilities; @@ -5,6 +6,7 @@ using GenerativeAI; using GenerativeAI.Core; using GenerativeAI.Types; +using static BotSharp.Abstraction.Diagnostics.Telemetry.TelemetryConstants; namespace BotSharp.Plugin.GoogleAi.Providers.Chat; @@ -12,6 +14,8 @@ public class ChatCompletionProvider : IChatCompletion { private readonly IServiceProvider _services; private readonly ILogger _logger; + + protected readonly ITelemetryService _telemetryService; private List renderedInstructions = []; private string _model; @@ -22,10 +26,12 @@ public class ChatCompletionProvider : IChatCompletion private GoogleAiSettings _settings; public ChatCompletionProvider( IServiceProvider services, + ITelemetryService telemetryService, GoogleAiSettings googleSettings, ILogger logger) { _settings = googleSettings; + _telemetryService = telemetryService; _services = services; _logger = logger; } @@ -33,6 +39,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) @@ -43,49 +50,58 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs index abb592ae4..c7a9cd36c 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -38,7 +38,7 @@ public ChatCompletionProvider( public async Task GetChatCompletions(Agent agent, List conversations) { var contentHooks = _services.GetHooks(agent.Id); - var convService = _services.GetService(); + var convService = _services.GetRequiredService(); // Before chat completion hook foreach (var hook in contentHooks) From 5dc6e88ddbfbefc49fb5f974ec804908558508e4 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sat, 22 Nov 2025 12:32:51 +0800 Subject: [PATCH 13/18] =?UTF-8?q?fix=EF=BC=9A=20build=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 4 +++- .../BotSharp.PizzaBot.MCPServer.csproj | 7 ++----- tests/UnitTest/UnitTest.csproj | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d0c878d52..edeb9dfe3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -156,11 +156,13 @@ - + + + diff --git a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj index 0e7f08a74..669255e86 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj +++ b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj @@ -1,17 +1,14 @@ - + - net8.0 + $(TargetFramework) enable 12.0 enable - - - diff --git a/tests/UnitTest/UnitTest.csproj b/tests/UnitTest/UnitTest.csproj index f76867360..90f903d4e 100644 --- a/tests/UnitTest/UnitTest.csproj +++ b/tests/UnitTest/UnitTest.csproj @@ -12,7 +12,6 @@ - all From 92809237676ec0df8c64cd8e03f6d781f04baaef Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sat, 22 Nov 2025 12:34:45 +0800 Subject: [PATCH 14/18] Delete LangfuseSettings.cs --- .../LangfuseSettings.cs | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/BotSharp.ServiceDefaults/LangfuseSettings.cs diff --git a/src/BotSharp.ServiceDefaults/LangfuseSettings.cs b/src/BotSharp.ServiceDefaults/LangfuseSettings.cs deleted file mode 100644 index 4c79832c6..000000000 --- a/src/BotSharp.ServiceDefaults/LangfuseSettings.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace BotSharp.Langfuse; - -/// -/// Langfuse Settings -/// -public class LangfuseSettings -{ - public string SecretKey { get; set; } - - public string PublicKey { get; set; } - - public string Host { get; set; } -} From 3288bc5c6ae21de260f99158b1a6135deb58819d Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sat, 22 Nov 2025 12:37:54 +0800 Subject: [PATCH 15/18] Update Extensions.cs --- src/BotSharp.ServiceDefaults/Extensions.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/BotSharp.ServiceDefaults/Extensions.cs b/src/BotSharp.ServiceDefaults/Extensions.cs index f470eac1f..44548298b 100644 --- a/src/BotSharp.ServiceDefaults/Extensions.cs +++ b/src/BotSharp.ServiceDefaults/Extensions.cs @@ -1,14 +1,9 @@ -using BotSharp.Langfuse; using Langfuse.OpenTelemetry; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; -using OpenTelemetry; -using OpenTelemetry.Exporter; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; From b167c5370143fddc8e04dcb7cd487cf538430ee8 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Tue, 25 Nov 2025 08:38:46 +0800 Subject: [PATCH 16/18] Update BotSharp.sln --- BotSharp.sln | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/BotSharp.sln b/BotSharp.sln index 304e81d3f..e502b4b12 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -150,6 +150,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.FuzzySharp" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Membase", "src\Plugins\BotSharp.Plugin.Membase\BotSharp.Plugin.Membase.csproj", "{13223C71-9EAC-9835-28ED-5A4833E6F915}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{C548FDFF-B882-B552-D428-5C8EC4478187}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -638,6 +640,14 @@ Global {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|Any CPU.Build.0 = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.ActiveCfg = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.Build.0 = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.ActiveCfg = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.Build.0 = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.Build.0 = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.ActiveCfg = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -711,6 +721,7 @@ Global {50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA} {E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89} + {C548FDFF-B882-B552-D428-5C8EC4478187} = {51AFE054-AE99-497D-A593-69BAEFB5106F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} From 62f9066d835a1e0bc6be6a74c8726109ac0d9f17 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Wed, 26 Nov 2025 12:05:01 +0800 Subject: [PATCH 17/18] Update Extensions.cs --- src/BotSharp.ServiceDefaults/Extensions.cs | 31 +++------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/src/BotSharp.ServiceDefaults/Extensions.cs b/src/BotSharp.ServiceDefaults/Extensions.cs index 44548298b..b4790f3e7 100644 --- a/src/BotSharp.ServiceDefaults/Extensions.cs +++ b/src/BotSharp.ServiceDefaults/Extensions.cs @@ -131,35 +131,10 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli { builder.Services.Configure(logging => logging.AddOtlpExporter()); builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - //if (useLangfuse) - //{ - // var publicKey = langfuseSection.GetValue(nameof(LangfuseSettings.PublicKey)) ?? string.Empty; - // var secretKey = langfuseSection.GetValue(nameof(LangfuseSettings.SecretKey)) ?? string.Empty; - // var host = langfuseSection.GetValue(nameof(LangfuseSettings.Host)) ?? string.Empty; - // var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}"); - // string base64EncodedAuth = Convert.ToBase64String(plainTextBytes); - - // builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter(options => - // { - // options.Endpoint = new Uri(host); - // options.Protocol = OtlpExportProtocol.HttpProtobuf; - // options.Headers = $"Authorization=Basic {base64EncodedAuth}"; - // }) - // ); - - //} - //else - //{ - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); - //} + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} + return builder; } From d0fe71bf8b95aaea9b33efd9a569d1f7ba263396 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 15 Jan 2026 16:52:25 +0800 Subject: [PATCH 18/18] save --- .../features/anthropic-agent-skills/README.md | 38 ++ .../features/anthropic-agent-skills/design.md | 203 +++++++++ .../anthropic-agent-skills/requirements.md | 43 ++ .../features/anthropic-agent-skills/tasks.md | 30 ++ .vscode/extensions.json | 19 + .vscode/mcp.json | 13 + .vscode/settings.json | 182 ++++++++ BotSharp.sln | 280 +++++++++++++ Directory.Packages.props | 395 +++++++++--------- skills/test-skill/SKILL.md | 8 + skills/test-skill/scripts/hello.py | 4 + .../AgentSkillsPlugin.cs | 38 ++ .../BotSharp.Plugin.AgentSkills.csproj | 20 + .../Functions/LoadSkillFn.cs | 55 +++ .../Functions/RunSkillScriptFn.cs | 77 ++++ .../Hooks/AgentSkillHook.cs | 68 +++ .../Models/AgentSkill.cs | 13 + .../Models/SkillFrontmatter.cs | 12 + .../Services/AgentSkillService.cs | 183 ++++++++ .../Services/IAgentSkillService.cs | 13 + .../Settings/AgentSkillsSettings.cs | 6 + .../Interfaces/IPyScriptRunner.cs | 8 + .../PythonInterpreterPlugin.cs | 4 +- .../Services/PyScriptRunner.cs | 74 ++++ src/WebStarter/WebStarter.csproj | 4 +- src/WebStarter/appsettings.json | 45 +- tests/UnitTest/AgentSkillsTest.cs | 92 ++++ tests/UnitTest/UnitTest.csproj | 7 + 28 files changed, 1711 insertions(+), 223 deletions(-) create mode 100644 .spec-coding/features/anthropic-agent-skills/README.md create mode 100644 .spec-coding/features/anthropic-agent-skills/design.md create mode 100644 .spec-coding/features/anthropic-agent-skills/requirements.md create mode 100644 .spec-coding/features/anthropic-agent-skills/tasks.md create mode 100644 .vscode/extensions.json create mode 100644 .vscode/mcp.json create mode 100644 .vscode/settings.json create mode 100644 skills/test-skill/SKILL.md create mode 100644 skills/test-skill/scripts/hello.py create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/BotSharp.Plugin.AgentSkills.csproj create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Functions/LoadSkillFn.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Functions/RunSkillScriptFn.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Hooks/AgentSkillHook.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Models/AgentSkill.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Models/SkillFrontmatter.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Services/AgentSkillService.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Services/IAgentSkillService.cs create mode 100644 src/Plugins/BotSharp.Plugin.AgentSkills/Settings/AgentSkillsSettings.cs create mode 100644 src/Plugins/BotSharp.Plugin.PythonInterpreter/Interfaces/IPyScriptRunner.cs create mode 100644 src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyScriptRunner.cs create mode 100644 tests/UnitTest/AgentSkillsTest.cs diff --git a/.spec-coding/features/anthropic-agent-skills/README.md b/.spec-coding/features/anthropic-agent-skills/README.md new file mode 100644 index 000000000..65c93f0e8 --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/README.md @@ -0,0 +1,38 @@ +# Anthropic Agent Skills 需求文档 + +## 1. 概述 +在 BotSharp 中实现 Antrhopic 推出的 "Agent Skills" 标准。该标准采用“文件系统中心化”设计和“渐进式披露”范式。Agent 不会在初始阶段加载所有工具,而是根据任务需求动态发现、加载并执行能力。 + +## 2. 核心组件 (基于文件系统的技能单元) +每个 Skill 是一个独立的目录,包含以下核心文件: +* **`SKILL.md`**: 核心定义文件。 + * **YAML Frontmatter**: 包含 `name` (唯一标识) 和 `description` (用于发现)。 + * **Markdown Body**: 包含详细的 SOP (标准作业程序/指令)。这部分仅在加载后对 LLM 可见。 +* **`scripts/`**: 包含可执行逻辑的代码文件 (如 `.py`, `.sh`)。 +* **`resources/`**: (可选) 静态资源文件。 + +## 3. 交互生命周期 (The Progressive Paradigm) +系统需支持以下四个关键阶段: + +1. **索引 (Indexing)**: + * 系统需扫描指定目录下所有的 Skill 文件夹。 + * 解析每个 Skill 的 `SKILL.md` 中的 YAML 头信息。 + +2. **感知 (Awareness)**: + * 将所有已发现 Skill 的 `name` 和 `description` 注入到 Agent 的初始 System Prompt 中。 + * 目的:让 LLM 知道有哪些技能“可用”,但不知道具体的指令细节。 + +3. **加载 (Loading)**: + * **机制**: 提供内置工具 `load_skill(skill_name)` (或类似命名)。 + * **流程**: 当 LLM 决定使用某技能并调用此工具时,系统读取对应 `SKILL.md` 的 Markdown 正文。 + * **结果**: 将正文内容追加到当前的对话上下文中 (Context),使 LLM 获得执行该任务的详细 SOP。 + +4. **调用 (Invocation)**: + * **机制**: 提供内置工具 `run_skill_script(skill_name, script_file, args)` (方案A)。 + * **流程**: LLM 根据 SOP 中的指示,构造参数调用此工具。 + * **执行**: 系统在宿主环境中执行对应的 `scripts/` 下的代码文件 (支持 Python 等),并将标准输出作为结果返回给 LLM。 + +## 4. 技术约束 +* **插件化**: 实现为 `BotSharp.Plugin.AgentSkills`。 +* **脚本执行**: 需复用或扩展现有的 Python 执行能力,支持“预定义代码”的运行,而非仅支持 REPL 模式。 +* **配置**: 需支持配置 Skill 库的根路径。 diff --git a/.spec-coding/features/anthropic-agent-skills/design.md b/.spec-coding/features/anthropic-agent-skills/design.md new file mode 100644 index 000000000..9b14772ea --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/design.md @@ -0,0 +1,203 @@ +# Anthropic Agent Skills 设计文档 (Design) + +## 1. 概述 (Overview) +本设计旨在 BotSharp 中实现 Agent Skills 标准。通过一个新的插件 `BotSharp.Plugin.AgentSkills`,提供技能发现、动态加载和脚本执行的能力。该设计遵循“渐进式披露”原则,降低 Agent 的初始上下文负载。 + +## 2. 架构 (Architecture) +架构主要由以下几个核心组件组成: + +1. **AgentSkillPlugin**: 插件入口,注册服务、配置和钩子。 +2. **AgentSkillService**: 核心服务,负责扫描文件系统、解析 `SKILL.md`、缓存技能元数据。 +3. **AgentSkillHook**: 实现 `IAgentHook`,在 Agent 加载阶段注入可用技能的摘要(Awareness)。 +4. **Function Tools**: + * `LoadSkillFn`: 实现 `IFunctionCallback`,用于动态加载技能详情。 + * `RunSkillScriptFn`: 实现 `IFunctionCallback`,用于执行技能脚本。 +5. **ScriptExecutor**: 负责安全地在宿主环境中执行脚本(主要是 Python)。 + +### 2.1 依赖关系 +* `BotSharp.Abstraction`: 核心接口 (`IBotSharpPlugin`, `IAgentHook`, `IFunctionCallback`). +* `BotSharp.Plugin.PythonInterpreter`: 用于执行 Python 脚本。需要扩展以支持执行预定义文件。 +* `YamlDotNet`: 用于解析 YAML Frontmatter。 +* `System.Diagnostics.Process`: 用于执行外部脚本。 + +## 3. 组件与接口 (Components & Interfaces) + +### 3.1 数据模型 (Models) + +为了准确解析 `SKILL.md` 的 YAML Frontmatter,我们需要定义对应的强类型模型: + +```csharp +public class AgentSkill +{ + public string Name { get; set; } + public string Description { get; set; } + public string MarkdownBody { get; set; } // 正文指令内容 + public string BaseDir { get; set; } // 技能所在的物理目录路径 + public List Scripts { get; set; } // 可执行脚本列表 + public List Resources { get; set; } // 静态资源列表 +} + +/// +/// 用于 YAML 反序列化的中间模型 +/// +public class SkillFrontmatter +{ + [YamlMember(Alias = "name")] + public string Name { get; set; } + + [YamlMember(Alias = "description")] + public string Description { get; set; } +} + +public class AgentSkillsSettings +{ + public string DataDir { get; set; } = "skills"; +} +``` + +### 3.2 核心服务 (AgentSkillService) + +`AgentSkillService` 应该注册为 **Singleton**,以维护技能缓存并处理热重载。 + +* **职责**: + 1. 启动时递归扫描配置的 Skills 目录。 + 2. 解析 `SKILL.md` 并构建 `AgentSkill` 对象。 + 3. 维护 `ConcurrentDictionary` 缓存。 + 4. 使用 `FileSystemWatcher` 监听 `*.md` 文件的变更,实现 Hot Reload。 + +```csharp +public interface IAgentSkillService +{ + // 扫描并返回所有可用技能的摘要(Name & Description) + Task> GetAvailableSkills(); + + // 获取特定技能的详细信息(包含 MarkdownBody) + Task GetSkill(string name); + + // 验证并获取脚本路径 + string GetScriptPath(string skillName, string scriptFile); + + // 手动刷新缓存(可选,配合 Watcher 使用) + Task RefreshSkills(); +} +``` + +### 3.3 生命周期钩子 (AgentSkillHook) (实现细节) + +**阶段一:发现 (Discovery)** +* **Hook**: `OnInstructionLoaded` +* **逻辑**: + 1. 调用 `IAgentSkillService.GetAvailableSkills()`。 + 2. 构造“技能菜单”提示词: + ```text + You have access to the following specialized skills. If a task requires one, call the 'load_skill' function with the skill name. + + - pdf-processing: Handles PDF files... + - data-analysis: Performs statistical analysis... + ``` + 3. 此段提示词作为 System Instruction 的一部分注入。 + +**阶段二:激活 (Activation)** +* **Hook**: `OnInstructionLoaded` (再次触发时) +* **逻辑**: + 1. 检查 `conversation_state` 中的 `active_skills` 列表。 + 2. 如果发现已激活的技能(例如 "pdf-processing"),从 `AgentSkillService` 获取其 `MarkdownBody`。 + 3. 将正文注入 Prompt: + ```text + ### ACTIVE SKILL: PDF PROCESSING + (Markdown content...) + ``` + +**阶段三:执行 (Execution)** +* **Hook**: `OnFunctionsLoaded` +* **逻辑**: + 1. 虽然 `run_skill_script` 是通用函数,但可以在此 Hook 中做权限校验或动态提示。 + 2. 确保 `load_skill` 和 `run_skill_script` 工具已添加到当前 Agent 的功能列表中。 + 3. (可选) 遍历 `active_skills`,显式提示用户当前可执行的脚本列表(虽然主要依靠 Markdown Body 中的 SOP 指引)。 + +### 3.4 工具 (Functions) + +#### LoadSkillFn +* **Name**: `load_skill` +* **Logic**: + 1. 接收 `skill_name`。 + 2. 更新 `conversation_state["active_skills"]`,加入该技能。 + 3. **关键**: 返回提示 "Skill '{skill_name}' activated. Please continue.",促使 LLM 在下一轮对话中利用新注入的 Prompt(因 `OnInstructionLoaded` 会在每轮生成 Prompt 时运行)。 + +#### RunSkillScriptFn +* **Name**: `run_skill_script` +* **Input**: `skill_name`, `script_file`, `args` (string/json) +* **Logic**: + 1. 验证 `skill_name` 和 `script_file` 合法性(防止目录遍历)。 + 2. 定位物理文件路径。 + 3. 启动 `python` 进程执行脚本。 + 4. 捕获 Standard Output 和 Standard Error。 + +### 3.5 脚本执行增强 (BotSharp.Plugin.PythonInterpreter Extension) + +为了支持执行预定义的本地 Python 文件,我们需要扩展 `PyCodeInterpreter` 或提供新的服务接口。 + +* **新增接口**: `IPyScriptExecutor` (或在 `ICodeProcessor` 中增加方法) + ```csharp + public interface IPyScriptRunner + { + // 直接运行指定路径的 py 文件,并传入参数 + Task RunScript(string filePath, string args); + } + ``` +* **实现逻辑**: + * 复用现有的 Python 环境配置 (`PythonInterpreterSettings`)。 + * 使用 `Process.Start` 启动 python 进程。 + * Argument 传递:`python "path/to/script.py" "args_string"`。 + * 在 `AgentSkill` 插件中,通过 DI 获取此服务来执行 `RunSkillScriptFn` 中的请求。 + +## 4. 流程设计 (Flows) + +### 4.1 总体交互流程 + +```mermaid +sequenceDiagram + participant User + participant Agent + participant SkillHook + participant SkillService + participant LoadFn + participant RunFn + participant FileSystem + + User->>Agent: "Start conversation" + Agent->>SkillHook: OnInstructionLoaded + SkillHook->>SkillService: GetAvailableSkills() + SkillService->>FileSystem: Scan SKILL.md + SkillService-->>SkillHook: List + SkillHook-->>Agent: Inject "Available Skills..." + + User->>Agent: "Help me process this PDF..." + Agent->>Agent: Decide to use 'pdf-processing' + Agent->>LoadSkillFn: Call load_skill("pdf-processing") + LoadSkillFn->>SkillService: GetSkill("pdf-processing") + SkillService->>FileSystem: Read Markdown Body + LoadSkillFn-->>Agent: Return Instructions (SOP) + + Agent->>Agent: Read SOP, decide to run script + Agent->>RunSkillScriptFn: Call run_skill_script("pdf-processing", "analyze.py", ...) + RunSkillScriptFn->>FileSystem: Execute script + FileSystem-->>RunSkillScriptFn: StdOut / StdErr + RunSkillScriptFn-->>Agent: Return Result + Agent-->>User: "Here is the result..." +``` + +## 5. 错误处理 (Error Handling) +* **文件未找到**: 如果加载不存在的 Skill,返回明确错误 "Skill not found". +* **脚本安全**: + * 严禁 `script_file` 包含 `..` 或绝对路径。 + * 仅允许执行 `scripts/` 子目录下的文件。 +* **执行失败**: 脚本退出码非 0 时,捕获 Stderr 并以 "Execution Failed: ..." 格式返回。 + +## 6. 测试策略 (Testing Strategy) +* **单元测试**: 针对 `AgentSkillService` 的 YAML 解析逻辑。 +* **集成测试**: + * 构建一个真实的 `skills/test-skill` 目录。 + * 模拟 Agent 调用流程,验证 System Prompt 注入是否正确。 + * 验证 `load_skill` 是否返回各个 section。 + * 验证 `run_skill_script` 能否执行简单的 python print 脚本。 diff --git a/.spec-coding/features/anthropic-agent-skills/requirements.md b/.spec-coding/features/anthropic-agent-skills/requirements.md new file mode 100644 index 000000000..4592a5afd --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/requirements.md @@ -0,0 +1,43 @@ +# Anthropic Agent Skills 需求文档 (Requirements) + +## 1. 概述 (Overview) +本功能旨在为 BotSharp 引入 Anthropic 的 "Agent Skills" 标准支持。通过实现一个新的插件 `BotSharp.Plugin.AgentSkills`,允许 Agent 采用“渐进式披露”(Progressive Disclosure)模式进行交互。与传统的一次性加载所有工具不同,Agent 将首先感知可用技能的摘要,根据需要“加载”特定技能的详细指令,并通过通用接口“执行”技能中定义的脚本。 + +## 2. 需求列表 (Requirements) + +### 2.1 技能索引与感知 (Indexing & Awareness) +**用户故事**: 作为 BotSharp 管理员,我希望系统能自动发现指定目录下的技能,以便 Agent 在初始状态下知道有哪些能力可用,而无需预加载所有繁重的指令。 + +* **REQ-001**: 当 Agent 初始化或系统启动时,组件 **必须** 扫描配置的技能根目录。 +* **REQ-002**: 当发现从属目录中包含 `SKILL.md` 时,系统 **必须** 解析其 YAML Frontmatter 以提取 `name` 和 `description`。 +* **REQ-003**: 如果 YAML 解析成功,系统 **必须** 将格式化后的技能列表(包含名称和描述)注入到 Agent 的 System Prompt(或可用的 Context Window)中。 +* **REQ-004**: 如果 `SKILL.md` 格式无效或元数据缺失,系统 **应该** 记录警告日志并跳过该技能,即不中断整体启动流程。 + +### 2.2 技能加载 (Skill Loading) +**用户故事**: 作为 LLM Agent,我希望在判断某个技能对当前任务有用时能够动态加载其详细指令,以便获取执行任务所需的标准作业程序(SOP)。 + +* **REQ-005**: 系统 **必须** 向 LLM 暴露一个名为 `load_skill`(或语义等效)的 Function Tool。 +* **REQ-006**: 当 `load_skill` 被调用且 `skill_name` 有效时,系统 **必须** 读取对应 `SKILL.md` 的 Markdown 正文(Body)。 +* **REQ-007**: 当获取到 Markdown 正文后,系统 **必须** 将其作为消息(System 或 User 角色)追加到当前的对话上下文中,使其对后续推理可见。 +* **REQ-008**: 如果请求的 `skill_name` 不存在,工具调用 **必须** 返回明确的错误提示给 Agent。 + +### 2.3 技能脚本执行 (Skill Invocation) +**用户故事**: 作为 LLM Agent,我希望能通过一个通用的执行接口运行技能包中定义的脚本,以便按照 SOP 完成具体的操作。 + +* **REQ-009**: 系统 **必须** 向 LLM 暴露一个名为 `run_skill_script` 的 Function Tool,接受 `skill_name`, `script_file`, `args` 等参数。 +* **REQ-010**: 当 `run_skill_script` 被调用时,系统 **必须** 验证请求的脚本文件是否存在于该技能的 `scripts/` 子目录下,防止路径遍历攻击。 +* **REQ-011**: 如果脚本是 Python 文件(`.py`),系统 **必须** 调用宿主环境的 Python 解释器执行该脚本,并将 `args` 传递给进程。 +* **REQ-012**: 当脚本执行完成,系统 **必须** 捕获标准输出(Stdout)作为工具的成功返回值。 +* **REQ-013**: 如果脚本执行失败(退出码非0或抛出异常),系统 **必须** 捕获错误输出(Stderr)并将其包装为错误信息返回给 Agent。 + +### 2.4 配置与扩展性 (Configuration & Extensibility) +**用户故事**: 作为开发者,我希望能够灵活配置技能库的位置,并以插件形式集成此功能。 + +* **REQ-014**: 系统 **必须** 利用 BotSharp 的配置机制(如 `appsettings.json`)读取技能库的根路径(例如 `AgentSkills:DataDir`)。 +* **REQ-015**: 如果未提供配置,系统 **应该** 默认使用应用工作目录下的 `skills` 文件夹。 +* **REQ-016**: 所有功能 **必须** 封装在 `BotSharp.Plugin.AgentSkills` 项目中,通过实现 `IBotSharpPlugin` 接口进行服务注册。 + +## 3. 验收标准示例 (Acceptance Criteria Example) +* **Case 1: 发现技能**: 在 `skills/pdf-processing/SKILL.md` 存在的情况下,启动 Agent,System Prompt 中应包含 "pdf-processing" 及其描述。 +* **Case 2: 动态加载**: 对 Agent 说 "我要处理 PDF",Agent 调用 `load_skill("pdf-processing")`,随后的 Prompt 中包含了 PDF 处理的具体步骤。 +* **Case 3: 脚本执行**: Agent 调用 `run_skill_script("pdf-processing", "analyze.py", ...)`,系统成功执行本地 Python 脚本并返回结果字符串。 diff --git a/.spec-coding/features/anthropic-agent-skills/tasks.md b/.spec-coding/features/anthropic-agent-skills/tasks.md new file mode 100644 index 000000000..d5179cb46 --- /dev/null +++ b/.spec-coding/features/anthropic-agent-skills/tasks.md @@ -0,0 +1,30 @@ +# Implementation Tasks - Anthropic Agent Skills + +- [x] **Infrastructure - Create Plugin Project**: Create `BotSharp.Plugin.AgentSkills` project and register it in `BotSharp.csproj`. +- [x] **Infrastructure - Define Data Models**: Create `AgentSkill` and `SkillFrontmatter` models to map `SKILL.md` structure. +- [x] **Core Service - Implement AgentSkillService**: + - [x] Create `IAgentSkillService` and `AgentSkillService`. + - [x] Implement directory scanning and `SKILL.md` YAML parsing using `YamlDotNet`. + - [x] Implement `GetAvailableSkills` and `GetSkill` methods. + - [x] Implement `FileSystemWatcher` for hot reload support. +- [x] **Hooks - Implement AgentSkillHook**: + - [x] Create `AgentSkillHook` inheriting `AgentHookBase`. + - [x] Implement `OnInstructionLoaded` for "Discovery" phase (inject available skills list). + - [x] Implement logic to inject active skill instructions for "Activation" phase. +- [x] **Functions - Implement LoadSkillFn**: + - [x] Create `LoadSkillFn` implementing `IFunctionCallback`. + - [x] Implement logic to add skill to `active_skills` in conversation state. + - [x] Return appropriate message to prompt LLM for next step. +- [x] **Execution - Extend Python Interpreter**: + - [x] Define `IPyScriptRunner` interface in `BotSharp.Plugin.PythonInterpreter`. + - [x] Implement `PyScriptRunner` to execute local `.py` files using `Process.Start`. + - [x] Register the new service in `PythonInterpreterPlugin`. +- [x] **Functions - Implement RunSkillScriptFn**: + - [x] Create `RunSkillScriptFn` implementing `IFunctionCallback`. + - [x] Implement parameter validation (path security). + - [x] Use `IPyScriptRunner` to execute the requested script. +- [x] **Testing - Integration**: + - [x] Create a sample `skills/demo-skill` structure. + - [x] Verify discovery (System Prompt injection). + - [x] Verify loading (Context injection). + - [x] Verify execution (Script result). diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..f5df4a814 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "aaron-bond.better-comments", + "codezombiech.gitignore", + "cpylua.language-postcss", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "foxundermoon.shell-format", + "jasonnutter.search-node-modules", + "jock.svg", + "mikestead.dotenv", + "streetsidesoftware.code-spell-checker", + "stylelint.vscode-stylelint" + ], + "unwantedRecommendations": [ + "nucllear.vscode-extension-auto-import", + "steoates.autoimport" + ] +} diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 000000000..e3ea61c6b --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,13 @@ +{ + "servers": { + "SpecCodingMcpServer": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "C:\\workshop\\ai4c\\speccodingmcpserver\\src\\SpecCodingMcpServer\\SpecCodingMcpServer.csproj" + ] + } + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..0fc8a800d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,182 @@ +{ + "eslint.nodePath": "frontend/config/eslint-config/node_modules/eslint", + "prettier.prettierPath": "frontend/config/eslint-config/node_modules/prettier", + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.formatOnType": false, + "editor.formatOnPaste": false, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "never", + "source.fixAll.eslint": "never", + "source.removeUnused": "never", + "source.organizeImports": "never" + }, + "editor.formatOnSaveMode": "modificationsIfAvailable", + "search.followSymlinks": false, + "search.exclude": { + "**/node_modules": true, + "**/.nyc_output": true, + "**/.rush": true, + "**/pnpm-lock.yaml": true, + "**/CHANGELOG.json": true, + "**/CHANGELOG.md": true, + "common/changes": true, + "**/output": true, + "**/lib": true, + "**/rush-logs": true, + "**/dist": true, + "**/coverage": true, + "common/temp": true + }, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "files.defaultLanguage": "plaintext", + "files.associations": { + ".code-workspace": "jsonc", + ".babelrc": "json", + ".eslintrc": "jsonc", + ".eslintrc*.json": "jsonc", + ".stylelintrc": "javascript", + "stylelintrc": "jsonc", + "*.json": "jsonc", + "package.json": "json", + ".htmlhintrc": "jsonc", + "htmlhintrc": "jsonc", + "Procfile*": "shellscript", + "README": "markdown", + "**/coverage/**/*.*": "plaintext", + "OWNERS": "yaml", + "**/pnpm-lock.yaml": "plaintext", + "**/dist/**": "plaintext", + "**/dist_*/**": "plaintext", + "*.map": "plaintext", + "*.log": "plaintext" + }, + "files.exclude": { + "**/.git": true, + "**/rush-logs": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.rush": true, + "**/.swc": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/*/**": true + }, + "search.useIgnoreFiles": true, + // + "editor.rulers": [80, 120], + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "cSpell.diagnosticLevel": "Warning", + "eslint.probe": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.semanticHighlighting.enabled": false, + "eslint.format.enable": false, + "eslint.enable": true, + "eslint.useFlatConfig": true, + "eslint.codeActionsOnSave.mode": "problems", + "eslint.lintTask.enable": false, + "javascript.validate.enable": false, + "typescript.tsdk": "config/ts-config/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.disableAutomaticTypeAcquisition": true, + "typescript.format.enable": false, + "typescript.referencesCodeLens.enabled": false, + "typescript.preferGoToSourceDefinition": true, + "typescript.updateImportsOnFileMove.enabled": "never", + "typescript.tsserver.log": "off", + "typescript.tsserver.experimental.enableProjectDiagnostics": false, + "typescript.workspaceSymbols.excludeLibrarySymbols": true, + "editor.minimap.enabled": true, + "typescript.preferences.includePackageJsonAutoImports": "off", + "typescript.suggest.autoImports": true, + "typescript.tsserver.maxTsServerMemory": 10240, + "typescript.tsserver.enableRegionDiagnostics": false, + "typescript.tsserver.watchOptions": { + "fallbackPolling": "dynamicPriorityPolling", + "synchronousWatchDirectory": true, + "watchDirectory": "useFsEvents", + "watchFile": "useFsEventsOnParentDirectory", + "excludeDirectories": ["/**/node_modules"], + "excludeLibrarySymbols": true, + "excludeFiles": ["/**/node_modules/**"] + }, + "css.validate": false, + "scss.validate": false, + "less.validate": false, + "stylelint.enable": true, + "stylelint.validate": ["css", "scss", "less"], + "stylelint.stylelintPath": "frontend/config/stylelint-config/node_modules/stylelint", + "emmet.triggerExpansionOnTab": true, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[less]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint" + }, + "[scss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[ignore]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[dotenv]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[svg]": { + "editor.defaultFormatter": "jock.svg" + }, + "[xml]": { + "editor.defaultFormatter": "mblode.pretty-formatter" + }, + "[sql]": { + "editor.defaultFormatter": "adpyke.vscode-sql-formatter" + }, + "svg.preview.background": "white", + "git.openRepositoryInParentFolders": "always", + "references.preferredLocation": "view", + "makefile.configureOnOpen": false +} diff --git a/BotSharp.sln b/BotSharp.sln index e502b4b12..7fbc908ae 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11217.181 @@ -152,502 +153,778 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Membase", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{C548FDFF-B882-B552-D428-5C8EC4478187}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.AgentSkills", "src\Plugins\BotSharp.Plugin.AgentSkills\BotSharp.Plugin.AgentSkills.csproj", "{54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest", "tests\UnitTest\UnitTest.csproj", "{5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skills", "Skills", "{416C0757-85C6-430B-B973-B72FCDC7EB94}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x64.ActiveCfg = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x64.Build.0 = Debug|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Debug|x86.Build.0 = Debug|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|Any CPU.Build.0 = Release|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x64.ActiveCfg = Release|Any CPU {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x64.Build.0 = Release|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x86.ActiveCfg = Release|Any CPU + {197885F1-2EB2-4709-B9AA-A777878D74B3}.Release|x86.Build.0 = Release|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|Any CPU.Build.0 = Debug|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x64.ActiveCfg = Debug|x64 {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x64.Build.0 = Debug|x64 + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x86.ActiveCfg = Debug|Any CPU + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Debug|x86.Build.0 = Debug|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|Any CPU.ActiveCfg = Release|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|Any CPU.Build.0 = Release|Any CPU {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x64.ActiveCfg = Release|x64 {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x64.Build.0 = Release|x64 + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x86.ActiveCfg = Release|Any CPU + {36F5CEBD-31A8-4BEF-8BAA-BAC4E63E4815}.Release|x86.Build.0 = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|Any CPU.Build.0 = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x64.ActiveCfg = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x64.Build.0 = Debug|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x86.ActiveCfg = Debug|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Debug|x86.Build.0 = Debug|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|Any CPU.ActiveCfg = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|Any CPU.Build.0 = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x64.ActiveCfg = Release|Any CPU {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x64.Build.0 = Release|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x86.ActiveCfg = Release|Any CPU + {07AD18C5-CE7B-495A-815F-170E93CCC42A}.Release|x86.Build.0 = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|Any CPU.Build.0 = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x64.ActiveCfg = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x64.Build.0 = Debug|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Debug|x86.Build.0 = Debug|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|Any CPU.ActiveCfg = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|Any CPU.Build.0 = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x64.ActiveCfg = Release|Any CPU {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x64.Build.0 = Release|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x86.ActiveCfg = Release|Any CPU + {3EAB9CF3-0F47-4BFB-8BAC-8ADFF24AD899}.Release|x86.Build.0 = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|Any CPU.Build.0 = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x64.ActiveCfg = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x64.Build.0 = Debug|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x86.ActiveCfg = Debug|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Debug|x86.Build.0 = Debug|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|Any CPU.ActiveCfg = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|Any CPU.Build.0 = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x64.ActiveCfg = Release|Any CPU {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x64.Build.0 = Release|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x86.ActiveCfg = Release|Any CPU + {57806BAF-7736-425A-B499-13A2A2DF1E63}.Release|x86.Build.0 = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|Any CPU.Build.0 = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x64.ActiveCfg = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x64.Build.0 = Debug|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x86.ActiveCfg = Debug|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Debug|x86.Build.0 = Debug|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|Any CPU.ActiveCfg = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|Any CPU.Build.0 = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x64.ActiveCfg = Release|Any CPU {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x64.Build.0 = Release|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x86.ActiveCfg = Release|Any CPU + {68C7C9E9-496B-4004-A1F8-75FFB8C06C76}.Release|x86.Build.0 = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x64.ActiveCfg = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x64.Build.0 = Debug|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Debug|x86.Build.0 = Debug|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|Any CPU.Build.0 = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x64.ActiveCfg = Release|Any CPU {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x64.Build.0 = Release|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x86.ActiveCfg = Release|Any CPU + {2323A7A3-E938-488D-A57E-638638054BC4}.Release|x86.Build.0 = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x64.ActiveCfg = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x64.Build.0 = Debug|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Debug|x86.Build.0 = Debug|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|Any CPU.Build.0 = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x64.ActiveCfg = Release|Any CPU {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x64.Build.0 = Release|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x86.ActiveCfg = Release|Any CPU + {6D8D18A9-86D7-455E-81EC-9682C30AB7E7}.Release|x86.Build.0 = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x64.ActiveCfg = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x64.Build.0 = Debug|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Debug|x86.Build.0 = Debug|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|Any CPU.Build.0 = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x64.ActiveCfg = Release|Any CPU {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x64.Build.0 = Release|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x86.ActiveCfg = Release|Any CPU + {FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x86.Build.0 = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|Any CPU.Build.0 = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x64.ActiveCfg = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x64.Build.0 = Debug|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x86.ActiveCfg = Debug|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x86.Build.0 = Debug|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|Any CPU.ActiveCfg = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|Any CPU.Build.0 = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x64.ActiveCfg = Release|Any CPU {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x64.Build.0 = Release|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x86.ActiveCfg = Release|Any CPU + {0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Release|x86.Build.0 = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x64.ActiveCfg = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x64.Build.0 = Debug|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Debug|x86.Build.0 = Debug|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|Any CPU.Build.0 = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x64.ActiveCfg = Release|Any CPU {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x64.Build.0 = Release|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x86.ActiveCfg = Release|Any CPU + {8300F66D-9EB8-438A-BF0F-70DFBE07D9DE}.Release|x86.Build.0 = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x64.ActiveCfg = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x64.Build.0 = Debug|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Debug|x86.Build.0 = Debug|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|Any CPU.Build.0 = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x64.ActiveCfg = Release|Any CPU {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x64.Build.0 = Release|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x86.ActiveCfg = Release|Any CPU + {7E63F5F8-4EA0-498B-ABFE-2BBE4D7DDBA7}.Release|x86.Build.0 = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|Any CPU.Build.0 = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x64.ActiveCfg = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x64.Build.0 = Debug|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x86.ActiveCfg = Debug|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Debug|x86.Build.0 = Debug|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|Any CPU.ActiveCfg = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|Any CPU.Build.0 = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x64.ActiveCfg = Release|Any CPU {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x64.Build.0 = Release|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x86.ActiveCfg = Release|Any CPU + {46B7B54F-1425-4C9D-824A-9B826855D249}.Release|x86.Build.0 = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x64.ActiveCfg = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x64.Build.0 = Debug|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Debug|x86.Build.0 = Debug|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|Any CPU.Build.0 = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x64.ActiveCfg = Release|Any CPU {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x64.Build.0 = Release|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x86.ActiveCfg = Release|Any CPU + {A1118A2C-C6D7-4E22-9462-964AEC7CC46E}.Release|x86.Build.0 = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|Any CPU.Build.0 = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x64.ActiveCfg = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x64.Build.0 = Debug|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x86.ActiveCfg = Debug|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Debug|x86.Build.0 = Debug|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|Any CPU.ActiveCfg = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|Any CPU.Build.0 = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x64.ActiveCfg = Release|Any CPU {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x64.Build.0 = Release|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x86.ActiveCfg = Release|Any CPU + {631D9C12-86C4-44F0-99C3-D32C0754BF37}.Release|x86.Build.0 = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x64.ActiveCfg = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x64.Build.0 = Debug|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Debug|x86.Build.0 = Debug|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|Any CPU.Build.0 = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x64.ActiveCfg = Release|Any CPU {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x64.Build.0 = Release|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x86.ActiveCfg = Release|Any CPU + {298AC787-A104-414C-B114-82BE764FBD9C}.Release|x86.Build.0 = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x64.ActiveCfg = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x64.Build.0 = Debug|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Debug|x86.Build.0 = Debug|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|Any CPU.Build.0 = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x64.ActiveCfg = Release|Any CPU {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x64.Build.0 = Release|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x86.ActiveCfg = Release|Any CPU + {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C}.Release|x86.Build.0 = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x64.ActiveCfg = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x64.Build.0 = Debug|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Debug|x86.Build.0 = Debug|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|Any CPU.ActiveCfg = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|Any CPU.Build.0 = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x64.ActiveCfg = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x64.Build.0 = Release|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x86.ActiveCfg = Release|Any CPU + {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x86.Build.0 = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x64.ActiveCfg = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x64.Build.0 = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x86.Build.0 = Debug|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.Build.0 = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.ActiveCfg = Release|Any CPU {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.Build.0 = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x86.ActiveCfg = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x86.Build.0 = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.Build.0 = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.ActiveCfg = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.Build.0 = Debug|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x86.ActiveCfg = Debug|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x86.Build.0 = Debug|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.ActiveCfg = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.Build.0 = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.ActiveCfg = Release|Any CPU {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.Build.0 = Release|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x86.ActiveCfg = Release|Any CPU + {72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x86.Build.0 = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.ActiveCfg = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.Build.0 = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x86.Build.0 = Debug|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|Any CPU.Build.0 = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x64.ActiveCfg = Release|Any CPU {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x64.Build.0 = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x86.ActiveCfg = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x86.Build.0 = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x64.ActiveCfg = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x64.Build.0 = Debug|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x86.Build.0 = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|Any CPU.Build.0 = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x64.ActiveCfg = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x64.Build.0 = Release|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x86.ActiveCfg = Release|Any CPU + {E627F1E3-BE03-443A-83A2-86A855A278EB}.Release|x86.Build.0 = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|Any CPU.Build.0 = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x64.ActiveCfg = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x64.Build.0 = Debug|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x86.ActiveCfg = Debug|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Debug|x86.Build.0 = Debug|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|Any CPU.ActiveCfg = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|Any CPU.Build.0 = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x64.ActiveCfg = Release|Any CPU {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x64.Build.0 = Release|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x86.ActiveCfg = Release|Any CPU + {F06B22CB-B143-4680-8FFF-35B9E50E6C47}.Release|x86.Build.0 = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x64.ActiveCfg = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x64.Build.0 = Debug|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x86.ActiveCfg = Debug|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Debug|x86.Build.0 = Debug|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|Any CPU.Build.0 = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x64.ActiveCfg = Release|Any CPU {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x64.Build.0 = Release|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x86.ActiveCfg = Release|Any CPU + {EDCD9C20-2D9D-4098-A16E-03F97B306CB8}.Release|x86.Build.0 = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|Any CPU.Build.0 = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x64.ActiveCfg = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x64.Build.0 = Debug|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x86.ActiveCfg = Debug|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Debug|x86.Build.0 = Debug|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|Any CPU.ActiveCfg = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|Any CPU.Build.0 = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x64.ActiveCfg = Release|Any CPU {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x64.Build.0 = Release|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x86.ActiveCfg = Release|Any CPU + {DCA18996-4D3A-4E98-BCD0-1FB77C59253E}.Release|x86.Build.0 = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|Any CPU.Build.0 = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x64.ActiveCfg = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x64.Build.0 = Debug|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x86.ActiveCfg = Debug|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Debug|x86.Build.0 = Debug|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|Any CPU.ActiveCfg = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|Any CPU.Build.0 = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x64.ActiveCfg = Release|Any CPU {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x64.Build.0 = Release|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x86.ActiveCfg = Release|Any CPU + {5CA3335E-E6AD-46FD-B277-29BBC3A16500}.Release|x86.Build.0 = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|Any CPU.Build.0 = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x64.ActiveCfg = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x64.Build.0 = Debug|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x86.ActiveCfg = Debug|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Debug|x86.Build.0 = Debug|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|Any CPU.ActiveCfg = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|Any CPU.Build.0 = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x64.ActiveCfg = Release|Any CPU {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x64.Build.0 = Release|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x86.ActiveCfg = Release|Any CPU + {32D9E720-6FE6-4F29-94B1-B10B05BFAD75}.Release|x86.Build.0 = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|Any CPU.Build.0 = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x64.ActiveCfg = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x64.Build.0 = Debug|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x86.ActiveCfg = Debug|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Debug|x86.Build.0 = Debug|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|Any CPU.ActiveCfg = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|Any CPU.Build.0 = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|x64.ActiveCfg = Release|Any CPU {D775DB67-A4B4-44E5-9144-522689590057}.Release|x64.Build.0 = Release|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Release|x86.ActiveCfg = Release|Any CPU + {D775DB67-A4B4-44E5-9144-522689590057}.Release|x86.Build.0 = Release|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x64.ActiveCfg = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x64.Build.0 = Debug|Any CPU + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x86.ActiveCfg = Debug|Any CPU + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Debug|x86.Build.0 = Debug|Any CPU {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {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 + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x86.ActiveCfg = Release|Any CPU + {267998C1-55C2-4ADC-8361-2CDFA5EA6D6C}.Release|x86.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}.Debug|x86.ActiveCfg = Debug|Any CPU + {289E25C8-63F1-4D52-9909-207724DB40CB}.Debug|x86.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 + {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x86.ActiveCfg = Release|Any CPU + {289E25C8-63F1-4D52-9909-207724DB40CB}.Release|x86.Build.0 = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x64.ActiveCfg = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x64.Build.0 = Debug|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Debug|x86.Build.0 = Debug|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|Any CPU.Build.0 = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.ActiveCfg = Release|Any CPU {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.Build.0 = Release|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x86.ActiveCfg = Release|Any CPU + {CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x86.Build.0 = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.Build.0 = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.ActiveCfg = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.Build.0 = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x86.ActiveCfg = Debug|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x86.Build.0 = Debug|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.ActiveCfg = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.Build.0 = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.ActiveCfg = Release|Any CPU {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.Build.0 = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x86.ActiveCfg = Release|Any CPU + {806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x86.Build.0 = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|Any CPU.Build.0 = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x64.ActiveCfg = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x64.Build.0 = Debug|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x86.ActiveCfg = Debug|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Debug|x86.Build.0 = Debug|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|Any CPU.ActiveCfg = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|Any CPU.Build.0 = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x64.ActiveCfg = Release|Any CPU {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x64.Build.0 = Release|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x86.ActiveCfg = Release|Any CPU + {6406DC61-0F30-42E8-A1DB-B38CDF454273}.Release|x86.Build.0 = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x64.ActiveCfg = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x64.Build.0 = Debug|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Debug|x86.Build.0 = Debug|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|Any CPU.Build.0 = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x64.ActiveCfg = Release|Any CPU {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x64.Build.0 = Release|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x86.ActiveCfg = Release|Any CPU + {E04FBBEF-744E-4EF3-B634-42AD9F8B68B1}.Release|x86.Build.0 = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x64.ActiveCfg = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x64.Build.0 = Debug|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Debug|x86.Build.0 = Debug|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|Any CPU.Build.0 = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x64.ActiveCfg = Release|Any CPU {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x64.Build.0 = Release|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x86.ActiveCfg = Release|Any CPU + {6507D336-3A4D-41D4-81C0-2B900173A5FE}.Release|x86.Build.0 = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x64.ActiveCfg = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x64.Build.0 = Debug|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Debug|x86.Build.0 = Debug|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|Any CPU.Build.0 = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x64.ActiveCfg = Release|Any CPU {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x64.Build.0 = Release|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x86.ActiveCfg = Release|Any CPU + {A72B3BEB-E14B-4917-BE44-97EAE4E122D2}.Release|x86.Build.0 = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x64.ActiveCfg = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x64.Build.0 = Debug|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Debug|x86.Build.0 = Debug|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|Any CPU.Build.0 = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x64.ActiveCfg = Release|Any CPU {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x64.Build.0 = Release|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x86.ActiveCfg = Release|Any CPU + {D6A99D4F-6248-419E-8A43-B38ADEBABA2C}.Release|x86.Build.0 = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|Any CPU.Build.0 = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x64.ActiveCfg = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x64.Build.0 = Debug|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x86.ActiveCfg = Debug|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Debug|x86.Build.0 = Debug|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|Any CPU.ActiveCfg = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|Any CPU.Build.0 = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.ActiveCfg = Release|Any CPU {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.Build.0 = Release|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x86.ActiveCfg = Release|Any CPU + {54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x86.Build.0 = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.ActiveCfg = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.Build.0 = Debug|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x86.ActiveCfg = Debug|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x86.Build.0 = Debug|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.Build.0 = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.ActiveCfg = Release|Any CPU {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.Build.0 = Release|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x86.ActiveCfg = Release|Any CPU + {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x86.Build.0 = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x64.ActiveCfg = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x64.Build.0 = Debug|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x86.ActiveCfg = Debug|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Debug|x86.Build.0 = Debug|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|Any CPU.Build.0 = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x64.ActiveCfg = Release|Any CPU {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x64.Build.0 = Release|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x86.ActiveCfg = Release|Any CPU + {05E6E405-5021-406E-8A5E-0A7CEC881F6D}.Release|x86.Build.0 = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x64.ActiveCfg = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x64.Build.0 = Debug|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Debug|x86.Build.0 = Debug|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|Any CPU.Build.0 = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x64.ActiveCfg = Release|Any CPU {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x64.Build.0 = Release|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x86.ActiveCfg = Release|Any CPU + {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D}.Release|x86.Build.0 = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|Any CPU.Build.0 = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x64.ActiveCfg = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x64.Build.0 = Debug|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x86.ActiveCfg = Debug|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Debug|x86.Build.0 = Debug|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.ActiveCfg = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.Build.0 = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.ActiveCfg = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.Build.0 = Release|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x86.ActiveCfg = Release|Any CPU + {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x86.Build.0 = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.ActiveCfg = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.Build.0 = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x86.Build.0 = Debug|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.Build.0 = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.ActiveCfg = Release|Any CPU {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.Build.0 = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x86.ActiveCfg = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x86.Build.0 = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x64.ActiveCfg = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x64.Build.0 = Debug|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x86.Build.0 = Debug|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|Any CPU.Build.0 = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.ActiveCfg = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.Build.0 = Release|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x86.ActiveCfg = Release|Any CPU + {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x86.Build.0 = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|Any CPU.Build.0 = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x64.ActiveCfg = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x64.Build.0 = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x86.Build.0 = Debug|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|Any CPU.ActiveCfg = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|Any CPU.Build.0 = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x64.ActiveCfg = Release|Any CPU {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x64.Build.0 = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x86.ActiveCfg = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x86.Build.0 = Release|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|Any CPU.Build.0 = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x64.ActiveCfg = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x64.Build.0 = Debug|Any CPU + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x86.ActiveCfg = Debug|Any CPU + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Debug|x86.Build.0 = Debug|Any CPU {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|Any CPU.ActiveCfg = Release|Any CPU {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 + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x86.ActiveCfg = Release|Any CPU + {AFD64412-4D6A-452E-82A2-79E5D8842E29}.Release|x86.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}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Debug|x86.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 + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x86.ActiveCfg = Release|Any CPU + {AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x86.Build.0 = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x64.ActiveCfg = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x64.Build.0 = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x86.Build.0 = Debug|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|Any CPU.Build.0 = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x64.ActiveCfg = Release|Any CPU {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x64.Build.0 = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x86.ActiveCfg = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x86.Build.0 = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|Any CPU.Build.0 = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x64.ActiveCfg = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x64.Build.0 = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x86.Build.0 = Debug|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|Any CPU.Build.0 = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x64.ActiveCfg = Release|Any CPU {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x64.Build.0 = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x86.ActiveCfg = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x86.Build.0 = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x64.ActiveCfg = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x64.Build.0 = Debug|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Debug|x86.Build.0 = Debug|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|Any CPU.Build.0 = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x64.ActiveCfg = Release|Any CPU {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x64.Build.0 = Release|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x86.ActiveCfg = Release|Any CPU + {C19D9AC1-97DD-8E65-E8DB-D295A095AA2D}.Release|x86.Build.0 = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|Any CPU.Build.0 = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x64.ActiveCfg = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x64.Build.0 = Debug|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x86.ActiveCfg = Debug|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Debug|x86.Build.0 = Debug|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|Any CPU.ActiveCfg = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|Any CPU.Build.0 = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x64.ActiveCfg = Release|Any CPU {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x64.Build.0 = Release|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x86.ActiveCfg = Release|Any CPU + {B268E2F0-060F-8466-7D81-ABA4D735CA59}.Release|x86.Build.0 = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x64.ActiveCfg = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x64.Build.0 = Debug|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x86.ActiveCfg = Debug|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Debug|x86.Build.0 = Debug|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|Any CPU.Build.0 = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x64.ActiveCfg = Release|Any CPU {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x64.Build.0 = Release|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x86.ActiveCfg = Release|Any CPU + {970BE341-9AC8-99A5-6572-E703C1E02FCB}.Release|x86.Build.0 = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x64.ActiveCfg = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x64.Build.0 = Debug|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Debug|x86.Build.0 = Debug|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|Any CPU.Build.0 = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x64.ActiveCfg = Release|Any CPU {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x64.Build.0 = Release|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x86.ActiveCfg = Release|Any CPU + {7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x86.Build.0 = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x64.ActiveCfg = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x64.Build.0 = Debug|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x86.Build.0 = Debug|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|Any CPU.Build.0 = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x64.ActiveCfg = Release|Any CPU {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x64.Build.0 = Release|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x86.ActiveCfg = Release|Any CPU + {7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x86.Build.0 = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x64.ActiveCfg = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x64.Build.0 = Debug|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Debug|x86.Build.0 = Debug|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|Any CPU.Build.0 = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x64.ActiveCfg = Release|Any CPU {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x64.Build.0 = Release|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x86.ActiveCfg = Release|Any CPU + {B067B126-88CD-4282-BEEF-7369B64423EF}.Release|x86.Build.0 = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|Any CPU.Build.0 = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x64.ActiveCfg = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x64.Build.0 = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x86.ActiveCfg = Debug|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Debug|x86.Build.0 = Debug|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|Any CPU.ActiveCfg = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|Any CPU.Build.0 = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x64.ActiveCfg = Release|Any CPU {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x64.Build.0 = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x86.ActiveCfg = Release|Any CPU + {0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702}.Release|x86.Build.0 = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x64.ActiveCfg = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x64.Build.0 = Debug|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x86.ActiveCfg = Debug|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Debug|x86.Build.0 = Debug|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.Build.0 = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.ActiveCfg = Release|Any CPU {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.Build.0 = Release|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x86.ActiveCfg = Release|Any CPU + {FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x86.Build.0 = Release|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.ActiveCfg = Debug|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.Build.0 = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x86.Build.0 = Debug|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.Build.0 = Release|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.ActiveCfg = Release|Any CPU {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.Build.0 = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x86.ActiveCfg = Release|Any CPU + {50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x86.Build.0 = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.ActiveCfg = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x64.Build.0 = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Debug|x86.Build.0 = Debug|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|Any CPU.Build.0 = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.ActiveCfg = Release|Any CPU {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x64.Build.0 = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x86.ActiveCfg = Release|Any CPU + {E7C243B9-E751-B3B4-8F16-95C76CA90D31}.Release|x86.Build.0 = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|Any CPU.Build.0 = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x64.ActiveCfg = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x64.Build.0 = Debug|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x86.ActiveCfg = Debug|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Debug|x86.Build.0 = Debug|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|Any CPU.ActiveCfg = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|Any CPU.Build.0 = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.ActiveCfg = Release|Any CPU {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.Build.0 = Release|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x86.ActiveCfg = Release|Any CPU + {13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x86.Build.0 = Release|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.Build.0 = Debug|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.ActiveCfg = Debug|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.Build.0 = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x86.ActiveCfg = Debug|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x86.Build.0 = Debug|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.ActiveCfg = Release|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.Build.0 = Release|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.ActiveCfg = Release|Any CPU {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.Build.0 = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x86.ActiveCfg = Release|Any CPU + {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x86.Build.0 = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x64.ActiveCfg = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x64.Build.0 = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x86.ActiveCfg = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Debug|x86.Build.0 = Debug|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|Any CPU.Build.0 = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x64.ActiveCfg = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x64.Build.0 = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x86.ActiveCfg = Release|Any CPU + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72}.Release|x86.Build.0 = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x64.Build.0 = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Debug|x86.Build.0 = Debug|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|Any CPU.Build.0 = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x64.ActiveCfg = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x64.Build.0 = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x86.ActiveCfg = Release|Any CPU + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -722,6 +999,9 @@ Global {E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89} {C548FDFF-B882-B552-D428-5C8EC4478187} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {54C08FAD-3092-4E3C-9137-AFC2B1D3CB72} = {416C0757-85C6-430B-B973-B72FCDC7EB94} + {5F4BFCA4-A4D4-AA08-38BB-866B0CBB71E5} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} + {416C0757-85C6-430B-B973-B72FCDC7EB94} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Packages.props b/Directory.Packages.props index 84e09a1fa..8ef34f23b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,199 +1,200 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skills/test-skill/SKILL.md b/skills/test-skill/SKILL.md new file mode 100644 index 000000000..948dca60b --- /dev/null +++ b/skills/test-skill/SKILL.md @@ -0,0 +1,8 @@ +--- +name: test-skill +description: A test skill to verify the Agent Skills framework. +--- + +### Test Skill Instructions +This is a test skill. You can use it to verify if the loading mechanism works. +You can run the `hello.py` script to print a greeting. diff --git a/skills/test-skill/scripts/hello.py b/skills/test-skill/scripts/hello.py new file mode 100644 index 000000000..73f0bea64 --- /dev/null +++ b/skills/test-skill/scripts/hello.py @@ -0,0 +1,4 @@ +import sys + +name = sys.argv[1] if len(sys.argv) > 1 else "World" +print(f"Hello, {name} from Agent Skills!") diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs new file mode 100644 index 000000000..1eda6cebe --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs @@ -0,0 +1,38 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Abstraction.Settings; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using BotSharp.Plugin.AgentSkills.Services; +using BotSharp.Plugin.AgentSkills.Settings; +using BotSharp.Plugin.AgentSkills.Hooks; +using BotSharp.Plugin.AgentSkills.Functions; +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Functions; + +namespace BotSharp.Plugin.AgentSkills; + +public class AgentSkillsPlugin : IBotSharpPlugin +{ + public string Id => "b6c93605-246e-4f7f-8559-467385501865"; + public string Name => "Agent Skills"; + public string Description => "Enables Anthropic's Agent Skills standard (progressive disclosure of tools)."; + public string IconUrl => "https://avatars.githubusercontent.com/u/108622152?s=200&v=4"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + // 注册设置 + var settings = new AgentSkillsSettings(); + config.Bind("AgentSkills", settings); + services.AddSingleton(settings); + + // 注册核心服务 + services.AddSingleton(); + + // 注册 Hook + services.AddScoped(); + + // 注册 Function Tools + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/BotSharp.Plugin.AgentSkills.csproj b/src/Plugins/BotSharp.Plugin.AgentSkills/BotSharp.Plugin.AgentSkills.csproj new file mode 100644 index 000000000..155722e79 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/BotSharp.Plugin.AgentSkills.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + net8.0 + latest + enable + enable + + + diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/LoadSkillFn.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/LoadSkillFn.cs new file mode 100644 index 000000000..47c0d3495 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/LoadSkillFn.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Conversations; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using System.Text.Json; + +namespace BotSharp.Plugin.AgentSkills.Functions; + +public class LoadSkillFn : IFunctionCallback +{ + public string Name => "load_skill"; + public string Indication => "Loading skill..."; + private readonly IServiceProvider _services; + + public LoadSkillFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var skillName = message.FunctionName == "load_skill" + ? JsonSerializer.Deserialize(message.FunctionArgs).GetProperty("skill_name").GetString() + : null; + + if (string.IsNullOrEmpty(skillName)) + { + message.Content = "Error: skill_name provided."; + return false; + } + + var stateService = _services.GetRequiredService(); + var currentActiveStr = stateService.GetState("active_skills"); + var currentActive = string.IsNullOrEmpty(currentActiveStr) + ? new List() + : currentActiveStr.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); + + if (!currentActive.Contains(skillName)) + { + currentActive.Add(skillName); + stateService.SetState("active_skills", string.Join(",", currentActive)); + message.Content = $"Skill '{skillName}' has been activated. The detailed instructions will be available in the next step."; + } + else + { + message.Content = $"Skill '{skillName}' is already active."; + } + + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/RunSkillScriptFn.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/RunSkillScriptFn.cs new file mode 100644 index 000000000..a78f8427c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Functions/RunSkillScriptFn.cs @@ -0,0 +1,77 @@ +using System.Threading.Tasks; +using System.Text.Json; +using System; +using Microsoft.Extensions.DependencyInjection; +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Plugin.AgentSkills.Services; +using BotSharp.Plugin.PythonInterpreter.Interfaces; + +namespace BotSharp.Plugin.AgentSkills.Functions; + +public class RunSkillScriptFn : IFunctionCallback +{ + public string Name => "run_skill_script"; + public string Indication => "Running skill script..."; + private readonly IServiceProvider _services; + + public RunSkillScriptFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var skillName = args.TryGetProperty("skill_name", out var t1) ? t1.GetString() : null; + var scriptFile = args.TryGetProperty("script_file", out var t2) ? t2.GetString() : null; + // 简单处理 args,假设直接是字符串形式的命令行参数,或者 JSON 字符串 + // 如果是 JSON 对象,需要转换。为了简单起见,这里假设 LLM 传入的是参数字符串 + var scriptArgs = args.TryGetProperty("args", out var t3) ? t3.GetString() : ""; + + if (string.IsNullOrEmpty(skillName) || string.IsNullOrEmpty(scriptFile)) + { + message.Content = "Error: skill_name and script_file are required."; + return false; + } + + var skillService = _services.GetRequiredService(); + string scriptPath; + try + { + scriptPath = skillService.GetScriptPath(skillName, scriptFile); + if (string.IsNullOrEmpty(scriptPath)) + { + message.Content = $"Error: Script '{scriptFile}' not found in skill '{skillName}'."; + return false; + } + } + catch (Exception ex) + { + message.Content = $"Error: {ex.Message}"; + return false; + } + + // 目前仅支持 .py + if (scriptPath.EndsWith(".py")) + { + var runner = _services.GetRequiredService(); + try + { + var output = await runner.RunScript(scriptPath, scriptArgs); + message.Content = output; + return true; + } + catch (Exception ex) + { + message.Content = $"Script execution error: {ex.Message}"; + return false; + } + } + else + { + message.Content = "Error: Only .py scripts are supported currently."; + return false; + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Hooks/AgentSkillHook.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Hooks/AgentSkillHook.cs new file mode 100644 index 000000000..7c4b562fa --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Hooks/AgentSkillHook.cs @@ -0,0 +1,68 @@ +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Settings; +using BotSharp.Plugin.AgentSkills.Services; +using Microsoft.Extensions.DependencyInjection; +using BotSharp.Abstraction.Conversations; +using System.Text.Json; + +namespace BotSharp.Plugin.AgentSkills.Hooks; + +public class AgentSkillHook : AgentHookBase +{ + public override string SelfId => string.Empty; + + public AgentSkillHook(IServiceProvider services, AgentSettings settings) + : base(services, settings) + { + } + + public override bool OnInstructionLoaded(string template, IDictionary dict) + { + if(Agent.Type == AgentType.Routing || Agent.Type == AgentType.Planning) + { + return base.OnInstructionLoaded(template, dict); + } + var skillService = _services.GetRequiredService(); + var stateService = _services.GetRequiredService(); + + // 1. Discovery Phase: Inject Available Skills + var availableSkills = skillService.GetAvailableSkills().Result; // Sync for hook + if (availableSkills.Any()) + { + var skillMenu = "\n\n[Available Agent Skills]\nYou have access to the following specialized skills. If a task requires one, call the 'load_skill' function with the skill name.\n"; + foreach (var skill in availableSkills) + { + skillMenu += $"- {skill.Name}: {skill.Description}\n"; + } + + // 将菜单追加到 System Instruction 中 + // 注意:BotSharp 的 OnInstructionLoaded 允许修改 dict 还是 template? + // 假设我们修改 Agent.Instruction 或追加到 Context + this.Agent.Instruction += skillMenu; + } + + // 2. Activation Phase: Inject Active Skills + var activeSkillsJson = stateService.GetState("active_skills"); + if (!string.IsNullOrEmpty(activeSkillsJson)) + { + // 简单的 CSV 解析或 Json 解析,视 load_skill 存储格式而定 + // 假设 active_skills 是逗号分隔的字符串 + var activeSkillNames = activeSkillsJson.Split(',', StringSplitOptions.RemoveEmptyEntries); + + foreach(var name in activeSkillNames) + { + var skill = skillService.GetSkill(name.Trim()).Result; + if (skill != null) + { + this.Agent.Instruction += $"\n\n### ACTIVE SKILL: {skill.Name.ToUpper()}\n{skill.MarkdownBody}\n"; + } + } + } + + return base.OnInstructionLoaded(template, dict); + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Models/AgentSkill.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/AgentSkill.cs new file mode 100644 index 000000000..36dcf61d4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/AgentSkill.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace BotSharp.Plugin.AgentSkills.Models; + +public class AgentSkill +{ + public string Name { get; set; } + public string Description { get; set; } + public string MarkdownBody { get; set; } + public string BaseDir { get; set; } + public List Scripts { get; set; } = new List(); + public List Resources { get; set; } = new List(); +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Models/SkillFrontmatter.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/SkillFrontmatter.cs new file mode 100644 index 000000000..392cc6b94 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Models/SkillFrontmatter.cs @@ -0,0 +1,12 @@ +using YamlDotNet.Serialization; + +namespace BotSharp.Plugin.AgentSkills.Models; + +public class SkillFrontmatter +{ + [YamlMember(Alias = "name")] + public string Name { get; set; } + + [YamlMember(Alias = "description")] + public string Description { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Services/AgentSkillService.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/AgentSkillService.cs new file mode 100644 index 000000000..e16d257bd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/AgentSkillService.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using BotSharp.Plugin.AgentSkills.Models; +using BotSharp.Plugin.AgentSkills.Settings; + +namespace BotSharp.Plugin.AgentSkills.Services; + +public class AgentSkillService : IAgentSkillService, IDisposable +{ + private readonly AgentSkillsSettings _settings; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _skills = new(); + private readonly FileSystemWatcher _watcher; + private readonly IDeserializer _yamlDeserializer; + + public AgentSkillService(AgentSkillsSettings settings, ILogger logger) + { + _settings = settings; + _logger = logger; + _yamlDeserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + // 初始扫描 + RefreshSkills().Wait(); + + // 配置 FileSystemWatcher + var skillDir = Path.IsPathRooted(_settings.DataDir) + ? _settings.DataDir + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _settings.DataDir); + + if (Directory.Exists(skillDir)) + { + _watcher = new FileSystemWatcher(skillDir); + _watcher.Filter = "SKILL.md"; + _watcher.IncludeSubdirectories = true; + _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; + _watcher.Changed += OnSkillChanged; + _watcher.Created += OnSkillChanged; + _watcher.Deleted += OnSkillChanged; + _watcher.EnableRaisingEvents = true; + } + } + + private void OnSkillChanged(object sender, FileSystemEventArgs e) + { + _logger.LogInformation($"Detected change in skills: {e.FullPath}. Refreshing..."); + // 简单暴力:重新扫描。优化点:只更新变动的文件。 + RefreshSkills().Wait(); + } + + public async Task RefreshSkills() + { + var skillDir = Path.IsPathRooted(_settings.DataDir) + ? _settings.DataDir + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _settings.DataDir); + + if (!Directory.Exists(skillDir)) + { + _logger.LogWarning($"Skills directory not found: {skillDir}"); + try + { + Directory.CreateDirectory(skillDir); + } + catch(Exception ex) + { + _logger.LogError(ex, $"Failed to create skills directory at {skillDir}"); + return; + } + } + + var newSkills = new Dictionary(); + var skillFiles = Directory.GetFiles(skillDir, "SKILL.md", SearchOption.AllDirectories); + + foreach (var file in skillFiles) + { + try + { + var skill = ParseSkill(file); + if (skill != null) + { + newSkills[skill.Name] = skill; + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to parse skill at {file}"); + } + } + + _skills.Clear(); + foreach (var kv in newSkills) + { + _skills[kv.Key] = kv.Value; + } + + _logger.LogInformation($"Loaded {_skills.Count} skills."); + } + + private AgentSkill ParseSkill(string filePath) + { + var content = File.ReadAllText(filePath); + + // 简单的 Frontmatter 解析:查找两个 --- 之间的内容 + // 注意:不完美,假设文件严格以 --- 开头 + if (!content.StartsWith("---")) return null; + + var endYaml = content.IndexOf("---", 3); + if (endYaml == -1) return null; + + var yaml = content.Substring(3, endYaml - 3); + var markdown = content.Substring(endYaml + 3).Trim(); + + var frontmatter = _yamlDeserializer.Deserialize(yaml); + if (string.IsNullOrWhiteSpace(frontmatter.Name)) return null; + + var baseDir = Path.GetDirectoryName(filePath); + var scriptDir = Path.Combine(baseDir, "scripts"); + var scripts = Directory.Exists(scriptDir) + ? Directory.GetFiles(scriptDir).Select(Path.GetFileName).ToList() + : new List(); + + var resourceDir = Path.Combine(baseDir, "resources"); + var resources = Directory.Exists(resourceDir) + ? Directory.GetFiles(resourceDir).Select(Path.GetFileName).ToList() + : new List(); + + return new AgentSkill + { + Name = frontmatter.Name, + Description = frontmatter.Description, + MarkdownBody = markdown, + BaseDir = baseDir, + Scripts = scripts, + Resources = resources + }; + } + + public Task> GetAvailableSkills() + { + return Task.FromResult(_skills.Values.ToList()); + } + + public Task GetSkill(string name) + { + if (_skills.TryGetValue(name, out var skill)) + { + return Task.FromResult(skill); + } + return Task.FromResult(null); + } + + public string GetScriptPath(string skillName, string scriptFile) + { + if (_skills.TryGetValue(skillName, out var skill)) + { + // 安全检查:防止路径遍历 + if (scriptFile.Contains("..") || Path.IsPathRooted(scriptFile)) + throw new ArgumentException("Invalid script path"); + + var path = Path.Combine(skill.BaseDir, "scripts", scriptFile); + if (File.Exists(path)) return path; + } + return null; + } + + public void Dispose() + { + if (_watcher != null) + { + _watcher.EnableRaisingEvents = false; + _watcher.Dispose(); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Services/IAgentSkillService.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/IAgentSkillService.cs new file mode 100644 index 000000000..fd5d6e706 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Services/IAgentSkillService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using BotSharp.Plugin.AgentSkills.Models; + +namespace BotSharp.Plugin.AgentSkills.Services; + +public interface IAgentSkillService +{ + Task> GetAvailableSkills(); + Task GetSkill(string name); + string GetScriptPath(string skillName, string scriptFile); + Task RefreshSkills(); +} diff --git a/src/Plugins/BotSharp.Plugin.AgentSkills/Settings/AgentSkillsSettings.cs b/src/Plugins/BotSharp.Plugin.AgentSkills/Settings/AgentSkillsSettings.cs new file mode 100644 index 000000000..f9dfa1cfb --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AgentSkills/Settings/AgentSkillsSettings.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Plugin.AgentSkills.Settings; + +public class AgentSkillsSettings +{ + public string DataDir { get; set; } = "skills"; +} diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Interfaces/IPyScriptRunner.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Interfaces/IPyScriptRunner.cs new file mode 100644 index 000000000..1e183a5dd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Interfaces/IPyScriptRunner.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace BotSharp.Plugin.PythonInterpreter.Interfaces; + +public interface IPyScriptRunner +{ + Task RunScript(string scriptPath, string args); +} diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs index 0ca84c3ff..c2f092637 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/PythonInterpreterPlugin.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Python.Runtime; using System.IO; +using BotSharp.Plugin.PythonInterpreter.Interfaces; namespace BotSharp.Plugin.PythonInterpreter; @@ -20,9 +21,10 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) var settings = new PythonInterpreterSettings(); config.Bind("PythonInterpreter", settings); services.AddSingleton(x => settings); - + services.AddScoped(); services.AddScoped(); + services.AddScoped(); } public void Configure(IApplicationBuilder app) diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyScriptRunner.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyScriptRunner.cs new file mode 100644 index 000000000..78393e931 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyScriptRunner.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using BotSharp.Plugin.PythonInterpreter.Interfaces; +using BotSharp.Plugin.PythonInterpreter.Settings; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.PythonInterpreter.Services; + +public class PyScriptRunner : IPyScriptRunner +{ + private readonly PythonInterpreterSettings _settings; + private readonly ILogger _logger; + + public PyScriptRunner(PythonInterpreterSettings settings, ILogger logger) + { + _settings = settings; + _logger = logger; + } + + public async Task RunScript(string scriptPath, string args) + { + if (!File.Exists(scriptPath)) + { + throw new FileNotFoundException($"Python script not found: {scriptPath}"); + } + + var cmd = _settings.PythonVersion == "python3" ? "python3" : "python"; + // 允许配置绝对路径 + if (!string.IsNullOrEmpty(_settings.InstallLocation)) + { + cmd = Path.Combine(_settings.InstallLocation, cmd); + } + + var processStartInfo = new ProcessStartInfo + { + FileName = cmd, + Arguments = $"\"{scriptPath}\" {args}", // 注意参数转义,特别是包含空格的路径 + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + try + { + using var process = Process.Start(processStartInfo); + if (process == null) throw new Exception("Failed to start python process."); + + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + var output = await outputTask; + var error = await errorTask; + + if (process.ExitCode != 0) + { + _logger.LogError($"Python script execution failed. Exit code: {process.ExitCode}. Error: {error}"); + throw new Exception($"Script exited with code {process.ExitCode}: {error}"); + } + + return output; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error running python script."); + throw; + } + } +} diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index dc95144b9..79cebe2a5 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -38,9 +38,10 @@ - + + @@ -84,6 +85,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 5abab2aca..c8fecf358 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -59,22 +59,15 @@ ] }, { - "Name": "gpt-35-turbo-instruct", - "Version": "0914", - "ApiKey": "", - "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/", - "Type": "text", - "Capabilities": [ - "Text" - ], - "Cost": { - "TextInputCost": 0.0015, - "CachedTextInputCost": 0, - "AudioInputCost": 0, - "CachedAudioInputCost": 0, - "TextOutputCost": 0.002, - "AudioOutputCost": 0 - } + "Id": "gpt-4.1-mini", + "Name": "gpt-4.1-mini", + "ApiKey": "7i8UdCUrqvUuAwvC5ECktLLTmT34cVPHI5WOY3iX9CXSjn0j8p49JQQJ99BBACHYHv6XJ3w3AAAAACOGIfSa", + "Endpoint": "https://ai-east2ai4c450341534958.cognitiveservices.azure.com/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 } ] }, @@ -500,9 +493,9 @@ "Provider": "gitee-ai", "Models": [ { - "Name": "DeepSeek-V3_1", - "ApiKey": " ", - "Endpoint": "https://ai.gitee.com/v1/", + "Name": "mimo-v2-flash", + "ApiKey": "sk-CXttCsbEbvEzW9dGmur4bEfXB5Tog9Bi4krP", + "Endpoint": "http://182.254.176.239/v1", "Type": "chat", "PromptCost": 0.0015, "CompletionCost": 0.002, @@ -548,8 +541,8 @@ "HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c", "EnableTranslator": false, "LlmConfig": { - "Provider": "azure-openai", - "Model": "gpt-4.1" + "Provider": "gitee-ai", + "Model": "mimo-v2-flash" } }, @@ -895,10 +888,13 @@ } }, "Langfuse": { - "PublicKey": "pk-lf- ", - "SecretKey": "sk-lf- ", + "PublicKey": "pk-lf-a03b0da6-c0f4-4805-959a-fe359a60cea5", + "SecretKey": "sk-lf-074f71f9-8b4b-4f08-8e5e-ebe4dd3fff02", "BaseUrl": "https://us.cloud.langfuse.com/" }, + "AgentSkills": { + "DataDir": "C:/workshop/github/BotSharp/skills/test-skill" + }, "PluginLoader": { "Assemblies": [ "BotSharp.Core", @@ -940,7 +936,8 @@ "BotSharp.Plugin.SqlDriver", "BotSharp.Plugin.TencentCos", "BotSharp.Plugin.PythonInterpreter", - "BotSharp.Plugin.FuzzySharp" + "BotSharp.Plugin.FuzzySharp", + "BotSharp.Plugin.AgentSkills" ] } } diff --git a/tests/UnitTest/AgentSkillsTest.cs b/tests/UnitTest/AgentSkillsTest.cs new file mode 100644 index 000000000..19b930f4d --- /dev/null +++ b/tests/UnitTest/AgentSkillsTest.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using BotSharp.Plugin.AgentSkills.Services; +using BotSharp.Plugin.AgentSkills.Settings; +using Xunit; +using Assert = Xunit.Assert; + +namespace BotSharp.UnitTest +{ + public class AgentSkillsTest + { + [Fact] + public async Task TestGetAvailableSkills() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), "BotSharpTests", "Skills"); + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + Directory.CreateDirectory(tempDir); + + // Create a dummy SKILL.md + var skillDir = Path.Combine(tempDir, "TestSkill"); + Directory.CreateDirectory(skillDir); + var skillFile = Path.Combine(skillDir, "SKILL.md"); + var content = @"--- +name: TestSkill +description: This is a test skill +version: 1.0.0 +--- +System Prompt"; + await File.WriteAllTextAsync(skillFile, content); + + // Mock Settings and Logger + var settings = new AgentSkillsSettings { DataDir = tempDir }; + var loggerMock = new Mock>(); + + // Act + var service = new AgentSkillService(settings, loggerMock.Object); + // Service constructor calls RefreshSkills().Wait(), so skils should be loaded. + + var skills = await service.GetAvailableSkills(); + + // Assert + Assert.Single(skills); + Assert.Equal("TestSkill", skills.First().Name); + Assert.Equal("This is a test skill", skills.First().Description); + + // Cleanup + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + + [Fact] + public async Task TestGetSkillDetails() + { + // Arrange + var tempDir = Path.Combine(Path.GetTempPath(), "BotSharpTests", "Skills2"); + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + Directory.CreateDirectory(tempDir); + + // Create a dummy SKILL.md + var skillDir = Path.Combine(tempDir, "MathSkill"); + Directory.CreateDirectory(skillDir); + var skillFile = Path.Combine(skillDir, "SKILL.md"); + var content = @"--- +name: MathSkill +description: Performs math operations +version: 1.0.0 +--- +You are a math expert."; + await File.WriteAllTextAsync(skillFile, content); + + // Mock Settings and Logger + var settings = new AgentSkillsSettings { DataDir = tempDir }; + var loggerMock = new Mock>(); + + var service = new AgentSkillService(settings, loggerMock.Object); + + // Act + var skill = await service.GetSkill("MathSkill"); + + // Assert + Assert.NotNull(skill); + Assert.Equal("MathSkill", skill.Name); + Assert.Equal("You are a math expert.", skill.MarkdownBody); + + // Cleanup + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); + } + } +} diff --git a/tests/UnitTest/UnitTest.csproj b/tests/UnitTest/UnitTest.csproj index 444869f2c..3ea815f19 100644 --- a/tests/UnitTest/UnitTest.csproj +++ b/tests/UnitTest/UnitTest.csproj @@ -11,6 +11,7 @@ + @@ -18,8 +19,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + +