diff --git a/BotSharp.sln b/BotSharp.sln
index f6a6f5d93..e502b4b12 100644
--- a/BotSharp.sln
+++ b/BotSharp.sln
@@ -1,4 +1,3 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11217.181
@@ -145,12 +144,14 @@ 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.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{242F2D93-FCCE-4982-8075-F3052ECCA92C}"
+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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Membase", "src\Plugins\BotSharp.Plugin.Membase\BotSharp.Plugin.Membase.csproj", "{13223C71-9EAC-9835-28ED-5A4833E6F915}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{C548FDFF-B882-B552-D428-5C8EC4478187}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -615,14 +616,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
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|x64.ActiveCfg = Debug|Any CPU
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Debug|x64.Build.0 = Debug|Any CPU
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|Any CPU.Build.0 = Release|Any CPU
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.Release|x64.ActiveCfg = Release|Any CPU
- {242F2D93-FCCE-4982-8075-F3052ECCA92C}.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
{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
@@ -639,6 +640,14 @@ Global
{13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|Any CPU.Build.0 = Release|Any CPU
{13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.ActiveCfg = Release|Any CPU
{13223C71-9EAC-9835-28ED-5A4833E6F915}.Release|x64.Build.0 = Release|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Debug|x64.Build.0 = Debug|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.ActiveCfg = Release|Any CPU
+ {C548FDFF-B882-B552-D428-5C8EC4478187}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -709,9 +718,10 @@ 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}
- {242F2D93-FCCE-4982-8075-F3052ECCA92C} = {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}
{13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89}
+ {C548FDFF-B882-B552-D428-5C8EC4478187} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index aae7921bf..84e09a1fa 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,11 @@
true
+
+
+
+
+
@@ -33,7 +38,7 @@
-
+
@@ -100,7 +105,8 @@
-
+
+
@@ -165,7 +171,7 @@
-
+
@@ -190,4 +196,4 @@
-
+
\ No newline at end of file
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.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 78ccdc056..f64f850b0 100644
--- a/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj
+++ b/src/BotSharp.ServiceDefaults/BotSharp.ServiceDefaults.csproj
@@ -1,4 +1,4 @@
-
+
$(TargetFramework)
@@ -21,6 +21,7 @@
+
diff --git a/src/BotSharp.ServiceDefaults/Extensions.cs b/src/BotSharp.ServiceDefaults/Extensions.cs
index bfc0bb687..b4790f3e7 100644
--- a/src/BotSharp.ServiceDefaults/Extensions.cs
+++ b/src/BotSharp.ServiceDefaults/Extensions.cs
@@ -1,12 +1,12 @@
+using Langfuse.OpenTelemetry;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.ServiceDiscovery;
-using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Serilog;
@@ -45,6 +45,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 +91,29 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati
})
.WithTracing(tracing =>
{
- tracing.AddAspNetCoreInstrumentation()
+ tracing.SetResourceBuilder(
+ ResourceBuilder.CreateDefault()
+ .AddService("apiservice", serviceVersion: "1.0.0")
+ )
+ .AddSource("BotSharp")
+ .AddSource("BotSharp.Server")
+ .AddSource("BotSharp.Abstraction.Diagnostics")
+ .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();
+ .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,6 +123,8 @@ 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)
@@ -107,15 +132,9 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli
builder.Services.Configure(logging => logging.AddOtlpExporter());
builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
-
+
}
-
- // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
- //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
- //{
- // builder.Services.AddOpenTelemetry()
- // .UseAzureMonitor();
- //}
+
return builder;
}
diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj
index 37f1f2520..761d60372 100644
--- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj
+++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj
@@ -33,6 +33,11 @@
+
+
+
+
+
+
-
diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs
new file mode 100644
index 000000000..11c44a987
--- /dev/null
+++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/ActivityExtensions.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using BotSharp.Abstraction.Diagnostics.Telemetry;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+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
+{
+ 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.
+ ///
+ public static Activity? StartFunctionActivity(this ActivitySource source, string functionName, string functionDescription)
+ {
+ const string OperationName = "execute_tool";
+
+ return source.StartActivityWithTags($"{OperationName} {functionName}", [
+ new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.Operation, OperationName),
+ new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.ToolName, functionName),
+ new KeyValuePair(TelemetryConstants.ModelDiagnosticsTags.ToolDescription, 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;
+ }
+}
diff --git a/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs
new file mode 100644
index 000000000..2add23728
--- /dev/null
+++ b/src/Infrastructure/BotSharp.Abstraction/Diagnostics/AppContextSwitchHelper.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+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/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/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