Skip to main content

agentforge_observability/
langsmith.rs

1use agentforge_core::Trace;
2use async_trait::async_trait;
3use reqwest::Client;
4use serde_json::json;
5
6use crate::{ExporterError, TraceExporter};
7
8/// Exports traces to LangSmith's Runs API.
9///
10/// Env vars:
11/// - `LANGSMITH_API_KEY` — LangSmith API key
12/// - `LANGSMITH_PROJECT` — project name (default: `agentforge`)
13/// - `LANGSMITH_API_URL` — API base URL (default: `https://api.smith.langchain.com`)
14pub struct LangSmithExporter {
15    api_key: String,
16    project: String,
17    api_url: String,
18    client: Client,
19}
20
21impl LangSmithExporter {
22    pub fn new(api_key: String, project: String, api_url: String) -> Self {
23        Self {
24            api_key,
25            project,
26            api_url,
27            client: Client::new(),
28        }
29    }
30
31    pub fn from_env() -> Self {
32        Self::new(
33            std::env::var("LANGSMITH_API_KEY").unwrap_or_default(),
34            std::env::var("LANGSMITH_PROJECT").unwrap_or_else(|_| "agentforge".to_string()),
35            std::env::var("LANGSMITH_API_URL")
36                .unwrap_or_else(|_| "https://api.smith.langchain.com".to_string()),
37        )
38    }
39}
40
41#[async_trait]
42impl TraceExporter for LangSmithExporter {
43    async fn export(&self, trace: &Trace) -> Result<(), ExporterError> {
44        if self.api_key.is_empty() {
45            return Err(ExporterError::Config(
46                "LANGSMITH_API_KEY is not set".to_string(),
47            ));
48        }
49
50        let body = json!({
51            "id": trace.id.to_string(),
52            "name": format!("agentforge-trace-{}", &trace.id.to_string()[..8]),
53            "run_type": "chain",
54            "project_name": self.project,
55            "inputs": {
56                "scenario_id": trace.scenario_id.to_string(),
57                "run_id": trace.run_id.to_string(),
58            },
59            "outputs": {
60                "final_output": trace.final_output,
61                "aggregate_score": trace.aggregate_score,
62            },
63            "extra": {
64                "status": trace.status.to_string(),
65                "latency_ms": trace.latency_ms,
66                "llm_calls": trace.llm_calls,
67                "input_tokens": trace.input_tokens,
68                "output_tokens": trace.output_tokens,
69                "failure_cluster": trace.failure_cluster.to_string(),
70            },
71        });
72
73        self.client
74            .post(format!("{}/api/v1/runs", self.api_url))
75            .header("x-api-key", &self.api_key)
76            .json(&body)
77            .send()
78            .await?
79            .error_for_status()
80            .map_err(ExporterError::Http)?;
81
82        tracing::debug!(trace_id = %trace.id, "Exported trace to LangSmith");
83        Ok(())
84    }
85}