lc_callbacks/
langsmith_client.rs1use reqwest::Client;
5use std::env;
6use std::time::Duration;
7
8const LANGSMITH_TIMEOUT: Duration = Duration::from_secs(15);
14
15use super::run_tree::{RunCreate, RunTree, RunUpdate};
16
17#[derive(Debug, Clone)]
19pub struct LangSmithConfig {
20 pub api_key: String,
22
23 pub api_url: String,
25
26 pub workspace_id: Option<String>,
28
29 pub project_name: String,
31
32 pub tracing_enabled: bool,
34}
35
36impl LangSmithConfig {
37 pub fn from_env() -> Result<Self, LangSmithError> {
39 let api_key = env::var("LANGSMITH_API_KEY").map_err(|_| {
40 LangSmithError::Config("LANGSMITH_API_KEY environment variable not set".to_string())
41 })?;
42
43 let tracing_enabled = env::var("LANGSMITH_TRACING")
44 .map(|v| v == "true")
45 .unwrap_or(true);
46
47 let project_name = env::var("LANGSMITH_PROJECT").unwrap_or_else(|_| "default".to_string());
48
49 let api_url = env::var("LANGSMITH_ENDPOINT")
50 .unwrap_or_else(|_| "https://api.smith.langchain.com".to_string());
51
52 let workspace_id = env::var("LANGSMITH_WORKSPACE_ID").ok();
53
54 Ok(Self {
55 api_key,
56 api_url,
57 workspace_id,
58 project_name,
59 tracing_enabled,
60 })
61 }
62
63 pub fn new(api_key: impl Into<String>) -> Self {
65 Self {
66 api_key: api_key.into(),
67 api_url: "https://api.smith.langchain.com".to_string(),
68 workspace_id: None,
69 project_name: "default".to_string(),
70 tracing_enabled: true,
71 }
72 }
73
74 pub fn with_project(mut self, project: impl Into<String>) -> Self {
76 self.project_name = project.into();
77 self
78 }
79
80 pub fn with_workspace(mut self, workspace_id: impl Into<String>) -> Self {
82 self.workspace_id = Some(workspace_id.into());
83 self
84 }
85
86 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
88 self.api_url = endpoint.into();
89 self
90 }
91
92 pub fn with_tracing(mut self, enabled: bool) -> Self {
94 self.tracing_enabled = enabled;
95 self
96 }
97}
98
99pub struct LangSmithClient {
101 pub config: LangSmithConfig,
103 http_client: Client,
104}
105
106impl LangSmithClient {
107 pub fn new(config: LangSmithConfig) -> Self {
109 let http_client = Client::builder()
113 .connect_timeout(Duration::from_secs(5))
114 .timeout(LANGSMITH_TIMEOUT)
115 .build()
116 .unwrap_or_else(|_| Client::new());
121 Self {
122 config,
123 http_client,
124 }
125 }
126
127 pub fn from_env() -> Result<Self, LangSmithError> {
129 let config = LangSmithConfig::from_env()?;
130 Ok(Self::new(config))
131 }
132
133 pub fn is_tracing_enabled(&self) -> bool {
135 self.config.tracing_enabled
136 }
137
138 pub fn project_name(&self) -> &str {
140 &self.config.project_name
141 }
142
143 pub async fn create_run(&self, run: &RunTree) -> Result<(), LangSmithError> {
145 if !self.config.tracing_enabled {
146 return Ok(());
147 }
148
149 let url = format!("{}/runs", self.config.api_url);
150 let mut run_create = RunCreate::from(run);
151 if run_create.session_name.is_none() {
152 run_create.session_name = Some(self.config.project_name.clone());
153 }
154
155 let mut request = self
156 .http_client
157 .post(&url)
158 .header("x-api-key", &self.config.api_key)
159 .json(&run_create);
160
161 if let Some(workspace_id) = &self.config.workspace_id {
162 request = request.header("x-tenant-id", workspace_id);
163 }
164
165 let response = request
166 .send()
167 .await
168 .map_err(|e| LangSmithError::Http(e.to_string()))?;
169
170 if !response.status().is_success() {
171 let status = response.status();
172 let body = response.text().await.unwrap_or_default();
173 return Err(LangSmithError::Api(format!("HTTP {}: {}", status, body)));
174 }
175
176 Ok(())
177 }
178
179 pub async fn update_run(&self, run: &RunTree) -> Result<(), LangSmithError> {
181 if !self.config.tracing_enabled {
182 return Ok(());
183 }
184
185 let url = format!("{}/runs/{}", self.config.api_url, run.id);
186 let body = RunUpdate::from(run);
187
188 let mut request = self
189 .http_client
190 .patch(&url)
191 .header("x-api-key", &self.config.api_key)
192 .json(&body);
193
194 if let Some(workspace_id) = &self.config.workspace_id {
195 request = request.header("x-tenant-id", workspace_id);
196 }
197
198 let response = request
199 .send()
200 .await
201 .map_err(|e| LangSmithError::Http(e.to_string()))?;
202
203 if !response.status().is_success() {
204 let status = response.status();
205 let body = response.text().await.unwrap_or_default();
206 return Err(LangSmithError::Api(format!("HTTP {}: {}", status, body)));
207 }
208
209 Ok(())
210 }
211}
212
213#[derive(Debug)]
215#[non_exhaustive]
216pub enum LangSmithError {
217 Http(String),
219 Api(String),
221 Config(String),
223}
224
225impl std::fmt::Display for LangSmithError {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 match self {
228 Self::Http(msg) => write!(f, "HTTP error: {}", msg),
229 Self::Api(msg) => write!(f, "API error: {}", msg),
230 Self::Config(msg) => write!(f, "Config error: {}", msg),
231 }
232 }
233}
234
235impl std::error::Error for LangSmithError {}