Skip to main content

netget/client/sip/
actions.rs

1//! SIP 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::{ConnectContext, EventType};
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::future::Future;
13use std::net::SocketAddr;
14use std::pin::Pin;
15use std::sync::LazyLock;
16
17pub struct SipClientProtocol;
18
19impl SipClientProtocol {
20    pub fn new() -> Self {
21        Self
22    }
23}
24
25// Implement Protocol trait (common functionality)
26impl Protocol for SipClientProtocol {
27    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
28        vec![
29            // User-triggered actions
30            sip_register_action(),
31            sip_invite_action(),
32            sip_ack_action(),
33            sip_bye_action(),
34            sip_options_action(),
35            sip_cancel_action(),
36        ]
37    }
38    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
39        vec![
40            // Response actions
41            disconnect_action(),
42            wait_for_more_action(),
43        ]
44    }
45    fn get_event_types(&self) -> Vec<EventType> {
46        vec![
47            SIP_CLIENT_CONNECTED_EVENT.clone(),
48            SIP_CLIENT_RESPONSE_RECEIVED_EVENT.clone(),
49        ]
50    }
51    fn protocol_name(&self) -> &'static str {
52        "SIP"
53    }
54    fn stack_name(&self) -> &'static str {
55        "ETH>IP>UDP>SIP"
56    }
57    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
58        vec![]
59    }
60    fn keywords(&self) -> Vec<&'static str> {
61        vec!["sip", "voip", "session initiation"]
62    }
63    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
64        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
65
66        ProtocolMetadataV2::builder()
67            .state(DevelopmentState::Experimental)
68            .implementation("Manual SIP client - RFC 3261 compliant request generation")
69            .llm_control("REGISTER, INVITE, BYE, OPTIONS, CANCEL methods")
70            .e2e_testing("Self-testing against NetGet SIP server - < 10 LLM calls")
71            .notes("VoIP signaling, no RTP media streams")
72            .build()
73    }
74    fn description(&self) -> &'static str {
75        "SIP client for VoIP signaling"
76    }
77    fn example_prompt(&self) -> &'static str {
78        "Connect to 192.168.1.100:5060 via SIP and register as alice@example.com"
79    }
80    fn group_name(&self) -> &'static str {
81        "VoIP & Multimedia"
82    }
83    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
84        use crate::llm::actions::StartupExamples;
85        use serde_json::json;
86
87        StartupExamples::new(
88            // LLM mode: LLM controls SIP registration and call flow
89            json!({
90                "type": "open_client",
91                "remote_addr": "192.168.1.100:5060",
92                "base_stack": "sip",
93                "instruction": "Register as alice@example.com and query server capabilities with OPTIONS"
94            }),
95            // Script mode: Code-based SIP message handling
96            json!({
97                "type": "open_client",
98                "remote_addr": "192.168.1.100:5060",
99                "base_stack": "sip",
100                "event_handlers": [{
101                    "event_pattern": "sip_client_response_received",
102                    "handler": {
103                        "type": "script",
104                        "language": "python",
105                        "code": "<sip_client_handler>"
106                    }
107                }]
108            }),
109            // Static mode: Fixed SIP registration flow
110            json!({
111                "type": "open_client",
112                "remote_addr": "192.168.1.100:5060",
113                "base_stack": "sip",
114                "event_handlers": [
115                    {
116                        "event_pattern": "sip_client_connected",
117                        "handler": {
118                            "type": "static",
119                            "actions": [{
120                                "type": "sip_register",
121                                "from": "sip:alice@example.com",
122                                "to": "sip:alice@example.com",
123                                "request_uri": "sip:example.com",
124                                "contact": "sip:alice@192.0.2.1:5060",
125                                "expires": 3600
126                            }]
127                        }
128                    },
129                    {
130                        "event_pattern": "sip_client_response_received",
131                        "handler": {
132                            "type": "static",
133                            "actions": [{
134                                "type": "disconnect"
135                            }]
136                        }
137                    }
138                ]
139            }),
140        )
141    }
142}
143
144// Implement Client trait (client-specific functionality)
145impl Client for SipClientProtocol {
146    fn connect(
147        &self,
148        ctx: ConnectContext,
149    ) -> Pin<Box<dyn Future<Output = Result<SocketAddr>> + Send>> {
150        Box::pin(async move {
151            crate::client::sip::SipClient::connect_with_llm_actions(
152                ctx.remote_addr,
153                ctx.llm_client,
154                ctx.state,
155                ctx.status_tx,
156                ctx.client_id,
157            )
158            .await
159        })
160    }
161    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
162        let action_type = action
163            .get("type")
164            .and_then(|v| v.as_str())
165            .context("Missing 'type' field in action")?;
166
167        match action_type {
168            "sip_register" => {
169                let from = action["from"]
170                    .as_str()
171                    .context("Missing 'from' field")?
172                    .to_string();
173                let to = action["to"]
174                    .as_str()
175                    .context("Missing 'to' field")?
176                    .to_string();
177                let request_uri = action["request_uri"]
178                    .as_str()
179                    .context("Missing 'request_uri' field")?
180                    .to_string();
181                let contact = action["contact"]
182                    .as_str()
183                    .context("Missing 'contact' field")?
184                    .to_string();
185                let expires = action["expires"].as_u64().unwrap_or(3600);
186
187                Ok(ClientActionResult::Custom {
188                    name: "sip_register".to_string(),
189                    data: json!({
190                        "from": from,
191                        "to": to,
192                        "request_uri": request_uri,
193                        "contact": contact,
194                        "expires": expires,
195                    }),
196                })
197            }
198            "sip_invite" => {
199                let from = action["from"]
200                    .as_str()
201                    .context("Missing 'from' field")?
202                    .to_string();
203                let to = action["to"]
204                    .as_str()
205                    .context("Missing 'to' field")?
206                    .to_string();
207                let request_uri = action["request_uri"]
208                    .as_str()
209                    .context("Missing 'request_uri' field")?
210                    .to_string();
211                let sdp = action["sdp"]
212                    .as_str()
213                    .context("Missing 'sdp' field")?
214                    .to_string();
215                let contact = action["contact"].as_str().unwrap_or("sip:user@127.0.0.1");
216
217                Ok(ClientActionResult::Custom {
218                    name: "sip_invite".to_string(),
219                    data: json!({
220                        "from": from,
221                        "to": to,
222                        "request_uri": request_uri,
223                        "contact": contact,
224                        "sdp": sdp,
225                    }),
226                })
227            }
228            "sip_bye" => {
229                let from = action["from"]
230                    .as_str()
231                    .context("Missing 'from' field")?
232                    .to_string();
233                let to = action["to"]
234                    .as_str()
235                    .context("Missing 'to' field")?
236                    .to_string();
237                let request_uri = action["request_uri"]
238                    .as_str()
239                    .context("Missing 'request_uri' field")?
240                    .to_string();
241
242                Ok(ClientActionResult::Custom {
243                    name: "sip_bye".to_string(),
244                    data: json!({
245                        "from": from,
246                        "to": to,
247                        "request_uri": request_uri,
248                    }),
249                })
250            }
251            "sip_options" => {
252                let from = action["from"]
253                    .as_str()
254                    .context("Missing 'from' field")?
255                    .to_string();
256                let to = action["to"]
257                    .as_str()
258                    .context("Missing 'to' field")?
259                    .to_string();
260                let request_uri = action["request_uri"]
261                    .as_str()
262                    .context("Missing 'request_uri' field")?
263                    .to_string();
264
265                Ok(ClientActionResult::Custom {
266                    name: "sip_options".to_string(),
267                    data: json!({
268                        "from": from,
269                        "to": to,
270                        "request_uri": request_uri,
271                    }),
272                })
273            }
274            "sip_ack" => {
275                let from = action["from"]
276                    .as_str()
277                    .context("Missing 'from' field")?
278                    .to_string();
279                let to = action["to"]
280                    .as_str()
281                    .context("Missing 'to' field")?
282                    .to_string();
283                let request_uri = action["request_uri"]
284                    .as_str()
285                    .context("Missing 'request_uri' field")?
286                    .to_string();
287
288                Ok(ClientActionResult::Custom {
289                    name: "sip_ack".to_string(),
290                    data: json!({
291                        "from": from,
292                        "to": to,
293                        "request_uri": request_uri,
294                    }),
295                })
296            }
297            "sip_cancel" => {
298                let from = action["from"]
299                    .as_str()
300                    .context("Missing 'from' field")?
301                    .to_string();
302                let to = action["to"]
303                    .as_str()
304                    .context("Missing 'to' field")?
305                    .to_string();
306                let request_uri = action["request_uri"]
307                    .as_str()
308                    .context("Missing 'request_uri' field")?
309                    .to_string();
310
311                Ok(ClientActionResult::Custom {
312                    name: "sip_cancel".to_string(),
313                    data: json!({
314                        "from": from,
315                        "to": to,
316                        "request_uri": request_uri,
317                    }),
318                })
319            }
320            "disconnect" => Ok(ClientActionResult::Disconnect),
321            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
322            _ => Err(anyhow::anyhow!(
323                "Unknown SIP client action: {}",
324                action_type
325            )),
326        }
327    }
328}
329
330// Event type constants
331pub static SIP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
332    EventType::new(
333        "sip_client_connected",
334        "SIP client connected to server",
335        json!({"type": "placeholder", "event_id": "sip_client_connected"}),
336    )
337    .with_parameters(vec![
338        Parameter {
339            name: "remote_addr".to_string(),
340            type_hint: "string".to_string(),
341            description: "Remote SIP server address".to_string(),
342            required: true,
343        },
344        Parameter {
345            name: "local_addr".to_string(),
346            type_hint: "string".to_string(),
347            description: "Local client address".to_string(),
348            required: true,
349        },
350    ])
351});
352
353pub static SIP_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
354    EventType::new(
355        "sip_client_response_received",
356        "SIP response received from server",
357        json!({
358            "type": "disconnect"
359        }),
360    )
361    .with_parameters(vec![
362        Parameter {
363            name: "status_code".to_string(),
364            type_hint: "number".to_string(),
365            description: "SIP status code (200=OK, 403=Forbidden, etc.)".to_string(),
366            required: true,
367        },
368        Parameter {
369            name: "reason_phrase".to_string(),
370            type_hint: "string".to_string(),
371            description: "Response reason phrase".to_string(),
372            required: true,
373        },
374        Parameter {
375            name: "method".to_string(),
376            type_hint: "string".to_string(),
377            description: "SIP method from CSeq (REGISTER, INVITE, etc.)".to_string(),
378            required: true,
379        },
380        Parameter {
381            name: "call_id".to_string(),
382            type_hint: "string".to_string(),
383            description: "SIP Call-ID header".to_string(),
384            required: true,
385        },
386        Parameter {
387            name: "from".to_string(),
388            type_hint: "string".to_string(),
389            description: "SIP From header".to_string(),
390            required: true,
391        },
392        Parameter {
393            name: "to".to_string(),
394            type_hint: "string".to_string(),
395            description: "SIP To header".to_string(),
396            required: true,
397        },
398        Parameter {
399            name: "body".to_string(),
400            type_hint: "string".to_string(),
401            description: "Response body (SDP if present)".to_string(),
402            required: false,
403        },
404    ])
405});
406
407// Action definitions
408fn sip_register_action() -> ActionDefinition {
409    ActionDefinition {
410        name: "sip_register".to_string(),
411        description: "Send SIP REGISTER request to register with server".to_string(),
412        parameters: vec![
413            Parameter {
414                name: "from".to_string(),
415                type_hint: "string".to_string(),
416                description: "From SIP URI (e.g., sip:alice@example.com)".to_string(),
417                required: true,
418            },
419            Parameter {
420                name: "to".to_string(),
421                type_hint: "string".to_string(),
422                description: "To SIP URI (e.g., sip:alice@example.com)".to_string(),
423                required: true,
424            },
425            Parameter {
426                name: "request_uri".to_string(),
427                type_hint: "string".to_string(),
428                description: "Request URI (e.g., sip:example.com)".to_string(),
429                required: true,
430            },
431            Parameter {
432                name: "contact".to_string(),
433                type_hint: "string".to_string(),
434                description: "Contact URI with IP:port (e.g., sip:alice@192.0.2.1:5060)"
435                    .to_string(),
436                required: true,
437            },
438            Parameter {
439                name: "expires".to_string(),
440                type_hint: "number".to_string(),
441                description: "Registration expiration in seconds (default 3600)".to_string(),
442                required: false,
443            },
444        ],
445        example: json!({
446            "type": "sip_register",
447            "from": "sip:alice@example.com",
448            "to": "sip:alice@example.com",
449            "request_uri": "sip:example.com",
450            "contact": "sip:alice@192.0.2.1:5060",
451            "expires": 3600
452        }),
453        log_template: None,
454    }
455}
456
457fn sip_invite_action() -> ActionDefinition {
458    ActionDefinition {
459        name: "sip_invite".to_string(),
460        description: "Send SIP INVITE request to initiate a call".to_string(),
461        parameters: vec![
462            Parameter {
463                name: "from".to_string(),
464                type_hint: "string".to_string(),
465                description: "Caller SIP URI".to_string(),
466                required: true,
467            },
468            Parameter {
469                name: "to".to_string(),
470                type_hint: "string".to_string(),
471                description: "Callee SIP URI".to_string(),
472                required: true,
473            },
474            Parameter {
475                name: "request_uri".to_string(),
476                type_hint: "string".to_string(),
477                description: "Request URI (usually same as 'to')".to_string(),
478                required: true,
479            },
480            Parameter {
481                name: "contact".to_string(),
482                type_hint: "string".to_string(),
483                description: "Contact URI with IP:port".to_string(),
484                required: false,
485            },
486            Parameter {
487                name: "sdp".to_string(),
488                type_hint: "string".to_string(),
489                description: "Session Description Protocol body (media offer)".to_string(),
490                required: true,
491            },
492        ],
493        example: json!({
494            "type": "sip_invite",
495            "from": "sip:alice@example.com",
496            "to": "sip:bob@example.com",
497            "request_uri": "sip:bob@example.com",
498            "sdp": "v=0\no=alice 2890844526 2890844526 IN IP4 192.0.2.1\ns=Call\nc=IN IP4 192.0.2.1\nt=0 0\nm=audio 49170 RTP/AVP 0\na=rtpmap:0 PCMU/8000\n"
499        }),
500        log_template: None,
501    }
502}
503
504fn sip_ack_action() -> ActionDefinition {
505    ActionDefinition {
506        name: "sip_ack".to_string(),
507        description: "Send SIP ACK request to acknowledge INVITE 200 OK (usually automatic)"
508            .to_string(),
509        parameters: vec![
510            Parameter {
511                name: "from".to_string(),
512                type_hint: "string".to_string(),
513                description: "Caller SIP URI".to_string(),
514                required: true,
515            },
516            Parameter {
517                name: "to".to_string(),
518                type_hint: "string".to_string(),
519                description: "Callee SIP URI".to_string(),
520                required: true,
521            },
522            Parameter {
523                name: "request_uri".to_string(),
524                type_hint: "string".to_string(),
525                description: "Request URI (usually same as 'to')".to_string(),
526                required: true,
527            },
528        ],
529        example: json!({
530            "type": "sip_ack",
531            "from": "sip:alice@example.com",
532            "to": "sip:bob@example.com",
533            "request_uri": "sip:bob@example.com"
534        }),
535        log_template: None,
536    }
537}
538
539fn sip_bye_action() -> ActionDefinition {
540    ActionDefinition {
541        name: "sip_bye".to_string(),
542        description: "Send SIP BYE request to terminate a call".to_string(),
543        parameters: vec![
544            Parameter {
545                name: "from".to_string(),
546                type_hint: "string".to_string(),
547                description: "Caller SIP URI".to_string(),
548                required: true,
549            },
550            Parameter {
551                name: "to".to_string(),
552                type_hint: "string".to_string(),
553                description: "Callee SIP URI".to_string(),
554                required: true,
555            },
556            Parameter {
557                name: "request_uri".to_string(),
558                type_hint: "string".to_string(),
559                description: "Request URI (usually same as 'to')".to_string(),
560                required: true,
561            },
562        ],
563        example: json!({
564            "type": "sip_bye",
565            "from": "sip:alice@example.com",
566            "to": "sip:bob@example.com",
567            "request_uri": "sip:bob@example.com"
568        }),
569        log_template: None,
570    }
571}
572
573fn sip_options_action() -> ActionDefinition {
574    ActionDefinition {
575        name: "sip_options".to_string(),
576        description: "Send SIP OPTIONS request to query server capabilities".to_string(),
577        parameters: vec![
578            Parameter {
579                name: "from".to_string(),
580                type_hint: "string".to_string(),
581                description: "Caller SIP URI".to_string(),
582                required: true,
583            },
584            Parameter {
585                name: "to".to_string(),
586                type_hint: "string".to_string(),
587                description: "Target SIP URI".to_string(),
588                required: true,
589            },
590            Parameter {
591                name: "request_uri".to_string(),
592                type_hint: "string".to_string(),
593                description: "Request URI (usually same as 'to')".to_string(),
594                required: true,
595            },
596        ],
597        example: json!({
598            "type": "sip_options",
599            "from": "sip:alice@example.com",
600            "to": "sip:bob@example.com",
601            "request_uri": "sip:bob@example.com"
602        }),
603        log_template: None,
604    }
605}
606
607fn sip_cancel_action() -> ActionDefinition {
608    ActionDefinition {
609        name: "sip_cancel".to_string(),
610        description: "Send SIP CANCEL request to cancel a pending INVITE".to_string(),
611        parameters: vec![
612            Parameter {
613                name: "from".to_string(),
614                type_hint: "string".to_string(),
615                description: "Caller SIP URI".to_string(),
616                required: true,
617            },
618            Parameter {
619                name: "to".to_string(),
620                type_hint: "string".to_string(),
621                description: "Callee SIP URI".to_string(),
622                required: true,
623            },
624            Parameter {
625                name: "request_uri".to_string(),
626                type_hint: "string".to_string(),
627                description: "Request URI (usually same as 'to')".to_string(),
628                required: true,
629            },
630        ],
631        example: json!({
632            "type": "sip_cancel",
633            "from": "sip:alice@example.com",
634            "to": "sip:bob@example.com",
635            "request_uri": "sip:bob@example.com"
636        }),
637        log_template: None,
638    }
639}
640
641fn disconnect_action() -> ActionDefinition {
642    ActionDefinition {
643        name: "disconnect".to_string(),
644        description: "Close SIP client connection".to_string(),
645        parameters: vec![],
646        example: json!({
647            "type": "disconnect"
648        }),
649        log_template: None,
650    }
651}
652
653fn wait_for_more_action() -> ActionDefinition {
654    ActionDefinition {
655        name: "wait_for_more".to_string(),
656        description: "Wait for more SIP responses before taking action".to_string(),
657        parameters: vec![],
658        example: json!({
659            "type": "wait_for_more"
660        }),
661        log_template: None,
662    }
663}