signalwire-agents 0.1.10__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.
- signalwire_agents/__init__.py +39 -4
- signalwire_agents/agent_server.py +46 -2
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/test_swaig.py +2545 -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/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/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/registry.py +23 -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 +2 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents/utils/serverless.py +38 -0
- signalwire_agents-0.1.11.dist-info/METADATA +756 -0
- signalwire_agents-0.1.11.dist-info/RECORD +58 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.11.dist-info/entry_points.txt +2 -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.11.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,229 @@
|
|
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 requests
|
11
|
+
import json
|
12
|
+
from typing import Optional, List, Dict, Any
|
13
|
+
|
14
|
+
from signalwire_agents.core.skill_base import SkillBase
|
15
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
16
|
+
|
17
|
+
class DataSphereSkill(SkillBase):
|
18
|
+
"""SignalWire DataSphere knowledge search capability"""
|
19
|
+
|
20
|
+
SKILL_NAME = "datasphere"
|
21
|
+
SKILL_DESCRIPTION = "Search knowledge using SignalWire DataSphere RAG stack"
|
22
|
+
SKILL_VERSION = "1.0.0"
|
23
|
+
REQUIRED_PACKAGES = ["requests"]
|
24
|
+
REQUIRED_ENV_VARS = [] # No required env vars since all config comes from params
|
25
|
+
|
26
|
+
# Enable multiple instances support
|
27
|
+
SUPPORTS_MULTIPLE_INSTANCES = True
|
28
|
+
|
29
|
+
def get_instance_key(self) -> str:
|
30
|
+
"""
|
31
|
+
Get the key used to track this skill instance
|
32
|
+
|
33
|
+
For DataSphere, we use 'search_knowledge' as the default tool name instead of 'datasphere'
|
34
|
+
"""
|
35
|
+
tool_name = self.params.get('tool_name', 'search_knowledge')
|
36
|
+
return f"{self.SKILL_NAME}_{tool_name}"
|
37
|
+
|
38
|
+
def setup(self) -> bool:
|
39
|
+
"""Setup the datasphere skill"""
|
40
|
+
# Validate required parameters
|
41
|
+
required_params = ['space_name', 'project_id', 'token', 'document_id']
|
42
|
+
missing_params = [param for param in required_params if not self.params.get(param)]
|
43
|
+
if missing_params:
|
44
|
+
self.logger.error(f"Missing required parameters: {missing_params}")
|
45
|
+
return False
|
46
|
+
|
47
|
+
# Set required parameters
|
48
|
+
self.space_name = self.params['space_name']
|
49
|
+
self.project_id = self.params['project_id']
|
50
|
+
self.token = self.params['token']
|
51
|
+
self.document_id = self.params['document_id']
|
52
|
+
|
53
|
+
# Set optional parameters with defaults
|
54
|
+
self.count = self.params.get('count', 1)
|
55
|
+
self.distance = self.params.get('distance', 3.0)
|
56
|
+
self.tags = self.params.get('tags', None) # None means don't include in request
|
57
|
+
self.language = self.params.get('language', None) # None means don't include in request
|
58
|
+
self.pos_to_expand = self.params.get('pos_to_expand', None) # None means don't include in request
|
59
|
+
self.max_synonyms = self.params.get('max_synonyms', None) # None means don't include in request
|
60
|
+
|
61
|
+
# Tool name (for multiple instances)
|
62
|
+
self.tool_name = self.params.get('tool_name', 'search_knowledge')
|
63
|
+
|
64
|
+
# No results message
|
65
|
+
self.no_results_message = self.params.get('no_results_message',
|
66
|
+
"I couldn't find any relevant information for '{query}' in the knowledge base. "
|
67
|
+
"Try rephrasing your question or asking about a different topic."
|
68
|
+
)
|
69
|
+
|
70
|
+
# Build API URL
|
71
|
+
self.api_url = f"https://{self.space_name}.signalwire.com/api/datasphere/documents/search"
|
72
|
+
|
73
|
+
# Setup session for requests
|
74
|
+
self.session = requests.Session()
|
75
|
+
|
76
|
+
return True
|
77
|
+
|
78
|
+
def register_tools(self) -> None:
|
79
|
+
"""Register knowledge search tool with the agent"""
|
80
|
+
self.agent.define_tool(
|
81
|
+
name=self.tool_name,
|
82
|
+
description="Search the knowledge base for information on any topic and return relevant results",
|
83
|
+
parameters={
|
84
|
+
"query": {
|
85
|
+
"type": "string",
|
86
|
+
"description": "The search query - what information you're looking for in the knowledge base"
|
87
|
+
}
|
88
|
+
},
|
89
|
+
handler=self._search_knowledge_handler,
|
90
|
+
**self.swaig_fields
|
91
|
+
)
|
92
|
+
|
93
|
+
def _search_knowledge_handler(self, args, raw_data):
|
94
|
+
"""Handler for knowledge search tool"""
|
95
|
+
query = args.get("query", "").strip()
|
96
|
+
|
97
|
+
if not query:
|
98
|
+
return SwaigFunctionResult(
|
99
|
+
"Please provide a search query. What would you like me to search for in the knowledge base?"
|
100
|
+
)
|
101
|
+
|
102
|
+
self.logger.info(f"DataSphere search requested: '{query}' (document: {self.document_id})")
|
103
|
+
|
104
|
+
# Build request payload
|
105
|
+
payload = {
|
106
|
+
"document_id": self.document_id,
|
107
|
+
"query_string": query,
|
108
|
+
"distance": self.distance,
|
109
|
+
"count": self.count
|
110
|
+
}
|
111
|
+
|
112
|
+
# Add optional parameters only if they were provided
|
113
|
+
if self.tags is not None:
|
114
|
+
payload["tags"] = self.tags
|
115
|
+
if self.language is not None:
|
116
|
+
payload["language"] = self.language
|
117
|
+
if self.pos_to_expand is not None:
|
118
|
+
payload["pos_to_expand"] = self.pos_to_expand
|
119
|
+
if self.max_synonyms is not None:
|
120
|
+
payload["max_synonyms"] = self.max_synonyms
|
121
|
+
|
122
|
+
try:
|
123
|
+
# Make API request
|
124
|
+
response = self.session.post(
|
125
|
+
self.api_url,
|
126
|
+
auth=(self.project_id, self.token),
|
127
|
+
headers={
|
128
|
+
'Content-Type': 'application/json',
|
129
|
+
'Accept': 'application/json'
|
130
|
+
},
|
131
|
+
json=payload,
|
132
|
+
timeout=30
|
133
|
+
)
|
134
|
+
|
135
|
+
response.raise_for_status()
|
136
|
+
data = response.json()
|
137
|
+
|
138
|
+
# Check if we have valid response data
|
139
|
+
if not data or not isinstance(data, dict):
|
140
|
+
self.logger.warning(f"DataSphere API returned invalid data: {data}")
|
141
|
+
formatted_message = self.no_results_message.format(query=query) if '{query}' in self.no_results_message else self.no_results_message
|
142
|
+
return SwaigFunctionResult(formatted_message)
|
143
|
+
|
144
|
+
# Extract results - DataSphere API returns 'chunks', not 'results'
|
145
|
+
chunks = data.get('chunks', [])
|
146
|
+
|
147
|
+
if not chunks:
|
148
|
+
formatted_message = self.no_results_message.format(query=query) if '{query}' in self.no_results_message else self.no_results_message
|
149
|
+
return SwaigFunctionResult(formatted_message)
|
150
|
+
|
151
|
+
# Format the results
|
152
|
+
formatted_results = self._format_search_results(query, chunks)
|
153
|
+
return SwaigFunctionResult(formatted_results)
|
154
|
+
|
155
|
+
except requests.exceptions.Timeout:
|
156
|
+
self.logger.error("DataSphere API request timed out")
|
157
|
+
return SwaigFunctionResult(
|
158
|
+
"Sorry, the knowledge search timed out. Please try again."
|
159
|
+
)
|
160
|
+
except requests.exceptions.HTTPError as e:
|
161
|
+
self.logger.error(f"DataSphere API HTTP error: {e}")
|
162
|
+
return SwaigFunctionResult(
|
163
|
+
"Sorry, there was an error accessing the knowledge base. Please try again later."
|
164
|
+
)
|
165
|
+
except Exception as e:
|
166
|
+
self.logger.error(f"Error performing DataSphere search: {e}")
|
167
|
+
return SwaigFunctionResult(
|
168
|
+
"Sorry, I encountered an error while searching the knowledge base. Please try again later."
|
169
|
+
)
|
170
|
+
|
171
|
+
def _format_search_results(self, query: str, chunks: List[Dict[str, Any]]) -> str:
|
172
|
+
"""Format search results for display"""
|
173
|
+
if len(chunks) == 1:
|
174
|
+
result_text = f"I found 1 result for '{query}':\n\n"
|
175
|
+
else:
|
176
|
+
result_text = f"I found {len(chunks)} results for '{query}':\n\n"
|
177
|
+
|
178
|
+
formatted_results = []
|
179
|
+
|
180
|
+
for i, chunk in enumerate(chunks, 1):
|
181
|
+
result_content = f"=== RESULT {i} ===\n"
|
182
|
+
|
183
|
+
# DataSphere API returns chunks with 'text' field
|
184
|
+
if 'text' in chunk:
|
185
|
+
result_content += chunk['text']
|
186
|
+
elif 'content' in chunk:
|
187
|
+
result_content += chunk['content']
|
188
|
+
elif 'chunk' in chunk:
|
189
|
+
result_content += chunk['chunk']
|
190
|
+
else:
|
191
|
+
# Fallback to the entire result as JSON if we don't recognize the format
|
192
|
+
result_content += json.dumps(chunk, indent=2)
|
193
|
+
|
194
|
+
result_content += f"\n{'='*50}\n\n"
|
195
|
+
formatted_results.append(result_content)
|
196
|
+
|
197
|
+
return result_text + '\n'.join(formatted_results)
|
198
|
+
|
199
|
+
def get_hints(self) -> List[str]:
|
200
|
+
"""Return speech recognition hints"""
|
201
|
+
# Currently no hints provided, but you could add them like:
|
202
|
+
# return [
|
203
|
+
# "knowledge", "search", "information", "database", "find",
|
204
|
+
# "look up", "research", "query", "datasphere", "document"
|
205
|
+
# ]
|
206
|
+
return []
|
207
|
+
|
208
|
+
def get_global_data(self) -> Dict[str, Any]:
|
209
|
+
"""Return global data for agent context"""
|
210
|
+
return {
|
211
|
+
"datasphere_enabled": True,
|
212
|
+
"document_id": self.document_id,
|
213
|
+
"knowledge_provider": "SignalWire DataSphere"
|
214
|
+
}
|
215
|
+
|
216
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
217
|
+
"""Return prompt sections to add to agent"""
|
218
|
+
return [
|
219
|
+
{
|
220
|
+
"title": "Knowledge Search Capability",
|
221
|
+
"body": f"You can search a knowledge base for information using the {self.tool_name} tool.",
|
222
|
+
"bullets": [
|
223
|
+
f"Use the {self.tool_name} tool when users ask for information that might be in the knowledge base",
|
224
|
+
"Search for relevant information using clear, specific queries",
|
225
|
+
"Summarize search results in a clear, helpful way",
|
226
|
+
"If no results are found, suggest the user try rephrasing their question"
|
227
|
+
]
|
228
|
+
}
|
229
|
+
]
|
@@ -0,0 +1 @@
|
|
1
|
+
"""DataSphere Serverless Skill for SignalWire Agents using DataMap"""
|
@@ -0,0 +1,156 @@
|
|
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 base64
|
11
|
+
from typing import Optional, List, Dict, Any
|
12
|
+
|
13
|
+
from signalwire_agents.core.skill_base import SkillBase
|
14
|
+
from signalwire_agents.core.data_map import DataMap
|
15
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
16
|
+
|
17
|
+
class DataSphereServerlessSkill(SkillBase):
|
18
|
+
"""SignalWire DataSphere knowledge search using DataMap (serverless execution)"""
|
19
|
+
|
20
|
+
SKILL_NAME = "datasphere_serverless"
|
21
|
+
SKILL_DESCRIPTION = "Search knowledge using SignalWire DataSphere with serverless DataMap execution"
|
22
|
+
SKILL_VERSION = "1.0.0"
|
23
|
+
REQUIRED_PACKAGES = [] # DataMap handles API calls serverlessly
|
24
|
+
REQUIRED_ENV_VARS = [] # No required env vars since all config comes from params
|
25
|
+
|
26
|
+
# Enable multiple instances support
|
27
|
+
SUPPORTS_MULTIPLE_INSTANCES = True
|
28
|
+
|
29
|
+
def get_instance_key(self) -> str:
|
30
|
+
"""
|
31
|
+
Get the key used to track this skill instance
|
32
|
+
|
33
|
+
For DataSphere Serverless, we use 'search_knowledge' as the default tool name
|
34
|
+
"""
|
35
|
+
tool_name = self.params.get('tool_name', 'search_knowledge')
|
36
|
+
return f"{self.SKILL_NAME}_{tool_name}"
|
37
|
+
|
38
|
+
def setup(self) -> bool:
|
39
|
+
"""Setup the datasphere serverless skill"""
|
40
|
+
# Validate required parameters
|
41
|
+
required_params = ['space_name', 'project_id', 'token', 'document_id']
|
42
|
+
missing_params = [param for param in required_params if not self.params.get(param)]
|
43
|
+
if missing_params:
|
44
|
+
self.logger.error(f"Missing required parameters: {missing_params}")
|
45
|
+
return False
|
46
|
+
|
47
|
+
# Set required parameters
|
48
|
+
self.space_name = self.params['space_name']
|
49
|
+
self.project_id = self.params['project_id']
|
50
|
+
self.token = self.params['token']
|
51
|
+
self.document_id = self.params['document_id']
|
52
|
+
|
53
|
+
# Set optional parameters with defaults
|
54
|
+
self.count = self.params.get('count', 1)
|
55
|
+
self.distance = self.params.get('distance', 3.0)
|
56
|
+
self.tags = self.params.get('tags', None)
|
57
|
+
self.language = self.params.get('language', None)
|
58
|
+
self.pos_to_expand = self.params.get('pos_to_expand', None)
|
59
|
+
self.max_synonyms = self.params.get('max_synonyms', None)
|
60
|
+
|
61
|
+
# Tool name (for multiple instances)
|
62
|
+
self.tool_name = self.params.get('tool_name', 'search_knowledge')
|
63
|
+
|
64
|
+
# No results message
|
65
|
+
self.no_results_message = self.params.get('no_results_message',
|
66
|
+
"I couldn't find any relevant information for '{query}' in the knowledge base. "
|
67
|
+
"Try rephrasing your question or asking about a different topic."
|
68
|
+
)
|
69
|
+
|
70
|
+
# Build API URL
|
71
|
+
self.api_url = f"https://{self.space_name}.signalwire.com/api/datasphere/documents/search"
|
72
|
+
|
73
|
+
# Build auth header for DataMap
|
74
|
+
auth_string = f"{self.project_id}:{self.token}"
|
75
|
+
self.auth_header = base64.b64encode(auth_string.encode()).decode()
|
76
|
+
|
77
|
+
return True
|
78
|
+
|
79
|
+
def register_tools(self) -> None:
|
80
|
+
"""Register knowledge search tool using DataMap"""
|
81
|
+
|
82
|
+
# Build webhook params with configuration values
|
83
|
+
webhook_params = {
|
84
|
+
"document_id": self.document_id,
|
85
|
+
"query_string": "${args.query}", # Only this is dynamic from user input
|
86
|
+
"distance": self.distance,
|
87
|
+
"count": self.count
|
88
|
+
}
|
89
|
+
|
90
|
+
# Add optional parameters only if they were provided
|
91
|
+
if self.tags is not None:
|
92
|
+
webhook_params["tags"] = self.tags
|
93
|
+
if self.language is not None:
|
94
|
+
webhook_params["language"] = self.language
|
95
|
+
if self.pos_to_expand is not None:
|
96
|
+
webhook_params["pos_to_expand"] = self.pos_to_expand
|
97
|
+
if self.max_synonyms is not None:
|
98
|
+
webhook_params["max_synonyms"] = self.max_synonyms
|
99
|
+
|
100
|
+
# Create DataMap tool for DataSphere search
|
101
|
+
datasphere_tool = (DataMap(self.tool_name)
|
102
|
+
.description("Search the knowledge base for information on any topic and return relevant results")
|
103
|
+
.parameter('query', 'string', 'The search query - what information you\'re looking for in the knowledge base', required=True)
|
104
|
+
.webhook('POST', self.api_url,
|
105
|
+
headers={
|
106
|
+
'Content-Type': 'application/json',
|
107
|
+
'Authorization': f'Basic {self.auth_header}'
|
108
|
+
})
|
109
|
+
.params(webhook_params)
|
110
|
+
.foreach({
|
111
|
+
"input_key": "chunks",
|
112
|
+
"output_key": "formatted_results",
|
113
|
+
"max": self.count,
|
114
|
+
"append": "=== RESULT ===\n${this.text}\n" + "="*50 + "\n\n"
|
115
|
+
})
|
116
|
+
.output(SwaigFunctionResult('I found results for "${args.query}":\n\n${formatted_results}'))
|
117
|
+
.error_keys(['error'])
|
118
|
+
.fallback_output(SwaigFunctionResult(self.no_results_message.replace('{query}', '${args.query}')))
|
119
|
+
)
|
120
|
+
|
121
|
+
# Convert DataMap to SWAIG function and apply swaig_fields
|
122
|
+
swaig_function = datasphere_tool.to_swaig_function()
|
123
|
+
|
124
|
+
# Merge swaig_fields from skill params into the function definition
|
125
|
+
swaig_function.update(self.swaig_fields)
|
126
|
+
|
127
|
+
# Register the enhanced DataMap tool with the agent
|
128
|
+
self.agent.register_swaig_function(swaig_function)
|
129
|
+
|
130
|
+
def get_hints(self) -> List[str]:
|
131
|
+
"""Return speech recognition hints"""
|
132
|
+
return []
|
133
|
+
|
134
|
+
def get_global_data(self) -> Dict[str, Any]:
|
135
|
+
"""Return global data for agent context"""
|
136
|
+
return {
|
137
|
+
"datasphere_serverless_enabled": True,
|
138
|
+
"document_id": self.document_id,
|
139
|
+
"knowledge_provider": "SignalWire DataSphere (Serverless)"
|
140
|
+
}
|
141
|
+
|
142
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
143
|
+
"""Return prompt sections to add to agent"""
|
144
|
+
return [
|
145
|
+
{
|
146
|
+
"title": "Knowledge Search Capability (Serverless)",
|
147
|
+
"body": f"You can search a knowledge base for information using the {self.tool_name} tool.",
|
148
|
+
"bullets": [
|
149
|
+
f"Use the {self.tool_name} tool when users ask for information that might be in the knowledge base",
|
150
|
+
"Search for relevant information using clear, specific queries",
|
151
|
+
"Summarize search results in a clear, helpful way",
|
152
|
+
"If no results are found, suggest the user try rephrasing their question",
|
153
|
+
"This tool executes on SignalWire servers for optimal performance"
|
154
|
+
]
|
155
|
+
}
|
156
|
+
]
|
@@ -30,7 +30,7 @@ class DateTimeSkill(SkillBase):
|
|
30
30
|
def register_tools(self) -> None:
|
31
31
|
"""Register datetime tools with the agent"""
|
32
32
|
|
33
|
-
self.
|
33
|
+
self.agent.define_tool(
|
34
34
|
name="get_current_time",
|
35
35
|
description="Get the current time, optionally in a specific timezone",
|
36
36
|
parameters={
|
@@ -39,10 +39,11 @@ class DateTimeSkill(SkillBase):
|
|
39
39
|
"description": "Timezone name (e.g., 'America/New_York', 'Europe/London'). Defaults to UTC."
|
40
40
|
}
|
41
41
|
},
|
42
|
-
handler=self._get_time_handler
|
42
|
+
handler=self._get_time_handler,
|
43
|
+
**self.swaig_fields
|
43
44
|
)
|
44
45
|
|
45
|
-
self.
|
46
|
+
self.agent.define_tool(
|
46
47
|
name="get_current_date",
|
47
48
|
description="Get the current date",
|
48
49
|
parameters={
|
@@ -51,7 +52,8 @@ class DateTimeSkill(SkillBase):
|
|
51
52
|
"description": "Timezone name for the date. Defaults to UTC."
|
52
53
|
}
|
53
54
|
},
|
54
|
-
handler=self._get_date_handler
|
55
|
+
handler=self._get_date_handler,
|
56
|
+
**self.swaig_fields
|
55
57
|
)
|
56
58
|
|
57
59
|
def _get_time_handler(self, args, raw_data):
|
@@ -92,7 +94,9 @@ class DateTimeSkill(SkillBase):
|
|
92
94
|
|
93
95
|
def get_hints(self) -> List[str]:
|
94
96
|
"""Return speech recognition hints"""
|
95
|
-
|
97
|
+
# Currently no hints provided, but you could add them like:
|
98
|
+
# return ["time", "date", "today", "now", "current", "timezone"]
|
99
|
+
return []
|
96
100
|
|
97
101
|
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
98
102
|
"""Return prompt sections to add to agent"""
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Joke Skill for SignalWire Agents using DataMap"""
|
@@ -0,0 +1,88 @@
|
|
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
|
+
from typing import List, Dict, Any
|
11
|
+
|
12
|
+
from signalwire_agents.core.skill_base import SkillBase
|
13
|
+
from signalwire_agents.core.data_map import DataMap
|
14
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
15
|
+
|
16
|
+
|
17
|
+
class JokeSkill(SkillBase):
|
18
|
+
"""Joke telling capability using API Ninjas with DataMap"""
|
19
|
+
|
20
|
+
SKILL_NAME = "joke"
|
21
|
+
SKILL_DESCRIPTION = "Tell jokes using the API Ninjas joke API"
|
22
|
+
SKILL_VERSION = "1.0.0"
|
23
|
+
REQUIRED_PACKAGES = [] # DataMap doesn't require local packages
|
24
|
+
REQUIRED_ENV_VARS = [] # API key comes from parameters
|
25
|
+
|
26
|
+
def setup(self) -> bool:
|
27
|
+
"""Setup the joke skill"""
|
28
|
+
# Validate required parameters
|
29
|
+
required_params = ['api_key']
|
30
|
+
missing_params = [param for param in required_params if not self.params.get(param)]
|
31
|
+
if missing_params:
|
32
|
+
self.logger.error(f"Missing required parameters: {missing_params}")
|
33
|
+
return False
|
34
|
+
|
35
|
+
# Set parameters from config
|
36
|
+
self.api_key = self.params['api_key']
|
37
|
+
|
38
|
+
# Optional parameters with defaults
|
39
|
+
self.tool_name = self.params.get('tool_name', 'get_joke')
|
40
|
+
|
41
|
+
return True
|
42
|
+
|
43
|
+
def register_tools(self) -> None:
|
44
|
+
"""Register joke tool using DataMap"""
|
45
|
+
|
46
|
+
# Create DataMap tool for jokes - uses required enum parameter
|
47
|
+
joke_tool = (DataMap(self.tool_name)
|
48
|
+
.description('Get a random joke from API Ninjas')
|
49
|
+
.parameter('type', 'string', 'Type of joke to get',
|
50
|
+
required=True, enum=['jokes', 'dadjokes'])
|
51
|
+
.webhook('GET', "https://api.api-ninjas.com/v1/${args.type}",
|
52
|
+
headers={'X-Api-Key': self.api_key})
|
53
|
+
.output(SwaigFunctionResult('Here\'s a joke: ${array[0].joke}'))
|
54
|
+
.error_keys(['error'])
|
55
|
+
.fallback_output(SwaigFunctionResult('Sorry, there is a problem with the joke service right now. Please try again later.'))
|
56
|
+
)
|
57
|
+
|
58
|
+
# Register the DataMap tool with the agent
|
59
|
+
self.agent.register_swaig_function(joke_tool.to_swaig_function())
|
60
|
+
|
61
|
+
def get_hints(self) -> List[str]:
|
62
|
+
"""Return speech recognition hints"""
|
63
|
+
# Currently no hints are provided, but you could add them like:
|
64
|
+
# return [
|
65
|
+
# "joke", "tell me a joke", "funny", "humor", "dad joke",
|
66
|
+
# "regular joke", "make me laugh", "comedy"
|
67
|
+
# ]
|
68
|
+
return []
|
69
|
+
|
70
|
+
def get_global_data(self) -> Dict[str, Any]:
|
71
|
+
"""Return global data to be available in DataMap variables"""
|
72
|
+
return {
|
73
|
+
"joke_skill_enabled": True
|
74
|
+
}
|
75
|
+
|
76
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
77
|
+
"""Return prompt sections to add to agent"""
|
78
|
+
return [
|
79
|
+
{
|
80
|
+
"title": "Joke Telling",
|
81
|
+
"body": "You can tell jokes to entertain users.",
|
82
|
+
"bullets": [
|
83
|
+
f"Use {self.tool_name} to tell jokes when users ask for humor",
|
84
|
+
"You can tell regular jokes or dad jokes",
|
85
|
+
"Be enthusiastic and fun when sharing jokes"
|
86
|
+
]
|
87
|
+
}
|
88
|
+
]
|
@@ -29,7 +29,7 @@ class MathSkill(SkillBase):
|
|
29
29
|
def register_tools(self) -> None:
|
30
30
|
"""Register math tools with the agent"""
|
31
31
|
|
32
|
-
self.
|
32
|
+
self.agent.define_tool(
|
33
33
|
name="calculate",
|
34
34
|
description="Perform a mathematical calculation with basic operations (+, -, *, /, %, **)",
|
35
35
|
parameters={
|
@@ -38,7 +38,8 @@ class MathSkill(SkillBase):
|
|
38
38
|
"description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', '(10 + 5) / 3')"
|
39
39
|
}
|
40
40
|
},
|
41
|
-
handler=self._calculate_handler
|
41
|
+
handler=self._calculate_handler,
|
42
|
+
**self.swaig_fields
|
42
43
|
)
|
43
44
|
|
44
45
|
def _calculate_handler(self, args, raw_data):
|
@@ -68,10 +69,12 @@ class MathSkill(SkillBase):
|
|
68
69
|
|
69
70
|
def get_hints(self) -> List[str]:
|
70
71
|
"""Return speech recognition hints"""
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
# Currently no hints provided, but you could add them like:
|
73
|
+
# return [
|
74
|
+
# "calculate", "math", "plus", "minus", "times", "multiply",
|
75
|
+
# "divide", "equals", "percent", "power", "squared"
|
76
|
+
# ]
|
77
|
+
return []
|
75
78
|
|
76
79
|
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
77
80
|
"""Return prompt sections to add to agent"""
|
@@ -11,9 +11,16 @@ 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
|
-
|
17
|
+
|
18
|
+
try:
|
19
|
+
import structlog
|
20
|
+
logger_available = True
|
21
|
+
except ImportError:
|
22
|
+
import logging
|
23
|
+
logger_available = False
|
17
24
|
|
18
25
|
from signalwire_agents.core.skill_base import SkillBase
|
19
26
|
|
@@ -22,7 +29,14 @@ class SkillRegistry:
|
|
22
29
|
|
23
30
|
def __init__(self):
|
24
31
|
self._skills: Dict[str, Type[SkillBase]] = {}
|
25
|
-
|
32
|
+
|
33
|
+
# Use structlog if available, fallback to logging
|
34
|
+
if logger_available:
|
35
|
+
self.logger = structlog.get_logger("skill_registry")
|
36
|
+
else:
|
37
|
+
import logging
|
38
|
+
self.logger = logging.getLogger("skill_registry")
|
39
|
+
|
26
40
|
self._discovered = False
|
27
41
|
|
28
42
|
def discover_skills(self) -> None:
|
@@ -39,7 +53,11 @@ class SkillRegistry:
|
|
39
53
|
self._load_skill_from_directory(item)
|
40
54
|
|
41
55
|
self._discovered = True
|
42
|
-
|
56
|
+
|
57
|
+
# Check if we're in raw mode (used by swaig-test --raw) and suppress logging
|
58
|
+
is_raw_mode = "--raw" in sys.argv
|
59
|
+
if not is_raw_mode:
|
60
|
+
self.logger.info(f"Discovered {len(self._skills)} skills")
|
43
61
|
|
44
62
|
def _load_skill_from_directory(self, skill_dir: Path) -> None:
|
45
63
|
"""Load a skill from a directory"""
|
@@ -89,7 +107,8 @@ class SkillRegistry:
|
|
89
107
|
"description": skill_class.SKILL_DESCRIPTION,
|
90
108
|
"version": skill_class.SKILL_VERSION,
|
91
109
|
"required_packages": skill_class.REQUIRED_PACKAGES,
|
92
|
-
"required_env_vars": skill_class.REQUIRED_ENV_VARS
|
110
|
+
"required_env_vars": skill_class.REQUIRED_ENV_VARS,
|
111
|
+
"supports_multiple_instances": skill_class.SUPPORTS_MULTIPLE_INSTANCES
|
93
112
|
}
|
94
113
|
for skill_class in self._skills.values()
|
95
114
|
]
|