tinyagents/harness/observability/langfuse/types.rs
1//! Type definitions for the Langfuse ingestion exporter.
2//!
3//! Split out of `langfuse/mod.rs`; see that module's doc comment for the
4//! exporter overview.
5
6use serde::Serialize;
7use serde_json::Value;
8
9/// Authentication mode for [`LangfuseClient`].
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum LangfuseAuth {
12 /// Send `Authorization: Basic base64(public_key:secret_key)`.
13 Basic {
14 /// Langfuse project public key.
15 public_key: String,
16 /// Langfuse project secret key.
17 secret_key: String,
18 },
19 /// Send `Authorization: Bearer <token>`.
20 ///
21 /// Use this when targeting the TinyHumans backend proxy at
22 /// `/telemetry/langfuse/ingestion`; the backend injects Langfuse Basic Auth.
23 Bearer {
24 /// Backend access token.
25 token: String,
26 },
27}
28
29/// Configuration for a Langfuse trace export.
30#[derive(Clone, Debug, Default, PartialEq, Serialize)]
31pub struct LangfuseTraceConfig {
32 /// Stable Langfuse trace id. Defaults to the first observation's root run id.
33 pub trace_id: Option<String>,
34 /// Human-readable trace name.
35 pub name: Option<String>,
36 /// End-user id to filter by in Langfuse.
37 pub user_id: Option<String>,
38 /// Session/thread id to group related traces.
39 pub session_id: Option<String>,
40 /// Langfuse environment name.
41 pub environment: Option<String>,
42 /// Release identifier.
43 pub release: Option<String>,
44 /// Version identifier.
45 pub version: Option<String>,
46 /// Tags attached to the trace.
47 #[serde(default)]
48 pub tags: Vec<String>,
49 /// Extra trace metadata.
50 #[serde(default)]
51 pub metadata: Value,
52}
53
54/// The value of a [`LangfuseScore`], typed the way Langfuse classifies scores.
55///
56/// Langfuse stores three score data types; the variant selected here maps to
57/// the `dataType` and the shape of the `value` field in the `score-create`
58/// ingestion event (a number for numeric/boolean, a string for categorical).
59#[derive(Clone, Debug, PartialEq)]
60pub enum LangfuseScoreValue {
61 /// A continuous numeric score (`dataType: "NUMERIC"`).
62 Numeric(f64),
63 /// A discrete label (`dataType: "CATEGORICAL"`), e.g. `"correct"`.
64 Categorical(String),
65 /// A pass/fail score (`dataType: "BOOLEAN"`), serialized as `1`/`0`.
66 Boolean(bool),
67}
68
69impl LangfuseScoreValue {
70 /// Returns the Langfuse `dataType` string for this value.
71 pub fn data_type(&self) -> &'static str {
72 match self {
73 LangfuseScoreValue::Numeric(_) => "NUMERIC",
74 LangfuseScoreValue::Categorical(_) => "CATEGORICAL",
75 LangfuseScoreValue::Boolean(_) => "BOOLEAN",
76 }
77 }
78
79 /// Returns the JSON `value` payload: a number for numeric/boolean scores, a
80 /// string for categorical ones.
81 pub fn to_value(&self) -> Value {
82 match self {
83 LangfuseScoreValue::Numeric(n) => Value::from(*n),
84 LangfuseScoreValue::Categorical(s) => Value::from(s.clone()),
85 LangfuseScoreValue::Boolean(b) => Value::from(if *b { 1 } else { 0 }),
86 }
87 }
88}
89
90/// An evaluation score attached to a Langfuse trace or a single observation.
91///
92/// Mirrors Langfuse's `createScore` / `score-create` ingestion event: a named,
93/// typed value scoped to a `trace_id` and optionally narrowed to one
94/// `observation_id` (a specific generation or span), with an optional free-text
95/// comment. This is how post-hoc evaluations — human ratings, automated
96/// LLM-as-judge checks, regression metrics — are correlated back to the run
97/// that produced them.
98#[derive(Clone, Debug, PartialEq)]
99pub struct LangfuseScore {
100 /// The trace the score is attached to.
101 pub trace_id: String,
102 /// A specific observation (generation/span) to scope the score to, when the
103 /// score grades one step rather than the whole trace.
104 pub observation_id: Option<String>,
105 /// The score name (its metric key), e.g. `"helpfulness"`.
106 pub name: String,
107 /// The typed score value.
108 pub value: LangfuseScoreValue,
109 /// Optional free-text rationale stored alongside the score.
110 pub comment: Option<String>,
111 /// Stable score id for idempotent re-ingestion. Defaults (in
112 /// [`LangfuseClient::build_score_batch`]) to a deterministic id derived from
113 /// the trace, observation, and name, so re-scoring the same target updates
114 /// the existing score instead of creating a duplicate.
115 pub id: Option<String>,
116}
117
118impl LangfuseScore {
119 /// Builds a trace-level numeric score.
120 pub fn numeric(trace_id: impl Into<String>, name: impl Into<String>, value: f64) -> Self {
121 Self {
122 trace_id: trace_id.into(),
123 observation_id: None,
124 name: name.into(),
125 value: LangfuseScoreValue::Numeric(value),
126 comment: None,
127 id: None,
128 }
129 }
130
131 /// Builds a trace-level categorical score.
132 pub fn categorical(
133 trace_id: impl Into<String>,
134 name: impl Into<String>,
135 value: impl Into<String>,
136 ) -> Self {
137 Self {
138 trace_id: trace_id.into(),
139 observation_id: None,
140 name: name.into(),
141 value: LangfuseScoreValue::Categorical(value.into()),
142 comment: None,
143 id: None,
144 }
145 }
146
147 /// Builds a trace-level boolean score.
148 pub fn boolean(trace_id: impl Into<String>, name: impl Into<String>, value: bool) -> Self {
149 Self {
150 trace_id: trace_id.into(),
151 observation_id: None,
152 name: name.into(),
153 value: LangfuseScoreValue::Boolean(value),
154 comment: None,
155 id: None,
156 }
157 }
158
159 /// Scopes the score to a single observation (generation/span) id.
160 pub fn on_observation(mut self, observation_id: impl Into<String>) -> Self {
161 self.observation_id = Some(observation_id.into());
162 self
163 }
164
165 /// Attaches a free-text comment to the score.
166 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
167 self.comment = Some(comment.into());
168 self
169 }
170
171 /// Overrides the auto-derived score id (for a caller-controlled identity).
172 pub fn with_id(mut self, id: impl Into<String>) -> Self {
173 self.id = Some(id.into());
174 self
175 }
176}
177
178/// Async Langfuse ingestion client.
179#[derive(Clone, Debug)]
180pub struct LangfuseClient {
181 pub(super) endpoint: String,
182 pub(super) auth: LangfuseAuth,
183 pub(super) client: reqwest::Client,
184}