ai_agents_tools/builtin/
http.rs1use 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 method: String,
46 url: String,
48 #[serde(default)]
50 headers: Option<HashMap<String, String>>,
51 #[serde(default)]
53 body: Option<String>,
54 #[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}