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 HTTP2_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "http2_connected",
18 "HTTP/2 client initialized and ready to send requests",
19 json!({
20 "type": "send_http2_request",
21 "method": "GET",
22 "path": "/api/status"
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/2 requests".to_string(),
29 required: true,
30 }])
31});
32
33pub static HTTP2_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35 EventType::new(
36 "http2_response_received",
37 "HTTP/2 response received from server",
38 json!({
39 "type": "send_http2_request",
40 "method": "POST",
41 "path": "/api/data",
42 "body": "{\"key\": \"value\"}"
43 }),
44 )
45 .with_parameters(vec![
46 Parameter {
47 name: "status_code".to_string(),
48 type_hint: "number".to_string(),
49 description: "HTTP status code".to_string(),
50 required: true,
51 },
52 Parameter {
53 name: "http_version".to_string(),
54 type_hint: "string".to_string(),
55 description: "HTTP version (should be HTTP/2.0)".to_string(),
56 required: true,
57 },
58 Parameter {
59 name: "headers".to_string(),
60 type_hint: "object".to_string(),
61 description: "Response headers".to_string(),
62 required: true,
63 },
64 Parameter {
65 name: "body".to_string(),
66 type_hint: "string".to_string(),
67 description: "Response body".to_string(),
68 required: true,
69 },
70 ])
71});
72
73#[derive(Default)]
75pub struct Http2ClientProtocol;
76
77impl Http2ClientProtocol {
78 pub fn new() -> Self {
79 Self::default()
80 }
81}
82
83impl Protocol for Http2ClientProtocol {
85 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
86 vec![ParameterDefinition {
87 name: "default_headers".to_string(),
88 description: "Default headers to include in all requests".to_string(),
89 type_hint: "object".to_string(),
90 required: false,
91 example: json!({
92 "User-Agent": "NetGet/1.0",
93 "Accept": "application/json"
94 }),
95 default: None,
96 }]
97 }
98 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
99 vec![
100 ActionDefinition {
101 name: "send_http2_request".to_string(),
102 description: "Send an HTTP/2 request to the server".to_string(),
103 parameters: vec![
104 Parameter {
105 name: "method".to_string(),
106 type_hint: "string".to_string(),
107 description: "HTTP method (GET, POST, PUT, DELETE, etc.)".to_string(),
108 required: true,
109 },
110 Parameter {
111 name: "path".to_string(),
112 type_hint: "string".to_string(),
113 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(),
114 required: true,
115 },
116 Parameter {
117 name: "headers".to_string(),
118 type_hint: "object".to_string(),
119 description: "Request headers".to_string(),
120 required: false,
121 },
122 Parameter {
123 name: "body".to_string(),
124 type_hint: "string".to_string(),
125 description: "Request body".to_string(),
126 required: false,
127 },
128 ],
129 example: json!({
130 "type": "send_http2_request",
131 "method": "GET",
132 "path": "/api/status",
133 "headers": {
134 "Accept": "application/json"
135 }
136 }),
137 log_template: None,
138 },
139 ActionDefinition {
140 name: "disconnect".to_string(),
141 description: "Disconnect from the HTTP/2 server".to_string(),
142 parameters: vec![],
143 example: json!({
144 "type": "disconnect"
145 }),
146 log_template: None,
147 },
148 ]
149 }
150 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
151 vec![
152 ActionDefinition {
153 name: "wait_for_more".to_string(),
154 description: "Do nothing and wait for the next HTTP/2 response. The correct \
155 answer when what arrived needs no follow-up -- without it the model has \
156 to invent an action it does not want."
157 .to_string(),
158 parameters: vec![],
159 example: json!({ "type": "wait_for_more" }),
160 log_template: None,
161 },
162 ActionDefinition {
163 name: "send_http2_request".to_string(),
164 description: "Send another HTTP/2 request in response to received data".to_string(),
165 parameters: vec![
166 Parameter {
167 name: "method".to_string(),
168 type_hint: "string".to_string(),
169 description: "HTTP method".to_string(),
170 required: true,
171 },
172 Parameter {
173 name: "path".to_string(),
174 type_hint: "string".to_string(),
175 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(),
176 required: true,
177 },
178 Parameter {
179 name: "headers".to_string(),
180 type_hint: "object".to_string(),
181 description: "Request headers".to_string(),
182 required: false,
183 },
184 Parameter {
185 name: "body".to_string(),
186 type_hint: "string".to_string(),
187 description: "Request body".to_string(),
188 required: false,
189 },
190 ],
191 example: json!({
192 "type": "send_http2_request",
193 "method": "POST",
194 "path": "/api/data",
195 "body": "{\"key\": \"value\"}"
196 }),
197 log_template: None,
198 },
199 ]
200 }
201 fn protocol_name(&self) -> &'static str {
202 "HTTP2"
203 }
204 fn get_event_types(&self) -> Vec<EventType> {
205 vec![
206 EventType::new(
207 "http2_connected",
208 "Triggered when HTTP/2 client is initialized",
209 json!({"type": "placeholder", "event_id": "http2_connected"}),
210 ),
211 EventType::new(
212 "http2_response_received",
213 "Triggered when HTTP/2 client receives a response",
214 json!({"type": "placeholder", "event_id": "http2_response_received"}),
215 ),
216 ]
217 }
218 fn stack_name(&self) -> &'static str {
219 "ETH>IP>TCP>HTTP/2"
220 }
221 fn keywords(&self) -> Vec<&'static str> {
222 vec![
223 "http2",
224 "http/2",
225 "http 2",
226 "http2 client",
227 "connect to http2",
228 ]
229 }
230 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
231 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
232
233 ProtocolMetadataV2::builder()
234 .state(DevelopmentState::Experimental)
235 .implementation("reqwest HTTP/2 client library with http2_prior_knowledge")
236 .llm_control("Full control over requests (method, path, headers, body)")
237 .e2e_testing("HTTP/2 test server or nghttp2.org")
238 .build()
239 }
240 fn description(&self) -> &'static str {
241 "HTTP/2 client for making multiplexed web requests"
242 }
243 fn example_prompt(&self) -> &'static str {
244 "Connect to https://http2.golang.org and fetch /reqinfo"
245 }
246 fn group_name(&self) -> &'static str {
247 "Core"
248 }
249
250 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
251 use crate::llm::actions::StartupExamples;
252 use serde_json::json;
253
254 StartupExamples::new(
255 json!({
257 "type": "open_client",
258 "remote_addr": "https://http2.golang.org",
259 "base_stack": "http2",
260 "instruction": "Fetch /reqinfo and display the HTTP/2 server information"
261 }),
262 json!({
264 "type": "open_client",
265 "remote_addr": "https://httpbin.org",
266 "base_stack": "http2",
267 "event_handlers": [{
268 "event_pattern": "http2_response_received",
269 "handler": {
270 "type": "script",
271 "language": "python",
272 "code": "<http2_handler>"
273 }
274 }]
275 }),
276 json!({
278 "type": "open_client",
279 "remote_addr": "https://httpbin.org",
280 "base_stack": "http2",
281 "event_handlers": [{
282 "event_pattern": "http2_connected",
283 "handler": {
284 "type": "static",
285 "actions": [{
286 "type": "send_http2_request",
287 "method": "GET",
288 "path": "/get",
289 "headers": {"Accept": "application/json"}
290 }]
291 }
292 }]
293 }),
294 )
295 }
296}
297
298impl Client for Http2ClientProtocol {
300 fn connect(
301 &self,
302 ctx: crate::protocol::ConnectContext,
303 ) -> std::pin::Pin<
304 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
305 > {
306 Box::pin(async move {
307 use crate::client::http2::Http2Client;
308 Http2Client::connect_with_llm_actions(
309 ctx.remote_addr,
310 ctx.llm_client,
311 ctx.state,
312 ctx.status_tx,
313 ctx.client_id,
314 ctx.startup_params,
315 )
316 .await
317 })
318 }
319 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
320 let action_type = action
321 .get("type")
322 .and_then(|v| v.as_str())
323 .context("Missing 'type' field in action")?;
324
325 match action_type {
326 "send_http2_request" => {
327 let method = action
328 .get("method")
329 .and_then(|v| v.as_str())
330 .context("Missing 'method' field")?
331 .to_string();
332
333 let path = action
334 .get("path")
335 .and_then(|v| v.as_str())
336 .context("Missing 'path' field")?
337 .to_string();
338
339 let headers = action.get("headers").and_then(|v| v.as_object()).cloned();
340
341 let body = action
342 .get("body")
343 .and_then(|v| v.as_str())
344 .map(|s| s.to_string());
345
346 Ok(ClientActionResult::Custom {
348 name: "http2_request".to_string(),
349 data: json!({
350 "method": method,
351 "path": path,
352 "headers": headers,
353 "body": body,
354 }),
355 })
356 }
357 "disconnect" => Ok(ClientActionResult::Disconnect),
358 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
360 _ => Err(anyhow::anyhow!(
361 "Unknown HTTP/2 client action: {}",
362 action_type
363 )),
364 }
365 }
366}