Skip to main content

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}