signalwire-agents 0.1.26__py3-none-any.whl → 0.1.28__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 +1 -4
- signalwire_agents/cli/config.py +11 -1
- signalwire_agents/cli/simulation/data_overrides.py +6 -2
- signalwire_agents/cli/test_swaig.py +6 -0
- signalwire_agents/core/agent_base.py +1 -12
- signalwire_agents/core/contexts.py +1 -0
- signalwire_agents/core/mixins/state_mixin.py +1 -67
- signalwire_agents/core/mixins/tool_mixin.py +0 -65
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +2789 -701
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +1 -0
- signalwire_agents/skills/mcp_gateway/skill.py +339 -0
- {signalwire_agents-0.1.26.dist-info → signalwire_agents-0.1.28.dist-info}/METADATA +1 -59
- {signalwire_agents-0.1.26.dist-info → signalwire_agents-0.1.28.dist-info}/RECORD +23 -23
- signalwire_agents/core/state/__init__.py +0 -17
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- {signalwire_agents-0.1.26.dist-info → signalwire_agents-0.1.28.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.26.dist-info → signalwire_agents-0.1.28.dist-info}/entry_points.txt +0 -0
- {signalwire_agents-0.1.26.dist-info → signalwire_agents-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.26.dist-info → signalwire_agents-0.1.28.dist-info}/top_level.txt +0 -0
@@ -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"""
|
@@ -0,0 +1,339 @@
|
|
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
|
+
|
10
|
+
import json
|
11
|
+
import requests
|
12
|
+
import logging
|
13
|
+
from typing import List, Dict, Any, Optional
|
14
|
+
from requests.auth import HTTPBasicAuth
|
15
|
+
|
16
|
+
from signalwire_agents.core.skill_base import SkillBase
|
17
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
class MCPGatewaySkill(SkillBase):
|
23
|
+
"""
|
24
|
+
MCP Gateway Skill - Bridge MCP servers with SWAIG functions
|
25
|
+
|
26
|
+
This skill connects SignalWire agents to MCP (Model Context Protocol) servers
|
27
|
+
through a gateway service, dynamically creating SWAIG functions for MCP tools.
|
28
|
+
"""
|
29
|
+
|
30
|
+
SKILL_NAME = "mcp_gateway"
|
31
|
+
SKILL_DESCRIPTION = "Bridge MCP servers with SWAIG functions"
|
32
|
+
SKILL_VERSION = "1.0.0"
|
33
|
+
REQUIRED_PACKAGES = ["requests"]
|
34
|
+
REQUIRED_ENV_VARS = []
|
35
|
+
|
36
|
+
def setup(self) -> bool:
|
37
|
+
"""Setup and validate skill configuration"""
|
38
|
+
# Check for auth method - either token or basic auth
|
39
|
+
self.auth_token = self.params.get('auth_token')
|
40
|
+
if not self.auth_token:
|
41
|
+
# Require basic auth if no token
|
42
|
+
required_params = ['gateway_url', 'auth_user', 'auth_password']
|
43
|
+
missing_params = [param for param in required_params if not self.params.get(param)]
|
44
|
+
if missing_params:
|
45
|
+
self.logger.error(f"Missing required parameters: {missing_params}")
|
46
|
+
return False
|
47
|
+
self.auth = HTTPBasicAuth(self.params['auth_user'], self.params['auth_password'])
|
48
|
+
else:
|
49
|
+
# Just need gateway URL with token auth
|
50
|
+
if not self.params.get('gateway_url'):
|
51
|
+
self.logger.error("Missing required parameter: gateway_url")
|
52
|
+
return False
|
53
|
+
self.auth = None
|
54
|
+
|
55
|
+
# Store configuration
|
56
|
+
self.gateway_url = self.params['gateway_url'].rstrip('/')
|
57
|
+
self.services = self.params.get('services', [])
|
58
|
+
self.session_timeout = self.params.get('session_timeout', 300)
|
59
|
+
self.tool_prefix = self.params.get('tool_prefix', 'mcp_')
|
60
|
+
self.retry_attempts = self.params.get('retry_attempts', 3)
|
61
|
+
self.request_timeout = self.params.get('request_timeout', 30)
|
62
|
+
self.verify_ssl = self.params.get('verify_ssl', True)
|
63
|
+
|
64
|
+
# Session ID will be set from call_id when first tool is used
|
65
|
+
self.session_id = None
|
66
|
+
|
67
|
+
# Validate gateway connection
|
68
|
+
try:
|
69
|
+
response = requests.get(
|
70
|
+
f"{self.gateway_url}/health",
|
71
|
+
timeout=self.request_timeout,
|
72
|
+
verify=self.verify_ssl
|
73
|
+
)
|
74
|
+
response.raise_for_status()
|
75
|
+
self.logger.info(f"Connected to MCP Gateway at {self.gateway_url}")
|
76
|
+
except Exception as e:
|
77
|
+
self.logger.error(f"Failed to connect to gateway: {e}")
|
78
|
+
return False
|
79
|
+
|
80
|
+
return True
|
81
|
+
|
82
|
+
def _make_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
83
|
+
"""Make HTTP request with appropriate authentication"""
|
84
|
+
headers = kwargs.get('headers', {})
|
85
|
+
if self.auth_token:
|
86
|
+
headers['Authorization'] = f'Bearer {self.auth_token}'
|
87
|
+
kwargs['headers'] = headers
|
88
|
+
|
89
|
+
if not self.auth_token:
|
90
|
+
kwargs['auth'] = self.auth
|
91
|
+
|
92
|
+
kwargs['timeout'] = kwargs.get('timeout', self.request_timeout)
|
93
|
+
kwargs['verify'] = kwargs.get('verify', self.verify_ssl)
|
94
|
+
|
95
|
+
return requests.request(method, url, **kwargs)
|
96
|
+
|
97
|
+
def register_tools(self) -> None:
|
98
|
+
"""Register SWAIG tools from MCP services"""
|
99
|
+
# If no services specified, get all available
|
100
|
+
if not self.services:
|
101
|
+
try:
|
102
|
+
response = self._make_request('GET', f"{self.gateway_url}/services")
|
103
|
+
response.raise_for_status()
|
104
|
+
all_services = response.json()
|
105
|
+
self.services = [{"name": name} for name in all_services.keys()]
|
106
|
+
except Exception as e:
|
107
|
+
self.logger.error(f"Failed to list services: {e}")
|
108
|
+
return
|
109
|
+
|
110
|
+
# Process each service
|
111
|
+
for service_config in self.services:
|
112
|
+
service_name = service_config.get('name')
|
113
|
+
if not service_name:
|
114
|
+
continue
|
115
|
+
|
116
|
+
# Get tools for this service
|
117
|
+
try:
|
118
|
+
response = self._make_request('GET', f"{self.gateway_url}/services/{service_name}/tools")
|
119
|
+
response.raise_for_status()
|
120
|
+
tools_data = response.json()
|
121
|
+
tools = tools_data.get('tools', [])
|
122
|
+
|
123
|
+
# Filter tools if specified
|
124
|
+
tool_filter = service_config.get('tools', '*')
|
125
|
+
if tool_filter != '*' and isinstance(tool_filter, list):
|
126
|
+
tools = [t for t in tools if t['name'] in tool_filter]
|
127
|
+
|
128
|
+
# Register each tool as a SWAIG function
|
129
|
+
for tool in tools:
|
130
|
+
self._register_mcp_tool(service_name, tool)
|
131
|
+
|
132
|
+
except Exception as e:
|
133
|
+
self.logger.error(f"Failed to get tools for service '{service_name}': {e}")
|
134
|
+
|
135
|
+
# Register the hangup hook for session cleanup
|
136
|
+
self.agent.define_tool(
|
137
|
+
name="_mcp_gateway_hangup",
|
138
|
+
description="Internal cleanup function for MCP sessions",
|
139
|
+
parameters={},
|
140
|
+
handler=self._hangup_handler,
|
141
|
+
is_hangup_hook=True
|
142
|
+
)
|
143
|
+
|
144
|
+
def _register_mcp_tool(self, service_name: str, tool_def: Dict[str, Any]):
|
145
|
+
"""Register a single MCP tool as a SWAIG function"""
|
146
|
+
tool_name = tool_def.get('name')
|
147
|
+
if not tool_name:
|
148
|
+
return
|
149
|
+
|
150
|
+
# Create SWAIG function name
|
151
|
+
swaig_name = f"{self.tool_prefix}{service_name}_{tool_name}"
|
152
|
+
|
153
|
+
# Build SWAIG parameters from MCP input schema
|
154
|
+
input_schema = tool_def.get('inputSchema', {})
|
155
|
+
properties = input_schema.get('properties', {})
|
156
|
+
required = input_schema.get('required', [])
|
157
|
+
|
158
|
+
# Convert MCP schema to SWAIG parameters
|
159
|
+
swaig_params = {}
|
160
|
+
for prop_name, prop_def in properties.items():
|
161
|
+
param_def = {
|
162
|
+
"type": prop_def.get('type', 'string'),
|
163
|
+
"description": prop_def.get('description', '')
|
164
|
+
}
|
165
|
+
|
166
|
+
# Add enum if present
|
167
|
+
if 'enum' in prop_def:
|
168
|
+
param_def['enum'] = prop_def['enum']
|
169
|
+
|
170
|
+
# Add default if present and not required
|
171
|
+
if 'default' in prop_def and prop_name not in required:
|
172
|
+
param_def['default'] = prop_def['default']
|
173
|
+
|
174
|
+
swaig_params[prop_name] = param_def
|
175
|
+
|
176
|
+
# Create handler function
|
177
|
+
def handler(args, raw_data):
|
178
|
+
return self._call_mcp_tool(service_name, tool_name, args, raw_data)
|
179
|
+
|
180
|
+
# Register the SWAIG function
|
181
|
+
self.agent.define_tool(
|
182
|
+
name=swaig_name,
|
183
|
+
description=f"[{service_name}] {tool_def.get('description', tool_name)}",
|
184
|
+
parameters=swaig_params,
|
185
|
+
handler=handler
|
186
|
+
)
|
187
|
+
|
188
|
+
self.logger.info(f"Registered SWAIG function: {swaig_name}")
|
189
|
+
|
190
|
+
def _call_mcp_tool(self, service_name: str, tool_name: str, args: Dict[str, Any],
|
191
|
+
raw_data: Dict[str, Any]) -> SwaigFunctionResult:
|
192
|
+
"""Call an MCP tool through the gateway"""
|
193
|
+
# Check for mcp_call_id in global_data first, then fall back to top-level call_id
|
194
|
+
global_data = raw_data.get('global_data', {})
|
195
|
+
if 'mcp_call_id' in global_data:
|
196
|
+
self.session_id = global_data['mcp_call_id']
|
197
|
+
self.logger.info(f"Using session ID from global_data.mcp_call_id: {self.session_id}")
|
198
|
+
else:
|
199
|
+
self.session_id = raw_data.get('call_id', 'unknown')
|
200
|
+
self.logger.info(f"Using session ID from call_id: {self.session_id}")
|
201
|
+
self.logger.debug(f"Raw data keys: {list(raw_data.keys())}")
|
202
|
+
if 'global_data' in raw_data:
|
203
|
+
self.logger.debug(f"global_data keys: {list(global_data.keys())}")
|
204
|
+
|
205
|
+
# Prepare request
|
206
|
+
request_data = {
|
207
|
+
"tool": tool_name,
|
208
|
+
"arguments": args,
|
209
|
+
"session_id": self.session_id,
|
210
|
+
"timeout": self.session_timeout,
|
211
|
+
"metadata": {
|
212
|
+
"agent_id": self.agent.name,
|
213
|
+
"timestamp": raw_data.get('timestamp'),
|
214
|
+
"call_id": raw_data.get('call_id')
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
# Call the gateway with retries
|
219
|
+
last_error = None
|
220
|
+
for attempt in range(self.retry_attempts):
|
221
|
+
try:
|
222
|
+
response = self._make_request(
|
223
|
+
'POST',
|
224
|
+
f"{self.gateway_url}/services/{service_name}/call",
|
225
|
+
json=request_data
|
226
|
+
)
|
227
|
+
|
228
|
+
if response.status_code == 200:
|
229
|
+
result_data = response.json()
|
230
|
+
result_text = result_data.get('result', 'No response')
|
231
|
+
|
232
|
+
# Create SWAIG result
|
233
|
+
return SwaigFunctionResult(result_text)
|
234
|
+
|
235
|
+
else:
|
236
|
+
error_data = response.json()
|
237
|
+
error_msg = error_data.get('error', f'HTTP {response.status_code}')
|
238
|
+
last_error = error_msg
|
239
|
+
|
240
|
+
if response.status_code >= 500:
|
241
|
+
# Server error, retry
|
242
|
+
self.logger.warning(f"Gateway error (attempt {attempt + 1}): {error_msg}")
|
243
|
+
continue
|
244
|
+
else:
|
245
|
+
# Client error, don't retry
|
246
|
+
break
|
247
|
+
|
248
|
+
except requests.exceptions.Timeout:
|
249
|
+
last_error = "Request timeout"
|
250
|
+
self.logger.warning(f"Timeout calling MCP tool (attempt {attempt + 1})")
|
251
|
+
|
252
|
+
except requests.exceptions.ConnectionError:
|
253
|
+
last_error = "Connection error"
|
254
|
+
self.logger.warning(f"Connection error (attempt {attempt + 1})")
|
255
|
+
|
256
|
+
except Exception as e:
|
257
|
+
last_error = str(e)
|
258
|
+
self.logger.error(f"Unexpected error: {e}")
|
259
|
+
break
|
260
|
+
|
261
|
+
# All attempts failed
|
262
|
+
error_msg = f"Failed to call {service_name}.{tool_name}: {last_error}"
|
263
|
+
self.logger.error(error_msg)
|
264
|
+
return SwaigFunctionResult(error_msg)
|
265
|
+
|
266
|
+
def _hangup_handler(self, args: Dict[str, Any], raw_data: Dict[str, Any]) -> SwaigFunctionResult:
|
267
|
+
"""Handle call hangup - cleanup MCP session"""
|
268
|
+
# Check for mcp_call_id in global_data first, then fall back to top-level call_id
|
269
|
+
global_data = raw_data.get('global_data', {})
|
270
|
+
if 'mcp_call_id' in global_data:
|
271
|
+
session_id = global_data['mcp_call_id']
|
272
|
+
self.logger.info(f"Cleanup using session ID from global_data.mcp_call_id: {session_id}")
|
273
|
+
else:
|
274
|
+
session_id = raw_data.get('call_id', 'unknown')
|
275
|
+
self.logger.info(f"Cleanup using session ID from call_id: {session_id}")
|
276
|
+
|
277
|
+
try:
|
278
|
+
response = self._make_request('DELETE', f"{self.gateway_url}/sessions/{session_id}")
|
279
|
+
|
280
|
+
if response.status_code in [200, 404]:
|
281
|
+
self.logger.info(f"Cleaned up MCP session: {session_id}")
|
282
|
+
else:
|
283
|
+
self.logger.warning(f"Failed to cleanup session: HTTP {response.status_code}")
|
284
|
+
|
285
|
+
except Exception as e:
|
286
|
+
self.logger.error(f"Error cleaning up session: {e}")
|
287
|
+
|
288
|
+
return SwaigFunctionResult("Session cleanup complete")
|
289
|
+
|
290
|
+
def get_hints(self) -> List[str]:
|
291
|
+
"""Return speech recognition hints"""
|
292
|
+
hints = ["MCP", "gateway"]
|
293
|
+
|
294
|
+
# Add service names as hints
|
295
|
+
for service in self.services:
|
296
|
+
if isinstance(service, dict) and 'name' in service:
|
297
|
+
hints.append(service['name'])
|
298
|
+
|
299
|
+
return hints
|
300
|
+
|
301
|
+
def get_global_data(self) -> Dict[str, Any]:
|
302
|
+
"""Return global data for DataMap variables"""
|
303
|
+
return {
|
304
|
+
"mcp_gateway_url": self.gateway_url,
|
305
|
+
"mcp_session_id": self.session_id,
|
306
|
+
"mcp_services": [s.get('name') if isinstance(s, dict) else str(s)
|
307
|
+
for s in self.services]
|
308
|
+
}
|
309
|
+
|
310
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
311
|
+
"""Return prompt sections to add to agent"""
|
312
|
+
sections = []
|
313
|
+
|
314
|
+
# Build service list for prompt
|
315
|
+
service_descriptions = []
|
316
|
+
for service in self.services:
|
317
|
+
if isinstance(service, dict):
|
318
|
+
name = service.get('name', 'Unknown')
|
319
|
+
tools = service.get('tools', '*')
|
320
|
+
if tools == '*':
|
321
|
+
service_descriptions.append(f"{name} (all tools)")
|
322
|
+
elif isinstance(tools, list):
|
323
|
+
service_descriptions.append(f"{name} ({len(tools)} tools)")
|
324
|
+
else:
|
325
|
+
service_descriptions.append(str(service))
|
326
|
+
|
327
|
+
if service_descriptions:
|
328
|
+
sections.append({
|
329
|
+
"title": "MCP Gateway Integration",
|
330
|
+
"body": "You have access to external MCP (Model Context Protocol) services through a gateway.",
|
331
|
+
"bullets": [
|
332
|
+
f"Connected to gateway at {self.gateway_url}",
|
333
|
+
f"Available services: {', '.join(service_descriptions)}",
|
334
|
+
f"Functions are prefixed with '{self.tool_prefix}' followed by service name",
|
335
|
+
"Each service maintains its own session state throughout the call"
|
336
|
+
]
|
337
|
+
})
|
338
|
+
|
339
|
+
return sections
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: signalwire_agents
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.28
|
4
4
|
Summary: SignalWire AI Agents SDK
|
5
5
|
Author-email: SignalWire Team <info@signalwire.com>
|
6
6
|
License: MIT
|
@@ -593,64 +593,6 @@ if __name__ == "__main__":
|
|
593
593
|
agent.serve(host="0.0.0.0", port=8000)
|
594
594
|
```
|
595
595
|
|
596
|
-
## Using State Management
|
597
|
-
|
598
|
-
```python
|
599
|
-
from signalwire_agents import AgentBase
|
600
|
-
from signalwire_agents.core.function_result import SwaigFunctionResult
|
601
|
-
from signalwire_agents.core.state import FileStateManager
|
602
|
-
|
603
|
-
class StatefulAgent(AgentBase):
|
604
|
-
def __init__(self):
|
605
|
-
# Configure state management
|
606
|
-
state_manager = FileStateManager(storage_dir="./state_data")
|
607
|
-
|
608
|
-
super().__init__(
|
609
|
-
name="stateful",
|
610
|
-
route="/stateful",
|
611
|
-
enable_state_tracking=True, # Enable state tracking
|
612
|
-
state_manager=state_manager # Use custom state manager
|
613
|
-
)
|
614
|
-
|
615
|
-
# When enable_state_tracking=True, startup_hook and hangup_hook
|
616
|
-
# are automatically registered to track session lifecycle
|
617
|
-
|
618
|
-
# Custom tool for accessing and updating state
|
619
|
-
@AgentBase.tool(
|
620
|
-
name="save_preference",
|
621
|
-
description="Save a user preference",
|
622
|
-
parameters={
|
623
|
-
"preference_name": {
|
624
|
-
"type": "string",
|
625
|
-
"description": "Name of the preference to save"
|
626
|
-
},
|
627
|
-
"preference_value": {
|
628
|
-
"type": "string",
|
629
|
-
"description": "Value of the preference"
|
630
|
-
}
|
631
|
-
}
|
632
|
-
)
|
633
|
-
def save_preference(self, args, raw_data):
|
634
|
-
# Get the call ID from the raw data
|
635
|
-
call_id = raw_data.get("call_id")
|
636
|
-
|
637
|
-
if call_id:
|
638
|
-
# Get current state or empty dict if none exists
|
639
|
-
state = self.get_state(call_id) or {}
|
640
|
-
|
641
|
-
# Update the state
|
642
|
-
preferences = state.get("preferences", {})
|
643
|
-
preferences[args.get("preference_name")] = args.get("preference_value")
|
644
|
-
state["preferences"] = preferences
|
645
|
-
|
646
|
-
# Save the updated state
|
647
|
-
self.update_state(call_id, state)
|
648
|
-
|
649
|
-
return SwaigFunctionResult("Preference saved successfully")
|
650
|
-
else:
|
651
|
-
return SwaigFunctionResult("Could not save preference: No call ID")
|
652
|
-
```
|
653
|
-
|
654
596
|
## Using Prefab Agents
|
655
597
|
|
656
598
|
```python
|