signalwire-agents 0.1.10__py3-none-any.whl → 0.1.12__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 +43 -4
- signalwire_agents/agent_server.py +268 -15
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +457 -0
- signalwire_agents/cli/test_swaig.py +2609 -0
- signalwire_agents/core/agent_base.py +691 -82
- signalwire_agents/core/contexts.py +289 -0
- signalwire_agents/core/data_map.py +499 -0
- signalwire_agents/core/function_result.py +57 -10
- signalwire_agents/core/logging_config.py +232 -0
- signalwire_agents/core/skill_base.py +27 -37
- signalwire_agents/core/skill_manager.py +89 -23
- signalwire_agents/core/swaig_function.py +13 -1
- signalwire_agents/core/swml_handler.py +37 -13
- signalwire_agents/core/swml_service.py +37 -28
- signalwire_agents/search/__init__.py +131 -0
- signalwire_agents/search/document_processor.py +764 -0
- signalwire_agents/search/index_builder.py +534 -0
- signalwire_agents/search/query_processor.py +371 -0
- signalwire_agents/search/search_engine.py +383 -0
- signalwire_agents/search/search_service.py +251 -0
- signalwire_agents/skills/datasphere/__init__.py +12 -0
- signalwire_agents/skills/datasphere/skill.py +229 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
- signalwire_agents/skills/datetime/skill.py +9 -5
- signalwire_agents/skills/joke/__init__.py +1 -0
- signalwire_agents/skills/joke/skill.py +88 -0
- signalwire_agents/skills/math/skill.py +9 -6
- signalwire_agents/skills/native_vector_search/__init__.py +1 -0
- signalwire_agents/skills/native_vector_search/skill.py +352 -0
- signalwire_agents/skills/registry.py +10 -4
- signalwire_agents/skills/web_search/skill.py +57 -21
- signalwire_agents/skills/wikipedia/__init__.py +9 -0
- signalwire_agents/skills/wikipedia/skill.py +180 -0
- signalwire_agents/utils/__init__.py +14 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents-0.1.12.dist-info/METADATA +863 -0
- signalwire_agents-0.1.12.dist-info/RECORD +67 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.12.dist-info/entry_points.txt +3 -0
- signalwire_agents-0.1.10.dist-info/METADATA +0 -319
- signalwire_agents-0.1.10.dist-info/RECORD +0 -44
- {signalwire_agents-0.1.10.data → signalwire_agents-0.1.12.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,352 @@
|
|
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 os
|
11
|
+
import tempfile
|
12
|
+
import shutil
|
13
|
+
from typing import Dict, Any, Optional, List
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
from signalwire_agents.core.skill_base import SkillBase
|
17
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
18
|
+
|
19
|
+
class NativeVectorSearchSkill(SkillBase):
|
20
|
+
"""Native vector search capability using local document indexes or remote search servers"""
|
21
|
+
|
22
|
+
SKILL_NAME = "native_vector_search"
|
23
|
+
SKILL_DESCRIPTION = "Search document indexes using vector similarity and keyword search (local or remote)"
|
24
|
+
SKILL_VERSION = "1.0.0"
|
25
|
+
REQUIRED_PACKAGES = [] # Optional packages checked at runtime
|
26
|
+
REQUIRED_ENV_VARS = [] # No required env vars since all config comes from params
|
27
|
+
|
28
|
+
# Enable multiple instances support
|
29
|
+
SUPPORTS_MULTIPLE_INSTANCES = True
|
30
|
+
|
31
|
+
def get_instance_key(self) -> str:
|
32
|
+
"""
|
33
|
+
Get the key used to track this skill instance
|
34
|
+
|
35
|
+
For native vector search, we use the tool name to differentiate instances
|
36
|
+
"""
|
37
|
+
tool_name = self.params.get('tool_name', 'search_knowledge')
|
38
|
+
index_file = self.params.get('index_file', 'default')
|
39
|
+
return f"{self.SKILL_NAME}_{tool_name}_{index_file}"
|
40
|
+
|
41
|
+
def setup(self) -> bool:
|
42
|
+
"""Setup the native vector search skill"""
|
43
|
+
|
44
|
+
# Check if search functionality is available
|
45
|
+
try:
|
46
|
+
from signalwire_agents.search import IndexBuilder, SearchEngine
|
47
|
+
from signalwire_agents.search.query_processor import preprocess_query
|
48
|
+
self.search_available = True
|
49
|
+
except ImportError as e:
|
50
|
+
self.search_available = False
|
51
|
+
self.import_error = str(e)
|
52
|
+
self.logger.warning(f"Search dependencies not available: {e}")
|
53
|
+
# Don't fail setup - we'll provide helpful error messages at runtime
|
54
|
+
|
55
|
+
# Get configuration
|
56
|
+
self.tool_name = self.params.get('tool_name', 'search_knowledge')
|
57
|
+
self.index_file = self.params.get('index_file')
|
58
|
+
self.build_index = self.params.get('build_index', False)
|
59
|
+
self.source_dir = self.params.get('source_dir')
|
60
|
+
self.count = self.params.get('count', 5)
|
61
|
+
self.distance_threshold = self.params.get('distance_threshold', 0.0)
|
62
|
+
self.tags = self.params.get('tags', [])
|
63
|
+
self.no_results_message = self.params.get(
|
64
|
+
'no_results_message',
|
65
|
+
"No information found for '{query}'"
|
66
|
+
)
|
67
|
+
self.response_prefix = self.params.get('response_prefix', '')
|
68
|
+
self.response_postfix = self.params.get('response_postfix', '')
|
69
|
+
|
70
|
+
# Remote search server configuration
|
71
|
+
self.remote_url = self.params.get('remote_url') # e.g., "http://localhost:8001"
|
72
|
+
self.index_name = self.params.get('index_name', 'default') # For remote searches
|
73
|
+
|
74
|
+
# SWAIG fields for function fillers
|
75
|
+
self.swaig_fields = self.params.get('swaig_fields', {})
|
76
|
+
|
77
|
+
# NLP backend configuration
|
78
|
+
self.nlp_backend = self.params.get('nlp_backend', 'nltk') # Default to faster NLTK
|
79
|
+
if self.nlp_backend not in ['nltk', 'spacy']:
|
80
|
+
self.logger.warning(f"Invalid nlp_backend '{self.nlp_backend}', using 'nltk'")
|
81
|
+
self.nlp_backend = 'nltk'
|
82
|
+
|
83
|
+
# Auto-build index if requested and search is available
|
84
|
+
if self.build_index and self.source_dir and self.search_available:
|
85
|
+
if not self.index_file:
|
86
|
+
# Generate index filename from source directory
|
87
|
+
source_name = Path(self.source_dir).name
|
88
|
+
self.index_file = f"{source_name}.swsearch"
|
89
|
+
|
90
|
+
# Build index if it doesn't exist
|
91
|
+
if not os.path.exists(self.index_file):
|
92
|
+
try:
|
93
|
+
self.logger.info(f"Building search index from {self.source_dir}...")
|
94
|
+
from signalwire_agents.search import IndexBuilder
|
95
|
+
|
96
|
+
builder = IndexBuilder(verbose=self.params.get('verbose', False))
|
97
|
+
builder.build_index(
|
98
|
+
source_dir=self.source_dir,
|
99
|
+
output_file=self.index_file,
|
100
|
+
file_types=self.params.get('file_types', ['md', 'txt']),
|
101
|
+
exclude_patterns=self.params.get('exclude_patterns'),
|
102
|
+
tags=self.params.get('global_tags')
|
103
|
+
)
|
104
|
+
self.logger.info(f"Search index created: {self.index_file}")
|
105
|
+
except Exception as e:
|
106
|
+
self.logger.error(f"Failed to build search index: {e}")
|
107
|
+
self.search_available = False
|
108
|
+
|
109
|
+
# Initialize search engine
|
110
|
+
self.search_engine = None
|
111
|
+
if self.search_available and self.index_file and os.path.exists(self.index_file):
|
112
|
+
try:
|
113
|
+
from signalwire_agents.search import SearchEngine
|
114
|
+
self.search_engine = SearchEngine(self.index_file)
|
115
|
+
except Exception as e:
|
116
|
+
self.logger.error(f"Failed to load search index {self.index_file}: {e}")
|
117
|
+
self.search_available = False
|
118
|
+
|
119
|
+
# Check if we should use remote search mode
|
120
|
+
self.use_remote = bool(self.remote_url)
|
121
|
+
if self.use_remote:
|
122
|
+
self.logger.info(f"Using remote search server: {self.remote_url}")
|
123
|
+
# Test remote connection
|
124
|
+
try:
|
125
|
+
import requests
|
126
|
+
response = requests.get(f"{self.remote_url}/health", timeout=5)
|
127
|
+
if response.status_code == 200:
|
128
|
+
self.logger.info("Remote search server is available")
|
129
|
+
self.search_available = True
|
130
|
+
else:
|
131
|
+
self.logger.error(f"Remote search server returned status {response.status_code}")
|
132
|
+
self.search_available = False
|
133
|
+
except Exception as e:
|
134
|
+
self.logger.error(f"Failed to connect to remote search server: {e}")
|
135
|
+
self.search_available = False
|
136
|
+
|
137
|
+
return True
|
138
|
+
|
139
|
+
def register_tools(self) -> None:
|
140
|
+
"""Register native vector search tool with the agent"""
|
141
|
+
|
142
|
+
# Get description from params or use default
|
143
|
+
description = self.params.get(
|
144
|
+
'description',
|
145
|
+
'Search the local knowledge base for information'
|
146
|
+
)
|
147
|
+
|
148
|
+
self.agent.define_tool(
|
149
|
+
name=self.tool_name,
|
150
|
+
description=description,
|
151
|
+
parameters={
|
152
|
+
"query": {
|
153
|
+
"type": "string",
|
154
|
+
"description": "Search query or question"
|
155
|
+
},
|
156
|
+
"count": {
|
157
|
+
"type": "integer",
|
158
|
+
"description": f"Number of results to return (default: {self.count})",
|
159
|
+
"default": self.count
|
160
|
+
}
|
161
|
+
},
|
162
|
+
handler=self._search_handler,
|
163
|
+
**self.swaig_fields
|
164
|
+
)
|
165
|
+
|
166
|
+
def _search_handler(self, args, raw_data):
|
167
|
+
"""Handle search requests"""
|
168
|
+
|
169
|
+
if not self.search_available:
|
170
|
+
return SwaigFunctionResult(
|
171
|
+
f"Search functionality is not available. {getattr(self, 'import_error', '')}\n"
|
172
|
+
f"Install with: pip install signalwire-agents[search]"
|
173
|
+
)
|
174
|
+
|
175
|
+
if not self.use_remote and not self.search_engine:
|
176
|
+
return SwaigFunctionResult(
|
177
|
+
f"Search index not available. "
|
178
|
+
f"{'Index file not found: ' + (self.index_file or 'not specified') if self.index_file else 'No index file configured'}"
|
179
|
+
)
|
180
|
+
|
181
|
+
query = args.get('query', '').strip()
|
182
|
+
if not query:
|
183
|
+
return SwaigFunctionResult("Please provide a search query.")
|
184
|
+
|
185
|
+
count = args.get('count', self.count)
|
186
|
+
|
187
|
+
try:
|
188
|
+
# Preprocess the query
|
189
|
+
from signalwire_agents.search.query_processor import preprocess_query
|
190
|
+
enhanced = preprocess_query(query, language='en', vector=True, nlp_backend=self.nlp_backend)
|
191
|
+
|
192
|
+
# Perform search (local or remote)
|
193
|
+
if self.use_remote:
|
194
|
+
results = self._search_remote(query, enhanced, count)
|
195
|
+
else:
|
196
|
+
results = self.search_engine.search(
|
197
|
+
query_vector=enhanced.get('vector', []),
|
198
|
+
enhanced_text=enhanced['enhanced_text'],
|
199
|
+
count=count,
|
200
|
+
distance_threshold=self.distance_threshold,
|
201
|
+
tags=self.tags
|
202
|
+
)
|
203
|
+
|
204
|
+
if not results:
|
205
|
+
no_results_msg = self.no_results_message.format(query=query)
|
206
|
+
if self.response_prefix:
|
207
|
+
no_results_msg = f"{self.response_prefix} {no_results_msg}"
|
208
|
+
if self.response_postfix:
|
209
|
+
no_results_msg = f"{no_results_msg} {self.response_postfix}"
|
210
|
+
return SwaigFunctionResult(no_results_msg)
|
211
|
+
|
212
|
+
# Format results
|
213
|
+
response_parts = []
|
214
|
+
|
215
|
+
# Add response prefix if configured
|
216
|
+
if self.response_prefix:
|
217
|
+
response_parts.append(self.response_prefix)
|
218
|
+
|
219
|
+
response_parts.append(f"Found {len(results)} relevant results for '{query}':\n")
|
220
|
+
|
221
|
+
for i, result in enumerate(results, 1):
|
222
|
+
filename = result['metadata']['filename']
|
223
|
+
section = result['metadata'].get('section', '')
|
224
|
+
score = result['score']
|
225
|
+
content = result['content']
|
226
|
+
|
227
|
+
result_text = f"**Result {i}** (from {filename}"
|
228
|
+
if section:
|
229
|
+
result_text += f", section: {section}"
|
230
|
+
result_text += f", relevance: {score:.2f})\n{content}\n"
|
231
|
+
|
232
|
+
response_parts.append(result_text)
|
233
|
+
|
234
|
+
# Add response postfix if configured
|
235
|
+
if self.response_postfix:
|
236
|
+
response_parts.append(self.response_postfix)
|
237
|
+
|
238
|
+
return SwaigFunctionResult("\n".join(response_parts))
|
239
|
+
|
240
|
+
except Exception as e:
|
241
|
+
return SwaigFunctionResult(f"Search error: {str(e)}")
|
242
|
+
|
243
|
+
def _search_remote(self, query: str, enhanced: dict, count: int) -> list:
|
244
|
+
"""Perform search using remote search server"""
|
245
|
+
try:
|
246
|
+
import requests
|
247
|
+
|
248
|
+
search_request = {
|
249
|
+
"query": query,
|
250
|
+
"index_name": self.index_name,
|
251
|
+
"count": count,
|
252
|
+
"distance": self.distance_threshold,
|
253
|
+
"tags": self.tags,
|
254
|
+
"language": "en"
|
255
|
+
}
|
256
|
+
|
257
|
+
response = requests.post(
|
258
|
+
f"{self.remote_url}/search",
|
259
|
+
json=search_request,
|
260
|
+
timeout=30
|
261
|
+
)
|
262
|
+
|
263
|
+
if response.status_code == 200:
|
264
|
+
data = response.json()
|
265
|
+
# Convert remote response format to local format
|
266
|
+
results = []
|
267
|
+
for result in data.get('results', []):
|
268
|
+
results.append({
|
269
|
+
'content': result['content'],
|
270
|
+
'score': result['score'],
|
271
|
+
'metadata': result['metadata']
|
272
|
+
})
|
273
|
+
return results
|
274
|
+
else:
|
275
|
+
self.logger.error(f"Remote search failed with status {response.status_code}: {response.text}")
|
276
|
+
return []
|
277
|
+
|
278
|
+
except Exception as e:
|
279
|
+
self.logger.error(f"Remote search error: {e}")
|
280
|
+
return []
|
281
|
+
|
282
|
+
def get_hints(self) -> List[str]:
|
283
|
+
"""Return speech recognition hints for this skill"""
|
284
|
+
hints = [
|
285
|
+
"search",
|
286
|
+
"find",
|
287
|
+
"look up",
|
288
|
+
"documentation",
|
289
|
+
"knowledge base"
|
290
|
+
]
|
291
|
+
|
292
|
+
# Add custom hints from params
|
293
|
+
custom_hints = self.params.get('hints', [])
|
294
|
+
hints.extend(custom_hints)
|
295
|
+
|
296
|
+
return hints
|
297
|
+
|
298
|
+
def get_global_data(self) -> Dict[str, Any]:
|
299
|
+
"""Return data to add to agent's global context"""
|
300
|
+
global_data = {}
|
301
|
+
|
302
|
+
if self.search_engine:
|
303
|
+
try:
|
304
|
+
stats = self.search_engine.get_stats()
|
305
|
+
global_data['search_stats'] = stats
|
306
|
+
except:
|
307
|
+
pass
|
308
|
+
|
309
|
+
return global_data
|
310
|
+
|
311
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
312
|
+
"""Return prompt sections to add to agent"""
|
313
|
+
search_mode = "remote search server" if self.use_remote else "local document indexes"
|
314
|
+
return [
|
315
|
+
{
|
316
|
+
"title": "Document Search",
|
317
|
+
"body": f"You can search {search_mode} using the {self.tool_name} tool.",
|
318
|
+
"bullets": [
|
319
|
+
f"Use the {self.tool_name} tool when users ask questions about topics that might be in the indexed documents",
|
320
|
+
"Search for relevant information using clear, specific queries",
|
321
|
+
"Provide helpful summaries of the search results",
|
322
|
+
"If no results are found, suggest the user try rephrasing their question or ask about different topics"
|
323
|
+
]
|
324
|
+
}
|
325
|
+
]
|
326
|
+
|
327
|
+
def _add_prompt_section(self, agent):
|
328
|
+
"""Add prompt section to agent (called during skill loading)"""
|
329
|
+
try:
|
330
|
+
agent.prompt_add_section(
|
331
|
+
title="Local Document Search",
|
332
|
+
body=f"You can search local document indexes using the {self.tool_name} tool.",
|
333
|
+
bullets=[
|
334
|
+
f"Use the {self.tool_name} tool when users ask questions about topics that might be in the indexed documents",
|
335
|
+
"Search for relevant information using clear, specific queries",
|
336
|
+
"Provide helpful summaries of the search results",
|
337
|
+
"If no results are found, suggest the user try rephrasing their question or ask about different topics"
|
338
|
+
]
|
339
|
+
)
|
340
|
+
except Exception as e:
|
341
|
+
self.logger.error(f"Failed to add prompt section: {e}")
|
342
|
+
# Continue without the prompt section
|
343
|
+
|
344
|
+
def cleanup(self) -> None:
|
345
|
+
"""Cleanup when skill is removed or agent shuts down"""
|
346
|
+
# Clean up any temporary files if we created them
|
347
|
+
if hasattr(self, '_temp_dirs'):
|
348
|
+
for temp_dir in self._temp_dirs:
|
349
|
+
try:
|
350
|
+
shutil.rmtree(temp_dir)
|
351
|
+
except:
|
352
|
+
pass
|
@@ -11,18 +11,19 @@ import os
|
|
11
11
|
import importlib
|
12
12
|
import importlib.util
|
13
13
|
import inspect
|
14
|
+
import sys
|
14
15
|
from typing import Dict, List, Type, Optional
|
15
16
|
from pathlib import Path
|
16
|
-
import logging
|
17
17
|
|
18
18
|
from signalwire_agents.core.skill_base import SkillBase
|
19
|
+
from signalwire_agents.core.logging_config import get_logger
|
19
20
|
|
20
21
|
class SkillRegistry:
|
21
22
|
"""Global registry for discovering and managing skills"""
|
22
23
|
|
23
24
|
def __init__(self):
|
24
25
|
self._skills: Dict[str, Type[SkillBase]] = {}
|
25
|
-
self.logger =
|
26
|
+
self.logger = get_logger("skill_registry")
|
26
27
|
self._discovered = False
|
27
28
|
|
28
29
|
def discover_skills(self) -> None:
|
@@ -39,7 +40,11 @@ class SkillRegistry:
|
|
39
40
|
self._load_skill_from_directory(item)
|
40
41
|
|
41
42
|
self._discovered = True
|
42
|
-
|
43
|
+
|
44
|
+
# Check if we're in raw mode (used by swaig-test --raw) and suppress logging
|
45
|
+
is_raw_mode = "--raw" in sys.argv
|
46
|
+
if not is_raw_mode:
|
47
|
+
self.logger.info(f"Discovered {len(self._skills)} skills")
|
43
48
|
|
44
49
|
def _load_skill_from_directory(self, skill_dir: Path) -> None:
|
45
50
|
"""Load a skill from a directory"""
|
@@ -89,7 +94,8 @@ class SkillRegistry:
|
|
89
94
|
"description": skill_class.SKILL_DESCRIPTION,
|
90
95
|
"version": skill_class.SKILL_VERSION,
|
91
96
|
"required_packages": skill_class.REQUIRED_PACKAGES,
|
92
|
-
"required_env_vars": skill_class.REQUIRED_ENV_VARS
|
97
|
+
"required_env_vars": skill_class.REQUIRED_ENV_VARS,
|
98
|
+
"supports_multiple_instances": skill_class.SUPPORTS_MULTIPLE_INSTANCES
|
93
99
|
}
|
94
100
|
for skill_class in self._skills.values()
|
95
101
|
]
|
@@ -21,9 +21,10 @@ from signalwire_agents.core.function_result import SwaigFunctionResult
|
|
21
21
|
class GoogleSearchScraper:
|
22
22
|
"""Google Search and Web Scraping functionality"""
|
23
23
|
|
24
|
-
def __init__(self, api_key: str, search_engine_id: str):
|
24
|
+
def __init__(self, api_key: str, search_engine_id: str, max_content_length: int = 2000):
|
25
25
|
self.api_key = api_key
|
26
26
|
self.search_engine_id = search_engine_id
|
27
|
+
self.max_content_length = max_content_length
|
27
28
|
self.session = requests.Session()
|
28
29
|
self.session.headers.update({
|
29
30
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
@@ -81,8 +82,8 @@ class GoogleSearchScraper:
|
|
81
82
|
text = ' '.join(chunk for chunk in chunks if chunk)
|
82
83
|
|
83
84
|
# Limit text length
|
84
|
-
if len(text) >
|
85
|
-
text = text[:
|
85
|
+
if len(text) > self.max_content_length:
|
86
|
+
text = text[:self.max_content_length] + "... [Content truncated]"
|
86
87
|
|
87
88
|
return text
|
88
89
|
|
@@ -128,29 +129,63 @@ class WebSearchSkill(SkillBase):
|
|
128
129
|
SKILL_DESCRIPTION = "Search the web for information using Google Custom Search API"
|
129
130
|
SKILL_VERSION = "1.0.0"
|
130
131
|
REQUIRED_PACKAGES = ["bs4", "requests"]
|
131
|
-
REQUIRED_ENV_VARS = [
|
132
|
+
REQUIRED_ENV_VARS = [] # No required env vars since all config comes from params
|
133
|
+
|
134
|
+
# Enable multiple instances support
|
135
|
+
SUPPORTS_MULTIPLE_INSTANCES = True
|
136
|
+
|
137
|
+
def get_instance_key(self) -> str:
|
138
|
+
"""
|
139
|
+
Get the key used to track this skill instance
|
140
|
+
|
141
|
+
For web search, we use the search_engine_id to differentiate instances
|
142
|
+
"""
|
143
|
+
search_engine_id = self.params.get('search_engine_id', 'default')
|
144
|
+
tool_name = self.params.get('tool_name', 'web_search')
|
145
|
+
return f"{self.SKILL_NAME}_{search_engine_id}_{tool_name}"
|
132
146
|
|
133
147
|
def setup(self) -> bool:
|
134
148
|
"""Setup the web search skill"""
|
135
|
-
|
149
|
+
# Validate required parameters
|
150
|
+
required_params = ['api_key', 'search_engine_id']
|
151
|
+
missing_params = [param for param in required_params if not self.params.get(param)]
|
152
|
+
if missing_params:
|
153
|
+
self.logger.error(f"Missing required parameters: {missing_params}")
|
154
|
+
return False
|
155
|
+
|
156
|
+
if not self.validate_packages():
|
136
157
|
return False
|
137
158
|
|
159
|
+
# Set parameters from config
|
160
|
+
self.api_key = self.params['api_key']
|
161
|
+
self.search_engine_id = self.params['search_engine_id']
|
162
|
+
|
138
163
|
# Set default parameters
|
139
164
|
self.default_num_results = self.params.get('num_results', 1)
|
140
165
|
self.default_delay = self.params.get('delay', 0)
|
166
|
+
self.max_content_length = self.params.get('max_content_length', 2000)
|
167
|
+
self.no_results_message = self.params.get('no_results_message',
|
168
|
+
"I couldn't find any results for '{query}'. "
|
169
|
+
"This might be due to a very specific query or temporary issues. "
|
170
|
+
"Try rephrasing your search or asking about a different topic."
|
171
|
+
)
|
172
|
+
|
173
|
+
# Tool name (for multiple instances)
|
174
|
+
self.tool_name = self.params.get('tool_name', 'web_search')
|
141
175
|
|
142
176
|
# Initialize the search scraper
|
143
177
|
self.search_scraper = GoogleSearchScraper(
|
144
|
-
api_key=
|
145
|
-
search_engine_id=
|
178
|
+
api_key=self.api_key,
|
179
|
+
search_engine_id=self.search_engine_id,
|
180
|
+
max_content_length=self.max_content_length
|
146
181
|
)
|
147
182
|
|
148
183
|
return True
|
149
184
|
|
150
185
|
def register_tools(self) -> None:
|
151
186
|
"""Register web search tool with the agent"""
|
152
|
-
self.
|
153
|
-
name=
|
187
|
+
self.agent.define_tool(
|
188
|
+
name=self.tool_name,
|
154
189
|
description="Search the web for information on any topic and return detailed results with content from multiple sources",
|
155
190
|
parameters={
|
156
191
|
"query": {
|
@@ -158,7 +193,8 @@ class WebSearchSkill(SkillBase):
|
|
158
193
|
"description": "The search query - what you want to find information about"
|
159
194
|
}
|
160
195
|
},
|
161
|
-
handler=self._web_search_handler
|
196
|
+
handler=self._web_search_handler,
|
197
|
+
**self.swaig_fields
|
162
198
|
)
|
163
199
|
|
164
200
|
def _web_search_handler(self, args, raw_data):
|
@@ -184,11 +220,9 @@ class WebSearchSkill(SkillBase):
|
|
184
220
|
)
|
185
221
|
|
186
222
|
if not search_results or "No search results found" in search_results:
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
"Try rephrasing your search or asking about a different topic."
|
191
|
-
)
|
223
|
+
# Format the no results message with the query if it contains a placeholder
|
224
|
+
formatted_message = self.no_results_message.format(query=query) if '{query}' in self.no_results_message else self.no_results_message
|
225
|
+
return SwaigFunctionResult(formatted_message)
|
192
226
|
|
193
227
|
response = f"I found {num_results} results for '{query}':\n\n{search_results}"
|
194
228
|
return SwaigFunctionResult(response)
|
@@ -201,10 +235,12 @@ class WebSearchSkill(SkillBase):
|
|
201
235
|
|
202
236
|
def get_hints(self) -> List[str]:
|
203
237
|
"""Return speech recognition hints"""
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
238
|
+
# Currently no hints provided, but you could add them like:
|
239
|
+
# return [
|
240
|
+
# "Google", "search", "internet", "web", "information",
|
241
|
+
# "find", "look up", "research", "query", "results"
|
242
|
+
# ]
|
243
|
+
return []
|
208
244
|
|
209
245
|
def get_global_data(self) -> Dict[str, Any]:
|
210
246
|
"""Return global data for agent context"""
|
@@ -218,9 +254,9 @@ class WebSearchSkill(SkillBase):
|
|
218
254
|
return [
|
219
255
|
{
|
220
256
|
"title": "Web Search Capability",
|
221
|
-
"body": "You can search the internet for current, accurate information on any topic.",
|
257
|
+
"body": f"You can search the internet for current, accurate information on any topic using the {self.tool_name} tool.",
|
222
258
|
"bullets": [
|
223
|
-
"Use the
|
259
|
+
f"Use the {self.tool_name} tool when users ask for information you need to look up",
|
224
260
|
"Search for news, current events, product information, or any current data",
|
225
261
|
"Summarize search results in a clear, helpful way",
|
226
262
|
"Include relevant URLs so users can read more if interested"
|