Skip to main content

codetether_agent/tool/
image.rs

1//! Image tool for loading and encoding images
2//!
3//! This tool allows agents to load images from files or URLs and encode them
4//! as base64 data URLs for use with vision-capable models.
5
6use super::{Tool, ToolResult};
7use anyhow::Result;
8use async_trait::async_trait;
9use base64::{Engine as _, engine::general_purpose::STANDARD};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13/// Input parameters for the image tool
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ImageToolInput {
16    /// Path to the image file or URL
17    pub path: String,
18    /// Optional detail level (low, high, auto) - for providers that support it
19    #[serde(default)]
20    pub detail: Option<String>,
21}
22
23/// Tool for loading images from files or URLs
24pub struct ImageTool;
25
26impl ImageTool {
27    pub fn new() -> Self {
28        Self
29    }
30
31    /// Detect MIME type from file extension
32    fn detect_mime_type(path: &str) -> &'static str {
33        let lower = path.to_lowercase();
34        if lower.ends_with(".png") {
35            "image/png"
36        } else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
37            "image/jpeg"
38        } else if lower.ends_with(".gif") {
39            "image/gif"
40        } else if lower.ends_with(".webp") {
41            "image/webp"
42        } else if lower.ends_with(".svg") {
43            "image/svg+xml"
44        } else if lower.ends_with(".bmp") {
45            "image/bmp"
46        } else if lower.ends_with(".tiff") || lower.ends_with(".tif") {
47            "image/tiff"
48        } else {
49            "image/jpeg" // Default fallback
50        }
51    }
52
53    /// Encode image data as base64 data URL
54    fn encode_as_data_url(data: &[u8], mime_type: &str) -> String {
55        let base64 = STANDARD.encode(data);
56        format!("data:{};base64,{}", mime_type, base64)
57    }
58}
59
60#[async_trait]
61impl Tool for ImageTool {
62    fn id(&self) -> &str {
63        "image"
64    }
65
66    fn name(&self) -> &str {
67        "Image Loader"
68    }
69
70    fn description(&self) -> &str {
71        "Load an image from a file path or URL and encode it for use with vision-capable models. \
72         Supports PNG, JPEG, GIF, WebP, SVG, BMP, and TIFF formats. \
73         Returns a base64-encoded data URL that can be used in image content parts."
74    }
75
76    fn parameters(&self) -> Value {
77        serde_json::json!({
78            "type": "object",
79            "properties": {
80                "path": {
81                    "type": "string",
82                    "description": "Path to the image file (absolute or relative) or URL"
83                },
84                "detail": {
85                    "type": "string",
86                    "enum": ["low", "high", "auto"],
87                    "description": "Optional detail level for vision models (low=512x512, high=full resolution with 512x512 tiles)"
88                }
89            },
90            "required": ["path"]
91        })
92    }
93
94    async fn execute(&self, args: Value) -> Result<ToolResult> {
95        let input: ImageToolInput = serde_json::from_value(args)?;
96
97        // Check if path is a URL
98        let data = if input.path.starts_with("http://") || input.path.starts_with("https://") {
99            // Fetch image from URL
100            let response = reqwest::get(&input.path).await?;
101
102            if !response.status().is_success() {
103                return Ok(ToolResult::error(format!(
104                    "Failed to fetch image from URL: HTTP {}",
105                    response.status()
106                )));
107            }
108
109            // Try to get MIME type from response headers
110            let mime_type = response
111                .headers()
112                .get("content-type")
113                .and_then(|v| v.to_str().ok())
114                .map(|s| s.to_string())
115                .unwrap_or_else(|| Self::detect_mime_type(&input.path).to_string());
116
117            let bytes = response.bytes().await?;
118            let data_url = Self::encode_as_data_url(&bytes, &mime_type);
119
120            serde_json::json!({
121                "data_url": data_url,
122                "mime_type": mime_type,
123                "size_bytes": bytes.len(),
124                "source": input.path,
125                "detail": input.detail.unwrap_or_else(|| "auto".to_string())
126            })
127        } else {
128            // Load from file path
129            let path = std::path::Path::new(&input.path);
130
131            if !path.exists() {
132                return Ok(ToolResult::error(format!(
133                    "Image file not found: {}",
134                    input.path
135                )));
136            }
137
138            let data = tokio::fs::read(path).await?;
139            let mime_type = Self::detect_mime_type(&input.path);
140            let data_url = Self::encode_as_data_url(&data, mime_type);
141
142            serde_json::json!({
143                "data_url": data_url,
144                "mime_type": mime_type,
145                "size_bytes": data.len(),
146                "source": input.path,
147                "detail": input.detail.unwrap_or_else(|| "auto".to_string())
148            })
149        };
150
151        Ok(ToolResult::success(serde_json::to_string_pretty(&data)?))
152    }
153}
154
155impl Default for ImageTool {
156    fn default() -> Self {
157        Self::new()
158    }
159}