runtara_agents/
types.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Shared types used across agents
4
5use base64::{Engine as _, engine::general_purpose};
6use runtara_agent_macro::CapabilityOutput;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Represents a base64-encoded file payload that can flow through mappings
11#[derive(Debug, Clone, Serialize, Deserialize, CapabilityOutput)]
12#[capability_output(
13    display_name = "File Data",
14    description = "Base64-encoded file with optional metadata"
15)]
16pub struct FileData {
17    #[field(display_name = "Content", description = "Base64-encoded file content")]
18    pub content: String,
19
20    #[field(
21        display_name = "Filename",
22        description = "Original filename (optional)"
23    )]
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub filename: Option<String>,
26
27    #[field(
28        display_name = "MIME Type",
29        description = "MIME type (e.g., 'text/plain', 'text/csv', 'application/xml')"
30    )]
31    #[serde(skip_serializing_if = "Option::is_none")]
32    #[serde(rename = "mimeType")]
33    pub mime_type: Option<String>,
34}
35
36impl FileData {
37    /// Decode the base64 content to raw bytes
38    pub fn decode(&self) -> Result<Vec<u8>, String> {
39        general_purpose::STANDARD
40            .decode(&self.content)
41            .map_err(|e| format!("Failed to decode base64 file content: {}", e))
42    }
43
44    /// Create FileData from raw bytes
45    pub fn from_bytes(data: Vec<u8>, filename: Option<String>, mime_type: Option<String>) -> Self {
46        FileData {
47            content: general_purpose::STANDARD.encode(&data),
48            filename,
49            mime_type,
50        }
51    }
52
53    /// Try to parse a Value as FileData
54    pub fn from_value(value: &Value) -> Result<Self, String> {
55        match value {
56            Value::String(s) => Ok(FileData {
57                content: s.clone(),
58                filename: None,
59                mime_type: None,
60            }),
61            Value::Object(_) => serde_json::from_value(value.clone())
62                .map_err(|e| format!("Invalid file data structure: {}", e)),
63            Value::Array(arr) => {
64                let mut bytes = Vec::with_capacity(arr.len());
65                for v in arr {
66                    let num = v
67                        .as_u64()
68                        .ok_or_else(|| "Byte array must contain only numbers".to_string())?;
69                    if num > 255 {
70                        return Err("Byte values must be in the range 0-255".to_string());
71                    }
72                    bytes.push(num as u8);
73                }
74                Ok(FileData::from_bytes(bytes, None, None))
75            }
76            _ => Err(
77                "File data must be a string (base64), byte array, or object with content field"
78                    .to_string(),
79            ),
80        }
81    }
82}
83
84/// Token usage statistics for LLM capabilities
85#[derive(Debug, Clone, Serialize, Deserialize, CapabilityOutput)]
86#[capability_output(
87    display_name = "LLM Usage",
88    description = "Token count statistics from LLM API calls"
89)]
90#[serde(rename_all = "camelCase")]
91pub struct LlmUsage {
92    #[field(
93        display_name = "Prompt Tokens",
94        description = "Token count for input prompt",
95        example = "150"
96    )]
97    pub prompt_tokens: i32,
98
99    #[field(
100        display_name = "Completion Tokens",
101        description = "Token count for generated response",
102        example = "50"
103    )]
104    pub completion_tokens: i32,
105
106    #[field(
107        display_name = "Total Tokens",
108        description = "Combined token count",
109        example = "200"
110    )]
111    pub total_tokens: i32,
112}