1use base64::{Engine as _, engine::general_purpose};
6use runtara_agent_macro::CapabilityOutput;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[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 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 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 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#[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}