signalwire-agents 0.1.7__py3-none-any.whl → 0.1.9__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 +4 -1
- signalwire_agents/core/agent_base.py +305 -41
- signalwire_agents/core/function_result.py +1031 -1
- signalwire_agents/core/skill_base.py +87 -0
- signalwire_agents/core/skill_manager.py +136 -0
- signalwire_agents/core/swml_builder.py +5 -1
- signalwire_agents/prefabs/info_gatherer.py +149 -33
- signalwire_agents/prefabs/receptionist.py +14 -22
- signalwire_agents/skills/__init__.py +14 -0
- signalwire_agents/skills/datetime/__init__.py +1 -0
- signalwire_agents/skills/datetime/skill.py +109 -0
- signalwire_agents/skills/math/__init__.py +1 -0
- signalwire_agents/skills/math/skill.py +88 -0
- signalwire_agents/skills/registry.py +98 -0
- signalwire_agents/skills/web_search/__init__.py +1 -0
- signalwire_agents/skills/web_search/skill.py +240 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/METADATA +113 -2
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/RECORD +22 -12
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/WHEEL +1 -1
- {signalwire_agents-0.1.7.data → signalwire_agents-0.1.9.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.7.dist-info → signalwire_agents-0.1.9.dist-info}/top_level.txt +0 -0
@@ -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
|
+
import re
|
11
|
+
from typing import List, Dict, Any
|
12
|
+
|
13
|
+
from signalwire_agents.core.skill_base import SkillBase
|
14
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
15
|
+
|
16
|
+
class MathSkill(SkillBase):
|
17
|
+
"""Provides basic mathematical calculation capabilities"""
|
18
|
+
|
19
|
+
SKILL_NAME = "math"
|
20
|
+
SKILL_DESCRIPTION = "Perform basic mathematical calculations"
|
21
|
+
SKILL_VERSION = "1.0.0"
|
22
|
+
REQUIRED_PACKAGES = []
|
23
|
+
REQUIRED_ENV_VARS = []
|
24
|
+
|
25
|
+
def setup(self) -> bool:
|
26
|
+
"""Setup the math skill"""
|
27
|
+
return True
|
28
|
+
|
29
|
+
def register_tools(self) -> None:
|
30
|
+
"""Register math tools with the agent"""
|
31
|
+
|
32
|
+
self.agent.define_tool(
|
33
|
+
name="calculate",
|
34
|
+
description="Perform a mathematical calculation with basic operations (+, -, *, /, %, **)",
|
35
|
+
parameters={
|
36
|
+
"expression": {
|
37
|
+
"type": "string",
|
38
|
+
"description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', '(10 + 5) / 3')"
|
39
|
+
}
|
40
|
+
},
|
41
|
+
handler=self._calculate_handler
|
42
|
+
)
|
43
|
+
|
44
|
+
def _calculate_handler(self, args, raw_data):
|
45
|
+
"""Handler for calculate tool"""
|
46
|
+
expression = args.get("expression", "").strip()
|
47
|
+
|
48
|
+
if not expression:
|
49
|
+
return SwaigFunctionResult("Please provide a mathematical expression to calculate.")
|
50
|
+
|
51
|
+
# Security: only allow safe mathematical operations
|
52
|
+
safe_chars = re.compile(r'^[0-9+\-*/().\s%**]+$')
|
53
|
+
if not safe_chars.match(expression):
|
54
|
+
return SwaigFunctionResult(
|
55
|
+
"Invalid expression. Only numbers and basic math operators (+, -, *, /, %, **, parentheses) are allowed."
|
56
|
+
)
|
57
|
+
|
58
|
+
try:
|
59
|
+
# Evaluate the expression safely
|
60
|
+
result = eval(expression, {"__builtins__": {}}, {})
|
61
|
+
|
62
|
+
return SwaigFunctionResult(f"{expression} = {result}")
|
63
|
+
|
64
|
+
except ZeroDivisionError:
|
65
|
+
return SwaigFunctionResult("Error: Division by zero is not allowed.")
|
66
|
+
except Exception as e:
|
67
|
+
return SwaigFunctionResult(f"Error calculating '{expression}': Invalid expression")
|
68
|
+
|
69
|
+
def get_hints(self) -> List[str]:
|
70
|
+
"""Return speech recognition hints"""
|
71
|
+
return [
|
72
|
+
"calculate", "math", "plus", "minus", "times", "multiply",
|
73
|
+
"divide", "equals", "percent", "power", "squared"
|
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": "Mathematical Calculations",
|
81
|
+
"body": "You can perform mathematical calculations for users.",
|
82
|
+
"bullets": [
|
83
|
+
"Use the calculate tool for any math expressions",
|
84
|
+
"Supports basic operations: +, -, *, /, %, ** (power)",
|
85
|
+
"Can handle parentheses for complex expressions"
|
86
|
+
]
|
87
|
+
}
|
88
|
+
]
|
@@ -0,0 +1,98 @@
|
|
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 importlib
|
12
|
+
import importlib.util
|
13
|
+
import inspect
|
14
|
+
from typing import Dict, List, Type, Optional
|
15
|
+
from pathlib import Path
|
16
|
+
import logging
|
17
|
+
|
18
|
+
from signalwire_agents.core.skill_base import SkillBase
|
19
|
+
|
20
|
+
class SkillRegistry:
|
21
|
+
"""Global registry for discovering and managing skills"""
|
22
|
+
|
23
|
+
def __init__(self):
|
24
|
+
self._skills: Dict[str, Type[SkillBase]] = {}
|
25
|
+
self.logger = logging.getLogger("skill_registry")
|
26
|
+
self._discovered = False
|
27
|
+
|
28
|
+
def discover_skills(self) -> None:
|
29
|
+
"""Discover skills from the skills directory"""
|
30
|
+
if self._discovered:
|
31
|
+
return
|
32
|
+
|
33
|
+
# Get the skills directory path
|
34
|
+
skills_dir = Path(__file__).parent
|
35
|
+
|
36
|
+
# Scan for skill directories
|
37
|
+
for item in skills_dir.iterdir():
|
38
|
+
if item.is_dir() and not item.name.startswith('__'):
|
39
|
+
self._load_skill_from_directory(item)
|
40
|
+
|
41
|
+
self._discovered = True
|
42
|
+
self.logger.info(f"Discovered {len(self._skills)} skills")
|
43
|
+
|
44
|
+
def _load_skill_from_directory(self, skill_dir: Path) -> None:
|
45
|
+
"""Load a skill from a directory"""
|
46
|
+
skill_file = skill_dir / "skill.py"
|
47
|
+
if not skill_file.exists():
|
48
|
+
return
|
49
|
+
|
50
|
+
try:
|
51
|
+
# Import the skill module
|
52
|
+
module_name = f"signalwire_agents.skills.{skill_dir.name}.skill"
|
53
|
+
spec = importlib.util.spec_from_file_location(module_name, skill_file)
|
54
|
+
module = importlib.util.module_from_spec(spec)
|
55
|
+
spec.loader.exec_module(module)
|
56
|
+
|
57
|
+
# Find SkillBase subclasses in the module
|
58
|
+
for name, obj in inspect.getmembers(module):
|
59
|
+
if (inspect.isclass(obj) and
|
60
|
+
issubclass(obj, SkillBase) and
|
61
|
+
obj != SkillBase and
|
62
|
+
obj.SKILL_NAME is not None):
|
63
|
+
|
64
|
+
self.register_skill(obj)
|
65
|
+
|
66
|
+
except Exception as e:
|
67
|
+
self.logger.error(f"Failed to load skill from {skill_dir}: {e}")
|
68
|
+
|
69
|
+
def register_skill(self, skill_class: Type[SkillBase]) -> None:
|
70
|
+
"""Register a skill class"""
|
71
|
+
if skill_class.SKILL_NAME in self._skills:
|
72
|
+
self.logger.warning(f"Skill '{skill_class.SKILL_NAME}' already registered")
|
73
|
+
return
|
74
|
+
|
75
|
+
self._skills[skill_class.SKILL_NAME] = skill_class
|
76
|
+
self.logger.debug(f"Registered skill '{skill_class.SKILL_NAME}'")
|
77
|
+
|
78
|
+
def get_skill_class(self, skill_name: str) -> Optional[Type[SkillBase]]:
|
79
|
+
"""Get skill class by name"""
|
80
|
+
self.discover_skills() # Ensure skills are discovered
|
81
|
+
return self._skills.get(skill_name)
|
82
|
+
|
83
|
+
def list_skills(self) -> List[Dict[str, str]]:
|
84
|
+
"""List all registered skills with metadata"""
|
85
|
+
self.discover_skills()
|
86
|
+
return [
|
87
|
+
{
|
88
|
+
"name": skill_class.SKILL_NAME,
|
89
|
+
"description": skill_class.SKILL_DESCRIPTION,
|
90
|
+
"version": skill_class.SKILL_VERSION,
|
91
|
+
"required_packages": skill_class.REQUIRED_PACKAGES,
|
92
|
+
"required_env_vars": skill_class.REQUIRED_ENV_VARS
|
93
|
+
}
|
94
|
+
for skill_class in self._skills.values()
|
95
|
+
]
|
96
|
+
|
97
|
+
# Global registry instance
|
98
|
+
skill_registry = SkillRegistry()
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Web Search Skill for SignalWire Agents"""
|
@@ -0,0 +1,240 @@
|
|
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 requests
|
12
|
+
import time
|
13
|
+
from urllib.parse import urljoin, urlparse
|
14
|
+
from bs4 import BeautifulSoup
|
15
|
+
import json
|
16
|
+
from typing import Optional, List, Dict, Any
|
17
|
+
|
18
|
+
from signalwire_agents.core.skill_base import SkillBase
|
19
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
20
|
+
|
21
|
+
class GoogleSearchScraper:
|
22
|
+
"""Google Search and Web Scraping functionality"""
|
23
|
+
|
24
|
+
def __init__(self, api_key: str, search_engine_id: str):
|
25
|
+
self.api_key = api_key
|
26
|
+
self.search_engine_id = search_engine_id
|
27
|
+
self.session = requests.Session()
|
28
|
+
self.session.headers.update({
|
29
|
+
'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'
|
30
|
+
})
|
31
|
+
|
32
|
+
def search_google(self, query: str, num_results: int = 5) -> list:
|
33
|
+
"""Search Google using Custom Search JSON API"""
|
34
|
+
url = "https://www.googleapis.com/customsearch/v1"
|
35
|
+
|
36
|
+
params = {
|
37
|
+
'key': self.api_key,
|
38
|
+
'cx': self.search_engine_id,
|
39
|
+
'q': query,
|
40
|
+
'num': min(num_results, 10)
|
41
|
+
}
|
42
|
+
|
43
|
+
try:
|
44
|
+
response = self.session.get(url, params=params)
|
45
|
+
response.raise_for_status()
|
46
|
+
data = response.json()
|
47
|
+
|
48
|
+
if 'items' not in data:
|
49
|
+
return []
|
50
|
+
|
51
|
+
results = []
|
52
|
+
for item in data['items'][:num_results]:
|
53
|
+
results.append({
|
54
|
+
'title': item.get('title', ''),
|
55
|
+
'url': item.get('link', ''),
|
56
|
+
'snippet': item.get('snippet', '')
|
57
|
+
})
|
58
|
+
|
59
|
+
return results
|
60
|
+
|
61
|
+
except Exception as e:
|
62
|
+
return []
|
63
|
+
|
64
|
+
def extract_text_from_url(self, url: str, timeout: int = 10) -> str:
|
65
|
+
"""Scrape a URL and extract readable text content"""
|
66
|
+
try:
|
67
|
+
response = self.session.get(url, timeout=timeout)
|
68
|
+
response.raise_for_status()
|
69
|
+
|
70
|
+
soup = BeautifulSoup(response.content, 'html.parser')
|
71
|
+
|
72
|
+
# Remove unwanted elements
|
73
|
+
for script in soup(["script", "style", "nav", "footer", "header", "aside"]):
|
74
|
+
script.decompose()
|
75
|
+
|
76
|
+
text = soup.get_text()
|
77
|
+
|
78
|
+
# Clean up the text
|
79
|
+
lines = (line.strip() for line in text.splitlines())
|
80
|
+
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
81
|
+
text = ' '.join(chunk for chunk in chunks if chunk)
|
82
|
+
|
83
|
+
# Limit text length
|
84
|
+
if len(text) > 2000:
|
85
|
+
text = text[:2000] + "... [Content truncated]"
|
86
|
+
|
87
|
+
return text
|
88
|
+
|
89
|
+
except Exception as e:
|
90
|
+
return ""
|
91
|
+
|
92
|
+
def search_and_scrape(self, query: str, num_results: int = 3, delay: float = 0.5) -> str:
|
93
|
+
"""Main function: search Google and scrape the resulting pages"""
|
94
|
+
search_results = self.search_google(query, num_results)
|
95
|
+
|
96
|
+
if not search_results:
|
97
|
+
return f"No search results found for query: {query}"
|
98
|
+
|
99
|
+
all_text = []
|
100
|
+
|
101
|
+
for i, result in enumerate(search_results, 1):
|
102
|
+
text_content = f"=== RESULT {i} ===\n"
|
103
|
+
text_content += f"Title: {result['title']}\n"
|
104
|
+
text_content += f"URL: {result['url']}\n"
|
105
|
+
text_content += f"Snippet: {result['snippet']}\n"
|
106
|
+
text_content += f"Content:\n"
|
107
|
+
|
108
|
+
page_text = self.extract_text_from_url(result['url'])
|
109
|
+
|
110
|
+
if page_text:
|
111
|
+
text_content += page_text
|
112
|
+
else:
|
113
|
+
text_content += "Failed to extract content from this page."
|
114
|
+
|
115
|
+
text_content += f"\n{'='*50}\n\n"
|
116
|
+
all_text.append(text_content)
|
117
|
+
|
118
|
+
if i < len(search_results):
|
119
|
+
time.sleep(delay)
|
120
|
+
|
121
|
+
return '\n'.join(all_text)
|
122
|
+
|
123
|
+
|
124
|
+
class WebSearchSkill(SkillBase):
|
125
|
+
"""Web search capability using Google Custom Search API"""
|
126
|
+
|
127
|
+
SKILL_NAME = "web_search"
|
128
|
+
SKILL_DESCRIPTION = "Search the web for information using Google Custom Search API"
|
129
|
+
SKILL_VERSION = "1.0.0"
|
130
|
+
REQUIRED_PACKAGES = ["bs4", "requests"]
|
131
|
+
REQUIRED_ENV_VARS = ["GOOGLE_SEARCH_API_KEY", "GOOGLE_SEARCH_ENGINE_ID"]
|
132
|
+
|
133
|
+
def setup(self) -> bool:
|
134
|
+
"""Setup the web search skill"""
|
135
|
+
if not self.validate_env_vars() or not self.validate_packages():
|
136
|
+
return False
|
137
|
+
|
138
|
+
# Set default parameters
|
139
|
+
self.default_num_results = self.params.get('num_results', 1)
|
140
|
+
self.default_delay = self.params.get('delay', 0)
|
141
|
+
|
142
|
+
# Initialize the search scraper
|
143
|
+
self.search_scraper = GoogleSearchScraper(
|
144
|
+
api_key=os.getenv('GOOGLE_SEARCH_API_KEY'),
|
145
|
+
search_engine_id=os.getenv('GOOGLE_SEARCH_ENGINE_ID')
|
146
|
+
)
|
147
|
+
|
148
|
+
return True
|
149
|
+
|
150
|
+
def register_tools(self) -> None:
|
151
|
+
"""Register web search tool with the agent"""
|
152
|
+
self.agent.define_tool(
|
153
|
+
name="web_search",
|
154
|
+
description="Search the web for information on any topic and return detailed results with content from multiple sources",
|
155
|
+
parameters={
|
156
|
+
"query": {
|
157
|
+
"type": "string",
|
158
|
+
"description": "The search query - what you want to find information about"
|
159
|
+
},
|
160
|
+
"num_results": {
|
161
|
+
"type": "integer",
|
162
|
+
"description": f"Number of web pages to search and extract content from (1-10, default: {self.default_num_results})",
|
163
|
+
"minimum": 1,
|
164
|
+
"maximum": 10
|
165
|
+
}
|
166
|
+
},
|
167
|
+
handler=self._web_search_handler
|
168
|
+
)
|
169
|
+
|
170
|
+
def _web_search_handler(self, args, raw_data):
|
171
|
+
"""Handler for web search tool"""
|
172
|
+
query = args.get("query", "").strip()
|
173
|
+
num_results = args.get("num_results", self.default_num_results)
|
174
|
+
|
175
|
+
if not query:
|
176
|
+
return SwaigFunctionResult(
|
177
|
+
"Please provide a search query. What would you like me to search for?"
|
178
|
+
)
|
179
|
+
|
180
|
+
# Validate num_results
|
181
|
+
try:
|
182
|
+
num_results = int(num_results)
|
183
|
+
num_results = max(1, min(num_results, 10))
|
184
|
+
except (ValueError, TypeError):
|
185
|
+
num_results = self.default_num_results
|
186
|
+
|
187
|
+
self.logger.info(f"Web search requested: '{query}' ({num_results} results)")
|
188
|
+
|
189
|
+
# Perform the search
|
190
|
+
try:
|
191
|
+
search_results = self.search_scraper.search_and_scrape(
|
192
|
+
query=query,
|
193
|
+
num_results=num_results,
|
194
|
+
delay=self.default_delay
|
195
|
+
)
|
196
|
+
|
197
|
+
if not search_results or "No search results found" in search_results:
|
198
|
+
return SwaigFunctionResult(
|
199
|
+
f"I couldn't find any results for '{query}'. "
|
200
|
+
"This might be due to a very specific query or temporary issues. "
|
201
|
+
"Try rephrasing your search or asking about a different topic."
|
202
|
+
)
|
203
|
+
|
204
|
+
response = f"I found {num_results} results for '{query}':\n\n{search_results}"
|
205
|
+
return SwaigFunctionResult(response)
|
206
|
+
|
207
|
+
except Exception as e:
|
208
|
+
self.logger.error(f"Error performing web search: {e}")
|
209
|
+
return SwaigFunctionResult(
|
210
|
+
"Sorry, I encountered an error while searching. Please try again later."
|
211
|
+
)
|
212
|
+
|
213
|
+
def get_hints(self) -> List[str]:
|
214
|
+
"""Return speech recognition hints"""
|
215
|
+
return [
|
216
|
+
"Google", "search", "internet", "web", "information",
|
217
|
+
"find", "look up", "research", "query", "results"
|
218
|
+
]
|
219
|
+
|
220
|
+
def get_global_data(self) -> Dict[str, Any]:
|
221
|
+
"""Return global data for agent context"""
|
222
|
+
return {
|
223
|
+
"web_search_enabled": True,
|
224
|
+
"search_provider": "Google Custom Search"
|
225
|
+
}
|
226
|
+
|
227
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
228
|
+
"""Return prompt sections to add to agent"""
|
229
|
+
return [
|
230
|
+
{
|
231
|
+
"title": "Web Search Capability",
|
232
|
+
"body": "You can search the internet for current, accurate information on any topic.",
|
233
|
+
"bullets": [
|
234
|
+
"Use the web_search tool when users ask for information you need to look up",
|
235
|
+
"Search for news, current events, product information, or any current data",
|
236
|
+
"Summarize search results in a clear, helpful way",
|
237
|
+
"Include relevant URLs so users can read more if interested"
|
238
|
+
]
|
239
|
+
}
|
240
|
+
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: signalwire_agents
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.9
|
4
4
|
Summary: SignalWire AI Agents SDK
|
5
5
|
Author-email: SignalWire Team <info@signalwire.com>
|
6
6
|
Project-URL: Homepage, https://github.com/signalwire/signalwire-ai-agents
|
@@ -24,6 +24,8 @@ Requires-Dist: setuptools==66.1.1
|
|
24
24
|
Requires-Dist: signalwire_pom==2.7.1
|
25
25
|
Requires-Dist: structlog==25.3.0
|
26
26
|
Requires-Dist: uvicorn==0.34.2
|
27
|
+
Requires-Dist: beautifulsoup4==4.12.3
|
28
|
+
Requires-Dist: pytz==2023.3
|
27
29
|
Dynamic: license-file
|
28
30
|
|
29
31
|
# SignalWire AI Agent SDK
|
@@ -35,12 +37,54 @@ A Python SDK for creating, hosting, and securing SignalWire AI agents as microse
|
|
35
37
|
- **Self-Contained Agents**: Each agent is both a web app and an AI persona
|
36
38
|
- **Prompt Object Model**: Structured prompt composition using POM
|
37
39
|
- **SWAIG Integration**: Easily define and handle AI tools/functions
|
40
|
+
- **Dynamic Configuration**: Configure agents per-request for multi-tenant apps and personalization
|
38
41
|
- **Custom Routing**: Dynamic request handling for different paths and content
|
39
42
|
- **SIP Integration**: Route SIP calls to agents based on SIP usernames
|
40
43
|
- **Security Built-In**: Session management, function-specific security tokens, and basic auth
|
41
44
|
- **State Management**: Persistent conversation state with automatic tracking
|
42
45
|
- **Prefab Archetypes**: Ready-to-use agent types for common scenarios
|
43
46
|
- **Multi-Agent Support**: Host multiple agents on a single server
|
47
|
+
- **Modular Skills System**: Add capabilities to agents with simple one-liner calls
|
48
|
+
|
49
|
+
## Skills System
|
50
|
+
|
51
|
+
The SignalWire Agents SDK includes a powerful modular skills system that allows you to add complex capabilities to your agents with simple one-liner calls:
|
52
|
+
|
53
|
+
```python
|
54
|
+
from signalwire_agents import AgentBase
|
55
|
+
|
56
|
+
# Create an agent
|
57
|
+
agent = AgentBase("My Assistant", route="/assistant")
|
58
|
+
|
59
|
+
# Add skills with one-liners
|
60
|
+
agent.add_skill("web_search") # Web search capability
|
61
|
+
agent.add_skill("datetime") # Current date/time info
|
62
|
+
agent.add_skill("math") # Mathematical calculations
|
63
|
+
|
64
|
+
# Configure skills with parameters
|
65
|
+
agent.add_skill("web_search", {
|
66
|
+
"num_results": 3, # Get 3 search results
|
67
|
+
"delay": 0.5 # Small delay between requests
|
68
|
+
})
|
69
|
+
|
70
|
+
agent.serve()
|
71
|
+
```
|
72
|
+
|
73
|
+
### Available Built-in Skills
|
74
|
+
|
75
|
+
- **web_search**: Google Custom Search API integration with web scraping
|
76
|
+
- **datetime**: Current date and time with timezone support
|
77
|
+
- **math**: Safe mathematical expression evaluation
|
78
|
+
|
79
|
+
### Benefits
|
80
|
+
|
81
|
+
- **One-liner integration**: `agent.add_skill("skill_name")`
|
82
|
+
- **Configurable parameters**: `agent.add_skill("skill_name", {"param": "value"})`
|
83
|
+
- **Automatic discovery**: Skills are automatically found from the skills directory
|
84
|
+
- **Dependency validation**: Clear error messages for missing requirements
|
85
|
+
- **Modular architecture**: Skills are self-contained and reusable
|
86
|
+
|
87
|
+
For detailed documentation, see [Skills System README](docs/SKILLS_SYSTEM_README.md).
|
44
88
|
|
45
89
|
## Installation
|
46
90
|
|
@@ -165,6 +209,73 @@ Available prefabs include:
|
|
165
209
|
- `FAQBotAgent`: Answers questions based on a knowledge base
|
166
210
|
- `ConciergeAgent`: Routes users to specialized agents
|
167
211
|
- `SurveyAgent`: Conducts structured surveys with questions and rating scales
|
212
|
+
- `ReceptionistAgent`: Greets callers and transfers them to appropriate departments
|
213
|
+
|
214
|
+
## Dynamic Agent Configuration
|
215
|
+
|
216
|
+
Configure agents dynamically based on request parameters for multi-tenant applications, A/B testing, and personalization.
|
217
|
+
|
218
|
+
### Static vs Dynamic Configuration
|
219
|
+
|
220
|
+
- **Static**: Agent configured once at startup (traditional approach)
|
221
|
+
- **Dynamic**: Agent configured fresh for each request based on parameters
|
222
|
+
|
223
|
+
### Basic Example
|
224
|
+
|
225
|
+
```python
|
226
|
+
from signalwire_agents import AgentBase
|
227
|
+
|
228
|
+
class DynamicAgent(AgentBase):
|
229
|
+
def __init__(self):
|
230
|
+
super().__init__(name="dynamic-agent", route="/dynamic")
|
231
|
+
|
232
|
+
# Set up dynamic configuration callback
|
233
|
+
self.set_dynamic_config_callback(self.configure_per_request)
|
234
|
+
|
235
|
+
def configure_per_request(self, query_params, body_params, headers, agent):
|
236
|
+
"""Configure agent based on request parameters"""
|
237
|
+
|
238
|
+
# Extract parameters from request
|
239
|
+
tier = query_params.get('tier', 'standard')
|
240
|
+
language = query_params.get('language', 'en')
|
241
|
+
customer_id = query_params.get('customer_id')
|
242
|
+
|
243
|
+
# Configure voice and language
|
244
|
+
if language == 'es':
|
245
|
+
agent.add_language("Spanish", "es-ES", "rime.spore:mistv2")
|
246
|
+
else:
|
247
|
+
agent.add_language("English", "en-US", "rime.spore:mistv2")
|
248
|
+
|
249
|
+
# Configure based on service tier
|
250
|
+
if tier == 'premium':
|
251
|
+
agent.set_params({"end_of_speech_timeout": 300}) # Faster response
|
252
|
+
agent.prompt_add_section("Service Level", "You provide premium support.")
|
253
|
+
else:
|
254
|
+
agent.set_params({"end_of_speech_timeout": 500}) # Standard response
|
255
|
+
agent.prompt_add_section("Service Level", "You provide standard support.")
|
256
|
+
|
257
|
+
# Personalize with customer data
|
258
|
+
global_data = {"tier": tier, "language": language}
|
259
|
+
if customer_id:
|
260
|
+
global_data["customer_id"] = customer_id
|
261
|
+
agent.set_global_data(global_data)
|
262
|
+
|
263
|
+
# Usage examples:
|
264
|
+
# curl "http://localhost:3000/dynamic?tier=premium&language=es&customer_id=123"
|
265
|
+
# curl "http://localhost:3000/dynamic?tier=standard&language=en"
|
266
|
+
```
|
267
|
+
|
268
|
+
### Use Cases
|
269
|
+
|
270
|
+
- **Multi-tenant SaaS**: Different configurations per customer/organization
|
271
|
+
- **A/B Testing**: Test different agent behaviors with different user groups
|
272
|
+
- **Personalization**: Customize voice, prompts, and behavior per user
|
273
|
+
- **Localization**: Language and cultural adaptation based on user location
|
274
|
+
- **Dynamic Pricing**: Adjust features and capabilities based on subscription tiers
|
275
|
+
|
276
|
+
The `EphemeralAgentConfig` object provides all the same familiar methods as `AgentBase` (like `add_language()`, `prompt_add_section()`, `set_global_data()`) but applies them per-request instead of at startup.
|
277
|
+
|
278
|
+
For detailed documentation and advanced examples, see the [Agent Guide](docs/agent_guide.md#dynamic-agent-configuration).
|
168
279
|
|
169
280
|
## Configuration
|
170
281
|
|
@@ -189,7 +300,7 @@ To enable HTTPS directly (without a reverse proxy), set `SWML_SSL_ENABLED` to "t
|
|
189
300
|
|
190
301
|
The package includes comprehensive documentation in the `docs/` directory:
|
191
302
|
|
192
|
-
- [Agent Guide](docs/agent_guide.md) - Detailed guide to creating and customizing agents
|
303
|
+
- [Agent Guide](docs/agent_guide.md) - Detailed guide to creating and customizing agents, including dynamic configuration
|
193
304
|
- [Architecture](docs/architecture.md) - Overview of the SDK architecture and core concepts
|
194
305
|
- [SWML Service Guide](docs/swml_service_guide.md) - Guide to the underlying SWML service
|
195
306
|
|
@@ -1,12 +1,14 @@
|
|
1
|
-
signalwire_agents/__init__.py,sha256=
|
1
|
+
signalwire_agents/__init__.py,sha256=aEEMPduIWEbHA41lbgxI6lOVAOmYI1C0nFc8_35bLj8,870
|
2
2
|
signalwire_agents/agent_server.py,sha256=se_YzOQE5UUoRUKCbTnOg9qr4G3qN7iVuQLutwXEwFU,12850
|
3
3
|
signalwire_agents/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
|
4
4
|
signalwire_agents/core/__init__.py,sha256=mVDLbpq1pg_WwiqsQR28NNZwJ6-VUXFIfg-vN7pk0ew,806
|
5
|
-
signalwire_agents/core/agent_base.py,sha256=
|
6
|
-
signalwire_agents/core/function_result.py,sha256=
|
5
|
+
signalwire_agents/core/agent_base.py,sha256=sQHFKzOHtQrmMn9TGSda-BzfLsqNV9JgG9npUPUrovE,114016
|
6
|
+
signalwire_agents/core/function_result.py,sha256=OXzm4GOvA6TKbzZFsdz3iBU5vQs9LXxsmQue7zWVolE,43538
|
7
7
|
signalwire_agents/core/pom_builder.py,sha256=ywuiIfP8BeLBPo_G4X1teZlG6zTCMkW71CZnmyoDTAQ,6636
|
8
|
+
signalwire_agents/core/skill_base.py,sha256=4IPSP422iKricMRcKWx0rIjy3XS5qaRUf5OSM_x_0QU,3015
|
9
|
+
signalwire_agents/core/skill_manager.py,sha256=kW17EOUR4u9mePo_90qKoCP6rusXDWzqZvJA0_FB-q4,5486
|
8
10
|
signalwire_agents/core/swaig_function.py,sha256=WoHGQuCmU9L9k39pttRunmfRtIa_PnNRn9W0Xq3MfIk,6316
|
9
|
-
signalwire_agents/core/swml_builder.py,sha256=
|
11
|
+
signalwire_agents/core/swml_builder.py,sha256=Q1ikU9pedgjW888mjbqDFv-jMDvDZ-tZgfyMfu4qQN0,6719
|
10
12
|
signalwire_agents/core/swml_handler.py,sha256=KvphI_YY47VWGVXaix_N3SuQSyygHEUr9We6xESQK44,7002
|
11
13
|
signalwire_agents/core/swml_renderer.py,sha256=iobMWWoBR7dkHndI3Qlwf4C0fg2p7DmAU2Rb7ZmmLhA,13891
|
12
14
|
signalwire_agents/core/swml_service.py,sha256=4zTtpIKDtL59MzKgkBdpPmBCT6s1wVIikWEzZ1hqnyc,49309
|
@@ -18,17 +20,25 @@ signalwire_agents/core/state/state_manager.py,sha256=76B4mDutMb826dK4c_IJhOXH09B
|
|
18
20
|
signalwire_agents/prefabs/__init__.py,sha256=MW11J63XH7KxF2MWguRsMFM9iqMWexaEO9ynDPL_PDM,715
|
19
21
|
signalwire_agents/prefabs/concierge.py,sha256=FQxSUBAVH2ECtNs4GGa3f0EPiOrAKaz14h7byNFy5K8,10106
|
20
22
|
signalwire_agents/prefabs/faq_bot.py,sha256=NrUn5AGmtdzYTyxTHPt8BZ14ZN1sh4xKA2SVQUlfPPQ,10834
|
21
|
-
signalwire_agents/prefabs/info_gatherer.py,sha256=
|
22
|
-
signalwire_agents/prefabs/receptionist.py,sha256=
|
23
|
+
signalwire_agents/prefabs/info_gatherer.py,sha256=dr9UUgNGX7MIKdCMth3jDVLf6OrHov5G6_zIuNvnrOY,15468
|
24
|
+
signalwire_agents/prefabs/receptionist.py,sha256=8EyQ6M0Egs3g7KKWukHFiO9UPoVUxT4MLkvyT3V8o64,10585
|
23
25
|
signalwire_agents/prefabs/survey.py,sha256=IIIQfgvMlfVNjEEEdWUn4lAJqVsCDlBsIAkOJ1ckyAE,14796
|
26
|
+
signalwire_agents/skills/__init__.py,sha256=xfxrQ0i-aTRomHiCsqelU4RlNlHPJFPgPu-UBDaBOqA,340
|
27
|
+
signalwire_agents/skills/registry.py,sha256=XffqhZQ4ZbAMRssMW8vnK5-Qk8oJBmiEoPwFCMcz1Gc,3515
|
28
|
+
signalwire_agents/skills/datetime/__init__.py,sha256=coPaY-k2EyZWuYckGunhSJ65Y1Jwz66h-iOo0QWb5WY,43
|
29
|
+
signalwire_agents/skills/datetime/skill.py,sha256=XtT-tvr-X07q15B96X1bmVCjFvWdXm3J0ZfJ1kxSJe4,3746
|
30
|
+
signalwire_agents/skills/math/__init__.py,sha256=lGAFWEmJH2fuwkuZUdDTY5dmucrIwtjfNT8bE2hOSP8,39
|
31
|
+
signalwire_agents/skills/math/skill.py,sha256=Li0oN_pSL3jlC36FGILeZO_P0txf1TVOMoKu1Hrnp_Q,3215
|
32
|
+
signalwire_agents/skills/web_search/__init__.py,sha256=wJlptYDExYw-nxZJVzlTLOgkKkDOLUUt1ZdoLt44ixs,45
|
33
|
+
signalwire_agents/skills/web_search/skill.py,sha256=whjY2v3k8hsHxn1KSyTibYl_rtkdVbi0kWAPZuegR5w,9018
|
24
34
|
signalwire_agents/utils/__init__.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
25
35
|
signalwire_agents/utils/pom_utils.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
26
36
|
signalwire_agents/utils/schema_utils.py,sha256=LvFCFvJTQk_xYK0B-NXbkXKEF7Zmv-LqpV_vfpPnOb4,13473
|
27
37
|
signalwire_agents/utils/token_generators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
28
38
|
signalwire_agents/utils/validators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
29
|
-
signalwire_agents-0.1.
|
30
|
-
signalwire_agents-0.1.
|
31
|
-
signalwire_agents-0.1.
|
32
|
-
signalwire_agents-0.1.
|
33
|
-
signalwire_agents-0.1.
|
34
|
-
signalwire_agents-0.1.
|
39
|
+
signalwire_agents-0.1.9.data/data/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
|
40
|
+
signalwire_agents-0.1.9.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
|
41
|
+
signalwire_agents-0.1.9.dist-info/METADATA,sha256=APX6OmsEYOL3iOdR6jwc5spU7pxy9lC8G1rSolR4SHU,12040
|
42
|
+
signalwire_agents-0.1.9.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
43
|
+
signalwire_agents-0.1.9.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
|
44
|
+
signalwire_agents-0.1.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|