Skip to main content

ai_agents_tools/builtin/
http.rs

1use async_trait::async_trait;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6use std::time::Duration;
7
8use ai_agents_core::{
9    DomainPolicyBinding, ToolCallClassification, ToolExecutionContext, ToolOperationKind,
10    ToolPolicyBindings, ToolSafetyMetadata, ToolSideEffectLevel,
11};
12
13use crate::{Tool, ToolResult, generate_schema};
14
15pub struct HttpTool {
16    client: reqwest::Client,
17    default_timeout: Duration,
18}
19
20impl HttpTool {
21    pub fn new() -> Self {
22        Self {
23            client: reqwest::Client::new(),
24            default_timeout: Duration::from_secs(30),
25        }
26    }
27
28    pub fn with_timeout(timeout: Duration) -> Self {
29        Self {
30            client: reqwest::Client::new(),
31            default_timeout: timeout,
32        }
33    }
34}
35
36impl Default for HttpTool {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42#[derive(Debug, Deserialize, JsonSchema)]
43struct HttpInput {
44    /// HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD)
45    method: String,
46    /// URL to request
47    url: String,
48    /// Optional request headers as key-value pairs
49    #[serde(default)]
50    headers: Option<HashMap<String, String>>,
51    /// Optional request body (for POST/PUT/PATCH)
52    #[serde(default)]
53    body: Option<String>,
54    /// Optional timeout in milliseconds (default: 30000)
55    #[serde(default)]
56    timeout_ms: Option<u64>,
57}
58
59#[derive(Debug, Serialize)]
60struct HttpOutput {
61    status: u16,
62    status_text: String,
63    headers: HashMap<String, String>,
64    body: String,
65}
66
67#[async_trait]
68impl Tool for HttpTool {
69    fn id(&self) -> &str {
70        "http"
71    }
72
73    fn name(&self) -> &str {
74        "HTTP Client"
75    }
76
77    fn description(&self) -> &str {
78        "Make HTTP requests to external APIs and websites. Supports GET, POST, PUT, DELETE, PATCH, and HEAD methods."
79    }
80
81    fn input_schema(&self) -> Value {
82        generate_schema::<HttpInput>()
83    }
84
85    fn safety_metadata(&self) -> ToolSafetyMetadata {
86        ToolSafetyMetadata {
87            read_only: false,
88            concurrency_safe: false,
89            operation: ToolOperationKind::Network,
90            side_effect_level: ToolSideEffectLevel::ExternalWrite,
91            requires_network: true,
92            destructive: false,
93            open_world: true,
94            host_dependent: false,
95            requires_user_interaction: false,
96            supports_cancellation: false,
97            default_requires_approval: true,
98            should_defer_schema: false,
99            max_output_chars: Some(20_000),
100            max_result_size_chars: Some(20_000),
101        }
102    }
103
104    fn policy_bindings(&self) -> ToolPolicyBindings {
105        ToolPolicyBindings {
106            domain_fields: vec![DomainPolicyBinding::url("url")],
107            operation_fields: vec!["method".to_string()],
108            ..Default::default()
109        }
110    }
111
112    fn classify_call(&self, args: &Value) -> ToolCallClassification {
113        let mut metadata = self.safety_metadata();
114        let method = args
115            .get("method")
116            .and_then(|v| v.as_str())
117            .unwrap_or_default();
118        if matches!(method.to_ascii_uppercase().as_str(), "GET" | "HEAD") {
119            metadata.read_only = true;
120            metadata.concurrency_safe = true;
121            metadata.side_effect_level = ToolSideEffectLevel::ExternalRead;
122            metadata.default_requires_approval = false;
123        }
124        ToolCallClassification::from_metadata(&metadata)
125    }
126
127    async fn execute(&self, args: Value, ctx: ToolExecutionContext) -> ToolResult {
128        let input: HttpInput = match serde_json::from_value(args) {
129            Ok(input) => input,
130            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
131        };
132
133        let timeout_ms = input
134            .timeout_ms
135            .unwrap_or(self.default_timeout.as_millis() as u64)
136            .min(
137                ctx.limits
138                    .timeout_ms
139                    .unwrap_or(self.default_timeout.as_millis() as u64),
140            );
141        let timeout = Duration::from_millis(timeout_ms);
142
143        let method = input.method.to_uppercase();
144        let mut request = match method.as_str() {
145            "GET" => self.client.get(&input.url),
146            "POST" => self.client.post(&input.url),
147            "PUT" => self.client.put(&input.url),
148            "DELETE" => self.client.delete(&input.url),
149            "PATCH" => self.client.patch(&input.url),
150            "HEAD" => self.client.head(&input.url),
151            _ => return ToolResult::error(format!("Invalid HTTP method: {}", method)),
152        };
153
154        request = request.timeout(timeout);
155
156        if let Some(headers) = input.headers {
157            for (key, value) in headers {
158                request = request.header(key, value);
159            }
160        }
161
162        if let Some(body) = input.body {
163            request = request.body(body);
164        }
165
166        match request.send().await {
167            Ok(response) => {
168                let status = response.status();
169                let status_code = status.as_u16();
170                let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
171
172                let headers: HashMap<String, String> = response
173                    .headers()
174                    .iter()
175                    .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
176                    .collect();
177
178                let body = response.text().await.unwrap_or_default();
179
180                let output = HttpOutput {
181                    status: status_code,
182                    status_text,
183                    headers,
184                    body,
185                };
186
187                match serde_json::to_string(&output) {
188                    Ok(json) => ToolResult::ok(json),
189                    Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
190                }
191            }
192            Err(e) => ToolResult::error(format!("Request failed: {}", e)),
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_http_tool_creation() {
203        let tool = HttpTool::new();
204        assert_eq!(tool.id(), "http");
205        assert_eq!(tool.name(), "HTTP Client");
206    }
207
208    #[test]
209    fn test_http_tool_with_timeout() {
210        let tool = HttpTool::with_timeout(Duration::from_secs(60));
211        assert_eq!(tool.default_timeout, Duration::from_secs(60));
212    }
213
214    #[test]
215    fn test_input_schema() {
216        let tool = HttpTool::new();
217        let schema = tool.input_schema();
218        assert!(schema.is_object());
219    }
220}