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.
- signalwire_agents/__init__.py +10 -1
- signalwire_agents/agent_server.py +73 -44
- signalwire_agents/core/__init__.py +9 -0
- signalwire_agents/core/agent_base.py +125 -33
- signalwire_agents/core/function_result.py +31 -12
- signalwire_agents/core/pom_builder.py +9 -0
- signalwire_agents/core/security/__init__.py +9 -0
- signalwire_agents/core/security/session_manager.py +9 -0
- signalwire_agents/core/state/__init__.py +9 -0
- signalwire_agents/core/state/file_state_manager.py +9 -0
- signalwire_agents/core/state/state_manager.py +9 -0
- signalwire_agents/core/swaig_function.py +9 -0
- signalwire_agents/core/swml_builder.py +9 -0
- signalwire_agents/core/swml_handler.py +9 -0
- signalwire_agents/core/swml_renderer.py +9 -0
- signalwire_agents/core/swml_service.py +88 -40
- signalwire_agents/prefabs/__init__.py +12 -1
- signalwire_agents/prefabs/concierge.py +9 -18
- signalwire_agents/prefabs/faq_bot.py +9 -18
- signalwire_agents/prefabs/info_gatherer.py +193 -183
- signalwire_agents/prefabs/receptionist.py +294 -0
- signalwire_agents/prefabs/survey.py +9 -18
- signalwire_agents/utils/__init__.py +9 -0
- signalwire_agents/utils/pom_utils.py +9 -0
- signalwire_agents/utils/schema_utils.py +9 -0
- signalwire_agents/utils/token_generators.py +9 -0
- signalwire_agents/utils/validators.py +9 -0
- {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/METADATA +75 -30
- signalwire_agents-0.1.2.dist-info/RECORD +34 -0
- {signalwire_agents-0.1.0.dist-info → signalwire_agents-0.1.2.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.2.dist-info/licenses/LICENSE +21 -0
- signalwire_agents-0.1.0.dist-info/RECORD +0 -32
- {signalwire_agents-0.1.0.data → signalwire_agents-0.1.2.data}/data/schema.json +0 -0
- {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.
|
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
|
-
|
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
|
570
|
-
if self
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
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]]
|
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,
|
596
|
-
created that will handle
|
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
|
-
|
607
|
-
|
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
|
674
|
-
if self
|
675
|
-
|
676
|
-
|
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
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
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.
|
791
|
-
print(f"
|
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
|
|