signalwire-agents 0.1.8__tar.gz → 0.1.9__tar.gz
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-0.1.8/signalwire_agents.egg-info → signalwire_agents-0.1.9}/PKG-INFO +44 -1
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/README.md +41 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/pyproject.toml +4 -2
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/__init__.py +4 -1
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/agent_base.py +40 -0
- signalwire_agents-0.1.9/signalwire_agents/core/skill_base.py +87 -0
- signalwire_agents-0.1.9/signalwire_agents/core/skill_manager.py +136 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/__init__.py +14 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/datetime/__init__.py +1 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/datetime/skill.py +109 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/math/__init__.py +1 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/math/skill.py +88 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/registry.py +98 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/web_search/__init__.py +1 -0
- signalwire_agents-0.1.9/signalwire_agents/skills/web_search/skill.py +240 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9/signalwire_agents.egg-info}/PKG-INFO +44 -1
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents.egg-info/SOURCES.txt +10 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents.egg-info/requires.txt +2 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/LICENSE +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/schema.json +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/setup.cfg +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/setup.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/agent_server.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/__init__.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/function_result.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/pom_builder.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/security/__init__.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/security/session_manager.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/state/__init__.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/state/file_state_manager.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/state/state_manager.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/swaig_function.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/swml_builder.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/swml_handler.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/swml_renderer.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/swml_service.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/__init__.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/concierge.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/faq_bot.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/info_gatherer.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/receptionist.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/survey.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/schema.json +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/utils/__init__.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/utils/pom_utils.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/utils/schema_utils.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/utils/token_generators.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/utils/validators.py +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents.egg-info/dependency_links.txt +0 -0
- {signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents.egg-info/top_level.txt +0 -0
@@ -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
|
@@ -42,6 +44,47 @@ A Python SDK for creating, hosting, and securing SignalWire AI agents as microse
|
|
42
44
|
- **State Management**: Persistent conversation state with automatic tracking
|
43
45
|
- **Prefab Archetypes**: Ready-to-use agent types for common scenarios
|
44
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).
|
45
88
|
|
46
89
|
## Installation
|
47
90
|
|
@@ -14,6 +14,47 @@ A Python SDK for creating, hosting, and securing SignalWire AI agents as microse
|
|
14
14
|
- **State Management**: Persistent conversation state with automatic tracking
|
15
15
|
- **Prefab Archetypes**: Ready-to-use agent types for common scenarios
|
16
16
|
- **Multi-Agent Support**: Host multiple agents on a single server
|
17
|
+
- **Modular Skills System**: Add capabilities to agents with simple one-liner calls
|
18
|
+
|
19
|
+
## Skills System
|
20
|
+
|
21
|
+
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:
|
22
|
+
|
23
|
+
```python
|
24
|
+
from signalwire_agents import AgentBase
|
25
|
+
|
26
|
+
# Create an agent
|
27
|
+
agent = AgentBase("My Assistant", route="/assistant")
|
28
|
+
|
29
|
+
# Add skills with one-liners
|
30
|
+
agent.add_skill("web_search") # Web search capability
|
31
|
+
agent.add_skill("datetime") # Current date/time info
|
32
|
+
agent.add_skill("math") # Mathematical calculations
|
33
|
+
|
34
|
+
# Configure skills with parameters
|
35
|
+
agent.add_skill("web_search", {
|
36
|
+
"num_results": 3, # Get 3 search results
|
37
|
+
"delay": 0.5 # Small delay between requests
|
38
|
+
})
|
39
|
+
|
40
|
+
agent.serve()
|
41
|
+
```
|
42
|
+
|
43
|
+
### Available Built-in Skills
|
44
|
+
|
45
|
+
- **web_search**: Google Custom Search API integration with web scraping
|
46
|
+
- **datetime**: Current date and time with timezone support
|
47
|
+
- **math**: Safe mathematical expression evaluation
|
48
|
+
|
49
|
+
### Benefits
|
50
|
+
|
51
|
+
- **One-liner integration**: `agent.add_skill("skill_name")`
|
52
|
+
- **Configurable parameters**: `agent.add_skill("skill_name", {"param": "value"})`
|
53
|
+
- **Automatic discovery**: Skills are automatically found from the skills directory
|
54
|
+
- **Dependency validation**: Clear error messages for missing requirements
|
55
|
+
- **Modular architecture**: Skills are self-contained and reusable
|
56
|
+
|
57
|
+
For detailed documentation, see [Skills System README](docs/SKILLS_SYSTEM_README.md).
|
17
58
|
|
18
59
|
## Installation
|
19
60
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "signalwire_agents"
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.9"
|
8
8
|
description = "SignalWire AI Agents SDK"
|
9
9
|
authors = [
|
10
10
|
{name = "SignalWire Team", email = "info@signalwire.com"}
|
@@ -31,13 +31,15 @@ dependencies = [
|
|
31
31
|
"signalwire_pom==2.7.1",
|
32
32
|
"structlog==25.3.0",
|
33
33
|
"uvicorn==0.34.2",
|
34
|
+
"beautifulsoup4==4.12.3",
|
35
|
+
"pytz==2023.3",
|
34
36
|
]
|
35
37
|
|
36
38
|
[project.urls]
|
37
39
|
Homepage = "https://github.com/signalwire/signalwire-ai-agents"
|
38
40
|
|
39
41
|
[tool.setuptools]
|
40
|
-
packages = ["signalwire_agents", "signalwire_agents.prefabs", "signalwire_agents.utils", "signalwire_agents.core", "signalwire_agents.core.state", "signalwire_agents.core.security"]
|
42
|
+
packages = ["signalwire_agents", "signalwire_agents.prefabs", "signalwire_agents.utils", "signalwire_agents.core", "signalwire_agents.core.state", "signalwire_agents.core.security", "signalwire_agents.skills", "signalwire_agents.skills.web_search", "signalwire_agents.skills.datetime", "signalwire_agents.skills.math"]
|
41
43
|
include-package-data = true
|
42
44
|
|
43
45
|
[tool.setuptools.package-data]
|
@@ -14,7 +14,7 @@ SignalWire AI Agents SDK
|
|
14
14
|
A package for building AI agents using SignalWire's AI and SWML capabilities.
|
15
15
|
"""
|
16
16
|
|
17
|
-
__version__ = "0.1.
|
17
|
+
__version__ = "0.1.9"
|
18
18
|
|
19
19
|
# Import core classes for easier access
|
20
20
|
from signalwire_agents.core.agent_base import AgentBase
|
@@ -23,4 +23,7 @@ from signalwire_agents.core.swml_service import SWMLService
|
|
23
23
|
from signalwire_agents.core.swml_builder import SWMLBuilder
|
24
24
|
from signalwire_agents.core.state import StateManager, FileStateManager
|
25
25
|
|
26
|
+
# Import skills to trigger discovery
|
27
|
+
import signalwire_agents.skills
|
28
|
+
|
26
29
|
__all__ = ["AgentBase", "AgentServer", "SWMLService", "SWMLBuilder", "StateManager", "FileStateManager"]
|
@@ -75,6 +75,7 @@ from signalwire_agents.core.security.session_manager import SessionManager
|
|
75
75
|
from signalwire_agents.core.state import StateManager, FileStateManager
|
76
76
|
from signalwire_agents.core.swml_service import SWMLService
|
77
77
|
from signalwire_agents.core.swml_handler import AIVerbHandler
|
78
|
+
from signalwire_agents.core.skill_manager import SkillManager
|
78
79
|
|
79
80
|
# Create a logger
|
80
81
|
logger = structlog.get_logger("agent_base")
|
@@ -404,6 +405,9 @@ class AgentBase(SWMLService):
|
|
404
405
|
|
405
406
|
# Dynamic configuration callback
|
406
407
|
self._dynamic_config_callback = None
|
408
|
+
|
409
|
+
# Initialize skill manager
|
410
|
+
self.skill_manager = SkillManager(self)
|
407
411
|
|
408
412
|
def _process_prompt_sections(self):
|
409
413
|
"""
|
@@ -2792,3 +2796,39 @@ class AgentBase(SWMLService):
|
|
2792
2796
|
self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
|
2793
2797
|
|
2794
2798
|
return self
|
2799
|
+
|
2800
|
+
# ----------------------------------------------------------------------
|
2801
|
+
# Skill Management Methods
|
2802
|
+
# ----------------------------------------------------------------------
|
2803
|
+
|
2804
|
+
def add_skill(self, skill_name: str, params: Optional[Dict[str, Any]] = None) -> 'AgentBase':
|
2805
|
+
"""
|
2806
|
+
Add a skill to this agent
|
2807
|
+
|
2808
|
+
Args:
|
2809
|
+
skill_name: Name of the skill to add
|
2810
|
+
params: Optional parameters to pass to the skill for configuration
|
2811
|
+
|
2812
|
+
Returns:
|
2813
|
+
Self for method chaining
|
2814
|
+
|
2815
|
+
Raises:
|
2816
|
+
ValueError: If skill not found or failed to load with detailed error message
|
2817
|
+
"""
|
2818
|
+
success, error_message = self.skill_manager.load_skill(skill_name, params=params)
|
2819
|
+
if not success:
|
2820
|
+
raise ValueError(f"Failed to load skill '{skill_name}': {error_message}")
|
2821
|
+
return self
|
2822
|
+
|
2823
|
+
def remove_skill(self, skill_name: str) -> 'AgentBase':
|
2824
|
+
"""Remove a skill from this agent"""
|
2825
|
+
self.skill_manager.unload_skill(skill_name)
|
2826
|
+
return self
|
2827
|
+
|
2828
|
+
def list_skills(self) -> List[str]:
|
2829
|
+
"""List currently loaded skills"""
|
2830
|
+
return self.skill_manager.list_loaded_skills()
|
2831
|
+
|
2832
|
+
def has_skill(self, skill_name: str) -> bool:
|
2833
|
+
"""Check if skill is loaded"""
|
2834
|
+
return self.skill_manager.has_skill(skill_name)
|
@@ -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)
|
@@ -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"""
|
@@ -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
|
@@ -42,6 +44,47 @@ A Python SDK for creating, hosting, and securing SignalWire AI agents as microse
|
|
42
44
|
- **State Management**: Persistent conversation state with automatic tracking
|
43
45
|
- **Prefab Archetypes**: Ready-to-use agent types for common scenarios
|
44
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).
|
45
88
|
|
46
89
|
## Installation
|
47
90
|
|
@@ -15,6 +15,8 @@ signalwire_agents/core/__init__.py
|
|
15
15
|
signalwire_agents/core/agent_base.py
|
16
16
|
signalwire_agents/core/function_result.py
|
17
17
|
signalwire_agents/core/pom_builder.py
|
18
|
+
signalwire_agents/core/skill_base.py
|
19
|
+
signalwire_agents/core/skill_manager.py
|
18
20
|
signalwire_agents/core/swaig_function.py
|
19
21
|
signalwire_agents/core/swml_builder.py
|
20
22
|
signalwire_agents/core/swml_handler.py
|
@@ -31,6 +33,14 @@ signalwire_agents/prefabs/faq_bot.py
|
|
31
33
|
signalwire_agents/prefabs/info_gatherer.py
|
32
34
|
signalwire_agents/prefabs/receptionist.py
|
33
35
|
signalwire_agents/prefabs/survey.py
|
36
|
+
signalwire_agents/skills/__init__.py
|
37
|
+
signalwire_agents/skills/registry.py
|
38
|
+
signalwire_agents/skills/datetime/__init__.py
|
39
|
+
signalwire_agents/skills/datetime/skill.py
|
40
|
+
signalwire_agents/skills/math/__init__.py
|
41
|
+
signalwire_agents/skills/math/skill.py
|
42
|
+
signalwire_agents/skills/web_search/__init__.py
|
43
|
+
signalwire_agents/skills/web_search/skill.py
|
34
44
|
signalwire_agents/utils/__init__.py
|
35
45
|
signalwire_agents/utils/pom_utils.py
|
36
46
|
signalwire_agents/utils/schema_utils.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/function_result.py
RENAMED
File without changes
|
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/security/__init__.py
RENAMED
File without changes
|
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/state/__init__.py
RENAMED
File without changes
|
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/state/state_manager.py
RENAMED
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/core/swaig_function.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/info_gatherer.py
RENAMED
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/prefabs/receptionist.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents/utils/token_generators.py
RENAMED
File without changes
|
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents.egg-info/dependency_links.txt
RENAMED
File without changes
|
{signalwire_agents-0.1.8 → signalwire_agents-0.1.9}/signalwire_agents.egg-info/top_level.txt
RENAMED
File without changes
|