signalwire-agents 0.1.0__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 +17 -0
- signalwire_agents/agent_server.py +336 -0
- signalwire_agents/core/__init__.py +20 -0
- signalwire_agents/core/agent_base.py +2449 -0
- signalwire_agents/core/function_result.py +104 -0
- signalwire_agents/core/pom_builder.py +195 -0
- signalwire_agents/core/security/__init__.py +0 -0
- signalwire_agents/core/security/session_manager.py +170 -0
- signalwire_agents/core/state/__init__.py +8 -0
- signalwire_agents/core/state/file_state_manager.py +210 -0
- signalwire_agents/core/state/state_manager.py +92 -0
- signalwire_agents/core/swaig_function.py +163 -0
- signalwire_agents/core/swml_builder.py +205 -0
- signalwire_agents/core/swml_handler.py +218 -0
- signalwire_agents/core/swml_renderer.py +359 -0
- signalwire_agents/core/swml_service.py +1009 -0
- signalwire_agents/prefabs/__init__.py +15 -0
- signalwire_agents/prefabs/concierge.py +276 -0
- signalwire_agents/prefabs/faq_bot.py +314 -0
- signalwire_agents/prefabs/info_gatherer.py +253 -0
- signalwire_agents/prefabs/survey.py +387 -0
- signalwire_agents/schema.json +5611 -0
- signalwire_agents/utils/__init__.py +0 -0
- signalwire_agents/utils/pom_utils.py +0 -0
- signalwire_agents/utils/schema_utils.py +348 -0
- signalwire_agents/utils/token_generators.py +0 -0
- signalwire_agents/utils/validators.py +0 -0
- signalwire_agents-0.1.0.data/data/schema.json +5611 -0
- signalwire_agents-0.1.0.dist-info/METADATA +154 -0
- signalwire_agents-0.1.0.dist-info/RECORD +32 -0
- signalwire_agents-0.1.0.dist-info/WHEEL +5 -0
- signalwire_agents-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
SignalWire AI Agents SDK
|
3
|
+
=======================
|
4
|
+
|
5
|
+
A package for building AI agents using SignalWire's AI and SWML capabilities.
|
6
|
+
"""
|
7
|
+
|
8
|
+
__version__ = "0.1.0"
|
9
|
+
|
10
|
+
# Import core classes for easier access
|
11
|
+
from signalwire_agents.core.agent_base import AgentBase
|
12
|
+
from signalwire_agents.agent_server import AgentServer
|
13
|
+
from signalwire_agents.core.swml_service import SWMLService
|
14
|
+
from signalwire_agents.core.swml_builder import SWMLBuilder
|
15
|
+
from signalwire_agents.core.state import StateManager, FileStateManager
|
16
|
+
|
17
|
+
__all__ = ["AgentBase", "AgentServer", "SWMLService", "SWMLBuilder", "StateManager", "FileStateManager"]
|
@@ -0,0 +1,336 @@
|
|
1
|
+
"""
|
2
|
+
AgentServer - Class for hosting multiple SignalWire AI Agents in a single server
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import re
|
7
|
+
from typing import Dict, Any, Optional, List, Tuple, Callable
|
8
|
+
|
9
|
+
try:
|
10
|
+
from fastapi import FastAPI, Request, Response
|
11
|
+
import uvicorn
|
12
|
+
except ImportError:
|
13
|
+
raise ImportError(
|
14
|
+
"fastapi and uvicorn are required. Install them with: pip install fastapi uvicorn"
|
15
|
+
)
|
16
|
+
|
17
|
+
from signalwire_agents.core.agent_base import AgentBase
|
18
|
+
from signalwire_agents.core.swml_service import SWMLService
|
19
|
+
|
20
|
+
|
21
|
+
class AgentServer:
|
22
|
+
"""
|
23
|
+
Server for hosting multiple SignalWire AI Agents under a single FastAPI application.
|
24
|
+
|
25
|
+
This allows you to run multiple agents on different routes of the same server,
|
26
|
+
which is useful for deployment and resource management.
|
27
|
+
|
28
|
+
Example:
|
29
|
+
server = AgentServer()
|
30
|
+
server.register(SupportAgent(), "/support")
|
31
|
+
server.register(SalesAgent(), "/sales")
|
32
|
+
server.run()
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, host: str = "0.0.0.0", port: int = 3000, log_level: str = "info"):
|
36
|
+
"""
|
37
|
+
Initialize a new agent server
|
38
|
+
|
39
|
+
Args:
|
40
|
+
host: Host to bind the server to
|
41
|
+
port: Port to bind the server to
|
42
|
+
log_level: Logging level (debug, info, warning, error)
|
43
|
+
"""
|
44
|
+
self.host = host
|
45
|
+
self.port = port
|
46
|
+
self.log_level = log_level.lower()
|
47
|
+
|
48
|
+
# Set up logging
|
49
|
+
numeric_level = getattr(logging, self.log_level.upper(), logging.INFO)
|
50
|
+
logging.basicConfig(
|
51
|
+
level=numeric_level,
|
52
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
53
|
+
)
|
54
|
+
self.logger = logging.getLogger("AgentServer")
|
55
|
+
|
56
|
+
# Create FastAPI app
|
57
|
+
self.app = FastAPI(
|
58
|
+
title="SignalWire AI Agents",
|
59
|
+
description="Hosted SignalWire AI Agents",
|
60
|
+
version="0.1.0"
|
61
|
+
)
|
62
|
+
|
63
|
+
# Keep track of registered agents
|
64
|
+
self.agents: Dict[str, AgentBase] = {}
|
65
|
+
|
66
|
+
# Keep track of SIP routing configuration
|
67
|
+
self._sip_routing_enabled = False
|
68
|
+
self._sip_route = None
|
69
|
+
self._sip_username_mapping: Dict[str, str] = {} # Maps SIP usernames to routes
|
70
|
+
|
71
|
+
def register(self, agent: AgentBase, route: Optional[str] = None) -> None:
|
72
|
+
"""
|
73
|
+
Register an agent with the server
|
74
|
+
|
75
|
+
Args:
|
76
|
+
agent: The agent to register
|
77
|
+
route: Optional route to override the agent's default route
|
78
|
+
|
79
|
+
Raises:
|
80
|
+
ValueError: If the route is already in use
|
81
|
+
"""
|
82
|
+
# Use agent's route if none provided
|
83
|
+
if route is None:
|
84
|
+
route = agent.route
|
85
|
+
|
86
|
+
# Normalize route format
|
87
|
+
if not route.startswith("/"):
|
88
|
+
route = f"/{route}"
|
89
|
+
|
90
|
+
route = route.rstrip("/")
|
91
|
+
|
92
|
+
# Check for conflicts
|
93
|
+
if route in self.agents:
|
94
|
+
raise ValueError(f"Route '{route}' is already in use")
|
95
|
+
|
96
|
+
# Store the agent
|
97
|
+
self.agents[route] = agent
|
98
|
+
|
99
|
+
# Get the router and register it
|
100
|
+
router = agent.as_router()
|
101
|
+
self.app.include_router(router, prefix=route)
|
102
|
+
|
103
|
+
self.logger.info(f"Registered agent '{agent.get_name()}' at route '{route}'")
|
104
|
+
|
105
|
+
# If SIP routing is enabled and auto-mapping is on, register SIP usernames for this agent
|
106
|
+
if hasattr(self, '_sip_auto_map') and self._sip_auto_map and self._sip_routing_enabled:
|
107
|
+
self._auto_map_agent_sip_usernames(agent, route)
|
108
|
+
|
109
|
+
def setup_sip_routing(self, route: str = "/sip", auto_map: bool = True) -> None:
|
110
|
+
"""
|
111
|
+
Set up central SIP-based routing for the server
|
112
|
+
|
113
|
+
This adds a special endpoint that can route SIP requests to the appropriate
|
114
|
+
agent based on the SIP username in the request.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
route: The route for SIP requests
|
118
|
+
auto_map: Whether to automatically map SIP usernames to agent routes
|
119
|
+
"""
|
120
|
+
if self._sip_routing_enabled:
|
121
|
+
self.logger.warning("SIP routing is already enabled")
|
122
|
+
return
|
123
|
+
|
124
|
+
# Normalize the route
|
125
|
+
if not route.startswith("/"):
|
126
|
+
route = f"/{route}"
|
127
|
+
|
128
|
+
route = route.rstrip("/")
|
129
|
+
|
130
|
+
# Store configuration
|
131
|
+
self._sip_routing_enabled = True
|
132
|
+
self._sip_route = route
|
133
|
+
self._sip_auto_map = auto_map
|
134
|
+
|
135
|
+
# If auto-mapping is enabled, map existing agents
|
136
|
+
if auto_map:
|
137
|
+
for agent_route, agent in self.agents.items():
|
138
|
+
self._auto_map_agent_sip_usernames(agent, agent_route)
|
139
|
+
|
140
|
+
# Register the SIP endpoint
|
141
|
+
@self.app.post(f"{route}")
|
142
|
+
@self.app.post(f"{route}/")
|
143
|
+
async def handle_sip_request(request: Request):
|
144
|
+
"""Handle SIP requests and route to the appropriate agent"""
|
145
|
+
self.logger.debug(f"Received request at SIP endpoint: {route}")
|
146
|
+
|
147
|
+
try:
|
148
|
+
# Extract the request body
|
149
|
+
body = await request.json()
|
150
|
+
|
151
|
+
# Extract the SIP username
|
152
|
+
sip_username = SWMLService.extract_sip_username(body)
|
153
|
+
|
154
|
+
if sip_username:
|
155
|
+
self.logger.info(f"Extracted SIP username: {sip_username}")
|
156
|
+
|
157
|
+
# Look up the route for this username
|
158
|
+
target_route = self._lookup_sip_route(sip_username)
|
159
|
+
|
160
|
+
if target_route:
|
161
|
+
self.logger.info(f"Routing SIP request to {target_route}")
|
162
|
+
|
163
|
+
# Create a redirect response to the target route
|
164
|
+
# Use 307 Temporary Redirect to preserve the POST method
|
165
|
+
response = Response(status_code=307)
|
166
|
+
response.headers["Location"] = target_route
|
167
|
+
return response
|
168
|
+
else:
|
169
|
+
self.logger.warning(f"No route found for SIP username: {sip_username}")
|
170
|
+
|
171
|
+
# If we get here, either no SIP username was found or no matching route exists
|
172
|
+
# Return a basic SWML response
|
173
|
+
return {"version": "1.0.0", "sections": {"main": []}}
|
174
|
+
|
175
|
+
except Exception as e:
|
176
|
+
self.logger.error(f"Error processing SIP request: {str(e)}")
|
177
|
+
return {"version": "1.0.0", "sections": {"main": []}}
|
178
|
+
|
179
|
+
self.logger.info(f"SIP routing enabled at {route}")
|
180
|
+
|
181
|
+
def register_sip_username(self, username: str, route: str) -> None:
|
182
|
+
"""
|
183
|
+
Register a mapping from SIP username to agent route
|
184
|
+
|
185
|
+
Args:
|
186
|
+
username: The SIP username
|
187
|
+
route: The route to the agent
|
188
|
+
"""
|
189
|
+
if not self._sip_routing_enabled:
|
190
|
+
self.logger.warning("SIP routing is not enabled. Call setup_sip_routing() first.")
|
191
|
+
return
|
192
|
+
|
193
|
+
# Normalize the route
|
194
|
+
if not route.startswith("/"):
|
195
|
+
route = f"/{route}"
|
196
|
+
|
197
|
+
route = route.rstrip("/")
|
198
|
+
|
199
|
+
# Check if the route exists
|
200
|
+
if route not in self.agents:
|
201
|
+
self.logger.warning(f"Route {route} not found. SIP username will be registered but may not work.")
|
202
|
+
|
203
|
+
# Add the mapping
|
204
|
+
self._sip_username_mapping[username.lower()] = route
|
205
|
+
self.logger.info(f"Registered SIP username '{username}' to route '{route}'")
|
206
|
+
|
207
|
+
def _lookup_sip_route(self, username: str) -> Optional[str]:
|
208
|
+
"""
|
209
|
+
Look up the route for a SIP username
|
210
|
+
|
211
|
+
Args:
|
212
|
+
username: The SIP username
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
The route or None if not found
|
216
|
+
"""
|
217
|
+
return self._sip_username_mapping.get(username.lower())
|
218
|
+
|
219
|
+
def _auto_map_agent_sip_usernames(self, agent: AgentBase, route: str) -> None:
|
220
|
+
"""
|
221
|
+
Automatically map SIP usernames for an agent
|
222
|
+
|
223
|
+
This creates mappings based on the agent name and route.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
agent: The agent to map
|
227
|
+
route: The route to the agent
|
228
|
+
"""
|
229
|
+
# Get the agent name and clean it for use as a SIP username
|
230
|
+
agent_name = agent.get_name().lower()
|
231
|
+
clean_name = re.sub(r'[^a-z0-9_]', '', agent_name)
|
232
|
+
|
233
|
+
if clean_name:
|
234
|
+
self.register_sip_username(clean_name, route)
|
235
|
+
|
236
|
+
# Also use the route path (without slashes) as a username
|
237
|
+
if route:
|
238
|
+
# Extract just the last part of the route
|
239
|
+
route_part = route.split("/")[-1]
|
240
|
+
clean_route = re.sub(r'[^a-z0-9_]', '', route_part)
|
241
|
+
|
242
|
+
if clean_route and clean_route != clean_name:
|
243
|
+
self.register_sip_username(clean_route, route)
|
244
|
+
|
245
|
+
def unregister(self, route: str) -> bool:
|
246
|
+
"""
|
247
|
+
Unregister an agent from the server
|
248
|
+
|
249
|
+
Args:
|
250
|
+
route: The route of the agent to unregister
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
True if the agent was unregistered, False if not found
|
254
|
+
"""
|
255
|
+
# Normalize route format
|
256
|
+
if not route.startswith("/"):
|
257
|
+
route = f"/{route}"
|
258
|
+
|
259
|
+
route = route.rstrip("/")
|
260
|
+
|
261
|
+
# Check if the agent exists
|
262
|
+
if route not in self.agents:
|
263
|
+
return False
|
264
|
+
|
265
|
+
# FastAPI doesn't support unregistering routes, so we'll just track it ourselves
|
266
|
+
# and rebuild the app if needed
|
267
|
+
del self.agents[route]
|
268
|
+
|
269
|
+
self.logger.info(f"Unregistered agent at route '{route}'")
|
270
|
+
return True
|
271
|
+
|
272
|
+
def get_agents(self) -> List[Tuple[str, AgentBase]]:
|
273
|
+
"""
|
274
|
+
Get all registered agents
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
List of (route, agent) tuples
|
278
|
+
"""
|
279
|
+
return [(route, agent) for route, agent in self.agents.items()]
|
280
|
+
|
281
|
+
def get_agent(self, route: str) -> Optional[AgentBase]:
|
282
|
+
"""
|
283
|
+
Get an agent by route
|
284
|
+
|
285
|
+
Args:
|
286
|
+
route: The route of the agent
|
287
|
+
|
288
|
+
Returns:
|
289
|
+
The agent or None if not found
|
290
|
+
"""
|
291
|
+
# Normalize route format
|
292
|
+
if not route.startswith("/"):
|
293
|
+
route = f"/{route}"
|
294
|
+
|
295
|
+
route = route.rstrip("/")
|
296
|
+
|
297
|
+
return self.agents.get(route)
|
298
|
+
|
299
|
+
def run(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
|
300
|
+
"""
|
301
|
+
Start the server
|
302
|
+
|
303
|
+
Args:
|
304
|
+
host: Optional host to override the default
|
305
|
+
port: Optional port to override the default
|
306
|
+
"""
|
307
|
+
if not self.agents:
|
308
|
+
self.logger.warning("Starting server with no registered agents")
|
309
|
+
|
310
|
+
# Add a health check endpoint
|
311
|
+
@self.app.get("/health")
|
312
|
+
def health_check():
|
313
|
+
return {
|
314
|
+
"status": "ok",
|
315
|
+
"agents": len(self.agents),
|
316
|
+
"routes": list(self.agents.keys())
|
317
|
+
}
|
318
|
+
|
319
|
+
# Print server info
|
320
|
+
host = host or self.host
|
321
|
+
port = port or self.port
|
322
|
+
|
323
|
+
self.logger.info(f"Starting server on {host}:{port}")
|
324
|
+
for route, agent in self.agents.items():
|
325
|
+
username, password = agent.get_basic_auth_credentials()
|
326
|
+
self.logger.info(f"Agent '{agent.get_name()}' available at:")
|
327
|
+
self.logger.info(f"URL: http://{host}:{port}{route}")
|
328
|
+
self.logger.info(f"Basic Auth: {username}:{password}")
|
329
|
+
|
330
|
+
# Start the server
|
331
|
+
uvicorn.run(
|
332
|
+
self.app,
|
333
|
+
host=host,
|
334
|
+
port=port,
|
335
|
+
log_level=self.log_level
|
336
|
+
)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""
|
2
|
+
Core components for SignalWire AI Agents
|
3
|
+
"""
|
4
|
+
|
5
|
+
from signalwire_agents.core.agent_base import AgentBase
|
6
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
7
|
+
from signalwire_agents.core.swaig_function import SwaigFunction
|
8
|
+
from signalwire_agents.core.swml_service import SWMLService
|
9
|
+
from signalwire_agents.core.swml_handler import SWMLVerbHandler, VerbHandlerRegistry
|
10
|
+
from signalwire_agents.core.swml_builder import SWMLBuilder
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
'AgentBase',
|
14
|
+
'SwaigFunctionResult',
|
15
|
+
'SwaigFunction',
|
16
|
+
'SWMLService',
|
17
|
+
'SWMLVerbHandler',
|
18
|
+
'VerbHandlerRegistry',
|
19
|
+
'SWMLBuilder'
|
20
|
+
]
|