cersei_tools/
web_fetch.rs1use super::*;
4use crate::tool_primitives::http as phttp;
5use serde::Deserialize;
6
7pub struct WebFetchTool;
8
9#[async_trait]
10impl Tool for WebFetchTool {
11 fn name(&self) -> &str {
12 "WebFetch"
13 }
14 fn description(&self) -> &str {
15 "Fetch a URL and return its content as readable text. HTML is converted to markdown."
16 }
17 fn permission_level(&self) -> PermissionLevel {
18 PermissionLevel::ReadOnly
19 }
20 fn category(&self) -> ToolCategory {
21 ToolCategory::Web
22 }
23
24 fn input_schema(&self) -> Value {
25 serde_json::json!({
26 "type": "object",
27 "properties": {
28 "url": { "type": "string", "description": "The URL to fetch" },
29 "max_chars": { "type": "integer", "description": "Max characters to return (default 50000)" }
30 },
31 "required": ["url"]
32 })
33 }
34
35 async fn execute(&self, input: Value, _ctx: &ToolContext) -> ToolResult {
36 #[derive(Deserialize)]
37 struct Input {
38 url: String,
39 max_chars: Option<usize>,
40 }
41
42 let input: Input = match serde_json::from_value(input) {
43 Ok(i) => i,
44 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
45 };
46
47 let max_chars = input.max_chars.unwrap_or(50_000);
48
49 match phttp::fetch_html(&input.url, max_chars, phttp::HttpOptions::default()).await {
50 Ok(text) => {
51 if text.len() >= max_chars {
52 ToolResult::success(format!(
53 "{}\n\n[Truncated: showing first {} chars]",
54 text, max_chars
55 ))
56 } else {
57 ToolResult::success(text)
58 }
59 }
60 Err(e) => ToolResult::error(format!("Fetch failed: {}", e)),
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn test_schema() {
71 let tool = WebFetchTool;
72 let schema = tool.input_schema();
73 assert!(schema["properties"]["url"].is_object());
74 assert_eq!(tool.permission_level(), PermissionLevel::ReadOnly);
75 assert_eq!(tool.category(), ToolCategory::Web);
76 }
77}