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 SQS_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "sqs_connected",
18 "SQS client initialized and ready to interact with queue",
19 json!({
20 "type": "send_message",
21 "message_body": "Processed successfully"
22 }),
23 )
24 .with_parameters(vec![Parameter {
25 name: "queue_url".to_string(),
26 type_hint: "string".to_string(),
27 description: "SQS queue URL".to_string(),
28 required: true,
29 }])
30});
31
32pub static SQS_MESSAGE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
34 EventType::new(
35 "sqs_message_received",
36 "Messages received from SQS queue. An empty `messages` array means the poll returned \
37 nothing, which is the normal case for short polling — answer with `receive_messages` \
38 to keep waiting, or stop.",
39 json!({"type": "delete_message", "receipt_handle": "AQEBw...=="}),
42 )
43 .with_parameters(vec![Parameter {
44 name: "messages".to_string(),
45 type_hint: "array".to_string(),
46 description: "Array of received messages".to_string(),
47 required: true,
48 }])
49});
50
51pub static SQS_MESSAGE_SENT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
53 EventType::new(
54 "sqs_message_sent",
55 "Message successfully sent to SQS queue",
56 json!({"type": "receive_messages", "max_messages": 5}),
57 )
58 .with_parameters(vec![Parameter {
59 name: "message_id".to_string(),
60 type_hint: "string".to_string(),
61 description: "ID of the sent message".to_string(),
62 required: true,
63 }])
64});
65
66pub struct SqsClientProtocol;
68
69impl SqsClientProtocol {
70 pub fn new() -> Self {
71 Self
72 }
73}
74
75impl Protocol for SqsClientProtocol {
77 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
78 vec![
79 ParameterDefinition {
80 name: "queue_url".to_string(),
81 description: "SQS queue URL to interact with".to_string(),
82 type_hint: "string".to_string(),
83 required: true,
84 example: json!("https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue"),
85 default: None,
86 },
87 ParameterDefinition {
88 name: "region".to_string(),
89 description: "AWS region (e.g., us-east-1, eu-west-1)".to_string(),
90 type_hint: "string".to_string(),
91 required: false,
92 example: json!("us-east-1"),
93 default: None,
94 },
95 ParameterDefinition {
96 name: "endpoint_url".to_string(),
97 description: "Custom endpoint URL for local testing (e.g., http://localhost:9324)"
98 .to_string(),
99 type_hint: "string".to_string(),
100 required: false,
101 example: json!("http://localhost:9324"),
102 default: None,
103 },
104 ParameterDefinition {
109 name: "access_key_id".to_string(),
110 description: "AWS access key ID. When this and secret_access_key are given, \
111 they are used instead of the ambient AWS credential chain \
112 (environment, ~/.aws/credentials, instance role)."
113 .to_string(),
114 type_hint: "string".to_string(),
115 required: false,
116 example: json!("AKIAIOSFODNN7EXAMPLE"),
117 default: None,
118 },
119 ParameterDefinition {
120 name: "secret_access_key".to_string(),
121 description: "AWS secret access key, used with access_key_id.".to_string(),
122 type_hint: "string".to_string(),
123 required: false,
124 example: json!("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
125 default: None,
126 },
127 ]
128 }
129 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
130 vec![
131 ActionDefinition {
132 name: "send_message".to_string(),
133 description: "Send a message to the SQS queue".to_string(),
134 parameters: vec![
135 Parameter {
136 name: "message_body".to_string(),
137 type_hint: "string".to_string(),
138 description: "The message content to send".to_string(),
139 required: true,
140 },
141 Parameter {
142 name: "message_attributes".to_string(),
143 type_hint: "object".to_string(),
144 description: "Optional message attributes (key-value pairs)".to_string(),
145 required: false,
146 },
147 Parameter {
148 name: "delay_seconds".to_string(),
149 type_hint: "number".to_string(),
150 description: "Delay in seconds before message becomes available"
151 .to_string(),
152 required: false,
153 },
154 ],
155 example: json!({
156 "type": "send_message",
157 "message_body": "Hello from NetGet",
158 "message_attributes": {
159 "priority": "high",
160 "source": "netget"
161 }
162 }),
163 log_template: None,
164 },
165 ActionDefinition {
166 name: "receive_messages".to_string(),
167 description: "Receive messages from the SQS queue".to_string(),
168 parameters: vec![
169 Parameter {
170 name: "max_messages".to_string(),
171 type_hint: "number".to_string(),
172 description: "Maximum number of messages to receive (1-10)".to_string(),
173 required: false,
174 },
175 Parameter {
176 name: "wait_time_seconds".to_string(),
177 type_hint: "number".to_string(),
178 description: "Long polling wait time in seconds (0-20)".to_string(),
179 required: false,
180 },
181 Parameter {
182 name: "visibility_timeout".to_string(),
183 type_hint: "number".to_string(),
184 description: "Visibility timeout in seconds for received messages"
185 .to_string(),
186 required: false,
187 },
188 ],
189 example: json!({
190 "type": "receive_messages",
191 "max_messages": 5,
192 "wait_time_seconds": 10
193 }),
194 log_template: None,
195 },
196 ActionDefinition {
197 name: "delete_message".to_string(),
198 description: "Delete a message from the queue using its receipt handle".to_string(),
199 parameters: vec![Parameter {
200 name: "receipt_handle".to_string(),
201 type_hint: "string".to_string(),
202 description: "Receipt handle of the message to delete".to_string(),
203 required: true,
204 }],
205 example: json!({
206 "type": "delete_message",
207 "receipt_handle": "AQEBwJnKyrHigUMZj6rY..."
208 }),
209 log_template: None,
210 },
211 ActionDefinition {
212 name: "purge_queue".to_string(),
213 description: "Delete all messages in the queue".to_string(),
214 parameters: vec![],
215 example: json!({
216 "type": "purge_queue"
217 }),
218 log_template: None,
219 },
220 ActionDefinition {
221 name: "get_queue_attributes".to_string(),
222 description: "Get attributes of the queue (message count, etc.)".to_string(),
223 parameters: vec![Parameter {
224 name: "attribute_names".to_string(),
225 type_hint: "array".to_string(),
226 description: "List of attribute names to retrieve".to_string(),
227 required: false,
228 }],
229 example: json!({
230 "type": "get_queue_attributes",
231 "attribute_names": ["ApproximateNumberOfMessages", "QueueArn"]
232 }),
233 log_template: None,
234 },
235 ActionDefinition {
236 name: "disconnect".to_string(),
237 description: "Disconnect from the SQS queue".to_string(),
238 parameters: vec![],
239 example: json!({
240 "type": "disconnect"
241 }),
242 log_template: None,
243 },
244 ]
245 }
246 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
247 vec![
248 ActionDefinition {
249 name: "send_message".to_string(),
250 description: "Send a message to the queue in response to received messages"
251 .to_string(),
252 parameters: vec![Parameter {
253 name: "message_body".to_string(),
254 type_hint: "string".to_string(),
255 description: "The message content to send".to_string(),
256 required: true,
257 }],
258 example: json!({
259 "type": "send_message",
260 "message_body": "Processed successfully"
261 }),
262 log_template: None,
263 },
264 ActionDefinition {
265 name: "delete_message".to_string(),
266 description: "Delete a received message".to_string(),
267 parameters: vec![Parameter {
268 name: "receipt_handle".to_string(),
269 type_hint: "string".to_string(),
270 description: "Receipt handle from received message".to_string(),
271 required: true,
272 }],
273 example: json!({
274 "type": "delete_message",
275 "receipt_handle": "AQEBwJnKyrHigUMZj6rY..."
276 }),
277 log_template: None,
278 },
279 ]
280 }
281 fn protocol_name(&self) -> &'static str {
282 "SQS"
283 }
284 fn get_event_types(&self) -> Vec<EventType> {
291 vec![
292 SQS_CLIENT_CONNECTED_EVENT.clone(),
293 SQS_MESSAGE_RECEIVED_EVENT.clone(),
294 SQS_MESSAGE_SENT_EVENT.clone(),
295 ]
296 }
297 fn stack_name(&self) -> &'static str {
298 "ETH>IP>TCP>HTTP>SQS"
299 }
300 fn keywords(&self) -> Vec<&'static str> {
301 vec!["sqs", "sqs client", "connect to sqs", "aws sqs", "queue"]
302 }
303 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
304 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
305
306 ProtocolMetadataV2::builder()
307 .state(DevelopmentState::Experimental)
308 .implementation("AWS SDK for SQS with HTTP/JSON protocol")
309 .llm_control("Full control over queue operations")
310 .e2e_testing("LocalStack SQS container")
311 .build()
312 }
313 fn description(&self) -> &'static str {
314 "AWS SQS client for queue-based messaging"
315 }
316 fn example_prompt(&self) -> &'static str {
317 "Connect to SQS queue MyQueue and receive messages"
318 }
319 fn group_name(&self) -> &'static str {
320 "Cloud"
321 }
322 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
323 use crate::llm::actions::StartupExamples;
324 use serde_json::json;
325
326 StartupExamples::new(
329 json!({
331 "type": "open_client",
332 "remote_addr": "localhost:9324",
333 "base_stack": "sqs",
334 "startup_params": {
335 "queue_url": "http://localhost:9324/000000000000/MyQueue",
336 "endpoint_url": "http://localhost:9324",
337 "access_key_id": "local",
338 "secret_access_key": "local"
339 },
340 "instruction": "Send a test message and receive messages from the queue"
341 }),
342 json!({
344 "type": "open_client",
345 "remote_addr": "localhost:9324",
346 "base_stack": "sqs",
347 "startup_params": {
348 "queue_url": "http://localhost:9324/000000000000/MyQueue",
349 "endpoint_url": "http://localhost:9324",
350 "access_key_id": "local",
351 "secret_access_key": "local"
352 },
353 "event_handlers": [{
354 "event_pattern": "sqs_message_received",
355 "handler": {
356 "type": "script",
357 "language": "python",
358 "code": "<sqs_client_handler>"
359 }
360 }]
361 }),
362 json!({
364 "type": "open_client",
365 "remote_addr": "localhost:9324",
366 "base_stack": "sqs",
367 "startup_params": {
368 "queue_url": "http://localhost:9324/000000000000/MyQueue",
369 "endpoint_url": "http://localhost:9324",
370 "access_key_id": "local",
371 "secret_access_key": "local"
372 },
373 "event_handlers": [
374 {
375 "event_pattern": "sqs_connected",
376 "handler": {
377 "type": "static",
378 "actions": [{
379 "type": "send_message",
380 "message_body": "Test message from NetGet"
381 }]
382 }
383 },
384 {
385 "event_pattern": "sqs_message_sent",
386 "handler": {
387 "type": "static",
388 "actions": [{
389 "type": "disconnect"
390 }]
391 }
392 }
393 ]
394 }),
395 )
396 }
397}
398
399impl Client for SqsClientProtocol {
401 fn connect(
402 &self,
403 ctx: crate::protocol::ConnectContext,
404 ) -> std::pin::Pin<
405 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
406 > {
407 Box::pin(async move {
408 use crate::client::sqs::SqsClient;
409 SqsClient::connect_with_llm_actions(
410 ctx.remote_addr,
411 ctx.llm_client,
412 ctx.state,
413 ctx.status_tx,
414 ctx.client_id,
415 ctx.startup_params,
416 )
417 .await
418 })
419 }
420 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
421 let action_type = action
422 .get("type")
423 .and_then(|v| v.as_str())
424 .context("Missing 'type' field in action")?;
425
426 match action_type {
427 "send_message" => {
428 let message_body = action
429 .get("message_body")
430 .and_then(|v| v.as_str())
431 .context("Missing 'message_body' field")?
432 .to_string();
433
434 let message_attributes = action.get("message_attributes").cloned();
435
436 let delay_seconds = action.get("delay_seconds").and_then(|v| v.as_i64());
437
438 Ok(ClientActionResult::Custom {
439 name: "send_message".to_string(),
440 data: json!({
441 "message_body": message_body,
442 "message_attributes": message_attributes,
443 "delay_seconds": delay_seconds,
444 }),
445 })
446 }
447 "receive_messages" => {
448 let max_messages = action.get("max_messages").and_then(|v| v.as_i64());
449
450 let wait_time_seconds = action.get("wait_time_seconds").and_then(|v| v.as_i64());
451
452 let visibility_timeout = action.get("visibility_timeout").and_then(|v| v.as_i64());
453
454 Ok(ClientActionResult::Custom {
455 name: "receive_messages".to_string(),
456 data: json!({
457 "max_messages": max_messages,
458 "wait_time_seconds": wait_time_seconds,
459 "visibility_timeout": visibility_timeout,
460 }),
461 })
462 }
463 "delete_message" => {
464 let receipt_handle = action
465 .get("receipt_handle")
466 .and_then(|v| v.as_str())
467 .context("Missing 'receipt_handle' field")?
468 .to_string();
469
470 Ok(ClientActionResult::Custom {
471 name: "delete_message".to_string(),
472 data: json!({
473 "receipt_handle": receipt_handle,
474 }),
475 })
476 }
477 "purge_queue" => Ok(ClientActionResult::Custom {
478 name: "purge_queue".to_string(),
479 data: json!({}),
480 }),
481 "get_queue_attributes" => {
482 let attribute_names = action
483 .get("attribute_names")
484 .and_then(|v| v.as_array())
485 .map(|arr| {
486 arr.iter()
487 .filter_map(|v| v.as_str().map(|s| s.to_string()))
488 .collect::<Vec<_>>()
489 });
490
491 Ok(ClientActionResult::Custom {
492 name: "get_queue_attributes".to_string(),
493 data: json!({
494 "attribute_names": attribute_names,
495 }),
496 })
497 }
498 "disconnect" => Ok(ClientActionResult::Disconnect),
499 _ => Err(anyhow::anyhow!(
500 "Unknown SQS client action: {}",
501 action_type
502 )),
503 }
504 }
505}