Skip to main content

langfuse/media/
types.rs

1//! Media types for the Langfuse SDK.
2
3use std::path::Path;
4
5use langfuse_core::error::LangfuseError;
6
7/// A media object that can be uploaded to Langfuse.
8#[derive(Debug, Clone)]
9pub struct LangfuseMedia {
10    /// MIME content type (e.g., `"image/png"`).
11    pub content_type: String,
12    /// Raw binary data.
13    pub data: Vec<u8>,
14}
15
16impl LangfuseMedia {
17    /// Create from a base64-encoded data URI (e.g., `"data:image/png;base64,..."`).
18    pub fn from_data_uri(data_uri: &str) -> Result<Self, LangfuseError> {
19        // Parse the data URI format: data:<content_type>;base64,<data>
20        let parts: Vec<&str> = data_uri.splitn(2, ',').collect();
21        if parts.len() != 2 {
22            return Err(LangfuseError::Media("Invalid data URI".into()));
23        }
24        let header = parts[0]; // "data:image/png;base64"
25        let base64_data = parts[1];
26
27        let content_type = header
28            .strip_prefix("data:")
29            .and_then(|s| s.strip_suffix(";base64"))
30            .ok_or_else(|| LangfuseError::Media("Invalid data URI format".into()))?;
31
32        use base64::Engine as _;
33        let data = base64::engine::general_purpose::STANDARD
34            .decode(base64_data)
35            .map_err(|e| LangfuseError::Media(format!("Base64 decode error: {e}")))?;
36
37        Ok(Self {
38            content_type: content_type.to_string(),
39            data,
40        })
41    }
42
43    /// Create from raw bytes.
44    pub fn from_bytes(content_type: &str, data: Vec<u8>) -> Self {
45        Self {
46            content_type: content_type.to_string(),
47            data,
48        }
49    }
50
51    /// Create from a file path.
52    pub fn from_file(content_type: &str, path: impl AsRef<Path>) -> Result<Self, LangfuseError> {
53        let data = std::fs::read(path.as_ref())
54            .map_err(|e| LangfuseError::Media(format!("File read error: {e}")))?;
55        Ok(Self {
56            content_type: content_type.to_string(),
57            data,
58        })
59    }
60
61    /// Create from a file path asynchronously.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`LangfuseError::Media`] if the file cannot be read.
66    pub async fn from_file_async(
67        content_type: &str,
68        path: impl AsRef<Path>,
69    ) -> Result<Self, LangfuseError> {
70        let data = tokio::fs::read(path.as_ref())
71            .await
72            .map_err(|e| LangfuseError::Media(format!("File read error: {e}")))?;
73        Ok(Self {
74            content_type: content_type.to_string(),
75            data,
76        })
77    }
78
79    /// Size in bytes.
80    pub fn size(&self) -> usize {
81        self.data.len()
82    }
83}
84
85/// Regex pattern for Langfuse media reference tokens.
86/// Format: `@@@langfuseMedia:type=<content_type>|id=<media_id>|source=<source>@@@`
87pub const MEDIA_REFERENCE_PATTERN: &str =
88    r"@@@langfuseMedia:type=([^|]+)\|id=([^|]+)\|source=([^@]+)@@@";
89
90/// A parsed media reference extracted from text.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct ParsedMediaReference {
93    /// MIME content type.
94    pub content_type: String,
95    /// Langfuse media ID.
96    pub media_id: String,
97    /// Source descriptor (e.g., `"base64_data_uri"`).
98    pub source: String,
99}
100
101/// Parse media reference tokens from a string.
102///
103/// Finds all occurrences of `@@@langfuseMedia:type=...|id=...|source=...@@@`
104/// and returns the parsed components.
105pub fn parse_media_references(text: &str) -> Vec<ParsedMediaReference> {
106    let mut refs = Vec::new();
107    let mut remaining = text;
108    while let Some(start) = remaining.find("@@@langfuseMedia:") {
109        let after = &remaining[start + 17..]; // skip "@@@langfuseMedia:"
110        if let Some(end) = after.find("@@@") {
111            let inner = &after[..end];
112            // Parse type=X|id=Y|source=Z
113            let mut content_type = None;
114            let mut media_id = None;
115            let mut source = None;
116            for part in inner.split('|') {
117                if let Some(val) = part.strip_prefix("type=") {
118                    content_type = Some(val.to_string());
119                } else if let Some(val) = part.strip_prefix("id=") {
120                    media_id = Some(val.to_string());
121                } else if let Some(val) = part.strip_prefix("source=") {
122                    source = Some(val.to_string());
123                }
124            }
125            if let (Some(ct), Some(id), Some(src)) = (content_type, media_id, source) {
126                refs.push(ParsedMediaReference {
127                    content_type: ct,
128                    media_id: id,
129                    source: src,
130                });
131            }
132            remaining = &after[end + 3..];
133        } else {
134            break;
135        }
136    }
137    refs
138}