Skip to main content

netget/client/http2/
actions.rs

1//! HTTP/2 client protocol actions implementation
2
3use 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
14/// HTTP/2 client connected event
15pub 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
33/// HTTP/2 client response received event
34pub 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/// HTTP/2 client protocol action handler
74#[derive(Default)]
75pub struct Http2ClientProtocol;
76
77impl Http2ClientProtocol {
78    pub fn new() -> Self {
79        Self::default()
80    }
81}
82
83// Implement Protocol trait (common functionality)
84impl 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            // LLM mode: LLM handles HTTP/2 client
256            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            // Script mode: Code-based HTTP/2 handling
263            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            // Static mode: Fixed HTTP/2 request
277            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
298// Implement Client trait (client-specific functionality)
299impl 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                // Return custom result with request data
347                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            // Declared above, so it must be executable here too.
359            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
360            _ => Err(anyhow::anyhow!(
361                "Unknown HTTP/2 client action: {}",
362                action_type
363            )),
364        }
365    }
366}