diff --git a/BotSharp.sln b/BotSharp.sln
index 5079435f3..cd02dfa71 100644
--- a/BotSharp.sln
+++ b/BotSharp.sln
@@ -145,6 +145,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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ImageHandler", "src\Plugins\BotSharp.Plugin.ImageHandler\BotSharp.Plugin.ImageHandler.csproj", "{242F2D93-FCCE-4982-8075-F3052ECCA92C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.FuzzySharp", "src\Plugins\BotSharp.Plugin.FuzzySharp\BotSharp.Plugin.FuzzySharp.csproj", "{E7C243B9-E751-B3B4-8F16-95C76CA90D31}"
@@ -153,6 +155,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MMPEmbeddin
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.A2A", "src\Infrastructure\BotSharp.Plugin.A2A\BotSharp.Plugin.A2A.csproj", "{89A13E1B-2BAC-493C-A194-183B8BE73230}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -617,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
{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
@@ -649,6 +661,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
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Debug|x64.Build.0 = Debug|Any CPU
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Release|x64.ActiveCfg = Release|Any CPU
+ {89A13E1B-2BAC-493C-A194-183B8BE73230}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -719,10 +739,12 @@ 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}
{242F2D93-FCCE-4982-8075-F3052ECCA92C} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{E7C243B9-E751-B3B4-8F16-95C76CA90D31} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{394B858B-9C26-B977-A2DA-8CC7BE5914CB} = {4F346DCE-087F-4368-AF88-EE9C720D0E69}
{13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89}
+ {89A13E1B-2BAC-493C-A194-183B8BE73230} = {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 5783e8002..ff4a7efe6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
true
+
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/Agents/Enums/AgentType.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs
index 3b9767845..9f79ef333 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs
@@ -23,5 +23,10 @@ public static class AgentType
/// Agent that cannot use external tools
///
public const string Static = "static";
+
+ ///
+ /// A2A remote agent for Microsoft Agent Framework integration
+ ///
+ public const string A2ARemote = "a2a-remote";
}
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