1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static HTTP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "http_connected",
18 "HTTP client initialized and ready to send requests",
19 serde_json::json!({
20 "type": "send_http_request",
21 "method": "GET",
22 "path": "/"
23 }),
24 )
25 .with_parameters(vec![Parameter {
26 name: "base_url".to_string(),
27 type_hint: "string".to_string(),
28 description: "Base URL for HTTP requests".to_string(),
29 required: true,
30 }])
31});
32
33pub static HTTP_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35 EventType::new(
36 "http_response_received",
37 "HTTP response received from server",
38 serde_json::json!({
39 "type": "send_http_request",
40 "method": "GET",
41 "path": "/api/next"
42 }),
43 )
44 .with_parameters(vec![
45 Parameter {
46 name: "status_code".to_string(),
47 type_hint: "number".to_string(),
48 description: "HTTP status code".to_string(),
49 required: true,
50 },
51 Parameter {
52 name: "headers".to_string(),
53 type_hint: "object".to_string(),
54 description: "Response headers".to_string(),
55 required: true,
56 },
57 Parameter {
58 name: "body".to_string(),
59 type_hint: "string".to_string(),
60 description: "Response body".to_string(),
61 required: true,
62 },
63 ])
64});
65
66pub struct HttpClientProtocol;
68
69impl Default for HttpClientProtocol {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl HttpClientProtocol {
76 pub fn new() -> Self {
77 Self
78 }
79}
80
81impl Protocol for HttpClientProtocol {
83 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
84 vec![ParameterDefinition {
85 name: "default_headers".to_string(),
86 description: "Default headers to include in all requests".to_string(),
87 type_hint: "object".to_string(),
88 required: false,
89 example: json!({
90 "User-Agent": "NetGet/1.0",
91 "Accept": "application/json"
92 }),
93 default: None,
94 }]
95 }
96 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
97 vec![
98 ActionDefinition {
99 name: "send_http_request".to_string(),
100 description: "Send an HTTP request to the server".to_string(),
101 parameters: vec![
102 Parameter {
103 name: "method".to_string(),
104 type_hint: "string".to_string(),
105 description: "HTTP method (GET, POST, PUT, DELETE, etc.)".to_string(),
106 required: true,
107 },
108 Parameter {
109 name: "path".to_string(),
110 type_hint: "string".to_string(),
111 description: "Request path (e.g., /api/users), resolved against the client's base URL. An absolute URL is accepted only on that same origin; another host needs its own client".to_string(),
112 required: true,
113 },
114 Parameter {
115 name: "headers".to_string(),
116 type_hint: "object".to_string(),
117 description: "Request headers".to_string(),
118 required: false,
119 },
120 Parameter {
121 name: "body".to_string(),
122 type_hint: "string".to_string(),
123 description: "Request body".to_string(),
124 required: false,
125 },
126 ],
127 example: json!({
128 "type": "send_http_request",
129 "method": "GET",
130 "path": "/api/status",
131 "headers": {
132 "Accept": "application/json"
133 }
134 }),
135 log_template: None,
136 },
137 ActionDefinition {
138 name: "disconnect".to_string(),
139 description: "Disconnect from the HTTP server".to_string(),
140 parameters: vec![],
141 example: json!({
142 "type": "disconnect"
143 }),
144 log_template: None,
145 },
146 ]
147 }
148 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
149 vec![
150 ActionDefinition {
151 name: "send_http_request".to_string(),
152 description: "Send another HTTP request in response to received data".to_string(),
153 parameters: vec![
154 Parameter {
155 name: "method".to_string(),
156 type_hint: "string".to_string(),
157 description: "HTTP method".to_string(),
158 required: true,
159 },
160 Parameter {
161 name: "path".to_string(),
162 type_hint: "string".to_string(),
163 description: "Request path, resolved against the client's base URL. An absolute URL is accepted only on that same origin; another host needs its own client".to_string(),
164 required: true,
165 },
166 Parameter {
167 name: "headers".to_string(),
168 type_hint: "object".to_string(),
169 description: "Request headers".to_string(),
170 required: false,
171 },
172 Parameter {
173 name: "body".to_string(),
174 type_hint: "string".to_string(),
175 description: "Request body".to_string(),
176 required: false,
177 },
178 ],
179 example: json!({
180 "type": "send_http_request",
181 "method": "POST",
182 "path": "/api/data",
183 "body": "{\"key\": \"value\"}"
184 }),
185 log_template: None,
186 },
187 ActionDefinition {
188 name: "wait_for_more".to_string(),
189 description:
190 "Take no action and wait for the next response. Use when nothing should be \
191 sent yet."
192 .to_string(),
193 parameters: vec![],
194 example: json!({ "type": "wait_for_more" }),
195 log_template: None,
196 },
197 ]
198 }
199 fn protocol_name(&self) -> &'static str {
200 "HTTP"
201 }
202 fn get_event_types(&self) -> Vec<EventType> {
203 vec![
204 EventType::new(
205 "http_connected",
206 "Triggered when HTTP client is initialized",
207 json!({"type": "placeholder", "event_id": "http_connected"}),
208 ),
209 EventType::new(
210 "http_response_received",
211 "Triggered when HTTP client receives a response",
212 json!({"type": "placeholder", "event_id": "http_response_received"}),
213 ),
214 ]
215 }
216 fn stack_name(&self) -> &'static str {
217 "ETH>IP>TCP>HTTP"
218 }
219 fn keywords(&self) -> Vec<&'static str> {
220 vec!["http", "http client", "connect to http", "https"]
221 }
222 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
223 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
224
225 ProtocolMetadataV2::builder()
226 .state(DevelopmentState::Beta)
227 .implementation(
228 "reqwest HTTP/HTTPS client library (HTTP/1.1, HTTP/2 via rustls); in the browser \
229 build hyper's HTTP/1.1 client over the virtual loopback (plain http:// only)",
230 )
231 .llm_control("Full control over requests (method, path, headers, body)")
232 .e2e_testing(
233 "tests/client/http/real_server_test.rs, 5 LLM calls, against nginx (C, its own \
234 HTTP parser; NetGet's client is reqwest/hyper). The model GETs a static file, \
235 POSTs a header built from that file with a 13-byte body, and GETs a missing \
236 path; nginx's own access log must hold exactly those request lines, the \
237 model's header value, the default_headers User-Agent, the body length and the \
238 200/200/404 statuses. HTTP is this client's protocol, so a real HTTP server is \
239 the right peer. Not #[ignore]d, and a missing nginx fails the test rather than \
240 skipping it.",
241 )
242 .notes(
243 "Validated against nginx over plain HTTP/1.1. HTTPS, HTTP/2, redirects and \
244 compressed bodies are not exercised against a real server. A response body is \
245 read whole up to 8 MiB and refused past it. The browser transport \
246 (transport.rs) is exercised against NetGet's own servers only, chunked \
247 responses included.",
248 )
249 .build()
250 }
251 fn description(&self) -> &'static str {
252 "HTTP/HTTPS client for making web requests (HTTP/1.1, HTTP/2)"
253 }
254 fn example_prompt(&self) -> &'static str {
255 "Connect to https://example.com and fetch /api/status"
256 }
257 fn group_name(&self) -> &'static str {
258 "Core"
259 }
260 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
261 use crate::llm::actions::StartupExamples;
262 use serde_json::json;
263
264 StartupExamples::new(
265 json!({
267 "type": "open_client",
268 "remote_addr": "https://api.example.com",
269 "base_stack": "http",
270 "instruction": "Fetch the API status and report the results"
271 }),
272 json!({
274 "type": "open_client",
275 "remote_addr": "https://api.example.com",
276 "base_stack": "http",
277 "event_handlers": [{
278 "event_pattern": "http_response_received",
279 "handler": {
280 "type": "script",
281 "language": "python",
282 "code": "<http_client_handler>"
283 }
284 }]
285 }),
286 json!({
288 "type": "open_client",
289 "remote_addr": "https://api.example.com",
290 "base_stack": "http",
291 "event_handlers": [
292 {
293 "event_pattern": "http_connected",
294 "handler": {
295 "type": "static",
296 "actions": [{
297 "type": "send_http_request",
298 "method": "GET",
299 "path": "/health",
300 "headers": {"Accept": "application/json"}
301 }]
302 }
303 },
304 {
305 "event_pattern": "http_response_received",
306 "handler": {
307 "type": "static",
308 "actions": [{
309 "type": "disconnect"
310 }]
311 }
312 }
313 ]
314 }),
315 )
316 }
317}
318
319impl Client for HttpClientProtocol {
321 fn connect(
322 &self,
323 ctx: crate::protocol::ConnectContext,
324 ) -> std::pin::Pin<
325 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
326 > {
327 Box::pin(async move {
328 use crate::client::http::HttpClient;
329 HttpClient::connect_with_llm_actions(
330 ctx.remote_addr,
331 ctx.llm_client,
332 ctx.state,
333 ctx.status_tx,
334 ctx.client_id,
335 ctx.startup_params,
336 )
337 .await
338 })
339 }
340 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
341 let action_type = action
342 .get("type")
343 .and_then(|v| v.as_str())
344 .context("Missing 'type' field in action")?;
345
346 match action_type {
347 "send_http_request" => {
348 let method = action
349 .get("method")
350 .and_then(|v| v.as_str())
351 .context("Missing 'method' field")?
352 .to_string();
353
354 let path = action
355 .get("path")
356 .and_then(|v| v.as_str())
357 .context("Missing 'path' field")?
358 .to_string();
359
360 let headers = action.get("headers").and_then(|v| v.as_object()).cloned();
361
362 let body = action
363 .get("body")
364 .and_then(|v| v.as_str())
365 .map(|s| s.to_string());
366
367 Ok(ClientActionResult::Custom {
369 name: "http_request".to_string(),
370 data: json!({
371 "method": method,
372 "path": path,
373 "headers": headers,
374 "body": body,
375 }),
376 })
377 }
378 "disconnect" => Ok(ClientActionResult::Disconnect),
379 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
385 _ => Err(anyhow::anyhow!(
386 "Unknown HTTP client action: {}",
387 action_type
388 )),
389 }
390 }
391}