signalwire-agents 0.1.37__py3-none-any.whl → 0.1.38__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 (28) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/cli/build_search.py +95 -19
  3. signalwire_agents/core/agent_base.py +38 -0
  4. signalwire_agents/core/mixins/ai_config_mixin.py +120 -0
  5. signalwire_agents/core/skill_manager.py +47 -0
  6. signalwire_agents/search/index_builder.py +105 -10
  7. signalwire_agents/search/pgvector_backend.py +523 -0
  8. signalwire_agents/search/search_engine.py +41 -4
  9. signalwire_agents/search/search_service.py +86 -35
  10. signalwire_agents/skills/api_ninjas_trivia/skill.py +37 -1
  11. signalwire_agents/skills/datasphere/skill.py +82 -0
  12. signalwire_agents/skills/datasphere_serverless/skill.py +82 -0
  13. signalwire_agents/skills/joke/skill.py +21 -0
  14. signalwire_agents/skills/mcp_gateway/skill.py +82 -0
  15. signalwire_agents/skills/native_vector_search/README.md +210 -0
  16. signalwire_agents/skills/native_vector_search/skill.py +197 -7
  17. signalwire_agents/skills/play_background_file/skill.py +36 -0
  18. signalwire_agents/skills/registry.py +36 -0
  19. signalwire_agents/skills/spider/skill.py +113 -0
  20. signalwire_agents/skills/swml_transfer/skill.py +90 -0
  21. signalwire_agents/skills/weather_api/skill.py +28 -0
  22. signalwire_agents/skills/wikipedia_search/skill.py +22 -0
  23. {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/METADATA +53 -1
  24. {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/RECORD +28 -26
  25. {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/WHEEL +0 -0
  26. {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/entry_points.txt +0 -0
  27. {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/licenses/LICENSE +0 -0
  28. {signalwire_agents-0.1.37.dist-info → signalwire_agents-0.1.38.dist-info}/top_level.txt +0 -0
@@ -82,16 +82,21 @@ else:
82
82
  self.query_analysis = query_analysis
83
83
 
84
84
  class SearchService:
85
- """Local search service with HTTP API"""
85
+ """Local search service with HTTP API supporting both SQLite and pgvector backends"""
86
86
 
87
87
  def __init__(self, port: int = 8001, indexes: Dict[str, str] = None,
88
88
  basic_auth: Optional[Tuple[str, str]] = None,
89
- config_file: Optional[str] = None):
89
+ config_file: Optional[str] = None,
90
+ backend: str = 'sqlite',
91
+ connection_string: Optional[str] = None):
90
92
  # Load configuration first
91
93
  self._load_config(config_file)
92
94
 
93
95
  # Override with constructor params if provided
94
96
  self.port = port
97
+ self.backend = backend
98
+ self.connection_string = connection_string
99
+
95
100
  if indexes is not None:
96
101
  self.indexes = indexes
97
102
 
@@ -119,6 +124,8 @@ class SearchService:
119
124
  """Load configuration from file if available"""
120
125
  # Initialize defaults
121
126
  self.indexes = {}
127
+ self.backend = 'sqlite'
128
+ self.connection_string = None
122
129
 
123
130
  # Find config file
124
131
  if not config_file:
@@ -140,6 +147,12 @@ class SearchService:
140
147
  if 'port' in service_config:
141
148
  self.port = int(service_config['port'])
142
149
 
150
+ if 'backend' in service_config:
151
+ self.backend = service_config['backend']
152
+
153
+ if 'connection_string' in service_config:
154
+ self.connection_string = service_config['connection_string']
155
+
143
156
  if 'indexes' in service_config and isinstance(service_config['indexes'], dict):
144
157
  self.indexes = service_config['indexes']
145
158
 
@@ -225,9 +238,11 @@ class SearchService:
225
238
  async def health():
226
239
  return {
227
240
  "status": "healthy",
241
+ "backend": self.backend,
228
242
  "indexes": list(self.indexes.keys()),
229
243
  "ssl_enabled": self.security.ssl_enabled,
230
- "auth_required": bool(security)
244
+ "auth_required": bool(security),
245
+ "connection_string": self.connection_string if self.backend == 'pgvector' else None
231
246
  }
232
247
 
233
248
  @self.app.post("/reload_index")
@@ -236,47 +251,83 @@ class SearchService:
236
251
  index_path: str,
237
252
  credentials: HTTPBasicCredentials = None if not security else Depends(security)
238
253
  ):
239
- """Reload or add new index"""
254
+ """Reload or add new index/collection"""
240
255
  if security:
241
256
  self._get_current_username(credentials)
242
-
243
- self.indexes[index_name] = index_path
244
- self.search_engines[index_name] = SearchEngine(index_path, self.model)
245
- return {"status": "reloaded", "index": index_name}
257
+
258
+ if self.backend == 'pgvector':
259
+ # For pgvector, index_path is actually the collection name
260
+ self.indexes[index_name] = index_path
261
+ try:
262
+ self.search_engines[index_name] = SearchEngine(
263
+ backend='pgvector',
264
+ connection_string=self.connection_string,
265
+ collection_name=index_path
266
+ )
267
+ return {"status": "reloaded", "index": index_name, "backend": "pgvector"}
268
+ except Exception as e:
269
+ raise HTTPException(status_code=500, detail=f"Failed to load pgvector collection: {e}")
270
+ else:
271
+ # SQLite backend
272
+ self.indexes[index_name] = index_path
273
+ self.search_engines[index_name] = SearchEngine(index_path, self.model)
274
+ return {"status": "reloaded", "index": index_name, "backend": "sqlite"}
246
275
 
247
276
  def _load_resources(self):
248
277
  """Load embedding model and search indexes"""
249
- # Load model (shared across all indexes)
250
- if self.indexes and SentenceTransformer:
251
- # Get model name from first index
252
- sample_index = next(iter(self.indexes.values()))
253
- model_name = self._get_model_name(sample_index)
254
- try:
255
- self.model = SentenceTransformer(model_name)
256
- except Exception as e:
257
- logger.warning(f"Could not load sentence transformer model: {e}")
258
- self.model = None
259
-
260
- # Load search engines for each index
261
- for index_name, index_path in self.indexes.items():
262
- try:
263
- self.search_engines[index_name] = SearchEngine(index_path, self.model)
264
- except Exception as e:
265
- logger.error(f"Error loading search engine for {index_name}: {e}")
278
+ if self.backend == 'pgvector':
279
+ # For pgvector, we don't need to load a model locally
280
+ # The embeddings are already stored in the database
281
+ # Load search engines for each collection
282
+ for collection_name in self.indexes.keys():
283
+ try:
284
+ self.search_engines[collection_name] = SearchEngine(
285
+ backend='pgvector',
286
+ connection_string=self.connection_string,
287
+ collection_name=collection_name
288
+ )
289
+ logger.info(f"Loaded pgvector collection: {collection_name}")
290
+ except Exception as e:
291
+ logger.error(f"Error loading pgvector collection {collection_name}: {e}")
292
+ else:
293
+ # SQLite backend - original behavior
294
+ # Load model (shared across all indexes)
295
+ if self.indexes and SentenceTransformer:
296
+ # Get model name from first index
297
+ sample_index = next(iter(self.indexes.values()))
298
+ model_name = self._get_model_name(sample_index)
299
+ try:
300
+ self.model = SentenceTransformer(model_name)
301
+ except Exception as e:
302
+ logger.warning(f"Could not load sentence transformer model: {e}")
303
+ self.model = None
304
+
305
+ # Load search engines for each index
306
+ for index_name, index_path in self.indexes.items():
307
+ try:
308
+ self.search_engines[index_name] = SearchEngine(index_path, self.model)
309
+ except Exception as e:
310
+ logger.error(f"Error loading search engine for {index_name}: {e}")
266
311
 
267
312
  def _get_model_name(self, index_path: str) -> str:
268
313
  """Get embedding model name from index config"""
269
- try:
270
- import sqlite3
271
- conn = sqlite3.connect(index_path)
272
- cursor = conn.cursor()
273
- cursor.execute("SELECT value FROM config WHERE key = 'embedding_model'")
274
- result = cursor.fetchone()
275
- conn.close()
276
- return result[0] if result else 'sentence-transformers/all-mpnet-base-v2'
277
- except Exception as e:
278
- logger.warning(f"Could not get model name from index: {e}")
314
+ if self.backend == 'pgvector':
315
+ # For pgvector, we might want to store model info in the database
316
+ # For now, return default model
279
317
  return 'sentence-transformers/all-mpnet-base-v2'
318
+ else:
319
+ # SQLite backend
320
+ try:
321
+ import sqlite3
322
+ conn = sqlite3.connect(index_path)
323
+ cursor = conn.cursor()
324
+ cursor.execute("SELECT value FROM config WHERE key = 'embedding_model'")
325
+ result = cursor.fetchone()
326
+ conn.close()
327
+ return result[0] if result else 'sentence-transformers/all-mpnet-base-v2'
328
+ except Exception as e:
329
+ logger.warning(f"Could not get model name from index: {e}")
330
+ return 'sentence-transformers/all-mpnet-base-v2'
280
331
 
281
332
  async def _handle_search(self, request: SearchRequest) -> SearchResponse:
282
333
  """Handle search request"""
@@ -198,4 +198,40 @@ class ApiNinjasTriviaSkill(SkillBase):
198
198
  }
199
199
  }
200
200
 
201
- return [tool]
201
+ return [tool]
202
+
203
+ @classmethod
204
+ def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
205
+ """
206
+ Get the parameter schema for the API Ninjas Trivia skill.
207
+
208
+ Returns parameter definitions for GUI configuration.
209
+ """
210
+ schema = super().get_parameter_schema()
211
+
212
+ # Build categories enum description
213
+ category_options = []
214
+ for key, desc in cls.VALID_CATEGORIES.items():
215
+ category_options.append(f"{key} ({desc})")
216
+
217
+ schema.update({
218
+ "api_key": {
219
+ "type": "string",
220
+ "description": "API Ninjas API key",
221
+ "required": True,
222
+ "hidden": True,
223
+ "env_var": "API_NINJAS_KEY"
224
+ },
225
+ "categories": {
226
+ "type": "array",
227
+ "description": "List of trivia categories to enable. Available: " + ", ".join(category_options),
228
+ "default": list(cls.VALID_CATEGORIES.keys()),
229
+ "required": False,
230
+ "items": {
231
+ "type": "string",
232
+ "enum": list(cls.VALID_CATEGORIES.keys())
233
+ }
234
+ }
235
+ })
236
+
237
+ return schema
@@ -26,6 +26,88 @@ class DataSphereSkill(SkillBase):
26
26
  # Enable multiple instances support
27
27
  SUPPORTS_MULTIPLE_INSTANCES = True
28
28
 
29
+ @classmethod
30
+ def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
31
+ """Get parameter schema for DataSphere skill"""
32
+ schema = super().get_parameter_schema()
33
+ schema.update({
34
+ "space_name": {
35
+ "type": "string",
36
+ "description": "SignalWire space name (e.g., 'mycompany' from mycompany.signalwire.com)",
37
+ "required": True
38
+ },
39
+ "project_id": {
40
+ "type": "string",
41
+ "description": "SignalWire project ID",
42
+ "required": True,
43
+ "env_var": "SIGNALWIRE_PROJECT_ID"
44
+ },
45
+ "token": {
46
+ "type": "string",
47
+ "description": "SignalWire API token",
48
+ "required": True,
49
+ "hidden": True,
50
+ "env_var": "SIGNALWIRE_TOKEN"
51
+ },
52
+ "document_id": {
53
+ "type": "string",
54
+ "description": "DataSphere document ID to search within",
55
+ "required": True
56
+ },
57
+ "count": {
58
+ "type": "integer",
59
+ "description": "Number of search results to return",
60
+ "default": 1,
61
+ "required": False,
62
+ "minimum": 1,
63
+ "maximum": 10
64
+ },
65
+ "distance": {
66
+ "type": "number",
67
+ "description": "Maximum distance threshold for results (lower is more relevant)",
68
+ "default": 3.0,
69
+ "required": False,
70
+ "minimum": 0.0,
71
+ "maximum": 10.0
72
+ },
73
+ "tags": {
74
+ "type": "array",
75
+ "description": "Tags to filter search results",
76
+ "required": False,
77
+ "items": {
78
+ "type": "string"
79
+ }
80
+ },
81
+ "language": {
82
+ "type": "string",
83
+ "description": "Language code for query expansion (e.g., 'en', 'es')",
84
+ "required": False
85
+ },
86
+ "pos_to_expand": {
87
+ "type": "array",
88
+ "description": "Parts of speech to expand with synonyms",
89
+ "required": False,
90
+ "items": {
91
+ "type": "string",
92
+ "enum": ["NOUN", "VERB", "ADJ", "ADV"]
93
+ }
94
+ },
95
+ "max_synonyms": {
96
+ "type": "integer",
97
+ "description": "Maximum number of synonyms to use for query expansion",
98
+ "required": False,
99
+ "minimum": 1,
100
+ "maximum": 10
101
+ },
102
+ "no_results_message": {
103
+ "type": "string",
104
+ "description": "Message to return when no results are found",
105
+ "default": "I couldn't find any relevant information for '{query}' in the knowledge base. Try rephrasing your question or asking about a different topic.",
106
+ "required": False
107
+ }
108
+ })
109
+ return schema
110
+
29
111
  def get_instance_key(self) -> str:
30
112
  """
31
113
  Get the key used to track this skill instance
@@ -26,6 +26,88 @@ class DataSphereServerlessSkill(SkillBase):
26
26
  # Enable multiple instances support
27
27
  SUPPORTS_MULTIPLE_INSTANCES = True
28
28
 
29
+ @classmethod
30
+ def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
31
+ """Get parameter schema for DataSphere Serverless skill"""
32
+ schema = super().get_parameter_schema()
33
+ schema.update({
34
+ "space_name": {
35
+ "type": "string",
36
+ "description": "SignalWire space name (e.g., 'mycompany' from mycompany.signalwire.com)",
37
+ "required": True
38
+ },
39
+ "project_id": {
40
+ "type": "string",
41
+ "description": "SignalWire project ID",
42
+ "required": True,
43
+ "env_var": "SIGNALWIRE_PROJECT_ID"
44
+ },
45
+ "token": {
46
+ "type": "string",
47
+ "description": "SignalWire API token",
48
+ "required": True,
49
+ "hidden": True,
50
+ "env_var": "SIGNALWIRE_TOKEN"
51
+ },
52
+ "document_id": {
53
+ "type": "string",
54
+ "description": "DataSphere document ID to search within",
55
+ "required": True
56
+ },
57
+ "count": {
58
+ "type": "integer",
59
+ "description": "Number of search results to return",
60
+ "default": 1,
61
+ "required": False,
62
+ "minimum": 1,
63
+ "maximum": 10
64
+ },
65
+ "distance": {
66
+ "type": "number",
67
+ "description": "Maximum distance threshold for results (lower is more relevant)",
68
+ "default": 3.0,
69
+ "required": False,
70
+ "minimum": 0.0,
71
+ "maximum": 10.0
72
+ },
73
+ "tags": {
74
+ "type": "array",
75
+ "description": "Tags to filter search results",
76
+ "required": False,
77
+ "items": {
78
+ "type": "string"
79
+ }
80
+ },
81
+ "language": {
82
+ "type": "string",
83
+ "description": "Language code for query expansion (e.g., 'en', 'es')",
84
+ "required": False
85
+ },
86
+ "pos_to_expand": {
87
+ "type": "array",
88
+ "description": "Parts of speech to expand with synonyms",
89
+ "required": False,
90
+ "items": {
91
+ "type": "string",
92
+ "enum": ["NOUN", "VERB", "ADJ", "ADV"]
93
+ }
94
+ },
95
+ "max_synonyms": {
96
+ "type": "integer",
97
+ "description": "Maximum number of synonyms to use for query expansion",
98
+ "required": False,
99
+ "minimum": 1,
100
+ "maximum": 10
101
+ },
102
+ "no_results_message": {
103
+ "type": "string",
104
+ "description": "Message to return when no results are found",
105
+ "default": "I couldn't find any relevant information for '{query}' in the knowledge base. Try rephrasing your question or asking about a different topic.",
106
+ "required": False
107
+ }
108
+ })
109
+ return schema
110
+
29
111
  def get_instance_key(self) -> str:
30
112
  """
31
113
  Get the key used to track this skill instance
@@ -23,6 +23,27 @@ class JokeSkill(SkillBase):
23
23
  REQUIRED_PACKAGES = [] # DataMap doesn't require local packages
24
24
  REQUIRED_ENV_VARS = [] # API key comes from parameters
25
25
 
26
+ @classmethod
27
+ def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
28
+ """Get parameter schema for joke skill"""
29
+ schema = super().get_parameter_schema()
30
+ schema.update({
31
+ "api_key": {
32
+ "type": "string",
33
+ "description": "API Ninjas API key for joke service",
34
+ "required": True,
35
+ "hidden": True,
36
+ "env_var": "API_NINJAS_KEY"
37
+ },
38
+ "tool_name": {
39
+ "type": "string",
40
+ "description": "Custom name for the joke tool",
41
+ "default": "get_joke",
42
+ "required": False
43
+ }
44
+ })
45
+ return schema
46
+
26
47
  def setup(self) -> bool:
27
48
  """Setup the joke skill"""
28
49
  # Validate required parameters
@@ -33,6 +33,88 @@ class MCPGatewaySkill(SkillBase):
33
33
  REQUIRED_PACKAGES = ["requests"]
34
34
  REQUIRED_ENV_VARS = []
35
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
+
36
118
  def setup(self) -> bool:
37
119
  """Setup and validate skill configuration"""
38
120
  # Check for auth method - either token or basic auth