agentforge_observability/
langsmith.rs1use agentforge_core::Trace;
2use async_trait::async_trait;
3use reqwest::Client;
4use serde_json::json;
5
6use crate::{ExporterError, TraceExporter};
7
8pub 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}