Skip to main content

langfuse_ergonomic/
traces.rs

1//! Trace-related functionality with builder patterns
2
3use bon::bon;
4use chrono::{DateTime, Utc};
5use serde_json::Value;
6use std::collections::hash_map::DefaultHasher;
7use std::hash::{Hash, Hasher};
8use uuid::Uuid;
9
10use crate::client::LangfuseClient;
11use crate::error::{Error, Result};
12
13/// Helper trait for ergonomic tag creation
14pub trait IntoTags {
15    fn into_tags(self) -> Vec<String>;
16}
17
18/// Helper to convert level strings to ObservationLevel
19pub fn parse_observation_level(level: &str) -> langfuse_client_base::models::ObservationLevel {
20    use langfuse_client_base::models::ObservationLevel;
21
22    match level.to_uppercase().as_str() {
23        "DEBUG" => ObservationLevel::Debug,
24        "INFO" | "DEFAULT" => ObservationLevel::Default, // Map INFO to Default
25        "WARN" | "WARNING" => ObservationLevel::Warning,
26        "ERROR" => ObservationLevel::Error,
27        _ => ObservationLevel::Default, // Fallback to Default for unknown levels
28    }
29}
30
31impl IntoTags for Vec<String> {
32    fn into_tags(self) -> Vec<String> {
33        self
34    }
35}
36
37impl IntoTags for Vec<&str> {
38    fn into_tags(self) -> Vec<String> {
39        self.into_iter().map(|s| s.to_string()).collect()
40    }
41}
42
43impl<const N: usize> IntoTags for [&str; N] {
44    fn into_tags(self) -> Vec<String> {
45        self.into_iter().map(|s| s.to_string()).collect()
46    }
47}
48
49impl<const N: usize> IntoTags for [String; N] {
50    fn into_tags(self) -> Vec<String> {
51        self.into_iter().collect()
52    }
53}
54
55/// Response from trace creation
56pub struct TraceResponse {
57    pub id: String,
58    pub base_url: String,
59}
60
61impl TraceResponse {
62    /// Get the Langfuse URL for this trace
63    pub fn url(&self) -> String {
64        // More robust URL construction that handles various base_url formats
65        let mut web_url = self.base_url.clone();
66
67        // Remove trailing slashes
68        web_url = web_url.trim_end_matches('/').to_string();
69
70        // Replace /api/public or /api at the end with empty string
71        if web_url.ends_with("/api/public") {
72            web_url = web_url[..web_url.len() - 11].to_string();
73        } else if web_url.ends_with("/api") {
74            web_url = web_url[..web_url.len() - 4].to_string();
75        }
76
77        format!("{}/trace/{}", web_url, self.id)
78    }
79}
80
81/// Helper functions for generating deterministic IDs
82pub struct IdGenerator;
83
84impl IdGenerator {
85    /// Generate a deterministic UUID v5 from a seed string
86    /// This ensures the same seed always produces the same ID
87    pub fn from_seed(seed: &str) -> String {
88        // Use UUID v5 with a namespace for deterministic generation
89        let namespace = Uuid::NAMESPACE_OID;
90        Uuid::new_v5(&namespace, seed.as_bytes()).to_string()
91    }
92
93    /// Generate a deterministic ID from multiple components
94    /// Useful for creating hierarchical IDs (e.g., trace -> span -> event)
95    pub fn from_components(components: &[&str]) -> String {
96        let combined = components.join(":");
97        Self::from_seed(&combined)
98    }
99
100    /// Generate a deterministic ID using a hash-based approach
101    /// Alternative to UUID v5 for simpler use cases
102    pub fn from_hash(seed: &str) -> String {
103        let mut hasher = DefaultHasher::new();
104        seed.hash(&mut hasher);
105        let hash = hasher.finish();
106        format!("{:016x}", hash)
107    }
108}
109
110#[bon]
111impl LangfuseClient {
112    async fn ingest_events(
113        &self,
114        events: Vec<langfuse_client_base::models::IngestionEvent>,
115    ) -> Result<langfuse_client_base::models::IngestionResponse> {
116        use langfuse_client_base::apis::ingestion_api;
117        use langfuse_client_base::models::IngestionBatchRequest;
118
119        let batch_request = IngestionBatchRequest::builder().batch(events).build();
120
121        ingestion_api::ingestion_batch()
122            .configuration(self.configuration())
123            .ingestion_batch_request(batch_request)
124            .call()
125            .await
126            .map_err(crate::error::map_api_error)
127    }
128
129    /// Create a new trace
130    #[builder]
131    pub async fn trace(
132        &self,
133        #[builder(into)] id: Option<String>,
134        #[builder(into)] name: Option<String>,
135        input: Option<Value>,
136        output: Option<Value>,
137        metadata: Option<Value>,
138        #[builder(default = Vec::new())] tags: Vec<String>,
139        #[builder(into)] user_id: Option<String>,
140        #[builder(into)] session_id: Option<String>,
141        timestamp: Option<DateTime<Utc>>,
142        #[builder(into)] release: Option<String>,
143        #[builder(into)] version: Option<String>,
144        public: Option<bool>,
145    ) -> Result<TraceResponse> {
146        use langfuse_client_base::models::{
147            ingestion_event_one_of::Type as TraceEventType, IngestionEvent, IngestionEventOneOf,
148            TraceBody,
149        };
150
151        let trace_id = id.unwrap_or_else(|| Uuid::new_v4().to_string());
152        let timestamp = timestamp
153            .unwrap_or_else(Utc::now)
154            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
155
156        let tags_option = if tags.is_empty() { None } else { Some(tags) };
157
158        let trace_body = TraceBody::builder()
159            .id(Some(trace_id.clone()))
160            .timestamp(Some(timestamp.clone()))
161            .maybe_name(name.map(Some))
162            .maybe_user_id(user_id.map(Some))
163            .maybe_input(input.map(Some))
164            .maybe_output(output.map(Some))
165            .maybe_session_id(session_id.map(Some))
166            .maybe_release(release.map(Some))
167            .maybe_version(version.map(Some))
168            .maybe_metadata(metadata.map(Some))
169            .maybe_tags(tags_option.map(Some))
170            .maybe_public(public.map(Some))
171            .build();
172
173        let event = IngestionEventOneOf::builder()
174            .body(Box::new(trace_body))
175            .id(Uuid::new_v4().to_string())
176            .timestamp(timestamp.clone())
177            .r#type(TraceEventType::TraceCreate)
178            .build();
179
180        self.ingest_events(vec![IngestionEvent::IngestionEventOneOf(Box::new(event))])
181            .await
182            .map(|_| TraceResponse {
183                id: trace_id,
184                base_url: self.configuration().base_path.clone(),
185            })
186    }
187
188    /// Get a trace by ID
189    pub async fn get_trace(
190        &self,
191        trace_id: impl Into<String>,
192    ) -> Result<langfuse_client_base::models::TraceWithFullDetails> {
193        use langfuse_client_base::apis::trace_api;
194
195        let trace_id = trace_id.into();
196
197        trace_api::trace_get()
198            .configuration(self.configuration())
199            .trace_id(trace_id.as_str())
200            .call()
201            .await
202            .map_err(crate::error::map_api_error)
203    }
204
205    /// List traces with optional filters
206    #[builder]
207    pub async fn list_traces(
208        &self,
209        page: Option<i32>,
210        limit: Option<i32>,
211        #[builder(into)] user_id: Option<String>,
212        #[builder(into)] name: Option<String>,
213        #[builder(into)] session_id: Option<String>,
214        #[builder(into)] version: Option<String>,
215        #[builder(into)] release: Option<String>,
216        #[builder(into)] from_timestamp: Option<String>,
217        #[builder(into)] to_timestamp: Option<String>,
218        #[builder(into)] order_by: Option<String>,
219        #[builder(into)] tags: Option<String>,
220    ) -> Result<langfuse_client_base::models::Traces> {
221        use langfuse_client_base::apis::trace_api;
222
223        let user_id_ref = user_id.as_deref();
224        let name_ref = name.as_deref();
225        let session_id_ref = session_id.as_deref();
226        let version_ref = version.as_deref();
227        let release_ref = release.as_deref();
228        let order_by_ref = order_by.as_deref();
229        let tags_vec = tags.map(|t| vec![t]);
230
231        trace_api::trace_list()
232            .configuration(self.configuration())
233            .maybe_page(page)
234            .maybe_limit(limit)
235            .maybe_user_id(user_id_ref)
236            .maybe_name(name_ref)
237            .maybe_session_id(session_id_ref)
238            .maybe_version(version_ref)
239            .maybe_release(release_ref)
240            .maybe_order_by(order_by_ref)
241            .maybe_from_timestamp(from_timestamp)
242            .maybe_to_timestamp(to_timestamp)
243            .maybe_tags(tags_vec)
244            .call()
245            .await
246            .map_err(|e| crate::error::Error::Api(format!("Failed to list traces: {}", e)))
247    }
248
249    /// Delete a trace
250    pub async fn delete_trace(&self, trace_id: impl Into<String>) -> Result<()> {
251        use langfuse_client_base::apis::trace_api;
252
253        let trace_id = trace_id.into();
254
255        trace_api::trace_delete()
256            .configuration(self.configuration())
257            .trace_id(trace_id.as_str())
258            .call()
259            .await
260            .map(|_| ())
261            .map_err(|e| {
262                crate::error::Error::Api(format!("Failed to delete trace '{}': {}", trace_id, e))
263            })
264    }
265
266    /// Delete multiple traces
267    pub async fn delete_multiple_traces(&self, trace_ids: Vec<String>) -> Result<()> {
268        use langfuse_client_base::apis::trace_api;
269        use langfuse_client_base::models::TraceDeleteMultipleRequest;
270
271        let trace_count = trace_ids.len();
272        let request = TraceDeleteMultipleRequest::builder()
273            .trace_ids(trace_ids)
274            .build();
275
276        trace_api::trace_delete_multiple()
277            .configuration(self.configuration())
278            .trace_delete_multiple_request(request)
279            .call()
280            .await
281            .map(|_| ())
282            .map_err(|e| {
283                crate::error::Error::Api(format!("Failed to delete {} traces: {}", trace_count, e))
284            })
285    }
286
287    // ===== OBSERVATIONS (SPANS, GENERATIONS, EVENTS) =====
288
289    /// Create a span observation
290    #[builder]
291    pub async fn span(
292        &self,
293        #[builder(into)] trace_id: String,
294        #[builder(into)] id: Option<String>,
295        #[builder(into)] parent_observation_id: Option<String>,
296        #[builder(into)] name: Option<String>,
297        input: Option<Value>,
298        output: Option<Value>,
299        metadata: Option<Value>,
300        #[builder(into)] level: Option<String>,
301        #[builder(into)] status_message: Option<String>,
302        start_time: Option<DateTime<Utc>>,
303        end_time: Option<DateTime<Utc>>,
304    ) -> Result<String> {
305        use langfuse_client_base::models::{
306            ingestion_event_one_of_2::Type as SpanEventType, CreateSpanBody, IngestionEvent,
307            IngestionEventOneOf2,
308        };
309
310        let observation_id = id.unwrap_or_else(|| Uuid::new_v4().to_string());
311        let timestamp = start_time
312            .unwrap_or_else(Utc::now)
313            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
314        let level = level.map(|l| parse_observation_level(&l));
315        let end_time_str = end_time.map(|t| t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
316
317        let span_body = CreateSpanBody::builder()
318            .id(Some(observation_id.clone()))
319            .trace_id(Some(trace_id))
320            .start_time(Some(timestamp.clone()))
321            .maybe_end_time(end_time_str.map(Some))
322            .maybe_name(name.map(Some))
323            .maybe_parent_observation_id(parent_observation_id.map(Some))
324            .maybe_input(input.map(Some))
325            .maybe_output(output.map(Some))
326            .maybe_level(level)
327            .maybe_status_message(status_message.map(Some))
328            .maybe_metadata(metadata.map(Some))
329            .build();
330
331        let event = IngestionEventOneOf2::builder()
332            .body(Box::new(span_body))
333            .id(Uuid::new_v4().to_string())
334            .timestamp(timestamp.clone())
335            .r#type(SpanEventType::SpanCreate)
336            .build();
337
338        self.ingest_events(vec![IngestionEvent::IngestionEventOneOf2(Box::new(event))])
339            .await
340            .map(|_| observation_id)
341            .map_err(|e| crate::error::Error::Api(format!("Failed to create span: {}", e)))
342    }
343
344    /// Create a generation observation
345    #[builder]
346    pub async fn generation(
347        &self,
348        #[builder(into)] trace_id: String,
349        #[builder(into)] id: Option<String>,
350        #[builder(into)] parent_observation_id: Option<String>,
351        #[builder(into)] name: Option<String>,
352        input: Option<Value>,
353        output: Option<Value>,
354        metadata: Option<Value>,
355        #[builder(into)] level: Option<String>,
356        #[builder(into)] status_message: Option<String>,
357        start_time: Option<DateTime<Utc>>,
358        end_time: Option<DateTime<Utc>>,
359        #[builder(into)] model: Option<String>,
360        _model_parameters: Option<Value>,
361        _prompt_tokens: Option<i32>,
362        _completion_tokens: Option<i32>,
363        _total_tokens: Option<i32>,
364    ) -> Result<String> {
365        use langfuse_client_base::models::{
366            ingestion_event_one_of_4::Type as GenerationEventType, CreateGenerationBody,
367            IngestionEvent, IngestionEventOneOf4,
368        };
369
370        let observation_id = id.unwrap_or_else(|| Uuid::new_v4().to_string());
371        let timestamp = start_time
372            .unwrap_or_else(Utc::now)
373            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
374
375        let level = level.map(|l| parse_observation_level(&l));
376        let end_time_str = end_time.map(|t| t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
377
378        let generation_body = CreateGenerationBody::builder()
379            .id(Some(observation_id.clone()))
380            .trace_id(Some(trace_id))
381            .start_time(Some(timestamp.clone()))
382            .maybe_name(name.map(Some))
383            .maybe_end_time(end_time_str.map(Some))
384            .maybe_model(model.map(Some))
385            .maybe_input(input.map(Some))
386            .maybe_output(output.map(Some))
387            .maybe_metadata(metadata.map(Some))
388            .maybe_level(level)
389            .maybe_status_message(status_message.map(Some))
390            .maybe_parent_observation_id(parent_observation_id.map(Some))
391            .build();
392
393        let event = IngestionEventOneOf4::builder()
394            .body(Box::new(generation_body))
395            .id(Uuid::new_v4().to_string())
396            .timestamp(timestamp.clone())
397            .r#type(GenerationEventType::GenerationCreate)
398            .build();
399
400        self.ingest_events(vec![IngestionEvent::IngestionEventOneOf4(Box::new(event))])
401            .await
402            .map(|_| observation_id)
403            .map_err(|e| crate::error::Error::Api(format!("Failed to create generation: {}", e)))
404    }
405
406    /// Create an event observation
407    #[builder]
408    pub async fn event(
409        &self,
410        #[builder(into)] trace_id: String,
411        #[builder(into)] id: Option<String>,
412        #[builder(into)] parent_observation_id: Option<String>,
413        #[builder(into)] name: Option<String>,
414        input: Option<Value>,
415        output: Option<Value>,
416        metadata: Option<Value>,
417        #[builder(into)] level: Option<String>,
418        #[builder(into)] status_message: Option<String>,
419        start_time: Option<DateTime<Utc>>,
420    ) -> Result<String> {
421        use langfuse_client_base::models::{
422            ingestion_event_one_of_6::Type as EventEventType, CreateEventBody, IngestionEvent,
423            IngestionEventOneOf6,
424        };
425
426        let observation_id = id.unwrap_or_else(|| Uuid::new_v4().to_string());
427        let timestamp = start_time
428            .unwrap_or_else(Utc::now)
429            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
430
431        let level = level.map(|l| parse_observation_level(&l));
432
433        let event_body = CreateEventBody::builder()
434            .id(Some(observation_id.clone()))
435            .trace_id(Some(trace_id))
436            .start_time(Some(timestamp.clone()))
437            .maybe_name(name.map(Some))
438            .maybe_input(input.map(Some))
439            .maybe_output(output.map(Some))
440            .maybe_level(level)
441            .maybe_status_message(status_message.map(Some))
442            .maybe_parent_observation_id(parent_observation_id.map(Some))
443            .maybe_metadata(metadata.map(Some))
444            .build();
445
446        let event = IngestionEventOneOf6::builder()
447            .body(Box::new(event_body))
448            .id(Uuid::new_v4().to_string())
449            .timestamp(timestamp.clone())
450            .r#type(EventEventType::EventCreate)
451            .build();
452
453        self.ingest_events(vec![IngestionEvent::IngestionEventOneOf6(Box::new(event))])
454            .await
455            .map(|_| observation_id)
456            .map_err(|e| crate::error::Error::Api(format!("Failed to create event: {}", e)))
457    }
458
459    // ===== OBSERVATION UPDATES AND RETRIEVAL =====
460
461    /// Get a specific observation
462    pub async fn get_observation(
463        &self,
464        observation_id: impl Into<String>,
465    ) -> Result<langfuse_client_base::models::ObservationsView> {
466        use langfuse_client_base::apis::observations_api;
467
468        let observation_id = observation_id.into();
469
470        observations_api::observations_get()
471            .configuration(self.configuration())
472            .observation_id(observation_id.as_str())
473            .call()
474            .await
475            .map_err(|e| crate::error::Error::Api(format!("Failed to get observation: {}", e)))
476    }
477
478    /// Get multiple observations
479    #[builder]
480    pub async fn get_observations(
481        &self,
482        page: Option<i32>,
483        limit: Option<i32>,
484        #[builder(into)] trace_id: Option<String>,
485        #[builder(into)] parent_observation_id: Option<String>,
486        #[builder(into)] name: Option<String>,
487        #[builder(into)] user_id: Option<String>,
488        observation_type: Option<String>,
489    ) -> Result<langfuse_client_base::models::ObservationsViews> {
490        use langfuse_client_base::apis::observations_api;
491
492        // Note: The API has more parameters but they're not all exposed in v0.2
493        // Using the actual signature from the base client
494        let trace_id_ref = trace_id.as_deref();
495        let parent_ref = parent_observation_id.as_deref();
496        let type_ref = observation_type.as_deref();
497        let user_id_ref = user_id.as_deref();
498        let name_ref = name.as_deref();
499
500        observations_api::observations_get_many()
501            .configuration(self.configuration())
502            .maybe_page(page)
503            .maybe_limit(limit)
504            .maybe_trace_id(trace_id_ref)
505            .maybe_parent_observation_id(parent_ref)
506            .maybe_type(type_ref)
507            .maybe_user_id(user_id_ref)
508            .maybe_name(name_ref)
509            .call()
510            .await
511            .map_err(|e| crate::error::Error::Api(format!("Failed to get observations: {}", e)))
512    }
513
514    /// Update an existing span
515    #[builder]
516    pub async fn update_span(
517        &self,
518        #[builder(into)] id: String,
519        #[builder(into)] trace_id: String,
520        #[builder(into)] name: Option<String>,
521        start_time: Option<DateTime<Utc>>,
522        end_time: Option<DateTime<Utc>>,
523        metadata: Option<Value>,
524        input: Option<Value>,
525        output: Option<Value>,
526        level: Option<String>,
527        status_message: Option<String>,
528        version: Option<String>,
529        #[builder(into)] parent_observation_id: Option<String>,
530    ) -> Result<String> {
531        use chrono::Utc as ChronoUtc;
532        use langfuse_client_base::models::{IngestionEvent, IngestionEventOneOf3, UpdateSpanBody};
533        use uuid::Uuid;
534
535        let event_body = UpdateSpanBody {
536            id: id.clone(),
537            trace_id: Some(Some(trace_id)),
538            name: Some(name),
539            start_time: Some(start_time.map(|dt| dt.to_rfc3339())),
540            end_time: Some(end_time.map(|dt| dt.to_rfc3339())),
541            metadata: Some(metadata),
542            input: Some(input),
543            output: Some(output),
544            level: level.map(|l| parse_observation_level(&l)),
545            status_message: Some(status_message),
546            version: Some(version),
547            parent_observation_id: Some(parent_observation_id),
548            environment: None,
549        };
550
551        let event = IngestionEventOneOf3 {
552            body: Box::new(event_body),
553            id: Uuid::new_v4().to_string(),
554            timestamp: ChronoUtc::now().to_rfc3339(),
555            metadata: None,
556            r#type: langfuse_client_base::models::ingestion_event_one_of_3::Type::SpanUpdate,
557        };
558
559        self.ingest_events(vec![IngestionEvent::IngestionEventOneOf3(Box::new(event))])
560            .await
561            .map_err(|e| Error::Api(format!("Failed to update span: {}", e)))?;
562
563        Ok(id)
564    }
565
566    /// Update an existing generation
567    #[builder]
568    pub async fn update_generation(
569        &self,
570        #[builder(into)] id: String,
571        #[builder(into)] trace_id: String,
572        #[builder(into)] name: Option<String>,
573        start_time: Option<DateTime<Utc>>,
574        end_time: Option<DateTime<Utc>>,
575        completion_start_time: Option<DateTime<Utc>>,
576        model: Option<String>,
577        input: Option<Value>,
578        output: Option<Value>,
579        metadata: Option<Value>,
580        level: Option<String>,
581        status_message: Option<String>,
582        version: Option<String>,
583        #[builder(into)] parent_observation_id: Option<String>,
584    ) -> Result<String> {
585        use chrono::Utc as ChronoUtc;
586        use langfuse_client_base::models::{
587            IngestionEvent, IngestionEventOneOf5, UpdateGenerationBody,
588        };
589        use uuid::Uuid;
590
591        // Note: In v0.2, model_parameters and usage have different types
592        // We'll leave them out for now as they require special handling
593        let event_body = UpdateGenerationBody {
594            id: id.clone(),
595            trace_id: Some(Some(trace_id)),
596            name: Some(name),
597            start_time: Some(start_time.map(|dt| dt.to_rfc3339())),
598            end_time: Some(end_time.map(|dt| dt.to_rfc3339())),
599            completion_start_time: Some(completion_start_time.map(|dt| dt.to_rfc3339())),
600            model: Some(model),
601            model_parameters: None, // Requires HashMap<String, MapValue>
602            input: Some(input),
603            output: Some(output),
604            usage: None, // Requires Box<IngestionUsage>
605            metadata: Some(metadata),
606            level: level.map(|l| parse_observation_level(&l)),
607            status_message: Some(status_message),
608            version: Some(version),
609            parent_observation_id: Some(parent_observation_id),
610            environment: None,
611            cost_details: None,
612            prompt_name: None,
613            prompt_version: None,
614            usage_details: None,
615        };
616
617        let event = IngestionEventOneOf5 {
618            body: Box::new(event_body),
619            id: Uuid::new_v4().to_string(),
620            timestamp: ChronoUtc::now().to_rfc3339(),
621            metadata: None,
622            r#type: langfuse_client_base::models::ingestion_event_one_of_5::Type::GenerationUpdate,
623        };
624
625        self.ingest_events(vec![IngestionEvent::IngestionEventOneOf5(Box::new(event))])
626            .await
627            .map_err(|e| Error::Api(format!("Failed to update generation: {}", e)))?;
628
629        Ok(id)
630    }
631
632    // Note: UpdateEventBody exists in v0.2 but doesn't have a corresponding IngestionEvent variant
633    // This functionality will need to wait for a later version
634
635    // ===== SCORING =====
636
637    /// Create a score
638    #[builder]
639    pub async fn score(
640        &self,
641        #[builder(into)] trace_id: String,
642        #[builder(into)] name: String,
643        #[builder(into)] observation_id: Option<String>,
644        value: Option<f64>,
645        #[builder(into)] string_value: Option<String>,
646        #[builder(into)] comment: Option<String>,
647        #[builder(into)] queue_id: Option<String>,
648        metadata: Option<Value>,
649    ) -> Result<String> {
650        // Validate that either value or string_value is set
651        if value.is_none() && string_value.is_none() {
652            return Err(crate::error::Error::Validation(
653                "Score must have either a numeric value or string value".to_string(),
654            ));
655        }
656
657        use langfuse_client_base::models::{
658            CreateScoreValue, IngestionEvent, IngestionEventOneOf1, ScoreBody, ScoreDataType,
659        };
660
661        let score_id = Uuid::new_v4().to_string();
662        let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
663
664        let score_value = if let Some(v) = value {
665            Box::new(CreateScoreValue::Number(v))
666        } else if let Some(s) = string_value {
667            Box::new(CreateScoreValue::String(s))
668        } else {
669            return Err(crate::error::Error::Validation(
670                "Score must have either a numeric value or string value".to_string(),
671            ));
672        };
673
674        let score_body = ScoreBody {
675            id: Some(Some(score_id.clone())),
676            trace_id: Some(Some(trace_id)),
677            name,
678            queue_id: queue_id.map(Some),
679            value: score_value,
680            observation_id: observation_id.map(Some),
681            comment: comment.map(Some),
682            data_type: if value.is_some() {
683                Some(ScoreDataType::Numeric)
684            } else {
685                Some(ScoreDataType::Categorical)
686            },
687            config_id: None,
688            session_id: None,
689            dataset_run_id: None,
690            environment: None,
691            metadata: metadata.map(Some),
692        };
693
694        let event = IngestionEventOneOf1 {
695            body: Box::new(score_body),
696            id: Uuid::new_v4().to_string(),
697            timestamp: timestamp.clone(),
698            metadata: None,
699            r#type: langfuse_client_base::models::ingestion_event_one_of_1::Type::ScoreCreate,
700        };
701
702        self.ingest_events(vec![IngestionEvent::IngestionEventOneOf1(Box::new(event))])
703            .await
704            .map(|_| score_id)
705            .map_err(|e| crate::error::Error::Api(format!("Failed to create score: {}", e)))
706    }
707
708    /// Create a binary score (0 or 1)
709    pub async fn binary_score(
710        &self,
711        trace_id: impl Into<String>,
712        name: impl Into<String>,
713        value: bool,
714    ) -> Result<String> {
715        self.score()
716            .trace_id(trace_id.into())
717            .name(name.into())
718            .value(if value { 1.0 } else { 0.0 })
719            .call()
720            .await
721    }
722
723    /// Create a rating score (e.g., 1-5 stars)
724    ///
725    /// # Validation
726    /// - `max_rating` must be greater than 0
727    /// - `rating` must be less than or equal to `max_rating`
728    pub async fn rating_score(
729        &self,
730        trace_id: impl Into<String>,
731        name: impl Into<String>,
732        rating: u8,
733        max_rating: u8,
734    ) -> Result<String> {
735        // Validate inputs
736        if max_rating == 0 {
737            return Err(Error::Validation(
738                "max_rating must be greater than 0".to_string(),
739            ));
740        }
741        if rating > max_rating {
742            return Err(Error::Validation(format!(
743                "rating ({}) must be less than or equal to max_rating ({})",
744                rating, max_rating
745            )));
746        }
747
748        let normalized = rating as f64 / max_rating as f64;
749        let final_metadata = serde_json::json!({
750            "rating": rating,
751            "max_rating": max_rating
752        });
753
754        self.score()
755            .trace_id(trace_id.into())
756            .name(name.into())
757            .value(normalized)
758            .metadata(final_metadata)
759            .call()
760            .await
761    }
762
763    /// Create a categorical score
764    pub async fn categorical_score(
765        &self,
766        trace_id: impl Into<String>,
767        name: impl Into<String>,
768        category: impl Into<String>,
769    ) -> Result<String> {
770        self.score()
771            .trace_id(trace_id.into())
772            .name(name.into())
773            .string_value(category.into())
774            .call()
775            .await
776    }
777
778    // ===== DATASET MANAGEMENT =====
779
780    /// Create a dataset
781    #[builder]
782    pub async fn create_dataset(
783        &self,
784        #[builder(into)] name: String,
785        #[builder(into)] description: Option<String>,
786        metadata: Option<Value>,
787        #[builder(into)] input_schema: Option<Value>,
788        #[builder(into)] expected_output_schema: Option<Value>,
789    ) -> Result<langfuse_client_base::models::Dataset> {
790        use langfuse_client_base::apis::datasets_api;
791        use langfuse_client_base::models::CreateDatasetRequest;
792
793        let request = CreateDatasetRequest {
794            name,
795            description: description.map(Some),
796            metadata: metadata.map(Some),
797            input_schema: input_schema.map(Some),
798            expected_output_schema: expected_output_schema.map(Some),
799        };
800
801        datasets_api::datasets_create()
802            .configuration(self.configuration())
803            .create_dataset_request(request)
804            .call()
805            .await
806            .map_err(|e| crate::error::Error::Api(format!("Failed to create dataset: {}", e)))
807    }
808
809    /// Get a dataset by name
810    pub async fn get_dataset(
811        &self,
812        dataset_name: impl Into<String>,
813    ) -> Result<langfuse_client_base::models::Dataset> {
814        use langfuse_client_base::apis::datasets_api;
815
816        let dataset_name = dataset_name.into();
817
818        datasets_api::datasets_get()
819            .configuration(self.configuration())
820            .dataset_name(dataset_name.as_str())
821            .call()
822            .await
823            .map_err(|e| crate::error::Error::Api(format!("Failed to get dataset: {}", e)))
824    }
825
826    /// List datasets with pagination
827    #[builder]
828    pub async fn list_datasets(
829        &self,
830        page: Option<i32>,
831        limit: Option<i32>,
832    ) -> Result<langfuse_client_base::models::PaginatedDatasets> {
833        use langfuse_client_base::apis::datasets_api;
834
835        datasets_api::datasets_list()
836            .configuration(self.configuration())
837            .maybe_page(page)
838            .maybe_limit(limit)
839            .call()
840            .await
841            .map_err(|e| crate::error::Error::Api(format!("Failed to list datasets: {}", e)))
842    }
843
844    /// Delete a dataset run
845    pub async fn delete_dataset_run(
846        &self,
847        dataset_name: impl Into<String>,
848        run_name: impl Into<String>,
849    ) -> Result<()> {
850        use langfuse_client_base::apis::datasets_api;
851
852        let dataset_name = dataset_name.into();
853        let run_name = run_name.into();
854
855        datasets_api::datasets_delete_run()
856            .configuration(self.configuration())
857            .dataset_name(dataset_name.as_str())
858            .run_name(run_name.as_str())
859            .call()
860            .await
861            .map(|_| ())
862            .map_err(|e| crate::error::Error::Api(format!("Failed to delete dataset run: {}", e)))
863    }
864
865    /// Get a dataset run
866    pub async fn get_dataset_run(
867        &self,
868        dataset_name: impl Into<String>,
869        run_name: impl Into<String>,
870    ) -> Result<langfuse_client_base::models::DatasetRunWithItems> {
871        use langfuse_client_base::apis::datasets_api;
872
873        let dataset_name = dataset_name.into();
874        let run_name = run_name.into();
875
876        datasets_api::datasets_get_run()
877            .configuration(self.configuration())
878            .dataset_name(dataset_name.as_str())
879            .run_name(run_name.as_str())
880            .call()
881            .await
882            .map_err(|e| crate::error::Error::Api(format!("Failed to get dataset run: {}", e)))
883    }
884
885    /// Get all runs for a dataset
886    pub async fn get_dataset_runs(
887        &self,
888        dataset_name: impl Into<String>,
889    ) -> Result<langfuse_client_base::models::PaginatedDatasetRuns> {
890        use langfuse_client_base::apis::datasets_api;
891
892        let dataset_name = dataset_name.into();
893
894        datasets_api::datasets_get_runs()
895            .configuration(self.configuration())
896            .dataset_name(dataset_name.as_str())
897            .call()
898            .await
899            .map_err(|e| crate::error::Error::Api(format!("Failed to get dataset runs: {}", e)))
900    }
901
902    // ===== DATASET ITEM OPERATIONS =====
903
904    /// Create a new dataset item
905    #[builder]
906    pub async fn create_dataset_item(
907        &self,
908        #[builder(into)] dataset_name: String,
909        input: Option<Value>,
910        expected_output: Option<Value>,
911        metadata: Option<Value>,
912        #[builder(into)] source_trace_id: Option<String>,
913        #[builder(into)] source_observation_id: Option<String>,
914        #[builder(into)] id: Option<String>,
915        _status: Option<String>,
916    ) -> Result<langfuse_client_base::models::DatasetItem> {
917        use langfuse_client_base::apis::dataset_items_api;
918        use langfuse_client_base::models::CreateDatasetItemRequest;
919
920        let item_request = CreateDatasetItemRequest {
921            dataset_name,
922            input: Some(input),
923            expected_output: Some(expected_output),
924            metadata: Some(metadata),
925            source_trace_id: Some(source_trace_id),
926            source_observation_id: Some(source_observation_id),
927            id: Some(id),
928            status: None, // Status field requires DatasetStatus enum, not available in public API
929        };
930
931        dataset_items_api::dataset_items_create()
932            .configuration(self.configuration())
933            .create_dataset_item_request(item_request)
934            .call()
935            .await
936            .map_err(|e| crate::error::Error::Api(format!("Failed to create dataset item: {}", e)))
937    }
938
939    /// Get a specific dataset item
940    pub async fn get_dataset_item(
941        &self,
942        item_id: impl Into<String>,
943    ) -> Result<langfuse_client_base::models::DatasetItem> {
944        use langfuse_client_base::apis::dataset_items_api;
945
946        let item_id = item_id.into();
947
948        dataset_items_api::dataset_items_get()
949            .configuration(self.configuration())
950            .id(item_id.as_str())
951            .call()
952            .await
953            .map_err(|e| crate::error::Error::Api(format!("Failed to get dataset item: {}", e)))
954    }
955
956    /// List dataset items
957    #[builder]
958    pub async fn list_dataset_items(
959        &self,
960        #[builder(into)] dataset_name: Option<String>,
961        #[builder(into)] source_trace_id: Option<String>,
962        #[builder(into)] source_observation_id: Option<String>,
963        page: Option<i32>,
964        limit: Option<i32>,
965    ) -> Result<langfuse_client_base::models::PaginatedDatasetItems> {
966        use langfuse_client_base::apis::dataset_items_api;
967
968        let dataset_name_ref = dataset_name.as_deref();
969        let source_trace_ref = source_trace_id.as_deref();
970        let source_observation_ref = source_observation_id.as_deref();
971
972        dataset_items_api::dataset_items_list()
973            .configuration(self.configuration())
974            .maybe_dataset_name(dataset_name_ref)
975            .maybe_source_trace_id(source_trace_ref)
976            .maybe_source_observation_id(source_observation_ref)
977            .maybe_page(page)
978            .maybe_limit(limit)
979            .call()
980            .await
981            .map_err(|e| crate::error::Error::Api(format!("Failed to list dataset items: {}", e)))
982    }
983
984    /// Delete a dataset item
985    pub async fn delete_dataset_item(&self, item_id: impl Into<String>) -> Result<()> {
986        use langfuse_client_base::apis::dataset_items_api;
987
988        let item_id = item_id.into();
989
990        dataset_items_api::dataset_items_delete()
991            .configuration(self.configuration())
992            .id(item_id.as_str())
993            .call()
994            .await
995            .map_err(|e| {
996                crate::error::Error::Api(format!("Failed to delete dataset item: {}", e))
997            })?;
998
999        Ok(())
1000    }
1001
1002    // Note: dataset_run_items_api doesn't exist in v0.2
1003    // We'll implement this when the API is available
1004
1005    // ===== PROMPT MANAGEMENT =====
1006
1007    /// Create a new prompt or a new version of an existing prompt
1008    #[builder]
1009    pub async fn create_prompt(
1010        &self,
1011        #[builder(into)] name: String,
1012        #[builder(into)] prompt: String,
1013        _is_active: Option<bool>,
1014        config: Option<Value>,
1015        labels: Option<Vec<String>>,
1016        tags: Option<Vec<String>>,
1017    ) -> Result<langfuse_client_base::models::Prompt> {
1018        use langfuse_client_base::apis::prompts_api;
1019        use langfuse_client_base::models::CreatePromptRequest;
1020
1021        // Create a text prompt request using the OneOf1 variant
1022        use langfuse_client_base::models::CreatePromptRequestOneOf1;
1023
1024        let prompt_request =
1025            CreatePromptRequest::CreatePromptRequestOneOf1(Box::new(CreatePromptRequestOneOf1 {
1026                name: name.clone(),
1027                prompt,
1028                config: Some(config),
1029                labels: Some(labels),
1030                tags: Some(tags),
1031                ..Default::default()
1032            }));
1033
1034        prompts_api::prompts_create()
1035            .configuration(self.configuration())
1036            .create_prompt_request(prompt_request)
1037            .call()
1038            .await
1039            .map_err(|e| crate::error::Error::Api(format!("Failed to create prompt: {}", e)))
1040    }
1041
1042    /// Create a chat prompt with messages
1043    #[builder]
1044    pub async fn create_chat_prompt(
1045        &self,
1046        #[builder(into)] name: String,
1047        messages: Vec<serde_json::Value>, // Array of chat messages as JSON
1048        config: Option<Value>,
1049        labels: Option<Vec<String>>,
1050        tags: Option<Vec<String>>,
1051    ) -> Result<langfuse_client_base::models::Prompt> {
1052        use langfuse_client_base::apis::prompts_api;
1053        use langfuse_client_base::models::{
1054            ChatMessageWithPlaceholders, CreatePromptRequest, CreatePromptRequestOneOf,
1055        };
1056
1057        // Convert JSON messages to ChatMessageWithPlaceholders
1058        // Since ChatMessageWithPlaceholders is an enum, we need to deserialize properly
1059        let chat_messages: Vec<ChatMessageWithPlaceholders> = messages
1060            .into_iter()
1061            .map(|msg| {
1062                // Try to deserialize the JSON into ChatMessageWithPlaceholders
1063                serde_json::from_value(msg).unwrap_or_else(|_| {
1064                    // Create a default message if parsing fails
1065                    ChatMessageWithPlaceholders::default()
1066                })
1067            })
1068            .collect();
1069
1070        let prompt_request =
1071            CreatePromptRequest::CreatePromptRequestOneOf(Box::new(CreatePromptRequestOneOf {
1072                name: name.clone(),
1073                prompt: chat_messages,
1074                config: Some(config),
1075                labels: Some(labels),
1076                tags: Some(tags),
1077                ..Default::default()
1078            }));
1079
1080        prompts_api::prompts_create()
1081            .configuration(self.configuration())
1082            .create_prompt_request(prompt_request)
1083            .call()
1084            .await
1085            .map_err(|e| crate::error::Error::Api(format!("Failed to create chat prompt: {}", e)))
1086    }
1087
1088    /// Update labels for a specific prompt version
1089    #[builder]
1090    pub async fn update_prompt_version(
1091        &self,
1092        #[builder(into)] name: String,
1093        version: i32,
1094        labels: Vec<String>,
1095    ) -> Result<langfuse_client_base::models::Prompt> {
1096        use langfuse_client_base::apis::prompt_version_api;
1097        use langfuse_client_base::models::PromptVersionUpdateRequest;
1098
1099        let update_request = PromptVersionUpdateRequest { new_labels: labels };
1100
1101        prompt_version_api::prompt_version_update()
1102            .configuration(self.configuration())
1103            .name(name.as_str())
1104            .version(version)
1105            .prompt_version_update_request(update_request)
1106            .call()
1107            .await
1108            .map_err(|e| {
1109                crate::error::Error::Api(format!("Failed to update prompt version: {}", e))
1110            })
1111    }
1112
1113    /// Get a prompt by name and version
1114    pub async fn get_prompt(
1115        &self,
1116        prompt_name: impl Into<String>,
1117        version: Option<i32>,
1118        label: Option<&str>,
1119    ) -> Result<langfuse_client_base::models::Prompt> {
1120        use langfuse_client_base::apis::prompts_api;
1121
1122        let prompt_name = prompt_name.into();
1123
1124        prompts_api::prompts_get()
1125            .configuration(self.configuration())
1126            .prompt_name(prompt_name.as_str())
1127            .maybe_version(version)
1128            .maybe_label(label)
1129            .call()
1130            .await
1131            .map_err(|e| crate::error::Error::Api(format!("Failed to get prompt: {}", e)))
1132    }
1133
1134    /// List prompts with filters
1135    #[builder]
1136    pub async fn list_prompts(
1137        &self,
1138        #[builder(into)] name: Option<String>,
1139        #[builder(into)] tag: Option<String>,
1140        #[builder(into)] label: Option<String>,
1141        page: Option<i32>,
1142        limit: Option<String>,
1143    ) -> Result<langfuse_client_base::models::PromptMetaListResponse> {
1144        use langfuse_client_base::apis::prompts_api;
1145
1146        let name_ref = name.as_deref();
1147        let tag_ref = tag.as_deref();
1148        let label_ref = label.as_deref();
1149        let limit_num = limit.and_then(|value| value.parse::<i32>().ok());
1150
1151        prompts_api::prompts_list()
1152            .configuration(self.configuration())
1153            .maybe_name(name_ref)
1154            .maybe_tag(tag_ref)
1155            .maybe_label(label_ref)
1156            .maybe_page(page)
1157            .maybe_limit(limit_num)
1158            .call()
1159            .await
1160            .map_err(|e| crate::error::Error::Api(format!("Failed to list prompts: {}", e)))
1161    }
1162}