Skip to main content

lc_callbacks/
langsmith_client.rs

1// lc-callbacks/src/langsmith_client.rs
2//! LangSmith API client
3
4use reqwest::Client;
5use std::env;
6
7use super::run_tree::{RunCreate, RunTree, RunUpdate};
8
9/// LangSmith configuration
10#[derive(Debug, Clone)]
11pub struct LangSmithConfig {
12    /// API key (starts with "ls_")
13    pub api_key: String,
14
15    /// API endpoint URL
16    pub api_url: String,
17
18    /// Workspace ID (required for org accounts)
19    pub workspace_id: Option<String>,
20
21    /// Project name
22    pub project_name: String,
23
24    /// Whether tracing is enabled
25    pub tracing_enabled: bool,
26}
27
28impl LangSmithConfig {
29    /// Create config from environment variables
30    pub fn from_env() -> Result<Self, LangSmithError> {
31        let api_key = env::var("LANGSMITH_API_KEY").map_err(|_| {
32            LangSmithError::Config("LANGSMITH_API_KEY environment variable not set".to_string())
33        })?;
34
35        let tracing_enabled = env::var("LANGSMITH_TRACING")
36            .map(|v| v == "true")
37            .unwrap_or(true);
38
39        let project_name = env::var("LANGSMITH_PROJECT").unwrap_or_else(|_| "default".to_string());
40
41        let api_url = env::var("LANGSMITH_ENDPOINT")
42            .unwrap_or_else(|_| "https://api.smith.langchain.com".to_string());
43
44        let workspace_id = env::var("LANGSMITH_WORKSPACE_ID").ok();
45
46        Ok(Self {
47            api_key,
48            api_url,
49            workspace_id,
50            project_name,
51            tracing_enabled,
52        })
53    }
54
55    /// Create config with API key
56    pub fn new(api_key: impl Into<String>) -> Self {
57        Self {
58            api_key: api_key.into(),
59            api_url: "https://api.smith.langchain.com".to_string(),
60            workspace_id: None,
61            project_name: "default".to_string(),
62            tracing_enabled: true,
63        }
64    }
65
66    /// Set project name
67    pub fn with_project(mut self, project: impl Into<String>) -> Self {
68        self.project_name = project.into();
69        self
70    }
71
72    /// Set workspace ID
73    pub fn with_workspace(mut self, workspace_id: impl Into<String>) -> Self {
74        self.workspace_id = Some(workspace_id.into());
75        self
76    }
77
78    /// Set API endpoint
79    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
80        self.api_url = endpoint.into();
81        self
82    }
83
84    /// Enable or disable tracing
85    pub fn with_tracing(mut self, enabled: bool) -> Self {
86        self.tracing_enabled = enabled;
87        self
88    }
89}
90
91/// LangSmith API client
92pub struct LangSmithClient {
93    /// Configuration
94    pub config: LangSmithConfig,
95    http_client: Client,
96}
97
98impl LangSmithClient {
99    /// Create a new client
100    pub fn new(config: LangSmithConfig) -> Self {
101        Self {
102            config,
103            http_client: Client::new(),
104        }
105    }
106
107    /// Create client from environment variables
108    pub fn from_env() -> Result<Self, LangSmithError> {
109        let config = LangSmithConfig::from_env()?;
110        Ok(Self::new(config))
111    }
112
113    /// Check if tracing is enabled
114    pub fn is_tracing_enabled(&self) -> bool {
115        self.config.tracing_enabled
116    }
117
118    /// Get the project name
119    pub fn project_name(&self) -> &str {
120        &self.config.project_name
121    }
122
123    /// Create a run (POST /runs)
124    pub async fn create_run(&self, run: &RunTree) -> Result<(), LangSmithError> {
125        if !self.config.tracing_enabled {
126            return Ok(());
127        }
128
129        let url = format!("{}/runs", self.config.api_url);
130        let mut run_create = RunCreate::from(run);
131        if run_create.session_name.is_none() {
132            run_create.session_name = Some(self.config.project_name.clone());
133        }
134
135        let mut request = self
136            .http_client
137            .post(&url)
138            .header("x-api-key", &self.config.api_key)
139            .json(&run_create);
140
141        if let Some(workspace_id) = &self.config.workspace_id {
142            request = request.header("x-tenant-id", workspace_id);
143        }
144
145        let response = request
146            .send()
147            .await
148            .map_err(|e| LangSmithError::Http(e.to_string()))?;
149
150        if !response.status().is_success() {
151            let status = response.status();
152            let body = response.text().await.unwrap_or_default();
153            return Err(LangSmithError::Api(format!("HTTP {}: {}", status, body)));
154        }
155
156        Ok(())
157    }
158
159    /// Update a run (PATCH /runs/{run_id})
160    pub async fn update_run(&self, run: &RunTree) -> Result<(), LangSmithError> {
161        if !self.config.tracing_enabled {
162            return Ok(());
163        }
164
165        let url = format!("{}/runs/{}", self.config.api_url, run.id);
166        let body = RunUpdate::from(run);
167
168        let mut request = self
169            .http_client
170            .patch(&url)
171            .header("x-api-key", &self.config.api_key)
172            .json(&body);
173
174        if let Some(workspace_id) = &self.config.workspace_id {
175            request = request.header("x-tenant-id", workspace_id);
176        }
177
178        let response = request
179            .send()
180            .await
181            .map_err(|e| LangSmithError::Http(e.to_string()))?;
182
183        if !response.status().is_success() {
184            let status = response.status();
185            let body = response.text().await.unwrap_or_default();
186            return Err(LangSmithError::Api(format!("HTTP {}: {}", status, body)));
187        }
188
189        Ok(())
190    }
191}
192
193/// LangSmith error type
194#[derive(Debug)]
195#[non_exhaustive]
196pub enum LangSmithError {
197    /// HTTP request error.
198    Http(String),
199    /// API returned an error.
200    Api(String),
201    /// Configuration error.
202    Config(String),
203}
204
205impl std::fmt::Display for LangSmithError {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        match self {
208            Self::Http(msg) => write!(f, "HTTP error: {}", msg),
209            Self::Api(msg) => write!(f, "API error: {}", msg),
210            Self::Config(msg) => write!(f, "Config error: {}", msg),
211        }
212    }
213}
214
215impl std::error::Error for LangSmithError {}