Skip to main content

lc_callbacks/
run_tree.rs

1// lc-callbacks/src/run_tree.rs
2//! Run tree data structure for tracing
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9use super::RunType;
10
11/// Run tree node for tracing
12///
13/// Each run is a node in a tree, with optional parent and children.
14/// The entire trace forms a tree structure, with the root being the top-level call.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RunTree {
17    /// Unique run ID (UUID v7 with timestamp)
18    pub id: Uuid,
19
20    /// Run name
21    pub name: String,
22
23    /// Run type
24    pub run_type: RunType,
25
26    /// Input data
27    pub inputs: serde_json::Value,
28
29    /// Output data (set when run ends)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub outputs: Option<serde_json::Value>,
32
33    /// Error message (if run failed)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub error: Option<String>,
36
37    /// Parent run ID
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub parent_run_id: Option<Uuid>,
40
41    /// Trace ID (ID of the root run)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub trace_id: Option<Uuid>,
44
45    /// Start time
46    pub start_time: DateTime<Utc>,
47
48    /// End time
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub end_time: Option<DateTime<Utc>>,
51
52    /// Metadata
53    #[serde(default)]
54    pub metadata: HashMap<String, serde_json::Value>,
55
56    /// Tags
57    #[serde(default)]
58    pub tags: Vec<String>,
59
60    /// Project name (LangSmith project)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub project_name: Option<String>,
63
64    /// Serialized representation of the component
65    #[serde(default)]
66    pub serialized: serde_json::Value,
67}
68
69impl RunTree {
70    /// Create a new run
71    pub fn new(name: impl Into<String>, run_type: RunType, inputs: serde_json::Value) -> Self {
72        Self {
73            id: Uuid::now_v7(),
74            name: name.into(),
75            run_type,
76            inputs,
77            outputs: None,
78            error: None,
79            parent_run_id: None,
80            trace_id: None,
81            start_time: Utc::now(),
82            end_time: None,
83            metadata: HashMap::new(),
84            tags: Vec::new(),
85            project_name: None,
86            serialized: serde_json::Value::Null,
87        }
88    }
89
90    /// Create a child run from this run
91    pub fn create_child(
92        &self,
93        name: impl Into<String>,
94        run_type: RunType,
95        inputs: serde_json::Value,
96    ) -> Self {
97        let mut child = Self::new(name, run_type, inputs);
98        child.parent_run_id = Some(self.id);
99        child.trace_id = self.trace_id.or(Some(self.id));
100        child.project_name = self.project_name.clone();
101        child
102    }
103
104    /// End the run with outputs
105    pub fn end(&mut self, outputs: serde_json::Value) {
106        self.outputs = Some(outputs);
107        self.end_time = Some(Utc::now());
108    }
109
110    /// End the run with an error
111    pub fn end_with_error(&mut self, error: impl Into<String>) {
112        self.error = Some(error.into());
113        self.end_time = Some(Utc::now());
114    }
115
116    /// Add a tag
117    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
118        self.tags.push(tag.into());
119        self
120    }
121
122    /// Add metadata
123    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
124        self.metadata.insert(key.into(), value);
125        self
126    }
127
128    /// Set project name
129    pub fn with_project(mut self, project: impl Into<String>) -> Self {
130        self.project_name = Some(project.into());
131        self
132    }
133
134    /// Calculate run duration in milliseconds
135    pub fn duration_ms(&self) -> Option<i64> {
136        self.end_time
137            .map(|end| (end - self.start_time).num_milliseconds())
138    }
139}
140
141/// LangSmith run 的 extra 载荷:自定义 metadata 放 `extra.metadata`。
142/// LangSmith v2 忽略 run 顶层 `metadata`,只认 `extra.metadata`。
143#[derive(Debug, Default, Serialize)]
144pub struct RunExtra {
145    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
146    pub metadata: HashMap<String, serde_json::Value>,
147}
148
149impl RunExtra {
150    pub fn is_empty(&self) -> bool {
151        self.metadata.is_empty()
152    }
153}
154
155/// Simplified run structure for API requests
156#[derive(Debug, Serialize)]
157pub struct RunCreate {
158    /// Run ID.
159    pub id: String,
160    /// Run name.
161    pub name: String,
162    /// Run type as a string.
163    pub run_type: String,
164    /// Inputs for the run.
165    pub inputs: serde_json::Value,
166    /// Outputs of the run.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub outputs: Option<serde_json::Value>,
169    /// Error message, if the run failed.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub error: Option<String>,
172    /// ID of the parent run, if any.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub parent_run_id: Option<String>,
175    /// Start time as an RFC 3339 timestamp.
176    pub start_time: String,
177    /// End time as an RFC 3339 timestamp, if the run finished.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub end_time: Option<String>,
180    /// Session/project name.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub session_name: Option<String>,
183    /// Tags attached to the run.
184    #[serde(default, skip_serializing_if = "Vec::is_empty")]
185    pub tags: Vec<String>,
186    /// 额外载荷:自定义 metadata 放 `extra.metadata`(顶层 `metadata` 会被 LangSmith v2 忽略)。
187    #[serde(default, skip_serializing_if = "RunExtra::is_empty")]
188    pub extra: RunExtra,
189}
190
191impl From<&RunTree> for RunCreate {
192    fn from(run: &RunTree) -> Self {
193        Self {
194            id: run.id.to_string(),
195            name: run.name.clone(),
196            run_type: run.run_type.as_str().to_string(),
197            inputs: run.inputs.clone(),
198            outputs: run.outputs.clone(),
199            error: run.error.clone(),
200            parent_run_id: run.parent_run_id.map(|id| id.to_string()),
201            start_time: run.start_time.to_rfc3339(),
202            end_time: run.end_time.map(|t| t.to_rfc3339()),
203            session_name: run.project_name.clone(),
204            tags: run.tags.clone(),
205            extra: RunExtra {
206                metadata: run.metadata.clone(),
207            },
208        }
209    }
210}
211
212/// Run update structure for PATCH requests
213#[derive(Debug, Serialize)]
214pub struct RunUpdate {
215    /// Updated outputs of the run.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub outputs: Option<serde_json::Value>,
218    /// Updated error message.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub error: Option<String>,
221    /// Updated end time as an RFC 3339 timestamp.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub end_time: Option<String>,
224}
225
226impl From<&RunTree> for RunUpdate {
227    fn from(run: &RunTree) -> Self {
228        Self {
229            outputs: run.outputs.clone(),
230            error: run.error.clone(),
231            end_time: run.end_time.map(|t| t.to_rfc3339()),
232        }
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn run_create_puts_metadata_into_extra() {
242        let run = RunTree::new("t", RunType::Chain, serde_json::json!({}))
243            .with_metadata("obs", serde_json::json!({"total_tokens": 451}));
244        let body = serde_json::to_value(RunCreate::from(&run)).unwrap();
245        // LangSmith v2 忽略顶层 metadata,只认 extra.metadata
246        assert!(body.get("metadata").is_none(), "顶层 metadata 不应再出现");
247        assert_eq!(body["extra"]["metadata"]["obs"]["total_tokens"], 451);
248    }
249}