signalwire-agents 0.1.9__py3-none-any.whl → 0.1.11__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 (37) hide show
  1. signalwire_agents/__init__.py +39 -4
  2. signalwire_agents/agent_server.py +46 -2
  3. signalwire_agents/cli/__init__.py +9 -0
  4. signalwire_agents/cli/test_swaig.py +2545 -0
  5. signalwire_agents/core/agent_base.py +691 -82
  6. signalwire_agents/core/contexts.py +289 -0
  7. signalwire_agents/core/data_map.py +499 -0
  8. signalwire_agents/core/function_result.py +57 -10
  9. signalwire_agents/core/skill_base.py +31 -1
  10. signalwire_agents/core/skill_manager.py +89 -23
  11. signalwire_agents/core/swaig_function.py +13 -1
  12. signalwire_agents/core/swml_handler.py +37 -13
  13. signalwire_agents/core/swml_service.py +37 -28
  14. signalwire_agents/skills/datasphere/__init__.py +12 -0
  15. signalwire_agents/skills/datasphere/skill.py +229 -0
  16. signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
  17. signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
  18. signalwire_agents/skills/datetime/skill.py +7 -3
  19. signalwire_agents/skills/joke/__init__.py +1 -0
  20. signalwire_agents/skills/joke/skill.py +88 -0
  21. signalwire_agents/skills/math/skill.py +8 -5
  22. signalwire_agents/skills/registry.py +23 -4
  23. signalwire_agents/skills/web_search/skill.py +58 -33
  24. signalwire_agents/skills/wikipedia/__init__.py +9 -0
  25. signalwire_agents/skills/wikipedia/skill.py +180 -0
  26. signalwire_agents/utils/__init__.py +2 -0
  27. signalwire_agents/utils/schema_utils.py +111 -44
  28. signalwire_agents/utils/serverless.py +38 -0
  29. signalwire_agents-0.1.11.dist-info/METADATA +756 -0
  30. signalwire_agents-0.1.11.dist-info/RECORD +58 -0
  31. {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/WHEEL +1 -1
  32. signalwire_agents-0.1.11.dist-info/entry_points.txt +2 -0
  33. signalwire_agents-0.1.9.dist-info/METADATA +0 -311
  34. signalwire_agents-0.1.9.dist-info/RECORD +0 -44
  35. {signalwire_agents-0.1.9.data → signalwire_agents-0.1.11.data}/data/schema.json +0 -0
  36. {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/licenses/LICENSE +0 -0
  37. {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,499 @@
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
+ """
11
+ DataMap class for building SWAIG data_map configurations
12
+ """
13
+
14
+ from typing import Dict, List, Any, Optional, Union, Pattern, Tuple
15
+ import re
16
+ from .function_result import SwaigFunctionResult
17
+
18
+
19
+ class DataMap:
20
+ """
21
+ Builder class for creating SWAIG data_map configurations.
22
+
23
+ This provides a fluent interface for building data_map tools that execute
24
+ on the SignalWire server without requiring webhook endpoints. Works similar
25
+ to SwaigFunctionResult but for building data_map structures.
26
+
27
+ Example usage:
28
+ # Simple API call - output goes inside webhook
29
+ data_map = (DataMap('get_weather')
30
+ .purpose('Get current weather information')
31
+ .parameter('location', 'string', 'City name', required=True)
32
+ .webhook('GET', 'https://api.weather.com/v1/current?key=API_KEY&q=${location}')
33
+ .output(SwaigFunctionResult('Weather in ${location}: ${response.current.condition.text}, ${response.current.temp_f}°F'))
34
+ )
35
+
36
+ # Multiple webhooks with fallback
37
+ data_map = (DataMap('search_multi')
38
+ .purpose('Search with fallback APIs')
39
+ .parameter('query', 'string', 'Search query', required=True)
40
+ .webhook('GET', 'https://api.primary.com/search?q=${query}')
41
+ .output(SwaigFunctionResult('Primary result: ${response.title}'))
42
+ .webhook('GET', 'https://api.fallback.com/search?q=${query}')
43
+ .output(SwaigFunctionResult('Fallback result: ${response.title}'))
44
+ .fallback_output(SwaigFunctionResult('Sorry, all search APIs are unavailable'))
45
+ )
46
+
47
+ # Expression-based responses (no API calls)
48
+ data_map = (DataMap('file_control')
49
+ .purpose('Control file playback')
50
+ .parameter('command', 'string', 'Playback command')
51
+ .parameter('filename', 'string', 'File to control', required=False)
52
+ .expression('${args.command}', r'start.*', SwaigFunctionResult().add_action('start_playbook', {'file': '${args.filename}'}))
53
+ .expression('${args.command}', r'stop.*', SwaigFunctionResult().add_action('stop_playback', True))
54
+ )
55
+
56
+ # API with array processing
57
+ data_map = (DataMap('search_docs')
58
+ .purpose('Search documentation')
59
+ .parameter('query', 'string', 'Search query', required=True)
60
+ .webhook('POST', 'https://api.docs.com/search', headers={'Authorization': 'Bearer TOKEN'})
61
+ .body({'query': '${query}', 'limit': 3})
62
+ .output(SwaigFunctionResult('Found: ${response.results[0].title} - ${response.results[0].summary}'))
63
+ .foreach('${response.results}')
64
+ )
65
+ """
66
+
67
+ def __init__(self, function_name: str):
68
+ """
69
+ Initialize a new DataMap builder
70
+
71
+ Args:
72
+ function_name: Name of the SWAIG function this data_map will create
73
+ """
74
+ self.function_name = function_name
75
+ self._purpose = ""
76
+ self._parameters = {}
77
+ self._expressions = []
78
+ self._webhooks = []
79
+ self._output = None
80
+ self._error_keys = []
81
+
82
+ def purpose(self, description: str) -> 'DataMap':
83
+ """
84
+ Set the function description/purpose
85
+
86
+ Args:
87
+ description: Human-readable description of what this function does
88
+
89
+ Returns:
90
+ Self for method chaining
91
+ """
92
+ self._purpose = description
93
+ return self
94
+
95
+ def description(self, description: str) -> 'DataMap':
96
+ """
97
+ Set the function description (alias for purpose)
98
+
99
+ Args:
100
+ description: Human-readable description of what this function does
101
+
102
+ Returns:
103
+ Self for method chaining
104
+ """
105
+ return self.purpose(description)
106
+
107
+ def parameter(self, name: str, param_type: str, description: str,
108
+ required: bool = False, enum: Optional[List[str]] = None) -> 'DataMap':
109
+ """
110
+ Add a function parameter
111
+
112
+ Args:
113
+ name: Parameter name
114
+ param_type: JSON schema type (string, number, boolean, array, object)
115
+ description: Parameter description
116
+ required: Whether parameter is required
117
+ enum: Optional list of allowed values
118
+
119
+ Returns:
120
+ Self for method chaining
121
+ """
122
+ param_def = {
123
+ "type": param_type,
124
+ "description": description
125
+ }
126
+
127
+ if enum:
128
+ param_def["enum"] = enum
129
+
130
+ self._parameters[name] = param_def
131
+
132
+ if required:
133
+ if "required" not in self._parameters:
134
+ self._parameters["required"] = []
135
+ if name not in self._parameters["required"]:
136
+ self._parameters["required"].append(name)
137
+
138
+ return self
139
+
140
+ def expression(self, test_value: str, pattern: Union[str, Pattern], output: SwaigFunctionResult,
141
+ nomatch_output: Optional[SwaigFunctionResult] = None) -> 'DataMap':
142
+ """
143
+ Add an expression pattern for pattern-based responses
144
+
145
+ Args:
146
+ test_value: Template string to test (e.g., "${args.command}")
147
+ pattern: Regex pattern string or compiled Pattern object to match against
148
+ output: SwaigFunctionResult to return when pattern matches
149
+ nomatch_output: Optional SwaigFunctionResult to return when pattern doesn't match
150
+
151
+ Returns:
152
+ Self for method chaining
153
+ """
154
+ if isinstance(pattern, Pattern):
155
+ pattern_str = pattern.pattern
156
+ else:
157
+ pattern_str = str(pattern)
158
+
159
+ expr_def = {
160
+ "string": test_value,
161
+ "pattern": pattern_str,
162
+ "output": output.to_dict()
163
+ }
164
+
165
+ if nomatch_output:
166
+ expr_def["nomatch-output"] = nomatch_output.to_dict()
167
+
168
+ self._expressions.append(expr_def)
169
+ return self
170
+
171
+ def webhook(self, method: str, url: str, headers: Optional[Dict[str, str]] = None,
172
+ form_param: Optional[str] = None,
173
+ input_args_as_params: bool = False,
174
+ require_args: Optional[List[str]] = None) -> 'DataMap':
175
+ """
176
+ Add a webhook API call
177
+
178
+ Args:
179
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
180
+ url: API endpoint URL (can include ${variable} substitutions)
181
+ headers: Optional HTTP headers
182
+ form_param: Send JSON body as single form parameter with this name
183
+ input_args_as_params: Merge function arguments into params
184
+ require_args: Only execute if these arguments are present
185
+
186
+ Returns:
187
+ Self for method chaining
188
+ """
189
+ webhook_def = {
190
+ "url": url,
191
+ "method": method.upper()
192
+ }
193
+
194
+ if headers:
195
+ webhook_def["headers"] = headers
196
+ if form_param:
197
+ webhook_def["form_param"] = form_param
198
+ if input_args_as_params:
199
+ webhook_def["input_args_as_params"] = True
200
+ if require_args:
201
+ webhook_def["require_args"] = require_args
202
+
203
+ self._webhooks.append(webhook_def)
204
+ return self
205
+
206
+ def webhook_expressions(self, expressions: List[Dict[str, Any]]) -> 'DataMap':
207
+ """
208
+ Add expressions that run after the most recent webhook completes
209
+
210
+ Args:
211
+ expressions: List of expression definitions to check post-webhook
212
+
213
+ Returns:
214
+ Self for method chaining
215
+ """
216
+ if not self._webhooks:
217
+ raise ValueError("Must add webhook before setting webhook expressions")
218
+
219
+ self._webhooks[-1]["expressions"] = expressions
220
+ return self
221
+
222
+ def body(self, data: Dict[str, Any]) -> 'DataMap':
223
+ """
224
+ Set request body for the last added webhook (POST/PUT requests)
225
+
226
+ Args:
227
+ data: Request body data (can include ${variable} substitutions)
228
+
229
+ Returns:
230
+ Self for method chaining
231
+ """
232
+ if not self._webhooks:
233
+ raise ValueError("Must add webhook before setting body")
234
+
235
+ self._webhooks[-1]["body"] = data
236
+ return self
237
+
238
+ def params(self, data: Dict[str, Any]) -> 'DataMap':
239
+ """
240
+ Set request params for the last added webhook (alias for body)
241
+
242
+ Args:
243
+ data: Request params data (can include ${variable} substitutions)
244
+
245
+ Returns:
246
+ Self for method chaining
247
+ """
248
+ if not self._webhooks:
249
+ raise ValueError("Must add webhook before setting params")
250
+
251
+ self._webhooks[-1]["params"] = data
252
+ return self
253
+
254
+ def foreach(self, foreach_config: Union[str, Dict[str, Any]]) -> 'DataMap':
255
+ """
256
+ Process an array from the webhook response using foreach mechanism
257
+
258
+ Args:
259
+ foreach_config: Either:
260
+ - String: JSON path to array in response (deprecated, kept for compatibility)
261
+ - Dict: Foreach configuration with keys:
262
+ - input_key: Key in API response containing the array
263
+ - output_key: Name for the built string variable
264
+ - max: Maximum number of items to process (optional)
265
+ - append: Template string to append for each item
266
+
267
+ Returns:
268
+ Self for method chaining
269
+
270
+ Example:
271
+ .foreach({
272
+ "input_key": "results",
273
+ "output_key": "formatted_results",
274
+ "max": 3,
275
+ "append": "Result: ${this.title} - ${this.summary}\n"
276
+ })
277
+ """
278
+ if not self._webhooks:
279
+ raise ValueError("Must add webhook before setting foreach")
280
+
281
+ if isinstance(foreach_config, str):
282
+ # Legacy support - convert string to basic foreach config
283
+ # Extract the key from "${response.key}" format if present
284
+ if foreach_config.startswith("${response.") and foreach_config.endswith("}"):
285
+ key = foreach_config[12:-1] # Remove "${response." and "}"
286
+ else:
287
+ key = foreach_config
288
+
289
+ foreach_data = {
290
+ "input_key": key,
291
+ "output_key": "foreach_output",
292
+ "append": "${this}"
293
+ }
294
+ else:
295
+ # New format - validate required keys
296
+ required_keys = ["input_key", "output_key", "append"]
297
+ missing_keys = [key for key in required_keys if key not in foreach_config]
298
+ if missing_keys:
299
+ raise ValueError(f"foreach config missing required keys: {missing_keys}")
300
+
301
+ foreach_data = foreach_config
302
+
303
+ self._webhooks[-1]["foreach"] = foreach_data
304
+ return self
305
+
306
+ def output(self, result: SwaigFunctionResult) -> 'DataMap':
307
+ """
308
+ Set the output result for the most recent webhook
309
+
310
+ Args:
311
+ result: SwaigFunctionResult defining the response for this webhook
312
+
313
+ Returns:
314
+ Self for method chaining
315
+ """
316
+ if not self._webhooks:
317
+ raise ValueError("Must add webhook before setting output")
318
+
319
+ self._webhooks[-1]["output"] = result.to_dict()
320
+ return self
321
+
322
+ def fallback_output(self, result: SwaigFunctionResult) -> 'DataMap':
323
+ """
324
+ Set a fallback output result at the top level (used when all webhooks fail)
325
+
326
+ Args:
327
+ result: SwaigFunctionResult defining the fallback response
328
+
329
+ Returns:
330
+ Self for method chaining
331
+ """
332
+ self._output = result.to_dict()
333
+ return self
334
+
335
+ def error_keys(self, keys: List[str]) -> 'DataMap':
336
+ """
337
+ Set error keys for the most recent webhook (if webhooks exist) or top-level
338
+
339
+ Args:
340
+ keys: List of JSON keys whose presence indicates an error
341
+
342
+ Returns:
343
+ Self for method chaining
344
+ """
345
+ if self._webhooks:
346
+ # Add to most recent webhook
347
+ self._webhooks[-1]["error_keys"] = keys
348
+ else:
349
+ # Store as top-level error keys
350
+ self._error_keys = keys
351
+ return self
352
+
353
+ def global_error_keys(self, keys: List[str]) -> 'DataMap':
354
+ """
355
+ Set top-level error keys (applies to all webhooks)
356
+
357
+ Args:
358
+ keys: List of JSON keys whose presence indicates an error
359
+
360
+ Returns:
361
+ Self for method chaining
362
+ """
363
+ self._error_keys = keys
364
+ return self
365
+
366
+ def to_swaig_function(self) -> Dict[str, Any]:
367
+ """
368
+ Convert this DataMap to a SWAIG function definition
369
+
370
+ Returns:
371
+ Dictionary with function definition and data_map instead of url
372
+ """
373
+ # Build parameter schema
374
+ if self._parameters:
375
+ # Extract required params without mutating original dict
376
+ required_params = self._parameters.get("required", [])
377
+ param_properties = {k: v for k, v in self._parameters.items() if k != "required"}
378
+
379
+ param_schema = {
380
+ "type": "object",
381
+ "properties": param_properties
382
+ }
383
+ if required_params:
384
+ param_schema["required"] = required_params
385
+ else:
386
+ param_schema = {"type": "object", "properties": {}}
387
+
388
+ # Build data_map structure
389
+ data_map = {}
390
+
391
+ # Add expressions if present
392
+ if self._expressions:
393
+ data_map["expressions"] = self._expressions
394
+
395
+ # Add webhooks if present
396
+ if self._webhooks:
397
+ data_map["webhooks"] = self._webhooks
398
+
399
+ # Add output if present
400
+ if self._output:
401
+ data_map["output"] = self._output
402
+
403
+ # Add error_keys if present
404
+ if self._error_keys:
405
+ data_map["error_keys"] = self._error_keys
406
+
407
+ # Build final function definition with correct field names
408
+ function_def = {
409
+ "function": self.function_name,
410
+ "description": self._purpose or f"Execute {self.function_name}",
411
+ "parameters": param_schema,
412
+ "data_map": data_map
413
+ }
414
+
415
+ return function_def
416
+
417
+
418
+ def create_simple_api_tool(name: str, url: str, response_template: str,
419
+ parameters: Optional[Dict[str, Dict]] = None,
420
+ method: str = "GET", headers: Optional[Dict[str, str]] = None,
421
+ body: Optional[Dict[str, Any]] = None,
422
+ error_keys: Optional[List[str]] = None) -> DataMap:
423
+ """
424
+ Create a simple API tool with minimal configuration
425
+
426
+ Args:
427
+ name: Function name
428
+ url: API endpoint URL
429
+ response_template: Template for formatting the response
430
+ parameters: Optional parameter definitions
431
+ method: HTTP method (default: GET)
432
+ headers: Optional HTTP headers
433
+ body: Optional request body (for POST/PUT)
434
+ error_keys: Optional list of error indicator keys
435
+
436
+ Returns:
437
+ Configured DataMap object
438
+ """
439
+ data_map = DataMap(name)
440
+
441
+ # Add parameters if provided
442
+ if parameters:
443
+ for param_name, param_def in parameters.items():
444
+ required = param_def.get("required", False)
445
+ data_map.parameter(
446
+ param_name,
447
+ param_def.get("type", "string"),
448
+ param_def.get("description", f"{param_name} parameter"),
449
+ required=required
450
+ )
451
+
452
+ # Add webhook
453
+ data_map.webhook(method, url, headers)
454
+
455
+ # Add body if provided
456
+ if body:
457
+ data_map.body(body)
458
+
459
+ # Add error keys if provided
460
+ if error_keys:
461
+ data_map.error_keys(error_keys)
462
+
463
+ # Set output
464
+ data_map.output(SwaigFunctionResult(response_template))
465
+
466
+ return data_map
467
+
468
+
469
+ def create_expression_tool(name: str, patterns: Dict[str, Tuple[str, SwaigFunctionResult]],
470
+ parameters: Optional[Dict[str, Dict]] = None) -> DataMap:
471
+ """
472
+ Create an expression-based tool for pattern matching responses
473
+
474
+ Args:
475
+ name: Function name
476
+ patterns: Dictionary mapping test_values to (pattern, SwaigFunctionResult) tuples
477
+ parameters: Optional parameter definitions
478
+
479
+ Returns:
480
+ Configured DataMap object
481
+ """
482
+ data_map = DataMap(name)
483
+
484
+ # Add parameters if provided
485
+ if parameters:
486
+ for param_name, param_def in parameters.items():
487
+ required = param_def.get("required", False)
488
+ data_map.parameter(
489
+ param_name,
490
+ param_def.get("type", "string"),
491
+ param_def.get("description", f"{param_name} parameter"),
492
+ required=required
493
+ )
494
+
495
+ # Add expressions with corrected signature
496
+ for test_value, (pattern, result) in patterns.items():
497
+ data_map.expression(test_value, pattern, result)
498
+
499
+ return data_map
@@ -187,6 +187,55 @@ class SwaigFunctionResult:
187
187
  # Add to actions list
188
188
  self.action.append(swml_action)
189
189
  return self
190
+
191
+ def swml_transfer(self, dest: str, ai_response: str) -> 'SwaigFunctionResult':
192
+ """
193
+ Add a SWML transfer action with AI response setup for when transfer completes.
194
+
195
+ This is a virtual helper that generates SWML to transfer the call to another
196
+ destination and sets up an AI response for when the transfer completes and
197
+ control returns to the agent.
198
+
199
+ For transfers, you typically want to enable post-processing so the AI speaks
200
+ the response first before executing the transfer.
201
+
202
+ Args:
203
+ dest: Destination URL for the transfer (SWML endpoint, SIP address, etc.)
204
+ ai_response: Message the AI should say when transfer completes and control returns
205
+
206
+ Returns:
207
+ Self for method chaining
208
+
209
+ Example:
210
+ # Transfer with post-processing (speak first, then transfer)
211
+ result = (
212
+ SwaigFunctionResult("I'm transferring you to support", post_process=True)
213
+ .swml_transfer(
214
+ "https://support.example.com/swml",
215
+ "The support call is complete. How else can I help?"
216
+ )
217
+ )
218
+
219
+ # Or enable post-processing with method chaining
220
+ result.swml_transfer(dest, ai_response).set_post_process(True)
221
+ """
222
+ # Create the SWML action structure directly
223
+ swml_action = {
224
+ "SWML": {
225
+ "version": "1.0.0",
226
+ "sections": {
227
+ "main": [
228
+ {"set": {"ai_response": ai_response}},
229
+ {"transfer": {"dest": dest}}
230
+ ]
231
+ }
232
+ }
233
+ }
234
+
235
+ # Add to actions list directly
236
+ self.action.append(swml_action)
237
+
238
+ return self
190
239
 
191
240
  def update_global_data(self, data: Dict[str, Any]) -> 'SwaigFunctionResult':
192
241
  """
@@ -310,32 +359,30 @@ class SwaigFunctionResult:
310
359
  action = {"say": text}
311
360
  return self.add_action("say", action)
312
361
 
313
- def play_background_audio(self, filename: str, wait: bool = False) -> 'SwaigFunctionResult':
362
+ def play_background_file(self, filename: str, wait: bool = False) -> 'SwaigFunctionResult':
314
363
  """
315
- Play audio file in background.
364
+ Play audio or video file in background.
316
365
 
317
366
  Args:
318
- filename: Audio filename/path
367
+ filename: Audio/video filename/path
319
368
  wait: Whether to suppress attention-getting behavior during playback
320
369
 
321
370
  Returns:
322
371
  self for method chaining
323
372
  """
324
373
  if wait:
325
- action = {"playback_bg": {"file": filename, "wait": True}}
374
+ return self.add_action("playback_bg", {"file": filename, "wait": True})
326
375
  else:
327
- action = {"playback_bg": filename}
328
- return self.add_action("playback_bg", action)
376
+ return self.add_action("playback_bg", filename)
329
377
 
330
- def stop_background_audio(self) -> 'SwaigFunctionResult':
378
+ def stop_background_file(self) -> 'SwaigFunctionResult':
331
379
  """
332
- Stop currently playing background audio.
380
+ Stop currently playing background file.
333
381
 
334
382
  Returns:
335
383
  self for method chaining
336
384
  """
337
- action = {"stop_playback_bg": True}
338
- return self.add_action("stop_playback_bg", action)
385
+ return self.add_action("stop_playback_bg", True)
339
386
 
340
387
  def set_end_of_speech_timeout(self, milliseconds: int) -> 'SwaigFunctionResult':
341
388
  """
@@ -24,6 +24,9 @@ class SkillBase(ABC):
24
24
  REQUIRED_PACKAGES: List[str] = [] # Python packages needed
25
25
  REQUIRED_ENV_VARS: List[str] = [] # Environment variables needed
26
26
 
27
+ # Multiple instance support
28
+ SUPPORTS_MULTIPLE_INSTANCES: bool = False # Set to True to allow multiple instances
29
+
27
30
  def __init__(self, agent: 'AgentBase', params: Optional[Dict[str, Any]] = None):
28
31
  if self.SKILL_NAME is None:
29
32
  raise ValueError(f"{self.__class__.__name__} must define SKILL_NAME")
@@ -34,6 +37,9 @@ class SkillBase(ABC):
34
37
  self.params = params or {}
35
38
  self.logger = logging.getLogger(f"skill.{self.SKILL_NAME}")
36
39
 
40
+ # Extract swaig_fields from params for merging into tool definitions
41
+ self.swaig_fields = self.params.pop('swaig_fields', {})
42
+
37
43
  @abstractmethod
38
44
  def setup(self) -> bool:
39
45
  """
@@ -47,6 +53,8 @@ class SkillBase(ABC):
47
53
  """Register SWAIG tools with the agent"""
48
54
  pass
49
55
 
56
+
57
+
50
58
  def get_hints(self) -> List[str]:
51
59
  """Return speech recognition hints for this skill"""
52
60
  return []
@@ -84,4 +92,26 @@ class SkillBase(ABC):
84
92
  if missing:
85
93
  self.logger.error(f"Missing required packages: {missing}")
86
94
  return False
87
- return True
95
+ return True
96
+
97
+ def get_instance_key(self) -> str:
98
+ """
99
+ Get the key used to track this skill instance
100
+
101
+ For skills that support multiple instances (SUPPORTS_MULTIPLE_INSTANCES = True),
102
+ this method can be overridden to provide a unique key for each instance.
103
+
104
+ Default implementation:
105
+ - If SUPPORTS_MULTIPLE_INSTANCES is False: returns SKILL_NAME
106
+ - If SUPPORTS_MULTIPLE_INSTANCES is True: returns SKILL_NAME + "_" + tool_name
107
+ (where tool_name comes from params['tool_name'] or defaults to the skill name)
108
+
109
+ Returns:
110
+ str: Unique key for this skill instance
111
+ """
112
+ if not self.SUPPORTS_MULTIPLE_INSTANCES:
113
+ return self.SKILL_NAME
114
+
115
+ # For multi-instance skills, create key from skill name + tool name
116
+ tool_name = self.params.get('tool_name', self.SKILL_NAME)
117
+ return f"{self.SKILL_NAME}_{tool_name}"