signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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 +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- 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 +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
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
|
+
@classmethod
|
|
37
|
+
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
|
|
38
|
+
"""Get parameter schema for MCP Gateway skill"""
|
|
39
|
+
schema = super().get_parameter_schema()
|
|
40
|
+
schema.update({
|
|
41
|
+
"gateway_url": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "URL of the MCP Gateway service",
|
|
44
|
+
"required": True
|
|
45
|
+
},
|
|
46
|
+
"auth_token": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Bearer token for authentication (alternative to basic auth)",
|
|
49
|
+
"required": False,
|
|
50
|
+
"hidden": True,
|
|
51
|
+
"env_var": "MCP_GATEWAY_AUTH_TOKEN"
|
|
52
|
+
},
|
|
53
|
+
"auth_user": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Username for basic authentication (required if auth_token not provided)",
|
|
56
|
+
"required": False,
|
|
57
|
+
"env_var": "MCP_GATEWAY_AUTH_USER"
|
|
58
|
+
},
|
|
59
|
+
"auth_password": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Password for basic authentication (required if auth_token not provided)",
|
|
62
|
+
"required": False,
|
|
63
|
+
"hidden": True,
|
|
64
|
+
"env_var": "MCP_GATEWAY_AUTH_PASSWORD"
|
|
65
|
+
},
|
|
66
|
+
"services": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"description": "List of MCP services to connect to (empty for all available)",
|
|
69
|
+
"default": [],
|
|
70
|
+
"required": False,
|
|
71
|
+
"items": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"properties": {
|
|
74
|
+
"name": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Service name"
|
|
77
|
+
},
|
|
78
|
+
"tools": {
|
|
79
|
+
"type": ["string", "array"],
|
|
80
|
+
"description": "Tools to expose ('*' for all, or list of tool names)"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"session_timeout": {
|
|
86
|
+
"type": "integer",
|
|
87
|
+
"description": "Session timeout in seconds",
|
|
88
|
+
"default": 300,
|
|
89
|
+
"required": False
|
|
90
|
+
},
|
|
91
|
+
"tool_prefix": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Prefix for registered SWAIG function names",
|
|
94
|
+
"default": "mcp_",
|
|
95
|
+
"required": False
|
|
96
|
+
},
|
|
97
|
+
"retry_attempts": {
|
|
98
|
+
"type": "integer",
|
|
99
|
+
"description": "Number of retry attempts for failed requests",
|
|
100
|
+
"default": 3,
|
|
101
|
+
"required": False
|
|
102
|
+
},
|
|
103
|
+
"request_timeout": {
|
|
104
|
+
"type": "integer",
|
|
105
|
+
"description": "Request timeout in seconds",
|
|
106
|
+
"default": 30,
|
|
107
|
+
"required": False
|
|
108
|
+
},
|
|
109
|
+
"verify_ssl": {
|
|
110
|
+
"type": "boolean",
|
|
111
|
+
"description": "Verify SSL certificates",
|
|
112
|
+
"default": True,
|
|
113
|
+
"required": False
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
return schema
|
|
117
|
+
|
|
118
|
+
def setup(self) -> bool:
|
|
119
|
+
"""Setup and validate skill configuration"""
|
|
120
|
+
# Check for auth method - either token or basic auth
|
|
121
|
+
self.auth_token = self.params.get('auth_token')
|
|
122
|
+
if not self.auth_token:
|
|
123
|
+
# Require basic auth if no token
|
|
124
|
+
required_params = ['gateway_url', 'auth_user', 'auth_password']
|
|
125
|
+
missing_params = [param for param in required_params if not self.params.get(param)]
|
|
126
|
+
if missing_params:
|
|
127
|
+
self.logger.error(f"Missing required parameters: {missing_params}")
|
|
128
|
+
return False
|
|
129
|
+
self.auth = HTTPBasicAuth(self.params['auth_user'], self.params['auth_password'])
|
|
130
|
+
else:
|
|
131
|
+
# Just need gateway URL with token auth
|
|
132
|
+
if not self.params.get('gateway_url'):
|
|
133
|
+
self.logger.error("Missing required parameter: gateway_url")
|
|
134
|
+
return False
|
|
135
|
+
self.auth = None
|
|
136
|
+
|
|
137
|
+
# Store configuration
|
|
138
|
+
self.gateway_url = self.params['gateway_url'].rstrip('/')
|
|
139
|
+
self.services = self.params.get('services', [])
|
|
140
|
+
self.session_timeout = self.params.get('session_timeout', 300)
|
|
141
|
+
self.tool_prefix = self.params.get('tool_prefix', 'mcp_')
|
|
142
|
+
self.retry_attempts = self.params.get('retry_attempts', 3)
|
|
143
|
+
self.request_timeout = self.params.get('request_timeout', 30)
|
|
144
|
+
self.verify_ssl = self.params.get('verify_ssl', True)
|
|
145
|
+
|
|
146
|
+
# Session ID will be set from call_id when first tool is used
|
|
147
|
+
self.session_id = None
|
|
148
|
+
|
|
149
|
+
# Validate gateway connection
|
|
150
|
+
try:
|
|
151
|
+
response = requests.get(
|
|
152
|
+
f"{self.gateway_url}/health",
|
|
153
|
+
timeout=self.request_timeout,
|
|
154
|
+
verify=self.verify_ssl
|
|
155
|
+
)
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
self.logger.info(f"Connected to MCP Gateway at {self.gateway_url}")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.logger.error(f"Failed to connect to gateway: {e}")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def _make_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
|
165
|
+
"""Make HTTP request with appropriate authentication"""
|
|
166
|
+
headers = kwargs.get('headers', {})
|
|
167
|
+
if self.auth_token:
|
|
168
|
+
headers['Authorization'] = f'Bearer {self.auth_token}'
|
|
169
|
+
kwargs['headers'] = headers
|
|
170
|
+
|
|
171
|
+
if not self.auth_token:
|
|
172
|
+
kwargs['auth'] = self.auth
|
|
173
|
+
|
|
174
|
+
kwargs['timeout'] = kwargs.get('timeout', self.request_timeout)
|
|
175
|
+
kwargs['verify'] = kwargs.get('verify', self.verify_ssl)
|
|
176
|
+
|
|
177
|
+
return requests.request(method, url, **kwargs)
|
|
178
|
+
|
|
179
|
+
def register_tools(self) -> None:
|
|
180
|
+
"""Register SWAIG tools from MCP services"""
|
|
181
|
+
# If no services specified, get all available
|
|
182
|
+
if not self.services:
|
|
183
|
+
try:
|
|
184
|
+
response = self._make_request('GET', f"{self.gateway_url}/services")
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
all_services = response.json()
|
|
187
|
+
self.services = [{"name": name} for name in all_services.keys()]
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self.logger.error(f"Failed to list services: {e}")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# Process each service
|
|
193
|
+
for service_config in self.services:
|
|
194
|
+
service_name = service_config.get('name')
|
|
195
|
+
if not service_name:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# Get tools for this service
|
|
199
|
+
try:
|
|
200
|
+
response = self._make_request('GET', f"{self.gateway_url}/services/{service_name}/tools")
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
tools_data = response.json()
|
|
203
|
+
tools = tools_data.get('tools', [])
|
|
204
|
+
|
|
205
|
+
# Filter tools if specified
|
|
206
|
+
tool_filter = service_config.get('tools', '*')
|
|
207
|
+
if tool_filter != '*' and isinstance(tool_filter, list):
|
|
208
|
+
tools = [t for t in tools if t['name'] in tool_filter]
|
|
209
|
+
|
|
210
|
+
# Register each tool as a SWAIG function
|
|
211
|
+
for tool in tools:
|
|
212
|
+
self._register_mcp_tool(service_name, tool)
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self.logger.error(f"Failed to get tools for service '{service_name}': {e}")
|
|
216
|
+
|
|
217
|
+
# Register the hangup hook for session cleanup
|
|
218
|
+
self.define_tool(
|
|
219
|
+
name="_mcp_gateway_hangup",
|
|
220
|
+
description="Internal cleanup function for MCP sessions",
|
|
221
|
+
parameters={},
|
|
222
|
+
handler=self._hangup_handler,
|
|
223
|
+
is_hangup_hook=True
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _register_mcp_tool(self, service_name: str, tool_def: Dict[str, Any]):
|
|
227
|
+
"""Register a single MCP tool as a SWAIG function"""
|
|
228
|
+
tool_name = tool_def.get('name')
|
|
229
|
+
if not tool_name:
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
# Create SWAIG function name
|
|
233
|
+
swaig_name = f"{self.tool_prefix}{service_name}_{tool_name}"
|
|
234
|
+
|
|
235
|
+
# Build SWAIG parameters from MCP input schema
|
|
236
|
+
input_schema = tool_def.get('inputSchema', {})
|
|
237
|
+
properties = input_schema.get('properties', {})
|
|
238
|
+
required = input_schema.get('required', [])
|
|
239
|
+
|
|
240
|
+
# Convert MCP schema to SWAIG parameters
|
|
241
|
+
swaig_params = {}
|
|
242
|
+
for prop_name, prop_def in properties.items():
|
|
243
|
+
param_def = {
|
|
244
|
+
"type": prop_def.get('type', 'string'),
|
|
245
|
+
"description": prop_def.get('description', '')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Add enum if present
|
|
249
|
+
if 'enum' in prop_def:
|
|
250
|
+
param_def['enum'] = prop_def['enum']
|
|
251
|
+
|
|
252
|
+
# Add default if present and not required
|
|
253
|
+
if 'default' in prop_def and prop_name not in required:
|
|
254
|
+
param_def['default'] = prop_def['default']
|
|
255
|
+
|
|
256
|
+
swaig_params[prop_name] = param_def
|
|
257
|
+
|
|
258
|
+
# Create handler function
|
|
259
|
+
def handler(args, raw_data):
|
|
260
|
+
return self._call_mcp_tool(service_name, tool_name, args, raw_data)
|
|
261
|
+
|
|
262
|
+
# Register the SWAIG function
|
|
263
|
+
self.define_tool(
|
|
264
|
+
name=swaig_name,
|
|
265
|
+
description=f"[{service_name}] {tool_def.get('description', tool_name)}",
|
|
266
|
+
parameters=swaig_params,
|
|
267
|
+
handler=handler
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
self.logger.info(f"Registered SWAIG function: {swaig_name}")
|
|
271
|
+
|
|
272
|
+
def _call_mcp_tool(self, service_name: str, tool_name: str, args: Dict[str, Any],
|
|
273
|
+
raw_data: Dict[str, Any]) -> SwaigFunctionResult:
|
|
274
|
+
"""Call an MCP tool through the gateway"""
|
|
275
|
+
# Check for mcp_call_id in global_data first, then fall back to top-level call_id
|
|
276
|
+
global_data = raw_data.get('global_data', {})
|
|
277
|
+
if 'mcp_call_id' in global_data:
|
|
278
|
+
self.session_id = global_data['mcp_call_id']
|
|
279
|
+
self.logger.info(f"Using session ID from global_data.mcp_call_id: {self.session_id}")
|
|
280
|
+
else:
|
|
281
|
+
self.session_id = raw_data.get('call_id', 'unknown')
|
|
282
|
+
self.logger.info(f"Using session ID from call_id: {self.session_id}")
|
|
283
|
+
self.logger.debug(f"Raw data keys: {list(raw_data.keys())}")
|
|
284
|
+
if 'global_data' in raw_data:
|
|
285
|
+
self.logger.debug(f"global_data keys: {list(global_data.keys())}")
|
|
286
|
+
|
|
287
|
+
# Prepare request
|
|
288
|
+
request_data = {
|
|
289
|
+
"tool": tool_name,
|
|
290
|
+
"arguments": args,
|
|
291
|
+
"session_id": self.session_id,
|
|
292
|
+
"timeout": self.session_timeout,
|
|
293
|
+
"metadata": {
|
|
294
|
+
"agent_id": self.agent.name,
|
|
295
|
+
"timestamp": raw_data.get('timestamp'),
|
|
296
|
+
"call_id": raw_data.get('call_id')
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Call the gateway with retries
|
|
301
|
+
last_error = None
|
|
302
|
+
for attempt in range(self.retry_attempts):
|
|
303
|
+
try:
|
|
304
|
+
response = self._make_request(
|
|
305
|
+
'POST',
|
|
306
|
+
f"{self.gateway_url}/services/{service_name}/call",
|
|
307
|
+
json=request_data
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if response.status_code == 200:
|
|
311
|
+
result_data = response.json()
|
|
312
|
+
result_text = result_data.get('result', 'No response')
|
|
313
|
+
|
|
314
|
+
# Create SWAIG result
|
|
315
|
+
return SwaigFunctionResult(result_text)
|
|
316
|
+
|
|
317
|
+
else:
|
|
318
|
+
error_data = response.json()
|
|
319
|
+
error_msg = error_data.get('error', f'HTTP {response.status_code}')
|
|
320
|
+
last_error = error_msg
|
|
321
|
+
|
|
322
|
+
if response.status_code >= 500:
|
|
323
|
+
# Server error, retry
|
|
324
|
+
self.logger.warning(f"Gateway error (attempt {attempt + 1}): {error_msg}")
|
|
325
|
+
continue
|
|
326
|
+
else:
|
|
327
|
+
# Client error, don't retry
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
except requests.exceptions.Timeout:
|
|
331
|
+
last_error = "Request timeout"
|
|
332
|
+
self.logger.warning(f"Timeout calling MCP tool (attempt {attempt + 1})")
|
|
333
|
+
|
|
334
|
+
except requests.exceptions.ConnectionError:
|
|
335
|
+
last_error = "Connection error"
|
|
336
|
+
self.logger.warning(f"Connection error (attempt {attempt + 1})")
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
last_error = str(e)
|
|
340
|
+
self.logger.error(f"Unexpected error: {e}")
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
# All attempts failed
|
|
344
|
+
error_msg = f"Failed to call {service_name}.{tool_name}: {last_error}"
|
|
345
|
+
self.logger.error(error_msg)
|
|
346
|
+
return SwaigFunctionResult(error_msg)
|
|
347
|
+
|
|
348
|
+
def _hangup_handler(self, args: Dict[str, Any], raw_data: Dict[str, Any]) -> SwaigFunctionResult:
|
|
349
|
+
"""Handle call hangup - cleanup MCP session"""
|
|
350
|
+
# Check for mcp_call_id in global_data first, then fall back to top-level call_id
|
|
351
|
+
global_data = raw_data.get('global_data', {})
|
|
352
|
+
if 'mcp_call_id' in global_data:
|
|
353
|
+
session_id = global_data['mcp_call_id']
|
|
354
|
+
self.logger.info(f"Cleanup using session ID from global_data.mcp_call_id: {session_id}")
|
|
355
|
+
else:
|
|
356
|
+
session_id = raw_data.get('call_id', 'unknown')
|
|
357
|
+
self.logger.info(f"Cleanup using session ID from call_id: {session_id}")
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
response = self._make_request('DELETE', f"{self.gateway_url}/sessions/{session_id}")
|
|
361
|
+
|
|
362
|
+
if response.status_code in [200, 404]:
|
|
363
|
+
self.logger.info(f"Cleaned up MCP session: {session_id}")
|
|
364
|
+
else:
|
|
365
|
+
self.logger.warning(f"Failed to cleanup session: HTTP {response.status_code}")
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
self.logger.error(f"Error cleaning up session: {e}")
|
|
369
|
+
|
|
370
|
+
return SwaigFunctionResult("Session cleanup complete")
|
|
371
|
+
|
|
372
|
+
def get_hints(self) -> List[str]:
|
|
373
|
+
"""Return speech recognition hints"""
|
|
374
|
+
hints = ["MCP", "gateway"]
|
|
375
|
+
|
|
376
|
+
# Add service names as hints
|
|
377
|
+
for service in self.services:
|
|
378
|
+
if isinstance(service, dict) and 'name' in service:
|
|
379
|
+
hints.append(service['name'])
|
|
380
|
+
|
|
381
|
+
return hints
|
|
382
|
+
|
|
383
|
+
def get_global_data(self) -> Dict[str, Any]:
|
|
384
|
+
"""Return global data for DataMap variables"""
|
|
385
|
+
return {
|
|
386
|
+
"mcp_gateway_url": self.gateway_url,
|
|
387
|
+
"mcp_session_id": self.session_id,
|
|
388
|
+
"mcp_services": [s.get('name') if isinstance(s, dict) else str(s)
|
|
389
|
+
for s in self.services]
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
|
393
|
+
"""Return prompt sections to add to agent"""
|
|
394
|
+
sections = []
|
|
395
|
+
|
|
396
|
+
# Build service list for prompt
|
|
397
|
+
service_descriptions = []
|
|
398
|
+
for service in self.services:
|
|
399
|
+
if isinstance(service, dict):
|
|
400
|
+
name = service.get('name', 'Unknown')
|
|
401
|
+
tools = service.get('tools', '*')
|
|
402
|
+
if tools == '*':
|
|
403
|
+
service_descriptions.append(f"{name} (all tools)")
|
|
404
|
+
elif isinstance(tools, list):
|
|
405
|
+
service_descriptions.append(f"{name} ({len(tools)} tools)")
|
|
406
|
+
else:
|
|
407
|
+
service_descriptions.append(str(service))
|
|
408
|
+
|
|
409
|
+
if service_descriptions:
|
|
410
|
+
sections.append({
|
|
411
|
+
"title": "MCP Gateway Integration",
|
|
412
|
+
"body": "You have access to external MCP (Model Context Protocol) services through a gateway.",
|
|
413
|
+
"bullets": [
|
|
414
|
+
f"Connected to gateway at {self.gateway_url}",
|
|
415
|
+
f"Available services: {', '.join(service_descriptions)}",
|
|
416
|
+
f"Functions are prefixed with '{self.tool_prefix}' followed by service name",
|
|
417
|
+
"Each service maintains its own session state throughout the call"
|
|
418
|
+
]
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
return sections
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Native Vector Search Skill
|
|
2
|
+
|
|
3
|
+
The Native Vector Search skill provides document search capabilities using vector similarity and keyword search. It supports multiple storage backends including SQLite (local files) and PostgreSQL with pgvector extension.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Hybrid Search**: Combines vector similarity and keyword search for better results
|
|
8
|
+
- **Multiple Backends**: SQLite for local deployment, pgvector for scalable production use
|
|
9
|
+
- **Remote Search**: Connect to remote search servers
|
|
10
|
+
- **Auto-indexing**: Automatically build indexes from source directories
|
|
11
|
+
- **NLP Enhancement**: Query expansion and synonym matching
|
|
12
|
+
- **Tag Filtering**: Filter results by document tags
|
|
13
|
+
|
|
14
|
+
## Backends
|
|
15
|
+
|
|
16
|
+
### SQLite Backend (Default)
|
|
17
|
+
- Stores indexes in `.swsearch` files
|
|
18
|
+
- Good for single-agent deployments
|
|
19
|
+
- Portable and self-contained
|
|
20
|
+
- No external dependencies
|
|
21
|
+
|
|
22
|
+
### pgvector Backend
|
|
23
|
+
- Uses PostgreSQL with pgvector extension
|
|
24
|
+
- Scalable for multi-agent deployments
|
|
25
|
+
- Real-time updates capability
|
|
26
|
+
- Efficient similarity search with specialized indexes
|
|
27
|
+
|
|
28
|
+
### Remote Search Server
|
|
29
|
+
- Connect to centralized search API
|
|
30
|
+
- Lower memory usage per agent
|
|
31
|
+
- Shared knowledge base
|
|
32
|
+
|
|
33
|
+
## Configuration Parameters
|
|
34
|
+
|
|
35
|
+
### Basic Parameters
|
|
36
|
+
- `tool_name`: Name of the search tool (default: "search_knowledge")
|
|
37
|
+
- `description`: Tool description for the AI
|
|
38
|
+
- `count`: Number of results to return (default: 5)
|
|
39
|
+
- `distance_threshold`: Minimum similarity score (default: 0.0)
|
|
40
|
+
- `tags`: Filter results by these tags
|
|
41
|
+
|
|
42
|
+
### Backend Selection
|
|
43
|
+
- `backend`: Storage backend - "sqlite" or "pgvector" (default: "sqlite")
|
|
44
|
+
|
|
45
|
+
### SQLite Backend
|
|
46
|
+
- `index_file`: Path to .swsearch index file
|
|
47
|
+
- `build_index`: Auto-build index from source (default: false)
|
|
48
|
+
- `source_dir`: Directory to index if build_index=true
|
|
49
|
+
|
|
50
|
+
### pgvector Backend
|
|
51
|
+
- `connection_string`: PostgreSQL connection string (required)
|
|
52
|
+
- `collection_name`: Name of the collection to search (required)
|
|
53
|
+
|
|
54
|
+
### Remote Backend
|
|
55
|
+
- `remote_url`: URL of remote search server
|
|
56
|
+
- `index_name`: Name of index on remote server
|
|
57
|
+
|
|
58
|
+
### Response Formatting
|
|
59
|
+
- `response_prefix`: Text to prepend to results
|
|
60
|
+
- `response_postfix`: Text to append to results
|
|
61
|
+
- `no_results_message`: Message when no results found
|
|
62
|
+
|
|
63
|
+
### NLP Configuration
|
|
64
|
+
- `query_nlp_backend`: NLP backend for queries ("nltk" or "spacy")
|
|
65
|
+
- `index_nlp_backend`: NLP backend for indexing ("nltk" or "spacy")
|
|
66
|
+
|
|
67
|
+
## Usage Examples
|
|
68
|
+
|
|
69
|
+
### SQLite Backend (Local File)
|
|
70
|
+
```python
|
|
71
|
+
agent.add_skill("native_vector_search", {
|
|
72
|
+
"tool_name": "search_docs",
|
|
73
|
+
"description": "Search technical documentation",
|
|
74
|
+
"index_file": "docs.swsearch",
|
|
75
|
+
"count": 5
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### pgvector Backend (PostgreSQL)
|
|
80
|
+
```python
|
|
81
|
+
agent.add_skill("native_vector_search", {
|
|
82
|
+
"tool_name": "search_knowledge",
|
|
83
|
+
"description": "Search the knowledge base",
|
|
84
|
+
"backend": "pgvector",
|
|
85
|
+
"connection_string": "postgresql://user:pass@localhost:5432/knowledge",
|
|
86
|
+
"collection_name": "docs_collection",
|
|
87
|
+
"count": 5
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Remote Search Server
|
|
92
|
+
```python
|
|
93
|
+
agent.add_skill("native_vector_search", {
|
|
94
|
+
"tool_name": "search_api",
|
|
95
|
+
"description": "Search API documentation",
|
|
96
|
+
"remote_url": "http://search-server:8001",
|
|
97
|
+
"index_name": "api_docs"
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Auto-build Index
|
|
102
|
+
```python
|
|
103
|
+
agent.add_skill("native_vector_search", {
|
|
104
|
+
"tool_name": "search_local",
|
|
105
|
+
"build_index": True,
|
|
106
|
+
"source_dir": "./documentation",
|
|
107
|
+
"file_types": ["md", "txt"],
|
|
108
|
+
"index_file": "auto_docs.swsearch"
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Multiple Search Instances
|
|
113
|
+
```python
|
|
114
|
+
# Documentation search
|
|
115
|
+
agent.add_skill("native_vector_search", {
|
|
116
|
+
"tool_name": "search_docs",
|
|
117
|
+
"index_file": "docs.swsearch",
|
|
118
|
+
"description": "Search documentation"
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
# Code examples search
|
|
122
|
+
agent.add_skill("native_vector_search", {
|
|
123
|
+
"tool_name": "search_examples",
|
|
124
|
+
"backend": "pgvector",
|
|
125
|
+
"connection_string": "postgresql://localhost/knowledge",
|
|
126
|
+
"collection_name": "examples",
|
|
127
|
+
"description": "Search code examples"
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Installation
|
|
132
|
+
|
|
133
|
+
### For SQLite Backend
|
|
134
|
+
```bash
|
|
135
|
+
pip install signalwire-agents[search]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### For pgvector Backend
|
|
139
|
+
```bash
|
|
140
|
+
pip install signalwire-agents[search,pgvector]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### For All Features
|
|
144
|
+
```bash
|
|
145
|
+
pip install signalwire-agents[search-all]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Building Indexes
|
|
149
|
+
|
|
150
|
+
### Using sw-search CLI
|
|
151
|
+
|
|
152
|
+
#### SQLite Backend
|
|
153
|
+
```bash
|
|
154
|
+
sw-search ./docs --output docs.swsearch
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### pgvector Backend
|
|
158
|
+
```bash
|
|
159
|
+
sw-search ./docs \
|
|
160
|
+
--backend pgvector \
|
|
161
|
+
--connection-string "postgresql://localhost/knowledge" \
|
|
162
|
+
--output docs_collection
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Performance Considerations
|
|
166
|
+
|
|
167
|
+
### SQLite
|
|
168
|
+
- Fast for small to medium datasets (<100k documents)
|
|
169
|
+
- Linear search for vector similarity
|
|
170
|
+
- Single-file deployment
|
|
171
|
+
|
|
172
|
+
### pgvector
|
|
173
|
+
- Efficient for large datasets
|
|
174
|
+
- Uses IVFFlat or HNSW indexes
|
|
175
|
+
- Handles concurrent access well
|
|
176
|
+
- Requires PostgreSQL server
|
|
177
|
+
|
|
178
|
+
### NLP Backends
|
|
179
|
+
- `nltk`: Fast, good for most use cases (~50-100ms)
|
|
180
|
+
- `spacy`: Better quality, slower (~150-300ms)
|
|
181
|
+
|
|
182
|
+
## Environment Variables
|
|
183
|
+
|
|
184
|
+
None required - all configuration comes through skill parameters.
|
|
185
|
+
|
|
186
|
+
## Troubleshooting
|
|
187
|
+
|
|
188
|
+
### "Search dependencies not available"
|
|
189
|
+
Install the search extras:
|
|
190
|
+
```bash
|
|
191
|
+
pip install signalwire-agents[search]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### "pgvector dependencies not available"
|
|
195
|
+
Install pgvector support:
|
|
196
|
+
```bash
|
|
197
|
+
pip install signalwire-agents[pgvector]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### "Failed to connect to pgvector"
|
|
201
|
+
1. Ensure PostgreSQL is running
|
|
202
|
+
2. Check connection string
|
|
203
|
+
3. Verify pgvector extension is installed
|
|
204
|
+
4. Check collection exists
|
|
205
|
+
|
|
206
|
+
### Poor Search Results
|
|
207
|
+
1. Try different NLP backends
|
|
208
|
+
2. Adjust distance_threshold
|
|
209
|
+
3. Check document preprocessing
|
|
210
|
+
4. Verify index quality
|