signalwire-agents 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. signalwire_agents/__init__.py +10 -1
  2. signalwire_agents/agent_server.py +73 -44
  3. signalwire_agents/core/__init__.py +9 -0
  4. signalwire_agents/core/agent_base.py +125 -33
  5. signalwire_agents/core/function_result.py +31 -12
  6. signalwire_agents/core/pom_builder.py +9 -0
  7. signalwire_agents/core/security/__init__.py +9 -0
  8. signalwire_agents/core/security/session_manager.py +9 -0
  9. signalwire_agents/core/state/__init__.py +9 -0
  10. signalwire_agents/core/state/file_state_manager.py +9 -0
  11. signalwire_agents/core/state/state_manager.py +9 -0
  12. signalwire_agents/core/swaig_function.py +9 -0
  13. signalwire_agents/core/swml_builder.py +9 -0
  14. signalwire_agents/core/swml_handler.py +9 -0
  15. signalwire_agents/core/swml_renderer.py +9 -0
  16. signalwire_agents/core/swml_service.py +88 -40
  17. signalwire_agents/prefabs/__init__.py +12 -1
  18. signalwire_agents/prefabs/concierge.py +9 -18
  19. signalwire_agents/prefabs/faq_bot.py +9 -18
  20. signalwire_agents/prefabs/info_gatherer.py +193 -183
  21. signalwire_agents/prefabs/receptionist.py +294 -0
  22. signalwire_agents/prefabs/survey.py +9 -18
  23. signalwire_agents/utils/__init__.py +9 -0
  24. signalwire_agents/utils/pom_utils.py +9 -0
  25. signalwire_agents/utils/schema_utils.py +9 -0
  26. signalwire_agents/utils/token_generators.py +9 -0
  27. signalwire_agents/utils/validators.py +9 -0
  28. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/METADATA +75 -30
  29. signalwire_agents-0.1.2.dist-info/RECORD +34 -0
  30. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/WHEEL +1 -1
  31. signalwire_agents-0.1.2.dist-info/licenses/LICENSE +21 -0
  32. signalwire_agents-0.1.0.dist-info/RECORD +0 -32
  33. {signalwire_agents-0.1.0.data → signalwire_agents-0.1.2.data}/data/schema.json +0 -0
  34. {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,12 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
1
10
  """
2
11
  SWMLService - Base class for SWML document creation and serving
3
12
 
@@ -167,8 +176,8 @@ class SWMLService:
167
176
  # Create auto-vivified methods for all verbs
168
177
  self._create_verb_methods()
169
178
 
170
- # Initialize routing callback
171
- self._routing_callback = None
179
+ # Initialize routing callbacks dictionary (path -> callback)
180
+ self._routing_callbacks = {}
172
181
 
173
182
  def _create_verb_methods(self) -> None:
174
183
  """
@@ -352,8 +361,14 @@ class SWMLService:
352
361
  import importlib.resources
353
362
  try:
354
363
  # Python 3.9+
355
- with importlib.resources.files("signalwire_agents").joinpath("schema.json") as path:
364
+ try:
365
+ # Python 3.13+
366
+ path = importlib.resources.files("signalwire_agents").joinpath("schema.json")
356
367
  return str(path)
368
+ except Exception:
369
+ # Python 3.9-3.12
370
+ with importlib.resources.files("signalwire_agents").joinpath("schema.json") as path:
371
+ return str(path)
357
372
  except AttributeError:
358
373
  # Python 3.7-3.8
359
374
  with importlib.resources.path("signalwire_agents", "schema.json") as path:
@@ -566,34 +581,47 @@ class SWMLService:
566
581
  """Handle GET/POST requests to the root endpoint with trailing slash"""
567
582
  return await self._handle_request(request, response)
568
583
 
569
- # Add SIP endpoint if routing callback is configured
570
- if self._routing_callback is not None:
571
- # SIP endpoint - without trailing slash
572
- @router.get("/sip")
573
- @router.post("/sip")
574
- async def handle_sip_no_slash(request: Request, response: Response):
575
- """Handle GET/POST requests to the SIP endpoint"""
576
- return await self._handle_request(request, response)
577
-
578
- # SIP endpoint - with trailing slash
579
- @router.get("/sip/")
580
- @router.post("/sip/")
581
- async def handle_sip_with_slash(request: Request, response: Response):
582
- """Handle GET/POST requests to the SIP endpoint with trailing slash"""
583
- return await self._handle_request(request, response)
584
-
585
- self.log.info("sip_endpoint_registered", path="/sip")
584
+ # Add endpoints for all registered routing callbacks
585
+ if hasattr(self, '_routing_callbacks') and self._routing_callbacks:
586
+ for callback_path, callback_fn in self._routing_callbacks.items():
587
+ # Skip the root path as it's already handled
588
+ if callback_path == "/":
589
+ continue
590
+
591
+ # Register the endpoint without trailing slash
592
+ @router.get(callback_path)
593
+ @router.post(callback_path)
594
+ async def handle_callback_no_slash(request: Request, response: Response, cb_path=callback_path):
595
+ """Handle GET/POST requests to a registered callback path"""
596
+ # Store the callback path in request state for _handle_request to use
597
+ request.state.callback_path = cb_path
598
+ return await self._handle_request(request, response)
599
+
600
+ # Register the endpoint with trailing slash if it doesn't already have one
601
+ if not callback_path.endswith('/'):
602
+ slash_path = f"{callback_path}/"
603
+
604
+ @router.get(slash_path)
605
+ @router.post(slash_path)
606
+ async def handle_callback_with_slash(request: Request, response: Response, cb_path=callback_path):
607
+ """Handle GET/POST requests to a registered callback path with trailing slash"""
608
+ # Store the callback path in request state for _handle_request to use
609
+ request.state.callback_path = cb_path
610
+ return await self._handle_request(request, response)
611
+
612
+ self.log.info("callback_endpoint_registered", path=callback_path)
586
613
 
587
614
  self._router = router
588
615
  return router
589
616
 
590
- def register_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]]) -> None:
617
+ def register_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]],
618
+ path: str = "/sip") -> None:
591
619
  """
592
620
  Register a callback function that will be called to determine routing
593
621
  based on POST data.
594
622
 
595
- When a routing callback is registered, a global `/sip` endpoint is automatically
596
- created that will handle SIP requests. This endpoint will use the callback to
623
+ When a routing callback is registered, an endpoint at the specified path is automatically
624
+ created that will handle requests. This endpoint will use the callback to
597
625
  determine if the request should be processed by this service or redirected.
598
626
 
599
627
  The callback should take a request object and request body dictionary and return:
@@ -602,9 +630,15 @@ class SWMLService:
602
630
 
603
631
  Args:
604
632
  callback_fn: The callback function to register
633
+ path: The path where this callback should be registered (default: "/sip")
605
634
  """
606
- self.log.info("registering_routing_callback")
607
- self._routing_callback = callback_fn
635
+ # Normalize the path to ensure consistent lookup
636
+ normalized_path = path.rstrip("/")
637
+ if not normalized_path.startswith("/"):
638
+ normalized_path = f"/{normalized_path}"
639
+
640
+ self.log.info("registering_routing_callback", path=normalized_path)
641
+ self._routing_callbacks[normalized_path] = callback_fn
608
642
 
609
643
  @staticmethod
610
644
  def extract_sip_username(request_body: Dict[str, Any]) -> Optional[str]:
@@ -662,6 +696,9 @@ class SWMLService:
662
696
  response.headers["WWW-Authenticate"] = "Basic"
663
697
  return HTTPException(status_code=401, detail="Unauthorized")
664
698
 
699
+ # Get callback path from request state
700
+ callback_path = getattr(request.state, "callback_path", None)
701
+
665
702
  # Process request body if it's a POST
666
703
  body = {}
667
704
  if request.method == "POST":
@@ -670,25 +707,33 @@ class SWMLService:
670
707
  if raw_body:
671
708
  body = await request.json()
672
709
 
673
- # Check if we have a routing callback and should reroute the request
674
- if self._routing_callback is not None:
675
- self.log.debug("checking_routing", body_keys=list(body.keys()))
676
- route = self._routing_callback(request, body)
710
+ # Check if this is a callback path and we have a callback registered for it
711
+ if callback_path and hasattr(self, '_routing_callbacks') and callback_path in self._routing_callbacks:
712
+ callback_fn = self._routing_callbacks[callback_path]
713
+ self.log.debug("checking_routing",
714
+ path=callback_path,
715
+ body_keys=list(body.keys()))
677
716
 
678
- if route is not None:
679
- self.log.info("routing_request", route=route)
680
- # We should return a redirect to the new route
681
- # Use 307 to preserve the POST method and its body
682
- response = Response(status_code=307)
683
- response.headers["Location"] = route
684
- return response
717
+ # Call the callback function
718
+ try:
719
+ route = callback_fn(request, body)
720
+
721
+ if route is not None:
722
+ self.log.info("routing_request", route=route)
723
+ # We should return a redirect to the new route
724
+ # Use 307 to preserve the POST method and its body
725
+ response = Response(status_code=307)
726
+ response.headers["Location"] = route
727
+ return response
728
+ except Exception as e:
729
+ self.log.error("error_in_routing_callback", error=str(e))
685
730
  except Exception as e:
686
731
  self.log.error("error_parsing_body", error=str(e))
687
732
  # Continue with empty body if parsing fails
688
733
  pass
689
734
 
690
735
  # Allow for customized handling in subclasses
691
- modifications = self.on_request(body)
736
+ modifications = self.on_request(body, callback_path)
692
737
 
693
738
  # Apply any modifications if needed
694
739
  if modifications and isinstance(modifications, dict):
@@ -711,7 +756,7 @@ class SWMLService:
711
756
  # Return the SWML document
712
757
  return Response(content=swml, media_type="application/json")
713
758
 
714
- def on_request(self, request_data: Optional[dict] = None) -> Optional[dict]:
759
+ def on_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
715
760
  """
716
761
  Called when SWML is requested, with request data when available
717
762
 
@@ -719,6 +764,7 @@ class SWMLService:
719
764
 
720
765
  Args:
721
766
  request_data: Optional dictionary containing the parsed POST body
767
+ callback_path: Optional callback path
722
768
 
723
769
  Returns:
724
770
  Optional dict to modify/augment the SWML document
@@ -787,8 +833,10 @@ class SWMLService:
787
833
  print(f"Basic Auth: {username}:{password}")
788
834
 
789
835
  # Check if SIP routing is enabled and print additional info
790
- if self._routing_callback is not None:
791
- print(f"SIP endpoint: {protocol}://{display_host}/sip")
836
+ if self._routing_callbacks:
837
+ print(f"Callback endpoints:")
838
+ for path in self._routing_callbacks:
839
+ print(f"{protocol}://{display_host}{path}")
792
840
 
793
841
  # Start uvicorn with or without SSL
794
842
  if self.ssl_enabled and ssl_cert_path and ssl_key_path:
@@ -1,3 +1,12 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
1
10
  """
2
11
  Prefab agents with specific functionality that can be used out-of-the-box
3
12
  """
@@ -6,10 +15,12 @@ from signalwire_agents.prefabs.info_gatherer import InfoGathererAgent
6
15
  from signalwire_agents.prefabs.faq_bot import FAQBotAgent
7
16
  from signalwire_agents.prefabs.concierge import ConciergeAgent
8
17
  from signalwire_agents.prefabs.survey import SurveyAgent
18
+ from signalwire_agents.prefabs.receptionist import ReceptionistAgent
9
19
 
10
20
  __all__ = [
11
21
  "InfoGathererAgent",
12
22
  "FAQBotAgent",
13
23
  "ConciergeAgent",
14
- "SurveyAgent"
24
+ "SurveyAgent",
25
+ "ReceptionistAgent"
15
26
  ]
@@ -1,3 +1,12 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
1
10
  """
2
11
  ConciergeAgent - Prefab agent for providing virtual concierge services
3
12
  """
@@ -41,7 +50,6 @@ class ConciergeAgent(AgentBase):
41
50
  hours_of_operation: Optional[Dict[str, str]] = None,
42
51
  special_instructions: Optional[List[str]] = None,
43
52
  welcome_message: Optional[str] = None,
44
- schema_path: Optional[str] = None,
45
53
  **kwargs
46
54
  ):
47
55
  """
@@ -54,30 +62,13 @@ class ConciergeAgent(AgentBase):
54
62
  hours_of_operation: Optional dictionary of operating hours
55
63
  special_instructions: Optional list of special instructions
56
64
  welcome_message: Optional custom welcome message
57
- schema_path: Optional path to a custom schema
58
65
  **kwargs: Additional arguments for AgentBase
59
66
  """
60
- # Find schema.json if not provided
61
- if not schema_path:
62
- current_dir = os.path.dirname(os.path.abspath(__file__))
63
- parent_dir = os.path.dirname(os.path.dirname(current_dir))
64
-
65
- schema_locations = [
66
- os.path.join(current_dir, "schema.json"),
67
- os.path.join(parent_dir, "schema.json")
68
- ]
69
-
70
- for loc in schema_locations:
71
- if os.path.exists(loc):
72
- schema_path = loc
73
- break
74
-
75
67
  # Initialize the base agent
76
68
  super().__init__(
77
69
  name="concierge",
78
70
  route="/concierge",
79
71
  use_pom=True,
80
- schema_path=schema_path,
81
72
  **kwargs
82
73
  )
83
74
 
@@ -1,3 +1,12 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
1
10
  """
2
11
  FAQBotAgent - Prefab agent for answering frequently asked questions
3
12
  """
@@ -42,7 +51,6 @@ class FAQBotAgent(AgentBase):
42
51
  persona: Optional[str] = None,
43
52
  name: str = "faq_bot",
44
53
  route: str = "/faq",
45
- schema_path: Optional[str] = None,
46
54
  **kwargs
47
55
  ):
48
56
  """
@@ -57,30 +65,13 @@ class FAQBotAgent(AgentBase):
57
65
  persona: Optional custom personality description
58
66
  name: Agent name for the route
59
67
  route: HTTP route for this agent
60
- schema_path: Optional path to a custom schema
61
68
  **kwargs: Additional arguments for AgentBase
62
69
  """
63
- # Find schema.json if not provided
64
- if not schema_path:
65
- current_dir = os.path.dirname(os.path.abspath(__file__))
66
- parent_dir = os.path.dirname(os.path.dirname(current_dir))
67
-
68
- schema_locations = [
69
- os.path.join(current_dir, "schema.json"),
70
- os.path.join(parent_dir, "schema.json")
71
- ]
72
-
73
- for loc in schema_locations:
74
- if os.path.exists(loc):
75
- schema_path = loc
76
- break
77
-
78
70
  # Initialize the base agent
79
71
  super().__init__(
80
72
  name=name,
81
73
  route=route,
82
74
  use_pom=True,
83
- schema_path=schema_path,
84
75
  **kwargs
85
76
  )
86
77