lc_callbacks/
langsmith_client.rs1use reqwest::Client;
5use std::env;
6
7use super::run_tree::{RunCreate, RunTree, RunUpdate};
8
9#[derive(Debug, Clone)]
11pub struct LangSmithConfig {
12 pub api_key: String,
14
15 pub api_url: String,
17
18 pub workspace_id: Option<String>,
20
21 pub project_name: String,
23
24 pub tracing_enabled: bool,
26}
27
28impl LangSmithConfig {
29 pub fn from_env() -> Result<Self, String> {
31 let api_key = env::var("LANGSMITH_API_KEY")
32 .map_err(|_| "LANGSMITH_API_KEY environment variable not set")?;
33
34 let tracing_enabled = env::var("LANGSMITH_TRACING")
35 .map(|v| v == "true")
36 .unwrap_or(true);
37
38 let project_name = env::var("LANGSMITH_PROJECT").unwrap_or_else(|_| "default".to_string());
39
40 let api_url = env::var("LANGSMITH_ENDPOINT")
41 .unwrap_or_else(|_| "https://api.smith.langchain.com".to_string());
42
43 let workspace_id = env::var("LANGSMITH_WORKSPACE_ID").ok();
44
45 Ok(Self {
46 api_key,
47 api_url,
48 workspace_id,
49 project_name,
50 tracing_enabled,
51 })
52 }
53
54 pub fn new(api_key: impl Into<String>) -> Self {
56 Self {
57 api_key: api_key.into(),
58 api_url: "https://api.smith.langchain.com".to_string(),
59 workspace_id: None,
60 project_name: "default".to_string(),
61 tracing_enabled: true,
62 }
63 }
64
65 pub fn with_project(mut self, project: impl Into<String>) -> Self {
67 self.project_name = project.into();
68 self
69 }
70
71 pub fn with_workspace(mut self, workspace_id: impl Into<String>) -> Self {
73 self.workspace_id = Some(workspace_id.into());
74 self
75 }
76
77 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
79 self.api_url = endpoint.into();
80 self
81 }
82
83 pub fn with_tracing(mut self, enabled: bool) -> Self {
85 self.tracing_enabled = enabled;
86 self
87 }
88}
89
90pub struct LangSmithClient {
92 pub config: LangSmithConfig,
94 http_client: Client,
95}
96
97impl LangSmithClient {
98 pub fn new(config: LangSmithConfig) -> Self {
100 Self {
101 config,
102 http_client: Client::new(),
103 }
104 }
105
106 pub fn from_env() -> Result<Self, String> {
108 let config = LangSmithConfig::from_env()?;
109 Ok(Self::new(config))
110 }
111
112 pub fn is_tracing_enabled(&self) -> bool {
114 self.config.tracing_enabled
115 }
116
117 pub fn project_name(&self) -> &str {
119 &self.config.project_name
120 }
121
122 pub async fn create_run(&self, run: &RunTree) -> Result<(), LangSmithError> {
124 if !self.config.tracing_enabled {
125 return Ok(());
126 }
127
128 let url = format!("{}/runs", self.config.api_url);
129 let mut run_create = RunCreate::from(run);
130 if run_create.session_name.is_none() {
131 run_create.session_name = Some(self.config.project_name.clone());
132 }
133
134 let mut request = self
135 .http_client
136 .post(&url)
137 .header("x-api-key", &self.config.api_key)
138 .json(&run_create);
139
140 if let Some(workspace_id) = &self.config.workspace_id {
141 request = request.header("x-tenant-id", workspace_id);
142 }
143
144 let response = request
145 .send()
146 .await
147 .map_err(|e| LangSmithError::Http(e.to_string()))?;
148
149 if !response.status().is_success() {
150 let status = response.status();
151 let body = response.text().await.unwrap_or_default();
152 return Err(LangSmithError::Api(format!("HTTP {}: {}", status, body)));
153 }
154
155 Ok(())
156 }
157
158 pub async fn update_run(&self, run: &RunTree) -> Result<(), LangSmithError> {
160 if !self.config.tracing_enabled {
161 return Ok(());
162 }
163
164 let url = format!("{}/runs/{}", self.config.api_url, run.id);
165 let body = RunUpdate::from(run);
166
167 let mut request = self
168 .http_client
169 .patch(&url)
170 .header("x-api-key", &self.config.api_key)
171 .json(&body);
172
173 if let Some(workspace_id) = &self.config.workspace_id {
174 request = request.header("x-tenant-id", workspace_id);
175 }
176
177 let response = request
178 .send()
179 .await
180 .map_err(|e| LangSmithError::Http(e.to_string()))?;
181
182 if !response.status().is_success() {
183 let status = response.status();
184 let body = response.text().await.unwrap_or_default();
185 return Err(LangSmithError::Api(format!("HTTP {}: {}", status, body)));
186 }
187
188 Ok(())
189 }
190
191 pub async fn batch_ingest(&self, runs: &[RunTree]) -> Result<(), LangSmithError> {
193 if !self.config.tracing_enabled || runs.is_empty() {
194 return Ok(());
195 }
196
197 let url = format!("{}/runs/multipart", self.config.api_url);
198
199 let runs_create: Vec<RunCreate> = runs
200 .iter()
201 .map(|run| {
202 let mut run_create = RunCreate::from(run);
203 if run_create.session_name.is_none() {
204 run_create.session_name = Some(self.config.project_name.clone());
205 }
206 run_create
207 })
208 .collect();
209
210 let runs_json =
211 serde_json::to_string(&runs_create).map_err(|e| LangSmithError::Api(e.to_string()))?;
212
213 let form = reqwest::multipart::Form::new().text("runs", runs_json);
214
215 let mut request = self
216 .http_client
217 .post(&url)
218 .header("x-api-key", &self.config.api_key)
219 .multipart(form);
220
221 if let Some(workspace_id) = &self.config.workspace_id {
222 request = request.header("x-tenant-id", workspace_id);
223 }
224
225 let response = request
226 .send()
227 .await
228 .map_err(|e| LangSmithError::Http(e.to_string()))?;
229
230 if !response.status().is_success() {
231 let status = response.status();
232 let body = response.text().await.unwrap_or_default();
233 return Err(LangSmithError::Api(format!("HTTP {}: {}", status, body)));
234 }
235
236 Ok(())
237 }
238
239 pub async fn batch_ingest_parallel(&self, runs: &[RunTree]) -> Result<(), LangSmithError> {
241 if !self.config.tracing_enabled || runs.is_empty() {
242 return Ok(());
243 }
244
245 use futures_util::future::join_all;
246
247 let futures: Vec<_> = runs.iter().map(|run| self.create_run(run)).collect();
248
249 let results = join_all(futures).await;
250
251 for result in results {
252 result?;
253 }
254
255 Ok(())
256 }
257}
258
259#[derive(Debug)]
261pub enum LangSmithError {
262 Http(String),
263 Api(String),
264 Config(String),
265}
266
267impl std::fmt::Display for LangSmithError {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 match self {
270 Self::Http(msg) => write!(f, "HTTP error: {}", msg),
271 Self::Api(msg) => write!(f, "API error: {}", msg),
272 Self::Config(msg) => write!(f, "Config error: {}", msg),
273 }
274 }
275}
276
277impl std::error::Error for LangSmithError {}