Skip to content

Commit 044cc2a

Browse files
committed
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.
1 parent 479087c commit 044cc2a

File tree

25 files changed

+1609
-205
lines changed

25 files changed

+1609
-205
lines changed

BotSharp.sln

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ChartHandle
147147
EndProject
148148
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.ExcelHandler", "src\Plugins\BotSharp.Plugin.ExcelHandler\BotSharp.Plugin.ExcelHandler.csproj", "{FC63C875-E880-D8BB-B8B5-978AB7B62983}"
149149
EndProject
150+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.GiteeAI", "src\Plugins\BotSharp.Plugin.GiteeAI\BotSharp.Plugin.GiteeAI.csproj", "{50B57066-3267-1D10-0F72-D2F5CC494F2C}"
151+
EndProject
150152
Global
151153
GlobalSection(SolutionConfigurationPlatforms) = preSolution
152154
Debug|Any CPU = Debug|Any CPU
@@ -619,6 +621,14 @@ Global
619621
{FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|Any CPU.Build.0 = Release|Any CPU
620622
{FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.ActiveCfg = Release|Any CPU
621623
{FC63C875-E880-D8BB-B8B5-978AB7B62983}.Release|x64.Build.0 = Release|Any CPU
624+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
625+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
626+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.ActiveCfg = Debug|Any CPU
627+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Debug|x64.Build.0 = Debug|Any CPU
628+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
629+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|Any CPU.Build.0 = Release|Any CPU
630+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.ActiveCfg = Release|Any CPU
631+
{50B57066-3267-1D10-0F72-D2F5CC494F2C}.Release|x64.Build.0 = Release|Any CPU
622632
EndGlobalSection
623633
GlobalSection(SolutionProperties) = preSolution
624634
HideSolutionNode = FALSE
@@ -690,6 +700,7 @@ Global
690700
{B067B126-88CD-4282-BEEF-7369B64423EF} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
691701
{0428DEAA-E4FE-4259-A6D8-6EDD1A9D0702} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
692702
{FC63C875-E880-D8BB-B8B5-978AB7B62983} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
703+
{50B57066-3267-1D10-0F72-D2F5CC494F2C} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
693704
EndGlobalSection
694705
GlobalSection(ExtensibilityGlobals) = postSolution
695706
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

src/BotSharp.AppHost/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
var apiService = builder.AddProject<Projects.WebStarter>("apiservice")
44
.WithExternalHttpEndpoints();
5-
var mcpService = builder.AddProject<Projects.BotSharp_PizzaBot_MCPServer>("mcpservice")
6-
.WithExternalHttpEndpoints();
5+
//var mcpService = builder.AddProject<Projects.BotSharp_PizzaBot_MCPServer>("mcpservice")
6+
// .WithExternalHttpEndpoints();
77

88
builder.AddNpmApp("BotSharpUI", "../../../BotSharp-UI")
99
.WithReference(apiService)

src/BotSharp.ServiceDefaults/Extensions.cs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
using BotSharp.Langfuse;
12
using Microsoft.AspNetCore.Builder;
23
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
4+
using Microsoft.Extensions.Configuration;
35
using Microsoft.Extensions.DependencyInjection;
46
using Microsoft.Extensions.Diagnostics.HealthChecks;
57
using Microsoft.Extensions.Logging;
68
using Microsoft.Extensions.ServiceDiscovery;
79
using OpenTelemetry;
10+
using OpenTelemetry.Exporter;
811
using OpenTelemetry.Logs;
912
using OpenTelemetry.Metrics;
13+
using OpenTelemetry.Resources;
1014
using OpenTelemetry.Trace;
1115
using Serilog;
1216

@@ -45,6 +49,10 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
4549

4650
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
4751
{
52+
// Enable model diagnostics with sensitive data.
53+
AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnostics", true);
54+
AppContext.SetSwitch("BotSharp.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true);
55+
4856
builder.Logging.AddOpenTelemetry(logging =>
4957
{ // Use Serilog
5058
Log.Logger = new LoggerConfiguration()
@@ -87,10 +95,28 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati
8795
})
8896
.WithTracing(tracing =>
8997
{
98+
tracing.SetResourceBuilder(
99+
ResourceBuilder.CreateDefault()
100+
.AddService("apiservice", serviceVersion: "1.0.0")
101+
)
102+
.AddSource("BotSharp")
103+
.AddSource("BotSharp.Abstraction.Diagnostics")
104+
.AddSource("BotSharp.Core.Routing.Executor");
105+
90106
tracing.AddAspNetCoreInstrumentation()
91107
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
92108
//.AddGrpcClientInstrumentation()
93-
.AddHttpClientInstrumentation();
109+
.AddHttpClientInstrumentation()
110+
//.AddOtlpExporter(options =>
111+
//{
112+
// //options.Endpoint = new Uri(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? "http://localhost:4317");
113+
// options.Endpoint = new Uri(host);
114+
// options.Protocol = OtlpExportProtocol.HttpProtobuf;
115+
// options.Headers = $"Authorization=Basic {base64EncodedAuth}";
116+
//})
117+
;
118+
119+
94120
});
95121

96122
builder.AddOpenTelemetryExporters();
@@ -100,14 +126,34 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati
100126

101127
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
102128
{
129+
var langfuseSection = builder.Configuration.GetSection("Langfuse");
130+
var useLangfuse = langfuseSection != null;
103131
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
104132

105133
if (useOtlpExporter)
106134
{
107135
builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());
108136
builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
109-
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
110-
137+
if (useLangfuse)
138+
{
139+
var publicKey = langfuseSection.GetValue<string>(nameof(LangfuseSettings.PublicKey)) ?? string.Empty;
140+
var secretKey = langfuseSection.GetValue<string>(nameof(LangfuseSettings.SecretKey)) ?? string.Empty;
141+
var host = langfuseSection.GetValue<string>(nameof(LangfuseSettings.Host)) ?? string.Empty;
142+
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{publicKey}:{secretKey}");
143+
string base64EncodedAuth = Convert.ToBase64String(plainTextBytes);
144+
145+
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter(options =>
146+
{
147+
options.Endpoint = new Uri(host);
148+
options.Protocol = OtlpExportProtocol.HttpProtobuf;
149+
options.Headers = $"Authorization=Basic {base64EncodedAuth}";
150+
})
151+
);
152+
}
153+
else
154+
{
155+
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
156+
}
111157
}
112158

113159
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace BotSharp.Langfuse;
8+
9+
/// <summary>
10+
/// Langfuse Settings
11+
/// </summary>
12+
public class LangfuseSettings
13+
{
14+
public string SecretKey { get; set; }
15+
16+
public string PublicKey { get; set; }
17+
18+
public string Host { get; set; }
19+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Runtime.CompilerServices;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace BotSharp.Abstraction.Diagnostics;
12+
13+
[ExcludeFromCodeCoverage]
14+
public static class ActivityExtensions
15+
{
16+
/// <summary>
17+
/// Starts an activity with the appropriate tags for a kernel function execution.
18+
/// </summary>
19+
public static Activity? StartFunctionActivity(this ActivitySource source, string functionName, string functionDescription)
20+
{
21+
const string OperationName = "execute_tool";
22+
23+
return source.StartActivityWithTags($"{OperationName} {functionName}", [
24+
new KeyValuePair<string, object?>("gen_ai.operation.name", OperationName),
25+
new KeyValuePair<string, object?>("gen_ai.tool.name", functionName),
26+
new KeyValuePair<string, object?>("gen_ai.tool.description", functionDescription)
27+
], ActivityKind.Internal);
28+
}
29+
30+
/// <summary>
31+
/// Starts an activity with the specified name and tags.
32+
/// </summary>
33+
public static Activity? StartActivityWithTags(this ActivitySource source, string name, IEnumerable<KeyValuePair<string, object?>> tags, ActivityKind kind = ActivityKind.Internal)
34+
=> source.StartActivity(name, kind, default(ActivityContext), tags);
35+
36+
/// <summary>
37+
/// Adds tags to the activity.
38+
/// </summary>
39+
public static Activity SetTags(this Activity activity, ReadOnlySpan<KeyValuePair<string, object?>> tags)
40+
{
41+
foreach (var tag in tags)
42+
{
43+
activity.SetTag(tag.Key, tag.Value);
44+
}
45+
;
46+
47+
return activity;
48+
}
49+
50+
/// <summary>
51+
/// Adds an event to the activity. Should only be used for events that contain sensitive data.
52+
/// </summary>
53+
public static Activity AttachSensitiveDataAsEvent(this Activity activity, string name, IEnumerable<KeyValuePair<string, object?>> tags)
54+
{
55+
activity.AddEvent(new ActivityEvent(
56+
name,
57+
tags: [.. tags]
58+
));
59+
60+
return activity;
61+
}
62+
63+
/// <summary>
64+
/// Sets the error status and type on the activity.
65+
/// </summary>
66+
public static Activity SetError(this Activity activity, Exception exception)
67+
{
68+
activity.SetTag("error.type", exception.GetType().FullName);
69+
activity.SetStatus(ActivityStatusCode.Error, exception.Message);
70+
return activity;
71+
}
72+
73+
public static async IAsyncEnumerable<TResult> RunWithActivityAsync<TResult>(
74+
Func<Activity?> getActivity,
75+
Func<IAsyncEnumerable<TResult>> operation,
76+
[EnumeratorCancellation] CancellationToken cancellationToken)
77+
{
78+
using var activity = getActivity();
79+
80+
ConfiguredCancelableAsyncEnumerable<TResult> result;
81+
82+
try
83+
{
84+
result = operation().WithCancellation(cancellationToken).ConfigureAwait(false);
85+
}
86+
catch (Exception ex) when (activity is not null)
87+
{
88+
activity.SetError(ex);
89+
throw;
90+
}
91+
92+
var resultEnumerator = result.ConfigureAwait(false).GetAsyncEnumerator();
93+
94+
try
95+
{
96+
while (true)
97+
{
98+
try
99+
{
100+
if (!await resultEnumerator.MoveNextAsync())
101+
{
102+
break;
103+
}
104+
}
105+
catch (Exception ex) when (activity is not null)
106+
{
107+
activity.SetError(ex);
108+
throw;
109+
}
110+
111+
yield return resultEnumerator.Current;
112+
}
113+
}
114+
finally
115+
{
116+
await resultEnumerator.DisposeAsync();
117+
}
118+
}
119+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
4+
namespace BotSharp.Abstraction.Diagnostics;
5+
6+
/// <summary>
7+
/// Helper class to get app context switch value
8+
/// </summary>
9+
[ExcludeFromCodeCoverage]
10+
internal static class AppContextSwitchHelper
11+
{
12+
/// <summary>
13+
/// Returns the value of the specified app switch or environment variable if it is set.
14+
/// If the switch or environment variable is not set, return false.
15+
/// The app switch value takes precedence over the environment variable.
16+
/// </summary>
17+
/// <param name="appContextSwitchName">The name of the app switch.</param>
18+
/// <param name="envVarName">The name of the environment variable.</param>
19+
/// <returns>The value of the app switch or environment variable if it is set; otherwise, false.</returns>
20+
public static bool GetConfigValue(string appContextSwitchName, string envVarName)
21+
{
22+
if (AppContext.TryGetSwitch(appContextSwitchName, out bool value))
23+
{
24+
return value;
25+
}
26+
27+
string? envVarValue = Environment.GetEnvironmentVariable(envVarName);
28+
if (envVarValue != null && bool.TryParse(envVarValue, out value))
29+
{
30+
return value;
31+
}
32+
33+
return false;
34+
}
35+
}

0 commit comments

Comments
 (0)