robit_chatbot/tool/
send_file.rs1use async_trait::async_trait;
8use serde::Deserialize;
9use serde_json::Value;
10
11use robit_agent::error::Result;
12use robit_agent::tool::{resolve_path, Tool, ToolContext, ToolResult};
13
14use crate::extensions::keys;
15use crate::extensions::PlatformExtWrapper;
16
17pub struct SendFileTool;
19
20#[derive(Debug, Deserialize)]
21struct SendFileArgs {
22 file_path: String,
24 #[serde(default)]
27 media_type: Option<String>,
28}
29
30#[async_trait]
31impl Tool for SendFileTool {
32 fn name(&self) -> &str {
33 "send_file"
34 }
35
36 fn description(&self) -> &str {
37 "Send a local image or file to the chat user. Use this tool when the user asks you \
38 to send them a file. Provide the full path to the file (absolute or relative to \
39 the working directory). Supported image formats: jpg, jpeg, png, gif, bmp, webp. \
40 Other file types (pdf, txt, zip, etc.) are also supported."
41 }
42
43 fn parameters_schema(&self) -> Value {
44 serde_json::json!({
45 "type": "object",
46 "properties": {
47 "file_path": {
48 "type": "string",
49 "description": "Path to the file to send (absolute or relative to working directory)"
50 },
51 "media_type": {
52 "type": "string",
53 "description": "Optional: 'image' or 'file'. When omitted, inferred from extension.",
54 "enum": ["image", "file"]
55 }
56 },
57 "required": ["file_path"]
58 })
59 }
60
61 fn requires_confirmation(&self) -> bool {
62 false
63 }
64
65 async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolResult> {
66 let parsed: SendFileArgs = match serde_json::from_value(args) {
67 Ok(a) => a,
68 Err(e) => return Ok(ToolResult::error(format!("Invalid arguments: {}", e))),
69 };
70
71 let file_path = resolve_path(&parsed.file_path, &ctx.working_dir);
73 let file_path_str = match file_path.to_str() {
74 Some(p) => p,
75 None => return Ok(ToolResult::error("Invalid file path encoding".to_string())),
76 };
77
78 if !file_path.exists() {
80 return Ok(ToolResult::error(format!("File not found: {}", file_path_str)));
81 }
82 if !file_path.is_file() {
83 return Ok(ToolResult::error(format!("Not a file: {}", file_path_str)));
84 }
85
86 let media_type = parsed
88 .media_type
89 .unwrap_or_else(|| classify_media_type(file_path_str).to_string());
90
91 let ext = match ctx
93 .extensions
94 .get(keys::PLATFORM_EXT)
95 .and_then(|e| e.downcast_ref::<PlatformExtWrapper>())
96 {
97 Some(wrapper) => &wrapper.0,
98 None => {
99 return Ok(ToolResult::error(
100 "send_file is only available on chat platforms (QQ, Feishu)".to_string(),
101 ));
102 }
103 };
104
105 let upload_result = match ext.upload_file(file_path_str, &media_type).await {
107 Ok(r) => r,
108 Err(e) => return Ok(ToolResult::error(format!("Failed to upload file: {}", e))),
109 };
110
111 let file_name = file_path
113 .file_name()
114 .and_then(|n| n.to_str())
115 .unwrap_or("file");
116
117 match ext
118 .send_media_message(&upload_result.url, file_name, &media_type)
119 .await
120 {
121 Ok(_) => Ok(ToolResult::success(format!("File sent: {}", file_name))),
122 Err(e) => Ok(ToolResult::error(format!("Failed to send file: {}", e))),
123 }
124 }
125}
126
127fn classify_media_type(path: &str) -> &str {
129 let ext = std::path::Path::new(path)
130 .extension()
131 .and_then(|e| e.to_str())
132 .unwrap_or("")
133 .to_lowercase();
134
135 match ext.as_str() {
136 "jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" | "svg" | "ico" | "tiff" | "tif" => {
137 "image"
138 }
139 _ => "file",
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_classify_image() {
149 assert_eq!(classify_media_type("/path/to/photo.jpg"), "image");
150 assert_eq!(classify_media_type("C:\\img.PNG"), "image");
151 assert_eq!(classify_media_type("/tmp/anim.gif"), "image");
152 }
153
154 #[test]
155 fn test_classify_file() {
156 assert_eq!(classify_media_type("/path/to/doc.pdf"), "file");
157 assert_eq!(classify_media_type("C:\\data.zip"), "file");
158 assert_eq!(classify_media_type("/tmp/notes.txt"), "file");
159 }
160
161 #[test]
162 fn test_send_file_schema() {
163 let tool = SendFileTool;
164 assert_eq!(tool.name(), "send_file");
165 let schema = tool.parameters_schema();
166 assert!(schema["required"][0].as_str() == Some("file_path"));
167 }
168}