codetether_agent/tool/
image.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ImageToolInput {
16 pub path: String,
18 #[serde(default)]
20 pub detail: Option<String>,
21}
22
23pub struct ImageTool;
25
26impl ImageTool {
27 pub fn new() -> Self {
28 Self
29 }
30
31 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" }
51 }
52
53 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 let data = if input.path.starts_with("http://") || input.path.starts_with("https://") {
99 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 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 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}