1use 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
13pub trait IntoTags {
15 fn into_tags(self) -> Vec<String>;
16}
17
18pub 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, "WARN" | "WARNING" => ObservationLevel::Warning,
26 "ERROR" => ObservationLevel::Error,
27 _ => ObservationLevel::Default, }
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
55pub struct TraceResponse {
57 pub id: String,
58 pub base_url: String,
59}
60
61impl TraceResponse {
62 pub fn url(&self) -> String {
64 let mut web_url = self.base_url.clone();
66
67 web_url = web_url.trim_end_matches('/').to_string();
69
70 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
81pub struct IdGenerator;
83
84impl IdGenerator {
85 pub fn from_seed(seed: &str) -> String {
88 let namespace = Uuid::NAMESPACE_OID;
90 Uuid::new_v5(&namespace, seed.as_bytes()).to_string()
91 }
92
93 pub fn from_components(components: &[&str]) -> String {
96 let combined = components.join(":");
97 Self::from_seed(&combined)
98 }
99
100 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 #[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 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 #[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 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 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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 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, input: Some(input),
603 output: Some(output),
604 usage: None, 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 #[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 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 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 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 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 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 #[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 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 #[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 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 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 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 #[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, };
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 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 #[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 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 #[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 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 #[builder]
1044 pub async fn create_chat_prompt(
1045 &self,
1046 #[builder(into)] name: String,
1047 messages: Vec<serde_json::Value>, 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 let chat_messages: Vec<ChatMessageWithPlaceholders> = messages
1060 .into_iter()
1061 .map(|msg| {
1062 serde_json::from_value(msg).unwrap_or_else(|_| {
1064 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 #[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 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 #[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}