pi_agent/tools/
web_fetch.rs1use async_trait::async_trait;
2use serde_json::{json, Value};
3
4use crate::types::{AgentTool, AgentToolResult};
5
6pub struct WebFetchTool;
11
12#[async_trait]
13impl AgentTool for WebFetchTool {
14 fn name(&self) -> &str {
15 "web_fetch"
16 }
17 fn description(&self) -> &str {
18 "Fetch a URL via HTTPS and return a text-only excerpt of the response body. Use for documentation pages, GitHub READMEs, status checks, etc."
19 }
20 fn parameters(&self) -> Value {
21 json!({
22 "type": "object",
23 "properties": {
24 "url": {"type": "string", "description": "Absolute URL to fetch"},
25 "max_chars": {"type": "integer", "default": 8000}
26 },
27 "required": ["url"]
28 })
29 }
30 async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
31 let url = args
32 .get("url")
33 .and_then(|v| v.as_str())
34 .ok_or("missing 'url'")?
35 .to_string();
36 let max_chars = args
37 .get("max_chars")
38 .and_then(|v| v.as_u64())
39 .unwrap_or(8000) as usize;
40
41 let resp = reqwest::Client::new()
42 .get(&url)
43 .header("user-agent", "pi-coding-agent/1.0")
44 .send()
45 .await
46 .map_err(|e| format!("fetch {url}: {e}"))?;
47 let status = resp.status();
48 let text = resp.text().await.map_err(|e| e.to_string())?;
49 let stripped = strip_html(&text);
50 let truncated = if stripped.len() > max_chars {
51 format!(
52 "{}\n...(truncated, {} chars total)",
53 &stripped[..max_chars],
54 stripped.len()
55 )
56 } else {
57 stripped
58 };
59 Ok(AgentToolResult::text(format!(
60 "GET {url} [{status}]\n{truncated}"
61 )))
62 }
63}
64
65fn strip_html(s: &str) -> String {
66 let mut out = String::with_capacity(s.len());
67 let mut in_tag = false;
68 let mut last_ws = false;
69 for ch in s.chars() {
70 match ch {
71 '<' => in_tag = true,
72 '>' => {
73 in_tag = false;
74 if !last_ws {
75 out.push(' ');
76 last_ws = true;
77 }
78 }
79 c if in_tag => {
80 let _ = c;
81 }
82 c if c.is_whitespace() => {
83 if !last_ws {
84 out.push(' ');
85 last_ws = true;
86 }
87 }
88 c => {
89 out.push(c);
90 last_ws = false;
91 }
92 }
93 }
94 out.trim().to_string()
95}