From b97162d5a42a0deabcb63a72c9322b39bbc1f801 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Thu, 9 Oct 2025 06:42:15 +0800 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] =?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/15] =?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/15] 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/15] 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/15] =?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/15] =?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/15] =?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/15] 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/15] 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;