signalwire-agents 0.1.27__py3-none-any.whl → 0.1.29__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 (30) hide show
  1. signalwire_agents/__init__.py +1 -4
  2. signalwire_agents/cli/config.py +11 -1
  3. signalwire_agents/cli/simulation/data_overrides.py +6 -2
  4. signalwire_agents/cli/test_swaig.py +6 -0
  5. signalwire_agents/core/agent_base.py +1 -12
  6. signalwire_agents/core/auth_handler.py +233 -0
  7. signalwire_agents/core/config_loader.py +259 -0
  8. signalwire_agents/core/contexts.py +75 -0
  9. signalwire_agents/core/mixins/state_mixin.py +1 -67
  10. signalwire_agents/core/mixins/tool_mixin.py +0 -65
  11. signalwire_agents/core/security_config.py +333 -0
  12. signalwire_agents/core/swml_service.py +19 -25
  13. signalwire_agents/prefabs/concierge.py +0 -3
  14. signalwire_agents/prefabs/faq_bot.py +0 -3
  15. signalwire_agents/prefabs/info_gatherer.py +0 -3
  16. signalwire_agents/prefabs/receptionist.py +0 -3
  17. signalwire_agents/prefabs/survey.py +0 -3
  18. signalwire_agents/search/search_service.py +200 -11
  19. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  20. signalwire_agents/skills/mcp_gateway/__init__.py +1 -0
  21. signalwire_agents/skills/mcp_gateway/skill.py +339 -0
  22. {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/METADATA +1 -59
  23. {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/RECORD +27 -24
  24. signalwire_agents/core/state/__init__.py +0 -17
  25. signalwire_agents/core/state/file_state_manager.py +0 -219
  26. signalwire_agents/core/state/state_manager.py +0 -101
  27. {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/WHEEL +0 -0
  28. {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/entry_points.txt +0 -0
  29. {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/licenses/LICENSE +0 -0
  30. {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/top_level.txt +0 -0
@@ -51,7 +51,6 @@ class FAQBotAgent(AgentBase):
51
51
  persona: Optional[str] = None,
52
52
  name: str = "faq_bot",
53
53
  route: str = "/faq",
54
- enable_state_tracking: bool = True, # Enable state tracking by default
55
54
  **kwargs
56
55
  ):
57
56
  """
@@ -66,7 +65,6 @@ class FAQBotAgent(AgentBase):
66
65
  persona: Optional custom personality description
67
66
  name: Agent name for the route
68
67
  route: HTTP route for this agent
69
- enable_state_tracking: Whether to enable state tracking (default: True)
70
68
  **kwargs: Additional arguments for AgentBase
71
69
  """
72
70
  # Initialize the base agent
@@ -74,7 +72,6 @@ class FAQBotAgent(AgentBase):
74
72
  name=name,
75
73
  route=route,
76
74
  use_pom=True,
77
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
78
75
  **kwargs
79
76
  )
80
77
 
@@ -45,7 +45,6 @@ class InfoGathererAgent(AgentBase):
45
45
  questions: Optional[List[Dict[str, str]]] = None,
46
46
  name: str = "info_gatherer",
47
47
  route: str = "/info_gatherer",
48
- enable_state_tracking: bool = True, # Enable state tracking by default for InfoGatherer
49
48
  **kwargs
50
49
  ):
51
50
  """
@@ -59,7 +58,6 @@ class InfoGathererAgent(AgentBase):
59
58
  - confirm: (Optional) If set to True, the agent will confirm the answer before submitting
60
59
  name: Agent name for the route
61
60
  route: HTTP route for this agent
62
- enable_state_tracking: Whether to enable state tracking (default: True)
63
61
  **kwargs: Additional arguments for AgentBase
64
62
  """
65
63
  # Initialize the base agent
@@ -67,7 +65,6 @@ class InfoGathererAgent(AgentBase):
67
65
  name=name,
68
66
  route=route,
69
67
  use_pom=True,
70
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
71
68
  **kwargs
72
69
  )
73
70
 
@@ -41,7 +41,6 @@ class ReceptionistAgent(AgentBase):
41
41
  route: str = "/receptionist",
42
42
  greeting: str = "Thank you for calling. How can I help you today?",
43
43
  voice: str = "rime.spore",
44
- enable_state_tracking: bool = True, # Enable state tracking by default
45
44
  **kwargs
46
45
  ):
47
46
  """
@@ -56,7 +55,6 @@ class ReceptionistAgent(AgentBase):
56
55
  route: HTTP route for this agent
57
56
  greeting: Initial greeting message
58
57
  voice: Voice ID to use
59
- enable_state_tracking: Whether to enable state tracking (default: True)
60
58
  **kwargs: Additional arguments for AgentBase
61
59
  """
62
60
  # Initialize the base agent
@@ -64,7 +62,6 @@ class ReceptionistAgent(AgentBase):
64
62
  name=name,
65
63
  route=route,
66
64
  use_pom=True,
67
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
68
65
  **kwargs
69
66
  )
70
67
 
@@ -62,7 +62,6 @@ class SurveyAgent(AgentBase):
62
62
  max_retries: int = 2,
63
63
  name: str = "survey",
64
64
  route: str = "/survey",
65
- enable_state_tracking: bool = True, # Enable state tracking by default
66
65
  **kwargs
67
66
  ):
68
67
  """
@@ -83,7 +82,6 @@ class SurveyAgent(AgentBase):
83
82
  max_retries: Maximum number of times to retry invalid answers
84
83
  name: Name for the agent (default: "survey")
85
84
  route: HTTP route for the agent (default: "/survey")
86
- enable_state_tracking: Whether to enable state tracking (default: True)
87
85
  **kwargs: Additional arguments for AgentBase
88
86
  """
89
87
  # Initialize the base agent
@@ -91,7 +89,6 @@ class SurveyAgent(AgentBase):
91
89
  name=name,
92
90
  route=route,
93
91
  use_pom=True,
94
- enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
95
92
  **kwargs
96
93
  )
97
94
 
@@ -8,15 +8,23 @@ See LICENSE file in the project root for full license information.
8
8
  """
9
9
 
10
10
  import logging
11
- from typing import Dict, Any, List, Optional
11
+ from typing import Dict, Any, List, Optional, Tuple
12
12
 
13
13
  try:
14
- from fastapi import FastAPI, HTTPException
14
+ from fastapi import FastAPI, HTTPException, Request, Response, Depends
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.security import HTTPBasic, HTTPBasicCredentials
15
17
  from pydantic import BaseModel
16
18
  except ImportError:
17
19
  FastAPI = None
18
20
  HTTPException = None
19
21
  BaseModel = None
22
+ Request = None
23
+ Response = None
24
+ Depends = None
25
+ CORSMiddleware = None
26
+ HTTPBasic = None
27
+ HTTPBasicCredentials = None
20
28
 
21
29
  try:
22
30
  from sentence_transformers import SentenceTransformer
@@ -25,8 +33,11 @@ except ImportError:
25
33
 
26
34
  from .query_processor import preprocess_query
27
35
  from .search_engine import SearchEngine
36
+ from signalwire_agents.core.security_config import SecurityConfig
37
+ from signalwire_agents.core.config_loader import ConfigLoader
38
+ from signalwire_agents.core.logging_config import get_logger
28
39
 
29
- logger = logging.getLogger(__name__)
40
+ logger = get_logger("search_service")
30
41
 
31
42
  # Pydantic models for API
32
43
  if BaseModel:
@@ -73,14 +84,30 @@ else:
73
84
  class SearchService:
74
85
  """Local search service with HTTP API"""
75
86
 
76
- def __init__(self, port: int = 8001, indexes: Dict[str, str] = None):
87
+ def __init__(self, port: int = 8001, indexes: Dict[str, str] = None,
88
+ basic_auth: Optional[Tuple[str, str]] = None,
89
+ config_file: Optional[str] = None):
90
+ # Load configuration first
91
+ self._load_config(config_file)
92
+
93
+ # Override with constructor params if provided
77
94
  self.port = port
78
- self.indexes = indexes or {}
95
+ if indexes is not None:
96
+ self.indexes = indexes
97
+
79
98
  self.search_engines = {}
80
99
  self.model = None
81
100
 
101
+ # Load security configuration with optional config file
102
+ self.security = SecurityConfig(config_file=config_file, service_name="search")
103
+ self.security.log_config("SearchService")
104
+
105
+ # Set up authentication
106
+ self._basic_auth = basic_auth or self.security.get_basic_auth()
107
+
82
108
  if FastAPI:
83
109
  self.app = FastAPI(title="SignalWire Local Search Service")
110
+ self._setup_security()
84
111
  self._setup_routes()
85
112
  else:
86
113
  self.app = None
@@ -88,22 +115,131 @@ class SearchService:
88
115
 
89
116
  self._load_resources()
90
117
 
118
+ def _load_config(self, config_file: Optional[str]):
119
+ """Load configuration from file if available"""
120
+ # Initialize defaults
121
+ self.indexes = {}
122
+
123
+ # Find config file
124
+ if not config_file:
125
+ config_file = ConfigLoader.find_config_file("search")
126
+
127
+ if not config_file:
128
+ return
129
+
130
+ # Load config
131
+ config_loader = ConfigLoader([config_file])
132
+ if not config_loader.has_config():
133
+ return
134
+
135
+ logger.info("loading_config_from_file", file=config_file)
136
+
137
+ # Get service section
138
+ service_config = config_loader.get_section('service')
139
+ if service_config:
140
+ if 'port' in service_config:
141
+ self.port = int(service_config['port'])
142
+
143
+ if 'indexes' in service_config and isinstance(service_config['indexes'], dict):
144
+ self.indexes = service_config['indexes']
145
+
146
+ def _setup_security(self):
147
+ """Setup security middleware and authentication"""
148
+ if not self.app:
149
+ return
150
+
151
+ # Add CORS middleware if FastAPI has it
152
+ if CORSMiddleware:
153
+ self.app.add_middleware(
154
+ CORSMiddleware,
155
+ **self.security.get_cors_config()
156
+ )
157
+
158
+ # Add security headers middleware
159
+ @self.app.middleware("http")
160
+ async def add_security_headers(request: Request, call_next):
161
+ response = await call_next(request)
162
+
163
+ # Add security headers
164
+ is_https = request.url.scheme == "https"
165
+ headers = self.security.get_security_headers(is_https)
166
+ for header, value in headers.items():
167
+ response.headers[header] = value
168
+
169
+ return response
170
+
171
+ # Add host validation middleware
172
+ @self.app.middleware("http")
173
+ async def validate_host(request: Request, call_next):
174
+ host = request.headers.get("host", "").split(":")[0]
175
+ if host and not self.security.should_allow_host(host):
176
+ return Response(content="Invalid host", status_code=400)
177
+
178
+ return await call_next(request)
179
+
180
+ def _get_current_username(self, credentials: HTTPBasicCredentials = None) -> str:
181
+ """Validate basic auth credentials"""
182
+ if not credentials:
183
+ return None
184
+
185
+ correct_username, correct_password = self._basic_auth
186
+
187
+ # Compare credentials
188
+ import secrets
189
+ username_correct = secrets.compare_digest(credentials.username, correct_username)
190
+ password_correct = secrets.compare_digest(credentials.password, correct_password)
191
+
192
+ if not (username_correct and password_correct):
193
+ raise HTTPException(
194
+ status_code=401,
195
+ detail="Invalid authentication credentials",
196
+ headers={"WWW-Authenticate": "Basic"},
197
+ )
198
+
199
+ return credentials.username
200
+
91
201
  def _setup_routes(self):
92
202
  """Setup FastAPI routes"""
93
203
  if not self.app:
94
204
  return
205
+
206
+ # Create security dependency if HTTPBasic is available
207
+ security = HTTPBasic() if HTTPBasic else None
208
+
209
+ # Create dependency for authenticated routes
210
+ def get_authenticated():
211
+ if security:
212
+ return security
213
+ return None
95
214
 
96
215
  @self.app.post("/search", response_model=SearchResponse)
97
- async def search(request: SearchRequest):
216
+ async def search(
217
+ request: SearchRequest,
218
+ credentials: HTTPBasicCredentials = None if not security else Depends(security)
219
+ ):
220
+ if security:
221
+ self._get_current_username(credentials)
98
222
  return await self._handle_search(request)
99
223
 
100
224
  @self.app.get("/health")
101
225
  async def health():
102
- return {"status": "healthy", "indexes": list(self.indexes.keys())}
226
+ return {
227
+ "status": "healthy",
228
+ "indexes": list(self.indexes.keys()),
229
+ "ssl_enabled": self.security.ssl_enabled,
230
+ "auth_required": bool(security)
231
+ }
103
232
 
104
233
  @self.app.post("/reload_index")
105
- async def reload_index(index_name: str, index_path: str):
234
+ async def reload_index(
235
+ index_name: str,
236
+ index_path: str,
237
+ credentials: HTTPBasicCredentials = None if not security else Depends(security)
238
+ ):
106
239
  """Reload or add new index"""
240
+ if security:
241
+ self._get_current_username(credentials)
242
+
107
243
  self.indexes[index_name] = index_path
108
244
  self.search_engines[index_name] = SearchEngine(index_path, self.model)
109
245
  return {"status": "reloaded", "index": index_name}
@@ -235,14 +371,67 @@ class SearchService:
235
371
  'query_analysis': response.query_analysis
236
372
  }
237
373
 
238
- def start(self):
239
- """Start the service"""
374
+ def start(self, host: str = "0.0.0.0", port: Optional[int] = None,
375
+ ssl_cert: Optional[str] = None, ssl_key: Optional[str] = None):
376
+ """
377
+ Start the service with optional HTTPS support.
378
+
379
+ Args:
380
+ host: Host to bind to (default: "0.0.0.0")
381
+ port: Port to bind to (default: self.port)
382
+ ssl_cert: Path to SSL certificate file (overrides environment)
383
+ ssl_key: Path to SSL key file (overrides environment)
384
+ """
240
385
  if not self.app:
241
386
  raise RuntimeError("FastAPI not available. Cannot start HTTP service.")
242
387
 
388
+ port = port or self.port
389
+
390
+ # Get SSL configuration
391
+ ssl_kwargs = {}
392
+ if ssl_cert and ssl_key:
393
+ # Use provided SSL files
394
+ ssl_kwargs = {
395
+ 'ssl_certfile': ssl_cert,
396
+ 'ssl_keyfile': ssl_key
397
+ }
398
+ else:
399
+ # Use security config SSL settings
400
+ ssl_kwargs = self.security.get_ssl_context_kwargs()
401
+
402
+ # Build startup URL
403
+ scheme = "https" if ssl_kwargs else "http"
404
+ startup_url = f"{scheme}://{host}:{port}"
405
+
406
+ # Get auth credentials
407
+ username, password = self._basic_auth
408
+
409
+ # Log startup information
410
+ logger.info(
411
+ "starting_search_service",
412
+ url=startup_url,
413
+ ssl_enabled=bool(ssl_kwargs),
414
+ indexes=list(self.indexes.keys()),
415
+ username=username
416
+ )
417
+
418
+ # Print user-friendly startup message
419
+ print(f"\nSignalWire Search Service starting...")
420
+ print(f"URL: {startup_url}")
421
+ print(f"Indexes: {', '.join(self.indexes.keys()) if self.indexes else 'None'}")
422
+ print(f"Basic Auth: {username}:{password}")
423
+ if ssl_kwargs:
424
+ print(f"SSL: Enabled")
425
+ print("")
426
+
243
427
  try:
244
428
  import uvicorn
245
- uvicorn.run(self.app, host="0.0.0.0", port=self.port)
429
+ uvicorn.run(
430
+ self.app,
431
+ host=host,
432
+ port=port,
433
+ **ssl_kwargs
434
+ )
246
435
  except ImportError:
247
436
  raise RuntimeError("uvicorn not available. Cannot start HTTP service.")
248
437
 
@@ -0,0 +1,230 @@
1
+ # MCP Gateway Skill
2
+
3
+ Bridge MCP (Model Context Protocol) servers with SignalWire SWAIG functions, allowing agents to seamlessly interact with MCP-based tools.
4
+
5
+ ## Description
6
+
7
+ The MCP Gateway skill connects SignalWire agents to MCP servers through a centralized gateway service. It dynamically discovers and registers MCP tools as SWAIG functions, maintaining session state throughout each call.
8
+
9
+ ## Features
10
+
11
+ - Dynamic tool discovery from MCP servers
12
+ - Session management tied to SignalWire call IDs
13
+ - Automatic cleanup on call hangup
14
+ - Support for multiple MCP services
15
+ - Selective tool loading
16
+ - HTTPS support with SSL verification
17
+ - Retry logic for resilient connections
18
+
19
+ ## Requirements
20
+
21
+ - Running MCP Gateway service
22
+ - Network access to gateway
23
+ - Gateway credentials (username/password)
24
+
25
+ ## Configuration
26
+
27
+ ### Required Parameters
28
+
29
+ Either Basic Auth credentials OR Bearer token:
30
+ - `gateway_url`: URL of the MCP gateway service (default: "http://localhost:8100")
31
+ - `auth_user` + `auth_password`: Basic auth credentials
32
+ - OR `auth_token`: Bearer token for authentication
33
+
34
+ ### Optional Parameters
35
+
36
+ - `services`: Array of services to load (default: all available)
37
+ - `name`: Service name
38
+ - `tools`: Array of tool names or "*" for all (default: all)
39
+ - `session_timeout`: Session timeout in seconds (default: 300)
40
+ - `tool_prefix`: Prefix for SWAIG function names (default: "mcp_")
41
+ - `retry_attempts`: Number of retry attempts (default: 3)
42
+ - `request_timeout`: HTTP request timeout in seconds (default: 30)
43
+ - `verify_ssl`: Verify SSL certificates (default: true)
44
+
45
+ ## Usage
46
+
47
+ ### Basic Usage (All Services)
48
+
49
+ ```python
50
+ from signalwire_agents import AgentBase
51
+
52
+ class MyAgent(AgentBase):
53
+ def __init__(self):
54
+ super().__init__(name="My Agent")
55
+
56
+ # Load all available MCP services
57
+ self.add_skill("mcp_gateway", {
58
+ "gateway_url": "http://localhost:8080",
59
+ "auth_user": "admin",
60
+ "auth_password": "changeme"
61
+ })
62
+
63
+ agent = MyAgent()
64
+ agent.run()
65
+ ```
66
+
67
+ ### Selective Service Loading
68
+
69
+ ```python
70
+ # Load specific services with specific tools
71
+ self.add_skill("mcp_gateway", {
72
+ "gateway_url": "https://gateway.example.com",
73
+ "auth_user": "admin",
74
+ "auth_password": "secret",
75
+ "services": [
76
+ {
77
+ "name": "todo",
78
+ "tools": ["add_todo", "list_todos"] # Only these tools
79
+ },
80
+ {
81
+ "name": "calculator",
82
+ "tools": "*" # All calculator tools
83
+ }
84
+ ],
85
+ "session_timeout": 600,
86
+ "tool_prefix": "ext_"
87
+ })
88
+ ```
89
+
90
+ ### HTTPS with Self-Signed Certificate
91
+
92
+ ```python
93
+ self.add_skill("mcp_gateway", {
94
+ "gateway_url": "https://localhost:8443",
95
+ "auth_user": "admin",
96
+ "auth_password": "secret",
97
+ "verify_ssl": False # For self-signed certificates
98
+ })
99
+ ```
100
+
101
+ ### Bearer Token Authentication
102
+
103
+ ```python
104
+ self.add_skill("mcp_gateway", {
105
+ "gateway_url": "https://gateway.example.com",
106
+ "auth_token": "your-bearer-token-here",
107
+ "services": [{
108
+ "name": "todo"
109
+ }]
110
+ })
111
+ ```
112
+
113
+ ## Generated Functions
114
+
115
+ The skill dynamically generates SWAIG functions based on discovered MCP tools. Function names follow the pattern:
116
+
117
+ `{tool_prefix}{service_name}_{tool_name}`
118
+
119
+ For example, with default settings:
120
+ - `mcp_todo_add_todo` - Add a todo item
121
+ - `mcp_todo_list_todos` - List todo items
122
+ - `mcp_calculator_add` - Calculator addition
123
+
124
+ ## Example Conversations
125
+
126
+ ### Using Todo Service
127
+
128
+ ```
129
+ User: "Add a task to buy milk"
130
+ Assistant: "I'll add that to your todo list."
131
+ [Calls mcp_todo_add_todo with text="buy milk"]
132
+ Assistant: "I've added 'buy milk' to your todo list."
133
+
134
+ User: "What's on my todo list?"
135
+ Assistant: "Let me check your todos."
136
+ [Calls mcp_todo_list_todos]
137
+ Assistant: "Here are your current todos:
138
+ ○ #1 [medium] buy milk"
139
+ ```
140
+
141
+ ### Multiple Services
142
+
143
+ ```
144
+ User: "Add 'finish report' to my todos and calculate 15% of 200"
145
+ Assistant: "I'll add that todo and do the calculation for you."
146
+ [Calls mcp_todo_add_todo with text="finish report"]
147
+ [Calls mcp_calculator_percent with value=200, percent=15]
148
+ Assistant: "I've added 'finish report' to your todos. 15% of 200 is 30."
149
+ ```
150
+
151
+ ## Session Management
152
+
153
+ - Each SignalWire call gets its own MCP session
154
+ - Sessions persist across multiple tool calls
155
+ - Automatic cleanup on call hangup
156
+ - Configurable timeout for inactive sessions
157
+
158
+ ### Custom Session ID
159
+
160
+ You can override the session ID by setting `mcp_call_id` in global_data:
161
+
162
+ ```python
163
+ # In your agent code
164
+ self.set_global_data({
165
+ "mcp_call_id": "custom-session-123"
166
+ })
167
+
168
+ # Or in a SWAIG function
169
+ result = SwaigFunctionResult("Session changed")
170
+ result.add_action("set_global_data", {"mcp_call_id": "new-session-456"})
171
+ ```
172
+
173
+ This is useful for:
174
+ - Managing multiple MCP sessions within a single call
175
+ - Sharing MCP sessions across different calls
176
+ - Custom session management strategies
177
+
178
+ ## Troubleshooting
179
+
180
+ ### Gateway Connection Failed
181
+
182
+ Check:
183
+ 1. Gateway service is running
184
+ 2. Correct URL and credentials
185
+ 3. Network connectivity
186
+ 4. Firewall rules
187
+
188
+ ### SSL Certificate Errors
189
+
190
+ For self-signed certificates:
191
+ ```python
192
+ "verify_ssl": False
193
+ ```
194
+
195
+ For custom CA certificates, ensure they're in the system trust store.
196
+
197
+ ### Tool Not Found
198
+
199
+ Verify:
200
+ 1. Service name is correct
201
+ 2. Tool name matches exactly
202
+ 3. Tool is included in service configuration
203
+ 4. MCP server is returning tools correctly
204
+
205
+ ### Session Timeouts
206
+
207
+ Increase timeout if needed:
208
+ ```python
209
+ "session_timeout": 600 # 10 minutes
210
+ ```
211
+
212
+ ## Gateway Setup
213
+
214
+ To run the MCP Gateway service:
215
+
216
+ ```bash
217
+ cd mcp_gateway
218
+ python3 gateway_service.py
219
+
220
+ # Or with custom config
221
+ python3 gateway_service.py -c myconfig.json
222
+ ```
223
+
224
+ ## Security Considerations
225
+
226
+ 1. Always use HTTPS in production
227
+ 2. Use strong authentication credentials
228
+ 3. Limit service access to required tools only
229
+ 4. Monitor gateway logs for suspicious activity
230
+ 5. Set appropriate session timeouts
@@ -0,0 +1 @@
1
+ """MCP Gateway Skill for SignalWire Agents"""