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.
Files changed (32) hide show
  1. signalwire_agents/__init__.py +17 -0
  2. signalwire_agents/agent_server.py +336 -0
  3. signalwire_agents/core/__init__.py +20 -0
  4. signalwire_agents/core/agent_base.py +2449 -0
  5. signalwire_agents/core/function_result.py +104 -0
  6. signalwire_agents/core/pom_builder.py +195 -0
  7. signalwire_agents/core/security/__init__.py +0 -0
  8. signalwire_agents/core/security/session_manager.py +170 -0
  9. signalwire_agents/core/state/__init__.py +8 -0
  10. signalwire_agents/core/state/file_state_manager.py +210 -0
  11. signalwire_agents/core/state/state_manager.py +92 -0
  12. signalwire_agents/core/swaig_function.py +163 -0
  13. signalwire_agents/core/swml_builder.py +205 -0
  14. signalwire_agents/core/swml_handler.py +218 -0
  15. signalwire_agents/core/swml_renderer.py +359 -0
  16. signalwire_agents/core/swml_service.py +1009 -0
  17. signalwire_agents/prefabs/__init__.py +15 -0
  18. signalwire_agents/prefabs/concierge.py +276 -0
  19. signalwire_agents/prefabs/faq_bot.py +314 -0
  20. signalwire_agents/prefabs/info_gatherer.py +253 -0
  21. signalwire_agents/prefabs/survey.py +387 -0
  22. signalwire_agents/schema.json +5611 -0
  23. signalwire_agents/utils/__init__.py +0 -0
  24. signalwire_agents/utils/pom_utils.py +0 -0
  25. signalwire_agents/utils/schema_utils.py +348 -0
  26. signalwire_agents/utils/token_generators.py +0 -0
  27. signalwire_agents/utils/validators.py +0 -0
  28. signalwire_agents-0.1.0.data/data/schema.json +5611 -0
  29. signalwire_agents-0.1.0.dist-info/METADATA +154 -0
  30. signalwire_agents-0.1.0.dist-info/RECORD +32 -0
  31. signalwire_agents-0.1.0.dist-info/WHEEL +5 -0
  32. 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
+ ]