1use 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
25impl Protocol for SipClientProtocol {
27 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
28 vec![
29 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 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 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 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 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
144impl 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
330pub 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
407fn 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}