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.
@@ -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, Self, TypeVar
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: List of questions to ask, each with:
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
- # Validate questions format
71
- self._validate_questions(questions)
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
- # Build a minimal prompt
81
- self._build_prompt()
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
- self.prompt_add_section(
100
- "Objective",
101
- 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."
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
- # Add actions to update global data
243
- result.add_actions([
244
- {"set_global_data": {
245
- "answers": new_answers,
246
- "question_index": new_question_index
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
- # Add actions to update global data
258
- result.add_actions([
259
- {"set_global_data": {
260
- "answers": new_answers,
261
- "question_index": new_question_index
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 = "elevenlabs.josh",
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 SWML
265
- result = SwaigFunctionResult(f"I'll transfer you to our {department_name} department now. Thank you for calling, {name}!")
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
- # Add the SWML to execute the transfer
268
- # Add actions to update global data
269
- result.add_actions([
270
- {
271
- "SWML": {
272
- "sections": {
273
- "main": [
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"""