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,87 @@
|
|
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 abc import ABC, abstractmethod
|
11
|
+
from typing import List, Dict, Any, TYPE_CHECKING, Optional
|
12
|
+
import logging
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from signalwire_agents.core.agent_base import AgentBase
|
16
|
+
|
17
|
+
class SkillBase(ABC):
|
18
|
+
"""Abstract base class for all agent skills"""
|
19
|
+
|
20
|
+
# Subclasses must define these
|
21
|
+
SKILL_NAME: str = None # Required: unique identifier
|
22
|
+
SKILL_DESCRIPTION: str = None # Required: human-readable description
|
23
|
+
SKILL_VERSION: str = "1.0.0" # Semantic version
|
24
|
+
REQUIRED_PACKAGES: List[str] = [] # Python packages needed
|
25
|
+
REQUIRED_ENV_VARS: List[str] = [] # Environment variables needed
|
26
|
+
|
27
|
+
def __init__(self, agent: 'AgentBase', params: Optional[Dict[str, Any]] = None):
|
28
|
+
if self.SKILL_NAME is None:
|
29
|
+
raise ValueError(f"{self.__class__.__name__} must define SKILL_NAME")
|
30
|
+
if self.SKILL_DESCRIPTION is None:
|
31
|
+
raise ValueError(f"{self.__class__.__name__} must define SKILL_DESCRIPTION")
|
32
|
+
|
33
|
+
self.agent = agent
|
34
|
+
self.params = params or {}
|
35
|
+
self.logger = logging.getLogger(f"skill.{self.SKILL_NAME}")
|
36
|
+
|
37
|
+
@abstractmethod
|
38
|
+
def setup(self) -> bool:
|
39
|
+
"""
|
40
|
+
Setup the skill (validate env vars, initialize APIs, etc.)
|
41
|
+
Returns True if setup successful, False otherwise
|
42
|
+
"""
|
43
|
+
pass
|
44
|
+
|
45
|
+
@abstractmethod
|
46
|
+
def register_tools(self) -> None:
|
47
|
+
"""Register SWAIG tools with the agent"""
|
48
|
+
pass
|
49
|
+
|
50
|
+
def get_hints(self) -> List[str]:
|
51
|
+
"""Return speech recognition hints for this skill"""
|
52
|
+
return []
|
53
|
+
|
54
|
+
def get_global_data(self) -> Dict[str, Any]:
|
55
|
+
"""Return data to add to agent's global context"""
|
56
|
+
return {}
|
57
|
+
|
58
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
59
|
+
"""Return prompt sections to add to agent"""
|
60
|
+
return []
|
61
|
+
|
62
|
+
def cleanup(self) -> None:
|
63
|
+
"""Cleanup when skill is removed or agent shuts down"""
|
64
|
+
pass
|
65
|
+
|
66
|
+
def validate_env_vars(self) -> bool:
|
67
|
+
"""Check if all required environment variables are set"""
|
68
|
+
import os
|
69
|
+
missing = [var for var in self.REQUIRED_ENV_VARS if not os.getenv(var)]
|
70
|
+
if missing:
|
71
|
+
self.logger.error(f"Missing required environment variables: {missing}")
|
72
|
+
return False
|
73
|
+
return True
|
74
|
+
|
75
|
+
def validate_packages(self) -> bool:
|
76
|
+
"""Check if all required packages are available"""
|
77
|
+
import importlib
|
78
|
+
missing = []
|
79
|
+
for package in self.REQUIRED_PACKAGES:
|
80
|
+
try:
|
81
|
+
importlib.import_module(package)
|
82
|
+
except ImportError:
|
83
|
+
missing.append(package)
|
84
|
+
if missing:
|
85
|
+
self.logger.error(f"Missing required packages: {missing}")
|
86
|
+
return False
|
87
|
+
return True
|
@@ -0,0 +1,136 @@
|
|
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 Dict, List, Type, Any, Optional
|
11
|
+
import logging
|
12
|
+
from signalwire_agents.core.skill_base import SkillBase
|
13
|
+
|
14
|
+
class SkillManager:
|
15
|
+
"""Manages loading and lifecycle of agent skills"""
|
16
|
+
|
17
|
+
def __init__(self, agent):
|
18
|
+
self.agent = agent
|
19
|
+
self.loaded_skills: Dict[str, SkillBase] = {}
|
20
|
+
self.logger = logging.getLogger("skill_manager")
|
21
|
+
|
22
|
+
def load_skill(self, skill_name: str, skill_class: Type[SkillBase] = None, params: Optional[Dict[str, Any]] = None) -> tuple[bool, str]:
|
23
|
+
"""
|
24
|
+
Load and setup a skill by name
|
25
|
+
|
26
|
+
Args:
|
27
|
+
skill_name: Name of the skill to load
|
28
|
+
skill_class: Optional skill class (if not provided, will try to find it)
|
29
|
+
params: Optional parameters to pass to the skill
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
tuple: (success, error_message) - error_message is empty string if successful
|
33
|
+
"""
|
34
|
+
if skill_name in self.loaded_skills:
|
35
|
+
self.logger.warning(f"Skill '{skill_name}' is already loaded")
|
36
|
+
return True, ""
|
37
|
+
|
38
|
+
# Get skill class from registry if not provided
|
39
|
+
if skill_class is None:
|
40
|
+
try:
|
41
|
+
from signalwire_agents.skills.registry import skill_registry
|
42
|
+
skill_class = skill_registry.get_skill_class(skill_name)
|
43
|
+
if skill_class is None:
|
44
|
+
error_msg = f"Skill '{skill_name}' not found in registry"
|
45
|
+
self.logger.error(error_msg)
|
46
|
+
return False, error_msg
|
47
|
+
except ImportError:
|
48
|
+
error_msg = f"Skills registry not available. Cannot load skill '{skill_name}'"
|
49
|
+
self.logger.error(error_msg)
|
50
|
+
return False, error_msg
|
51
|
+
|
52
|
+
try:
|
53
|
+
# Create skill instance with parameters
|
54
|
+
skill_instance = skill_class(self.agent, params)
|
55
|
+
|
56
|
+
# Validate environment variables with specific error details
|
57
|
+
import os
|
58
|
+
missing_env_vars = [var for var in skill_instance.REQUIRED_ENV_VARS if not os.getenv(var)]
|
59
|
+
if missing_env_vars:
|
60
|
+
error_msg = f"Missing required environment variables: {missing_env_vars}"
|
61
|
+
self.logger.error(error_msg)
|
62
|
+
return False, error_msg
|
63
|
+
|
64
|
+
# Validate packages with specific error details
|
65
|
+
import importlib
|
66
|
+
missing_packages = []
|
67
|
+
for package in skill_instance.REQUIRED_PACKAGES:
|
68
|
+
try:
|
69
|
+
importlib.import_module(package)
|
70
|
+
except ImportError:
|
71
|
+
missing_packages.append(package)
|
72
|
+
if missing_packages:
|
73
|
+
error_msg = f"Missing required packages: {missing_packages}"
|
74
|
+
self.logger.error(error_msg)
|
75
|
+
return False, error_msg
|
76
|
+
|
77
|
+
# Setup the skill
|
78
|
+
if not skill_instance.setup():
|
79
|
+
error_msg = f"Failed to setup skill '{skill_name}'"
|
80
|
+
self.logger.error(error_msg)
|
81
|
+
return False, error_msg
|
82
|
+
|
83
|
+
# Register tools with agent
|
84
|
+
skill_instance.register_tools()
|
85
|
+
|
86
|
+
# Add hints and global data to agent
|
87
|
+
hints = skill_instance.get_hints()
|
88
|
+
if hints:
|
89
|
+
self.agent.add_hints(hints)
|
90
|
+
|
91
|
+
global_data = skill_instance.get_global_data()
|
92
|
+
if global_data:
|
93
|
+
self.agent.update_global_data(global_data)
|
94
|
+
|
95
|
+
# Add prompt sections
|
96
|
+
prompt_sections = skill_instance.get_prompt_sections()
|
97
|
+
for section in prompt_sections:
|
98
|
+
self.agent.prompt_add_section(**section)
|
99
|
+
|
100
|
+
# Store loaded skill
|
101
|
+
self.loaded_skills[skill_name] = skill_instance
|
102
|
+
self.logger.info(f"Successfully loaded skill '{skill_name}'")
|
103
|
+
return True, ""
|
104
|
+
|
105
|
+
except Exception as e:
|
106
|
+
error_msg = f"Error loading skill '{skill_name}': {e}"
|
107
|
+
self.logger.error(error_msg)
|
108
|
+
return False, error_msg
|
109
|
+
|
110
|
+
def unload_skill(self, skill_name: str) -> bool:
|
111
|
+
"""Unload a skill and cleanup"""
|
112
|
+
if skill_name not in self.loaded_skills:
|
113
|
+
self.logger.warning(f"Skill '{skill_name}' is not loaded")
|
114
|
+
return False
|
115
|
+
|
116
|
+
try:
|
117
|
+
skill_instance = self.loaded_skills[skill_name]
|
118
|
+
skill_instance.cleanup()
|
119
|
+
del self.loaded_skills[skill_name]
|
120
|
+
self.logger.info(f"Successfully unloaded skill '{skill_name}'")
|
121
|
+
return True
|
122
|
+
except Exception as e:
|
123
|
+
self.logger.error(f"Error unloading skill '{skill_name}': {e}")
|
124
|
+
return False
|
125
|
+
|
126
|
+
def list_loaded_skills(self) -> List[str]:
|
127
|
+
"""List names of currently loaded skills"""
|
128
|
+
return list(self.loaded_skills.keys())
|
129
|
+
|
130
|
+
def has_skill(self, skill_name: str) -> bool:
|
131
|
+
"""Check if skill is currently loaded"""
|
132
|
+
return skill_name in self.loaded_skills
|
133
|
+
|
134
|
+
def get_skill(self, skill_name: str) -> Optional[SkillBase]:
|
135
|
+
"""Get a loaded skill instance by name"""
|
136
|
+
return self.loaded_skills.get(skill_name)
|
@@ -14,7 +14,11 @@ This module provides a fluent builder API for creating SWML documents.
|
|
14
14
|
It allows for chaining method calls to build up a document step by step.
|
15
15
|
"""
|
16
16
|
|
17
|
-
from typing import Dict, List, Any, Optional, Union,
|
17
|
+
from typing import Dict, List, Any, Optional, Union, TypeVar
|
18
|
+
try:
|
19
|
+
from typing import Self # Python 3.11+
|
20
|
+
except ImportError:
|
21
|
+
from typing_extensions import Self # For Python 3.9-3.10
|
18
22
|
|
19
23
|
from signalwire_agents.core.swml_service import SWMLService
|
20
24
|
|
@@ -9,9 +9,12 @@ See LICENSE file in the project root for full license information.
|
|
9
9
|
|
10
10
|
"""
|
11
11
|
InfoGathererAgent - Prefab agent for collecting answers to a series of questions
|
12
|
+
|
13
|
+
Supports both static (questions provided at init) and dynamic (questions determined
|
14
|
+
by a callback function) configuration modes.
|
12
15
|
"""
|
13
16
|
|
14
|
-
from typing import List, Dict, Any, Optional, Union
|
17
|
+
from typing import List, Dict, Any, Optional, Union, Callable
|
15
18
|
import json
|
16
19
|
|
17
20
|
from signalwire_agents.core.agent_base import AgentBase
|
@@ -39,7 +42,7 @@ class InfoGathererAgent(AgentBase):
|
|
39
42
|
|
40
43
|
def __init__(
|
41
44
|
self,
|
42
|
-
questions: List[Dict[str, str]],
|
45
|
+
questions: Optional[List[Dict[str, str]]] = None,
|
43
46
|
name: str = "info_gatherer",
|
44
47
|
route: str = "/info_gatherer",
|
45
48
|
enable_state_tracking: bool = True, # Enable state tracking by default for InfoGatherer
|
@@ -49,7 +52,8 @@ class InfoGathererAgent(AgentBase):
|
|
49
52
|
Initialize an information gathering agent
|
50
53
|
|
51
54
|
Args:
|
52
|
-
questions:
|
55
|
+
questions: Optional list of questions to ask. If None, questions will be determined
|
56
|
+
dynamically via a callback function. Each question dict should have:
|
53
57
|
- key_name: Identifier for storing the answer
|
54
58
|
- question_text: The actual question to ask the user
|
55
59
|
- confirm: (Optional) If set to True, the agent will confirm the answer before submitting
|
@@ -67,39 +71,84 @@ class InfoGathererAgent(AgentBase):
|
|
67
71
|
**kwargs
|
68
72
|
)
|
69
73
|
|
70
|
-
#
|
71
|
-
self.
|
72
|
-
|
73
|
-
# Set up global data with questions and initial state
|
74
|
-
self.set_global_data({
|
75
|
-
"questions": questions,
|
76
|
-
"question_index": 0,
|
77
|
-
"answers": []
|
78
|
-
})
|
74
|
+
# Store whether we're in static or dynamic mode
|
75
|
+
self._static_questions = questions
|
76
|
+
self._question_callback = None
|
79
77
|
|
80
|
-
|
81
|
-
|
78
|
+
if questions is not None:
|
79
|
+
# Static mode: validate questions and set up immediately
|
80
|
+
self._validate_questions(questions)
|
81
|
+
self.set_global_data({
|
82
|
+
"questions": questions,
|
83
|
+
"question_index": 0,
|
84
|
+
"answers": []
|
85
|
+
})
|
86
|
+
# Build prompt for static configuration
|
87
|
+
self._build_prompt()
|
88
|
+
else:
|
89
|
+
# Dynamic mode: questions will be set up via callback in on_swml_request
|
90
|
+
# Build a generic prompt
|
91
|
+
self._build_prompt("dynamic")
|
82
92
|
|
83
93
|
# Configure additional agent settings
|
84
94
|
self._configure_agent_settings()
|
85
95
|
|
96
|
+
def set_question_callback(self, callback: Callable[[dict, dict, dict], List[Dict[str, str]]]):
|
97
|
+
"""
|
98
|
+
Set a callback function for dynamic question configuration
|
99
|
+
|
100
|
+
Args:
|
101
|
+
callback: Function that takes (query_params, body_params, headers) and returns
|
102
|
+
a list of question dictionaries. Each question dict should have:
|
103
|
+
- key_name: Identifier for storing the answer
|
104
|
+
- question_text: The actual question to ask the user
|
105
|
+
- confirm: (Optional) If True, agent will confirm answer before submitting
|
106
|
+
|
107
|
+
Example:
|
108
|
+
def my_question_callback(query_params, body_params, headers):
|
109
|
+
question_set = query_params.get('set', 'default')
|
110
|
+
if question_set == 'support':
|
111
|
+
return [
|
112
|
+
{"key_name": "name", "question_text": "What is your name?"},
|
113
|
+
{"key_name": "issue", "question_text": "What's the issue?"}
|
114
|
+
]
|
115
|
+
else:
|
116
|
+
return [{"key_name": "name", "question_text": "What is your name?"}]
|
117
|
+
|
118
|
+
agent.set_question_callback(my_question_callback)
|
119
|
+
"""
|
120
|
+
self._question_callback = callback
|
121
|
+
|
86
122
|
def _validate_questions(self, questions):
|
87
123
|
"""Validate that questions are in the correct format"""
|
88
124
|
if not questions:
|
89
125
|
raise ValueError("At least one question is required")
|
90
126
|
|
127
|
+
if not isinstance(questions, list):
|
128
|
+
raise ValueError("Questions must be a list")
|
129
|
+
|
91
130
|
for i, question in enumerate(questions):
|
131
|
+
if not isinstance(question, dict):
|
132
|
+
raise ValueError(f"Question {i+1} must be a dictionary")
|
92
133
|
if "key_name" not in question:
|
93
134
|
raise ValueError(f"Question {i+1} is missing 'key_name' field")
|
94
135
|
if "question_text" not in question:
|
95
136
|
raise ValueError(f"Question {i+1} is missing 'question_text' field")
|
96
137
|
|
97
|
-
def _build_prompt(self):
|
138
|
+
def _build_prompt(self, mode="static"):
|
98
139
|
"""Build a minimal prompt with just the objective"""
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
140
|
+
if mode == "dynamic":
|
141
|
+
# Generic prompt for dynamic mode - will be customized later
|
142
|
+
self.prompt_add_section(
|
143
|
+
"Objective",
|
144
|
+
body="Your role is to gather information by asking questions. Begin by asking the user if they are ready to answer some questions. If they confirm they are ready, call the start_questions function to begin the process."
|
145
|
+
)
|
146
|
+
else:
|
147
|
+
# Original static prompt
|
148
|
+
self.prompt_add_section(
|
149
|
+
"Objective",
|
150
|
+
body="Your role is to get answers to a series of questions. Begin by asking the user if they are ready to answer some questions. If they confirm they are ready, call the start_questions function to begin the process."
|
151
|
+
)
|
103
152
|
|
104
153
|
def _configure_agent_settings(self):
|
105
154
|
"""Configure additional agent settings"""
|
@@ -109,6 +158,77 @@ class InfoGathererAgent(AgentBase):
|
|
109
158
|
"speech_event_timeout": 1000 # Slightly longer for thoughtful responses
|
110
159
|
})
|
111
160
|
|
161
|
+
def on_swml_request(self, request_data=None, callback_path=None, request=None):
|
162
|
+
"""
|
163
|
+
Handle dynamic configuration using the callback function
|
164
|
+
|
165
|
+
This method is called when SWML is requested and allows us to configure
|
166
|
+
the agent just-in-time using the provided callback.
|
167
|
+
"""
|
168
|
+
# Only process if we're in dynamic mode (no static questions)
|
169
|
+
if self._static_questions is not None:
|
170
|
+
return None
|
171
|
+
|
172
|
+
# If no callback is set, provide a basic fallback
|
173
|
+
if self._question_callback is None:
|
174
|
+
fallback_questions = [
|
175
|
+
{"key_name": "name", "question_text": "What is your name?"},
|
176
|
+
{"key_name": "message", "question_text": "How can I help you today?"}
|
177
|
+
]
|
178
|
+
return {
|
179
|
+
"global_data": {
|
180
|
+
"questions": fallback_questions,
|
181
|
+
"question_index": 0,
|
182
|
+
"answers": []
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
# Extract request information for callback
|
187
|
+
query_params = {}
|
188
|
+
body_params = request_data or {}
|
189
|
+
headers = {}
|
190
|
+
|
191
|
+
if request and hasattr(request, 'query_params'):
|
192
|
+
query_params = dict(request.query_params)
|
193
|
+
|
194
|
+
if request and hasattr(request, 'headers'):
|
195
|
+
headers = dict(request.headers)
|
196
|
+
|
197
|
+
try:
|
198
|
+
# Call the user-provided callback to get questions
|
199
|
+
print(f"Calling question callback with query_params: {query_params}")
|
200
|
+
questions = self._question_callback(query_params, body_params, headers)
|
201
|
+
print(f"Callback returned {len(questions)} questions")
|
202
|
+
|
203
|
+
# Validate the returned questions
|
204
|
+
self._validate_questions(questions)
|
205
|
+
|
206
|
+
# Return global data modifications
|
207
|
+
return {
|
208
|
+
"global_data": {
|
209
|
+
"questions": questions,
|
210
|
+
"question_index": 0,
|
211
|
+
"answers": []
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
except Exception as e:
|
216
|
+
# Log error and fall back to basic questions
|
217
|
+
print(f"Error in question callback: {e}")
|
218
|
+
fallback_questions = [
|
219
|
+
{"key_name": "name", "question_text": "What is your name?"},
|
220
|
+
{"key_name": "message", "question_text": "How can I help you today?"}
|
221
|
+
]
|
222
|
+
return {
|
223
|
+
"global_data": {
|
224
|
+
"questions": fallback_questions,
|
225
|
+
"question_index": 0,
|
226
|
+
"answers": []
|
227
|
+
}
|
228
|
+
}
|
229
|
+
|
230
|
+
|
231
|
+
|
112
232
|
def _generate_question_instruction(self, question_text: str, needs_confirmation: bool, is_first_question: bool = False) -> str:
|
113
233
|
"""
|
114
234
|
Generate the instruction text for asking a question
|
@@ -239,13 +359,11 @@ class InfoGathererAgent(AgentBase):
|
|
239
359
|
# Create response with the global data update and next question
|
240
360
|
result = SwaigFunctionResult(instruction)
|
241
361
|
|
242
|
-
#
|
243
|
-
result.
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
}}
|
248
|
-
])
|
362
|
+
# Use the helper method to update global data
|
363
|
+
result.update_global_data({
|
364
|
+
"answers": new_answers,
|
365
|
+
"question_index": new_question_index
|
366
|
+
})
|
249
367
|
|
250
368
|
return result
|
251
369
|
else:
|
@@ -254,13 +372,11 @@ class InfoGathererAgent(AgentBase):
|
|
254
372
|
"Thank you! All questions have been answered. You can now summarize the information collected or ask if there's anything else the user would like to discuss."
|
255
373
|
)
|
256
374
|
|
257
|
-
#
|
258
|
-
result.
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
}}
|
263
|
-
])
|
375
|
+
# Use the helper method to update global data
|
376
|
+
result.update_global_data({
|
377
|
+
"answers": new_answers,
|
378
|
+
"question_index": new_question_index
|
379
|
+
})
|
264
380
|
|
265
381
|
return result
|
266
382
|
|
@@ -40,7 +40,7 @@ class ReceptionistAgent(AgentBase):
|
|
40
40
|
name: str = "receptionist",
|
41
41
|
route: str = "/receptionist",
|
42
42
|
greeting: str = "Thank you for calling. How can I help you today?",
|
43
|
-
voice: str = "
|
43
|
+
voice: str = "rime.spore",
|
44
44
|
enable_state_tracking: bool = True, # Enable state tracking by default
|
45
45
|
**kwargs
|
46
46
|
):
|
@@ -261,28 +261,20 @@ class ReceptionistAgent(AgentBase):
|
|
261
261
|
# Get transfer number
|
262
262
|
transfer_number = department.get("number", "")
|
263
263
|
|
264
|
-
# Create result with transfer
|
265
|
-
|
264
|
+
# Create result with transfer using the connect helper method
|
265
|
+
# post_process=True allows the AI to speak the response before executing the transfer
|
266
|
+
result = SwaigFunctionResult(
|
267
|
+
f"I'll transfer you to our {department_name} department now. Thank you for calling, {name}!",
|
268
|
+
post_process=True
|
269
|
+
)
|
266
270
|
|
267
|
-
#
|
268
|
-
|
269
|
-
result.
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
{
|
275
|
-
"connect": {
|
276
|
-
"to": transfer_number
|
277
|
-
}
|
278
|
-
}
|
279
|
-
]
|
280
|
-
},
|
281
|
-
"version": "1.0.0"
|
282
|
-
},
|
283
|
-
"transfer": "true"
|
284
|
-
}
|
285
|
-
])
|
271
|
+
# Use the connect helper instead of manually constructing SWML
|
272
|
+
# final=True means this is a permanent transfer (call exits the agent)
|
273
|
+
result.connect(transfer_number, final=True)
|
274
|
+
|
275
|
+
# Alternative: Immediate transfer without AI speaking (faster but less friendly)
|
276
|
+
# result = SwaigFunctionResult() # No response text needed
|
277
|
+
# result.connect(transfer_number, final=True) # Executes immediately from function call
|
286
278
|
|
287
279
|
return result
|
288
280
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
SignalWire Agent Skills Package
|
3
|
+
|
4
|
+
This package contains built-in skills for SignalWire agents.
|
5
|
+
Skills are automatically discovered from subdirectories.
|
6
|
+
"""
|
7
|
+
|
8
|
+
# Import the registry to make it available
|
9
|
+
from .registry import skill_registry
|
10
|
+
|
11
|
+
# Trigger skill discovery on import
|
12
|
+
skill_registry.discover_skills()
|
13
|
+
|
14
|
+
__all__ = ["skill_registry"]
|
@@ -0,0 +1 @@
|
|
1
|
+
"""DateTime Skill for SignalWire Agents"""
|
@@ -0,0 +1,109 @@
|
|
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 datetime import datetime, timezone
|
11
|
+
import pytz
|
12
|
+
from typing import 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 DateTimeSkill(SkillBase):
|
18
|
+
"""Provides current date, time, and timezone information"""
|
19
|
+
|
20
|
+
SKILL_NAME = "datetime"
|
21
|
+
SKILL_DESCRIPTION = "Get current date, time, and timezone information"
|
22
|
+
SKILL_VERSION = "1.0.0"
|
23
|
+
REQUIRED_PACKAGES = ["pytz"]
|
24
|
+
REQUIRED_ENV_VARS = []
|
25
|
+
|
26
|
+
def setup(self) -> bool:
|
27
|
+
"""Setup the datetime skill"""
|
28
|
+
return self.validate_packages()
|
29
|
+
|
30
|
+
def register_tools(self) -> None:
|
31
|
+
"""Register datetime tools with the agent"""
|
32
|
+
|
33
|
+
self.agent.define_tool(
|
34
|
+
name="get_current_time",
|
35
|
+
description="Get the current time, optionally in a specific timezone",
|
36
|
+
parameters={
|
37
|
+
"timezone": {
|
38
|
+
"type": "string",
|
39
|
+
"description": "Timezone name (e.g., 'America/New_York', 'Europe/London'). Defaults to UTC."
|
40
|
+
}
|
41
|
+
},
|
42
|
+
handler=self._get_time_handler
|
43
|
+
)
|
44
|
+
|
45
|
+
self.agent.define_tool(
|
46
|
+
name="get_current_date",
|
47
|
+
description="Get the current date",
|
48
|
+
parameters={
|
49
|
+
"timezone": {
|
50
|
+
"type": "string",
|
51
|
+
"description": "Timezone name for the date. Defaults to UTC."
|
52
|
+
}
|
53
|
+
},
|
54
|
+
handler=self._get_date_handler
|
55
|
+
)
|
56
|
+
|
57
|
+
def _get_time_handler(self, args, raw_data):
|
58
|
+
"""Handler for get_current_time tool"""
|
59
|
+
timezone_name = args.get("timezone", "UTC")
|
60
|
+
|
61
|
+
try:
|
62
|
+
if timezone_name.upper() == "UTC":
|
63
|
+
tz = timezone.utc
|
64
|
+
else:
|
65
|
+
tz = pytz.timezone(timezone_name)
|
66
|
+
|
67
|
+
now = datetime.now(tz)
|
68
|
+
time_str = now.strftime("%I:%M:%S %p %Z")
|
69
|
+
|
70
|
+
return SwaigFunctionResult(f"The current time is {time_str}")
|
71
|
+
|
72
|
+
except Exception as e:
|
73
|
+
return SwaigFunctionResult(f"Error getting time: {str(e)}")
|
74
|
+
|
75
|
+
def _get_date_handler(self, args, raw_data):
|
76
|
+
"""Handler for get_current_date tool"""
|
77
|
+
timezone_name = args.get("timezone", "UTC")
|
78
|
+
|
79
|
+
try:
|
80
|
+
if timezone_name.upper() == "UTC":
|
81
|
+
tz = timezone.utc
|
82
|
+
else:
|
83
|
+
tz = pytz.timezone(timezone_name)
|
84
|
+
|
85
|
+
now = datetime.now(tz)
|
86
|
+
date_str = now.strftime("%A, %B %d, %Y")
|
87
|
+
|
88
|
+
return SwaigFunctionResult(f"Today's date is {date_str}")
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
return SwaigFunctionResult(f"Error getting date: {str(e)}")
|
92
|
+
|
93
|
+
def get_hints(self) -> List[str]:
|
94
|
+
"""Return speech recognition hints"""
|
95
|
+
return ["time", "date", "today", "now", "current", "timezone"]
|
96
|
+
|
97
|
+
def get_prompt_sections(self) -> List[Dict[str, Any]]:
|
98
|
+
"""Return prompt sections to add to agent"""
|
99
|
+
return [
|
100
|
+
{
|
101
|
+
"title": "Date and Time Information",
|
102
|
+
"body": "You can provide current date and time information.",
|
103
|
+
"bullets": [
|
104
|
+
"Use get_current_time to tell users what time it is",
|
105
|
+
"Use get_current_date to tell users today's date",
|
106
|
+
"Both tools support different timezones"
|
107
|
+
]
|
108
|
+
}
|
109
|
+
]
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Math Skill for SignalWire Agents"""
|