| | | 1 | | using NexusLabs.Needlr.AgentFramework.Diagnostics; |
| | | 2 | | |
| | | 3 | | namespace NexusLabs.Needlr.AgentFramework.Langfuse; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Configuration for exporting Needlr agent telemetry and evaluation scores to Langfuse. |
| | | 7 | | /// </summary> |
| | | 8 | | /// <remarks> |
| | | 9 | | /// <para> |
| | | 10 | | /// The common path is <see cref="FromEnvironment"/>, which reads the standard Langfuse |
| | | 11 | | /// environment variables. When the public/secret keys are absent the integration disables |
| | | 12 | | /// itself and behaves as a no-op, so credential-less CI runs never fail. |
| | | 13 | | /// </para> |
| | | 14 | | /// <para> |
| | | 15 | | /// Needlr emits OpenTelemetry traces and metrics under well-known source and meter names. |
| | | 16 | | /// The defaults here match <see cref="AgentFrameworkMetricsOptions"/>. If a consumer has |
| | | 17 | | /// customised those names via <c>ConfigureMetrics(...)</c>, set |
| | | 18 | | /// <see cref="AgentActivitySourceName"/>, <see cref="AgentMeterName"/>, and |
| | | 19 | | /// <see cref="GenAiMeterName"/> to the same values, or add them through |
| | | 20 | | /// <see cref="AdditionalActivitySources"/> / <see cref="AdditionalMeters"/>. |
| | | 21 | | /// </para> |
| | | 22 | | /// </remarks> |
| | | 23 | | public sealed class LangfuseOptions |
| | | 24 | | { |
| | 1 | 25 | | private static readonly AgentFrameworkMetricsOptions MetricsDefaults = new(); |
| | | 26 | | |
| | | 27 | | /// <summary> |
| | | 28 | | /// Environment variable read for the Langfuse public key by <see cref="FromEnvironment"/>. |
| | | 29 | | /// </summary> |
| | | 30 | | public const string PublicKeyEnvironmentVariable = "LANGFUSE_PUBLIC_KEY"; |
| | | 31 | | |
| | | 32 | | /// <summary> |
| | | 33 | | /// Environment variable read for the Langfuse secret key by <see cref="FromEnvironment"/>. |
| | | 34 | | /// </summary> |
| | | 35 | | public const string SecretKeyEnvironmentVariable = "LANGFUSE_SECRET_KEY"; |
| | | 36 | | |
| | | 37 | | /// <summary> |
| | | 38 | | /// Environment variable read for the Langfuse host (base URL) by <see cref="FromEnvironment"/>. |
| | | 39 | | /// </summary> |
| | | 40 | | public const string HostEnvironmentVariable = "LANGFUSE_HOST"; |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// Gets or sets the Langfuse public key (<c>pk-lf-...</c>). Required for export. |
| | | 44 | | /// </summary> |
| | 62 | 45 | | public string? PublicKey { get; set; } |
| | | 46 | | |
| | | 47 | | /// <summary> |
| | | 48 | | /// Gets or sets the Langfuse secret key (<c>sk-lf-...</c>). Required for export. |
| | | 49 | | /// </summary> |
| | 54 | 50 | | public string? SecretKey { get; set; } |
| | | 51 | | |
| | | 52 | | /// <summary> |
| | | 53 | | /// Gets or sets the Langfuse base URL (for example <c>https://cloud.langfuse.com</c> or a |
| | | 54 | | /// self-hosted <c>http://localhost:3000</c>). When set, this takes precedence over |
| | | 55 | | /// <see cref="Region"/>. When <see langword="null"/>, the URL is derived from |
| | | 56 | | /// <see cref="Region"/>. |
| | | 57 | | /// </summary> |
| | 32 | 58 | | public string? Host { get; set; } |
| | | 59 | | |
| | | 60 | | /// <summary> |
| | | 61 | | /// Gets or sets the Langfuse Cloud data region. <see langword="null"/> by default — exporting |
| | | 62 | | /// to a cloud region is therefore an explicit, opt-in choice. Ignored when <see cref="Host"/> |
| | | 63 | | /// is set. |
| | | 64 | | /// </summary> |
| | | 65 | | /// <remarks> |
| | | 66 | | /// To avoid silently sending traces (which may include prompts, agent outputs, and customer |
| | | 67 | | /// data) to Langfuse Cloud, this integration requires an <strong>explicit</strong> target: set |
| | | 68 | | /// <see cref="Host"/> for a self-hosted deployment, or set <see cref="Region"/> to deliberately |
| | | 69 | | /// opt in to a Langfuse Cloud region. When neither is set, export is disabled even if |
| | | 70 | | /// credentials are present (see <see cref="IsConfigured"/>) and a message is sent to |
| | | 71 | | /// <see cref="DiagnosticsCallback"/>. |
| | | 72 | | /// </remarks> |
| | 23 | 73 | | public LangfuseRegion? Region { get; set; } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Gets or sets a value indicating whether export is enabled. When <see langword="true"/> |
| | | 77 | | /// (the default) export still only occurs if <see cref="PublicKey"/> and |
| | | 78 | | /// <see cref="SecretKey"/> are both present — see <see cref="IsConfigured"/>. Set to |
| | | 79 | | /// <see langword="false"/> to force a no-op regardless of credentials. |
| | | 80 | | /// </summary> |
| | 54 | 81 | | public bool Enabled { get; set; } = true; |
| | | 82 | | |
| | | 83 | | /// <summary> |
| | | 84 | | /// Gets or sets the OpenTelemetry <c>service.name</c> resource attribute applied to exported |
| | | 85 | | /// telemetry. Surfaces in Langfuse as the originating service. Defaults to |
| | | 86 | | /// <c>"needlr-agent"</c>. |
| | | 87 | | /// </summary> |
| | 29 | 88 | | public string ServiceName { get; set; } = "needlr-agent"; |
| | | 89 | | |
| | | 90 | | /// <summary> |
| | | 91 | | /// Gets or sets the optional OpenTelemetry <c>service.version</c> resource attribute. |
| | | 92 | | /// </summary> |
| | 1 | 93 | | public string? ServiceVersion { get; set; } |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// Gets or sets the Langfuse deployment environment (for example <c>ci</c>, <c>local</c>, |
| | | 97 | | /// <c>staging</c>, or <c>production</c>). When set, it is emitted as <c>langfuse.environment</c> |
| | | 98 | | /// on every exported span so Langfuse partitions this run's data — keeping CI eval noise out of |
| | | 99 | | /// production dashboards. <see langword="null"/> by default (Langfuse uses its <c>default</c> |
| | | 100 | | /// environment). |
| | | 101 | | /// </summary> |
| | 1 | 102 | | public string? Environment { get; set; } |
| | | 103 | | |
| | | 104 | | /// <summary> |
| | | 105 | | /// Gets or sets the application release identifier (for example a git SHA or semantic version). |
| | | 106 | | /// When set, it is emitted as <c>langfuse.release</c> on every exported span so scores, cost, |
| | | 107 | | /// and latency can be compared across releases. <see langword="null"/> by default. |
| | | 108 | | /// </summary> |
| | 1 | 109 | | public string? Release { get; set; } |
| | | 110 | | |
| | | 111 | | /// <summary> |
| | | 112 | | /// Gets or sets a value indicating whether Needlr's <c>gen_ai</c> metrics (including the |
| | | 113 | | /// <c>gen_ai.client.token.usage</c> histogram) are exported alongside traces. Defaults to |
| | | 114 | | /// <see langword="false"/>. |
| | | 115 | | /// </summary> |
| | | 116 | | /// <remarks> |
| | | 117 | | /// As of Langfuse v3.x the OTLP metrics endpoint (<c>/api/public/otel/v1/metrics</c>) accepts |
| | | 118 | | /// requests (returns HTTP 200) but does <strong>not</strong> ingest the data — there is no |
| | | 119 | | /// corresponding metrics read API, so exported metrics are silently discarded. Token usage is |
| | | 120 | | /// already carried on the generation spans, so metrics export is off by default. Enable this |
| | | 121 | | /// only if you point the exporter at a backend that ingests OTLP metrics. |
| | | 122 | | /// </remarks> |
| | 2 | 123 | | public bool IncludeMetrics { get; set; } |
| | | 124 | | |
| | | 125 | | /// <summary> |
| | | 126 | | /// Gets or sets how a failed evaluation-score upload is handled. Defaults to |
| | | 127 | | /// <see cref="LangfuseScoreFailureMode.NonFatal"/> so a transient Langfuse outage does not turn |
| | | 128 | | /// a passing eval into a failure. |
| | | 129 | | /// </summary> |
| | 2 | 130 | | public LangfuseScoreFailureMode ScoreFailureMode { get; set; } = LangfuseScoreFailureMode.NonFatal; |
| | | 131 | | |
| | | 132 | | /// <summary> |
| | | 133 | | /// Gets or sets an optional callback invoked when a score upload fails under |
| | | 134 | | /// <see cref="LangfuseScoreFailureMode.NonFatal"/>. Use it to log the loss with your own logger. |
| | | 135 | | /// </summary> |
| | 1 | 136 | | public Action<LangfuseScoreError>? ScoreErrorCallback { get; set; } |
| | | 137 | | |
| | | 138 | | /// <summary> |
| | | 139 | | /// Gets or sets a value indicating whether evaluator metric names are normalised to |
| | | 140 | | /// <c>snake_case</c> before being sent as Langfuse score names. Off by default; names are sent |
| | | 141 | | /// verbatim. Enable for consistent dashboard filtering/grouping. |
| | | 142 | | /// </summary> |
| | 2 | 143 | | public bool NormalizeScoreNames { get; set; } |
| | | 144 | | |
| | | 145 | | /// <summary> |
| | | 146 | | /// Gets or sets an optional callback for library diagnostic messages — for example, the warning |
| | | 147 | | /// emitted when credentials are present but no export target (<see cref="Host"/> or |
| | | 148 | | /// <see cref="Region"/>) was chosen. Wire it to your logger to surface these conditions. |
| | | 149 | | /// </summary> |
| | 3 | 150 | | public Action<string>? DiagnosticsCallback { get; set; } |
| | | 151 | | |
| | | 152 | | /// <summary> |
| | | 153 | | /// Gets or sets the head-based trace sampling ratio in the range <c>0.0</c> to <c>1.0</c>. |
| | | 154 | | /// Defaults to <c>1.0</c> (sample everything), which is appropriate for eval workloads. |
| | | 155 | | /// </summary> |
| | 29 | 156 | | public double SamplingRatio { get; set; } = 1.0; |
| | | 157 | | |
| | | 158 | | /// <summary> |
| | | 159 | | /// Gets or sets the name of Needlr's agent <see cref="System.Diagnostics.ActivitySource"/> |
| | | 160 | | /// to export. Defaults to <see cref="AgentFrameworkMetricsOptions.MeterName"/>'s default. |
| | | 161 | | /// </summary> |
| | 30 | 162 | | public string AgentActivitySourceName { get; set; } = MetricsDefaults.MeterName; |
| | | 163 | | |
| | | 164 | | /// <summary> |
| | | 165 | | /// Gets or sets the name of Needlr's agent <see cref="System.Diagnostics.Metrics.Meter"/> |
| | | 166 | | /// to export. Defaults to <see cref="AgentFrameworkMetricsOptions.MeterName"/>'s default. |
| | | 167 | | /// </summary> |
| | 29 | 168 | | public string AgentMeterName { get; set; } = MetricsDefaults.MeterName; |
| | | 169 | | |
| | | 170 | | /// <summary> |
| | | 171 | | /// Gets or sets the name of the meter that owns the <c>gen_ai.client.token.usage</c> |
| | | 172 | | /// histogram shared by Needlr and MEAI. Defaults to |
| | | 173 | | /// <see cref="AgentFrameworkMetricsOptions.GenAiMeterName"/>'s default |
| | | 174 | | /// (<c>"Experimental.Microsoft.Extensions.AI"</c>). |
| | | 175 | | /// </summary> |
| | 29 | 176 | | public string GenAiMeterName { get; set; } = MetricsDefaults.GenAiMeterName; |
| | | 177 | | |
| | | 178 | | /// <summary> |
| | | 179 | | /// Gets a mutable list of additional <see cref="System.Diagnostics.ActivitySource"/> names |
| | | 180 | | /// to export — for example a host's own source or MAF's agent source. |
| | | 181 | | /// </summary> |
| | 29 | 182 | | public IList<string> AdditionalActivitySources { get; } = []; |
| | | 183 | | |
| | | 184 | | /// <summary> |
| | | 185 | | /// Gets a mutable list of additional <see cref="System.Diagnostics.Metrics.Meter"/> names |
| | | 186 | | /// to export. |
| | | 187 | | /// </summary> |
| | 28 | 188 | | public IList<string> AdditionalMeters { get; } = []; |
| | | 189 | | |
| | | 190 | | /// <summary> |
| | | 191 | | /// Gets a value indicating whether both API keys are present and export is enabled. Does not |
| | | 192 | | /// account for whether an export target was chosen — see <see cref="HasExplicitTarget"/> and |
| | | 193 | | /// <see cref="IsConfigured"/>. |
| | | 194 | | /// </summary> |
| | | 195 | | public bool HasCredentials => |
| | 23 | 196 | | Enabled |
| | 23 | 197 | | && !string.IsNullOrWhiteSpace(PublicKey) |
| | 23 | 198 | | && !string.IsNullOrWhiteSpace(SecretKey); |
| | | 199 | | |
| | | 200 | | /// <summary> |
| | | 201 | | /// Gets a value indicating whether an explicit export target was chosen — either a |
| | | 202 | | /// <see cref="Host"/> (self-hosted) or a <see cref="Region"/> (deliberate Langfuse Cloud |
| | | 203 | | /// opt-in). |
| | | 204 | | /// </summary> |
| | | 205 | | public bool HasExplicitTarget => |
| | 10 | 206 | | !string.IsNullOrWhiteSpace(Host) || Region.HasValue; |
| | | 207 | | |
| | | 208 | | /// <summary> |
| | | 209 | | /// Gets a value indicating whether the integration is fully configured to export: credentials |
| | | 210 | | /// are present, export is enabled, and an explicit target (<see cref="Host"/> or |
| | | 211 | | /// <see cref="Region"/>) was chosen. Requiring an explicit target prevents accidentally sending |
| | | 212 | | /// traces to Langfuse Cloud. |
| | | 213 | | /// </summary> |
| | 16 | 214 | | public bool IsConfigured => HasCredentials && HasExplicitTarget; |
| | | 215 | | |
| | | 216 | | /// <summary> |
| | | 217 | | /// Builds a <see cref="LangfuseOptions"/> from the standard Langfuse environment variables |
| | | 218 | | /// (<see cref="PublicKeyEnvironmentVariable"/>, <see cref="SecretKeyEnvironmentVariable"/>, |
| | | 219 | | /// and <see cref="HostEnvironmentVariable"/>). |
| | | 220 | | /// </summary> |
| | | 221 | | /// <returns> |
| | | 222 | | /// A populated <see cref="LangfuseOptions"/>. When the keys are absent the result has |
| | | 223 | | /// <see cref="IsConfigured"/> equal to <see langword="false"/> and the integration no-ops. |
| | | 224 | | /// </returns> |
| | | 225 | | public static LangfuseOptions FromEnvironment() |
| | | 226 | | { |
| | 5 | 227 | | var options = new LangfuseOptions |
| | 5 | 228 | | { |
| | 5 | 229 | | PublicKey = NullIfBlank(System.Environment.GetEnvironmentVariable(PublicKeyEnvironmentVariable)), |
| | 5 | 230 | | SecretKey = NullIfBlank(System.Environment.GetEnvironmentVariable(SecretKeyEnvironmentVariable)), |
| | 5 | 231 | | Host = NullIfBlank(System.Environment.GetEnvironmentVariable(HostEnvironmentVariable)), |
| | 5 | 232 | | }; |
| | | 233 | | |
| | 5 | 234 | | return options; |
| | | 235 | | } |
| | | 236 | | |
| | | 237 | | private static string? NullIfBlank(string? value) => |
| | 15 | 238 | | string.IsNullOrWhiteSpace(value) ? null : value; |
| | | 239 | | } |