signalwire-agents 0.1.10__py3-none-any.whl → 0.1.12__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 +43 -4
- signalwire_agents/agent_server.py +268 -15
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +457 -0
- signalwire_agents/cli/test_swaig.py +2609 -0
- signalwire_agents/core/agent_base.py +691 -82
- signalwire_agents/core/contexts.py +289 -0
- signalwire_agents/core/data_map.py +499 -0
- signalwire_agents/core/function_result.py +57 -10
- signalwire_agents/core/logging_config.py +232 -0
- signalwire_agents/core/skill_base.py +27 -37
- signalwire_agents/core/skill_manager.py +89 -23
- signalwire_agents/core/swaig_function.py +13 -1
- signalwire_agents/core/swml_handler.py +37 -13
- signalwire_agents/core/swml_service.py +37 -28
- signalwire_agents/search/__init__.py +131 -0
- signalwire_agents/search/document_processor.py +764 -0
- signalwire_agents/search/index_builder.py +534 -0
- signalwire_agents/search/query_processor.py +371 -0
- signalwire_agents/search/search_engine.py +383 -0
- signalwire_agents/search/search_service.py +251 -0
- signalwire_agents/skills/datasphere/__init__.py +12 -0
- signalwire_agents/skills/datasphere/skill.py +229 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
- signalwire_agents/skills/datetime/skill.py +9 -5
- signalwire_agents/skills/joke/__init__.py +1 -0
- signalwire_agents/skills/joke/skill.py +88 -0
- signalwire_agents/skills/math/skill.py +9 -6
- signalwire_agents/skills/native_vector_search/__init__.py +1 -0
- signalwire_agents/skills/native_vector_search/skill.py +352 -0
- signalwire_agents/skills/registry.py +10 -4
- signalwire_agents/skills/web_search/skill.py +57 -21
- signalwire_agents/skills/wikipedia/__init__.py +9 -0
- signalwire_agents/skills/wikipedia/skill.py +180 -0
- signalwire_agents/utils/__init__.py +14 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents-0.1.12.dist-info/METADATA +863 -0
- signalwire_agents-0.1.12.dist-info/RECORD +67 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.12.dist-info/entry_points.txt +3 -0
- signalwire_agents-0.1.10.dist-info/METADATA +0 -319
- signalwire_agents-0.1.10.dist-info/RECORD +0 -44
- {signalwire_agents-0.1.10.data → signalwire_agents-0.1.12.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,180 @@
|
|
1
|
+
"""
|
2
|
+
Wikipedia Search Skill
|
3
|
+
|
4
|
+
Provides Wikipedia search capabilities using the Wikipedia API.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import requests
|
8
|
+
from urllib.parse import quote
|
9
|
+
from typing import Dict, Any, Optional
|
10
|
+
from signalwire_agents.core.skill_base import SkillBase
|
11
|
+
|
12
|
+
|
13
|
+
class WikipediaSearchSkill(SkillBase):
|
14
|
+
"""
|
15
|
+
Skill for searching Wikipedia articles and retrieving content.
|
16
|
+
|
17
|
+
This skill uses the Wikipedia API to search for articles and retrieve
|
18
|
+
their introductory content, similar to getting a summary of a topic.
|
19
|
+
"""
|
20
|
+
|
21
|
+
# Skill metadata
|
22
|
+
SKILL_NAME = "wikipedia_search"
|
23
|
+
SKILL_DESCRIPTION = "Search Wikipedia for information about a topic and get article summaries"
|
24
|
+
SKILL_VERSION = "1.0.0"
|
25
|
+
REQUIRED_PACKAGES = ["requests"]
|
26
|
+
REQUIRED_ENV_VARS = [] # No environment variables required
|
27
|
+
|
28
|
+
# Does not support multiple instances
|
29
|
+
SUPPORTS_MULTIPLE_INSTANCES = False
|
30
|
+
|
31
|
+
def setup(self) -> bool:
|
32
|
+
"""
|
33
|
+
Setup the Wikipedia search skill.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
True if setup successful, False otherwise
|
37
|
+
"""
|
38
|
+
# Extract configuration from params
|
39
|
+
self.num_results = max(1, self.params.get('num_results', 1)) # Ensure at least 1 result
|
40
|
+
self.no_results_message = self.params.get('no_results_message') or (
|
41
|
+
"I couldn't find any Wikipedia articles for '{query}'. "
|
42
|
+
"Try rephrasing your search or using different keywords."
|
43
|
+
)
|
44
|
+
|
45
|
+
# Validate that requests package is available
|
46
|
+
if not self.validate_packages():
|
47
|
+
return False
|
48
|
+
|
49
|
+
self.logger.info(f"Wikipedia search skill initialized with {self.num_results} max results")
|
50
|
+
return True
|
51
|
+
|
52
|
+
def register_tools(self) -> None:
|
53
|
+
"""
|
54
|
+
Register the SWAIG tool for Wikipedia search.
|
55
|
+
"""
|
56
|
+
self.agent.define_tool(
|
57
|
+
name="search_wiki",
|
58
|
+
description="Search Wikipedia for information about a topic and get article summaries",
|
59
|
+
parameters={
|
60
|
+
"query": {
|
61
|
+
"type": "string",
|
62
|
+
"description": "The search term or topic to look up on Wikipedia"
|
63
|
+
}
|
64
|
+
},
|
65
|
+
handler=self._search_wiki_handler,
|
66
|
+
**self.swaig_fields
|
67
|
+
)
|
68
|
+
|
69
|
+
def _search_wiki_handler(self, args, raw_data):
|
70
|
+
"""Handler for search_wiki tool"""
|
71
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
72
|
+
|
73
|
+
query = args.get("query", "").strip()
|
74
|
+
if not query:
|
75
|
+
return SwaigFunctionResult("Please provide a search query for Wikipedia.")
|
76
|
+
|
77
|
+
result = self.search_wiki(query)
|
78
|
+
return SwaigFunctionResult(result)
|
79
|
+
|
80
|
+
def search_wiki(self, query: str) -> str:
|
81
|
+
"""
|
82
|
+
Search Wikipedia for articles matching the query.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
query: The search term to look up
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
String containing the Wikipedia article content or error message
|
89
|
+
"""
|
90
|
+
try:
|
91
|
+
# Step 1: Search for articles matching the query
|
92
|
+
search_url = (
|
93
|
+
"https://en.wikipedia.org/w/api.php"
|
94
|
+
"?action=query&list=search&format=json"
|
95
|
+
f"&srsearch={quote(query)}"
|
96
|
+
f"&srlimit={self.num_results}"
|
97
|
+
)
|
98
|
+
|
99
|
+
response = requests.get(search_url, timeout=10)
|
100
|
+
response.raise_for_status()
|
101
|
+
search_data = response.json()
|
102
|
+
|
103
|
+
# Check if we got any search results
|
104
|
+
search_results = search_data.get('query', {}).get('search', [])
|
105
|
+
if not search_results:
|
106
|
+
return self.no_results_message.format(query=query)
|
107
|
+
|
108
|
+
# Step 2: Get article extracts for each result
|
109
|
+
articles = []
|
110
|
+
for i, result in enumerate(search_results[:self.num_results]):
|
111
|
+
title = result['title']
|
112
|
+
|
113
|
+
# Get the page extract
|
114
|
+
extract_url = (
|
115
|
+
"https://en.wikipedia.org/w/api.php"
|
116
|
+
"?action=query&prop=extracts&exintro&explaintext&format=json"
|
117
|
+
f"&titles={quote(title)}"
|
118
|
+
)
|
119
|
+
|
120
|
+
extract_response = requests.get(extract_url, timeout=10)
|
121
|
+
extract_response.raise_for_status()
|
122
|
+
extract_data = extract_response.json()
|
123
|
+
|
124
|
+
# Get the first (and only) page from the response
|
125
|
+
pages = extract_data.get('query', {}).get('pages', {})
|
126
|
+
if pages:
|
127
|
+
page = next(iter(pages.values()))
|
128
|
+
extract = page.get('extract', '').strip()
|
129
|
+
|
130
|
+
if extract:
|
131
|
+
# Format the article with title and content
|
132
|
+
articles.append(f"**{title}**\n\n{extract}")
|
133
|
+
else:
|
134
|
+
# Handle case where extract is empty
|
135
|
+
articles.append(f"**{title}**\n\nNo summary available for this article.")
|
136
|
+
|
137
|
+
if not articles:
|
138
|
+
return self.no_results_message.format(query=query)
|
139
|
+
|
140
|
+
# Join multiple articles with separators
|
141
|
+
if len(articles) == 1:
|
142
|
+
return articles[0]
|
143
|
+
else:
|
144
|
+
return "\n\n" + "="*50 + "\n\n".join(articles)
|
145
|
+
|
146
|
+
except requests.exceptions.RequestException as e:
|
147
|
+
return f"Error accessing Wikipedia: {str(e)}"
|
148
|
+
except Exception as e:
|
149
|
+
return f"Error searching Wikipedia: {str(e)}"
|
150
|
+
|
151
|
+
def get_prompt_sections(self) -> list:
|
152
|
+
"""
|
153
|
+
Return additional context for the agent prompt.
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
List of prompt sections to add to the agent
|
157
|
+
"""
|
158
|
+
return [{
|
159
|
+
"title": "Wikipedia Search",
|
160
|
+
"body": f"You can search Wikipedia for factual information using search_wiki. This will return up to {self.num_results} Wikipedia article summaries.",
|
161
|
+
"bullets": [
|
162
|
+
"Use search_wiki for factual, encyclopedic information",
|
163
|
+
"Great for answering questions about people, places, concepts, and history",
|
164
|
+
"Returns reliable, well-sourced information from Wikipedia articles"
|
165
|
+
]
|
166
|
+
}]
|
167
|
+
|
168
|
+
def get_hints(self) -> list:
|
169
|
+
"""
|
170
|
+
Return speech recognition hints for better accuracy.
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
List of words/phrases to help with speech recognition
|
174
|
+
"""
|
175
|
+
# Currently no hints provided, but you could add them like:
|
176
|
+
# return [
|
177
|
+
# "Wikipedia", "wiki", "search Wikipedia", "look up", "tell me about",
|
178
|
+
# "what is", "who is", "information about", "facts about"
|
179
|
+
# ]
|
180
|
+
return []
|
@@ -7,3 +7,17 @@ Licensed under the MIT License.
|
|
7
7
|
See LICENSE file in the project root for full license information.
|
8
8
|
"""
|
9
9
|
|
10
|
+
from .schema_utils import SchemaUtils
|
11
|
+
from signalwire_agents.core.logging_config import get_execution_mode
|
12
|
+
|
13
|
+
def is_serverless_mode() -> bool:
|
14
|
+
"""
|
15
|
+
Check if running in any serverless environment.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
bool: True if in serverless mode, False if in server mode
|
19
|
+
"""
|
20
|
+
return get_execution_mode() != 'server'
|
21
|
+
|
22
|
+
__all__ = ["SchemaUtils", "get_execution_mode", "is_serverless_mode"]
|
23
|
+
|
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/env python3
|
1
2
|
"""
|
2
3
|
Copyright (c) 2025 SignalWire
|
3
4
|
|
@@ -7,62 +8,129 @@ Licensed under the MIT License.
|
|
7
8
|
See LICENSE file in the project root for full license information.
|
8
9
|
"""
|
9
10
|
|
11
|
+
# -*- coding: utf-8 -*-
|
10
12
|
"""
|
11
|
-
|
12
|
-
|
13
|
-
This module provides functions for loading, parsing, and validating SWML schemas.
|
14
|
-
It also provides utilities for working with SWML documents based on the schema.
|
13
|
+
Schema utilities for SWML validation and verb extraction
|
15
14
|
"""
|
16
15
|
|
17
|
-
import json
|
18
16
|
import os
|
19
|
-
import
|
20
|
-
|
17
|
+
import json
|
18
|
+
import logging
|
19
|
+
from typing import Dict, Any, List, Optional, Tuple
|
21
20
|
|
21
|
+
try:
|
22
|
+
import structlog
|
23
|
+
# Ensure structlog is configured
|
24
|
+
if not structlog.is_configured():
|
25
|
+
structlog.configure(
|
26
|
+
processors=[
|
27
|
+
structlog.stdlib.filter_by_level,
|
28
|
+
structlog.stdlib.add_logger_name,
|
29
|
+
structlog.stdlib.add_log_level,
|
30
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
31
|
+
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
32
|
+
structlog.processors.StackInfoRenderer(),
|
33
|
+
structlog.processors.format_exc_info,
|
34
|
+
structlog.processors.UnicodeDecoder(),
|
35
|
+
structlog.dev.ConsoleRenderer()
|
36
|
+
],
|
37
|
+
context_class=dict,
|
38
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
39
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
40
|
+
cache_logger_on_first_use=True,
|
41
|
+
)
|
42
|
+
except ImportError:
|
43
|
+
raise ImportError(
|
44
|
+
"structlog is required. Install it with: pip install structlog"
|
45
|
+
)
|
46
|
+
|
47
|
+
# Create a logger
|
48
|
+
logger = structlog.get_logger("schema_utils")
|
22
49
|
|
23
50
|
class SchemaUtils:
|
24
51
|
"""
|
25
|
-
|
26
|
-
|
27
|
-
This class provides methods for:
|
28
|
-
- Loading and parsing schema files
|
29
|
-
- Extracting verb definitions
|
30
|
-
- Validating SWML objects against the schema
|
31
|
-
- Generating helpers for schema operations
|
52
|
+
Utility class for loading and working with SWML schemas
|
32
53
|
"""
|
33
54
|
|
34
55
|
def __init__(self, schema_path: Optional[str] = None):
|
35
56
|
"""
|
36
|
-
Initialize the
|
57
|
+
Initialize the schema utilities
|
37
58
|
|
38
59
|
Args:
|
39
|
-
schema_path: Path to the schema file
|
60
|
+
schema_path: Path to the schema file
|
40
61
|
"""
|
62
|
+
self.log = logger.bind(component="schema_utils")
|
63
|
+
|
41
64
|
self.schema_path = schema_path
|
42
65
|
if not self.schema_path:
|
43
66
|
self.schema_path = self._get_default_schema_path()
|
44
|
-
|
67
|
+
self.log.debug("using_default_schema_path", path=self.schema_path)
|
45
68
|
|
46
69
|
self.schema = self.load_schema()
|
47
70
|
self.verbs = self._extract_verb_definitions()
|
48
|
-
|
71
|
+
self.log.debug("schema_initialized", verb_count=len(self.verbs))
|
49
72
|
if self.verbs:
|
50
|
-
|
73
|
+
self.log.debug("first_verbs_extracted", verbs=list(self.verbs.keys())[:5])
|
51
74
|
|
52
|
-
def _get_default_schema_path(self) -> str:
|
75
|
+
def _get_default_schema_path(self) -> Optional[str]:
|
53
76
|
"""
|
54
|
-
Get the default path to the schema file
|
77
|
+
Get the default path to the schema file using the same robust logic as SWMLService
|
55
78
|
|
56
79
|
Returns:
|
57
|
-
Path to the schema file
|
80
|
+
Path to the schema file or None if not found
|
58
81
|
"""
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
82
|
+
# Try package resources first (most reliable after pip install)
|
83
|
+
try:
|
84
|
+
import importlib.resources
|
85
|
+
try:
|
86
|
+
# Python 3.9+
|
87
|
+
try:
|
88
|
+
# Python 3.13+
|
89
|
+
path = importlib.resources.files("signalwire_agents").joinpath("schema.json")
|
90
|
+
return str(path)
|
91
|
+
except Exception:
|
92
|
+
# Python 3.9-3.12
|
93
|
+
with importlib.resources.files("signalwire_agents").joinpath("schema.json") as path:
|
94
|
+
return str(path)
|
95
|
+
except AttributeError:
|
96
|
+
# Python 3.7-3.8
|
97
|
+
with importlib.resources.path("signalwire_agents", "schema.json") as path:
|
98
|
+
return str(path)
|
99
|
+
except (ImportError, ModuleNotFoundError):
|
100
|
+
pass
|
101
|
+
|
102
|
+
# Fall back to pkg_resources for older Python or alternative lookup
|
103
|
+
try:
|
104
|
+
import pkg_resources
|
105
|
+
return pkg_resources.resource_filename("signalwire_agents", "schema.json")
|
106
|
+
except (ImportError, ModuleNotFoundError, pkg_resources.DistributionNotFound):
|
107
|
+
pass
|
108
|
+
|
109
|
+
# Fall back to manual search in various locations
|
110
|
+
import sys
|
111
|
+
|
112
|
+
# Get package directory relative to this file
|
113
|
+
package_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
114
|
+
|
115
|
+
# Potential locations for schema.json
|
116
|
+
potential_paths = [
|
117
|
+
os.path.join(os.getcwd(), "schema.json"), # Current working directory
|
118
|
+
os.path.join(package_dir, "schema.json"), # Package directory
|
119
|
+
os.path.join(os.path.dirname(package_dir), "schema.json"), # Parent of package directory
|
120
|
+
os.path.join(sys.prefix, "schema.json"), # Python installation directory
|
121
|
+
os.path.join(package_dir, "data", "schema.json"), # Data subdirectory
|
122
|
+
os.path.join(os.path.dirname(package_dir), "data", "schema.json"), # Parent's data subdirectory
|
123
|
+
]
|
124
|
+
|
125
|
+
# Try to find the schema file
|
126
|
+
for path in potential_paths:
|
127
|
+
self.log.debug("checking_schema_path", path=path, exists=os.path.exists(path))
|
128
|
+
if os.path.exists(path):
|
129
|
+
self.log.debug("schema_found_at", path=path)
|
130
|
+
return path
|
131
|
+
|
132
|
+
self.log.warning("schema_not_found_in_any_location")
|
133
|
+
return None
|
66
134
|
|
67
135
|
def load_schema(self) -> Dict[str, Any]:
|
68
136
|
"""
|
@@ -72,27 +140,26 @@ class SchemaUtils:
|
|
72
140
|
The schema as a dictionary
|
73
141
|
"""
|
74
142
|
if not self.schema_path:
|
75
|
-
|
143
|
+
self.log.warning("no_schema_path_provided")
|
76
144
|
return {}
|
77
145
|
|
78
146
|
try:
|
79
|
-
|
80
|
-
print(f"File exists: {os.path.exists(self.schema_path)}")
|
147
|
+
self.log.debug("loading_schema", path=self.schema_path, exists=os.path.exists(self.schema_path))
|
81
148
|
|
82
149
|
if os.path.exists(self.schema_path):
|
83
150
|
with open(self.schema_path, "r") as f:
|
84
151
|
schema = json.load(f)
|
85
|
-
|
86
|
-
|
152
|
+
self.log.debug("schema_loaded_successfully",
|
153
|
+
path=self.schema_path,
|
154
|
+
top_level_keys=len(schema.keys()) if schema else 0)
|
87
155
|
if "$defs" in schema:
|
88
|
-
|
156
|
+
self.log.debug("schema_definitions_found", count=len(schema['$defs']))
|
89
157
|
return schema
|
90
158
|
else:
|
91
|
-
|
159
|
+
self.log.error("schema_file_not_found", path=self.schema_path)
|
92
160
|
return {}
|
93
161
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
94
|
-
|
95
|
-
print(f"Using empty schema as fallback")
|
162
|
+
self.log.error("schema_loading_error", error=str(e), path=self.schema_path)
|
96
163
|
return {}
|
97
164
|
|
98
165
|
def _extract_verb_definitions(self) -> Dict[str, Dict[str, Any]]:
|
@@ -107,17 +174,17 @@ class SchemaUtils:
|
|
107
174
|
# Extract from SWMLMethod anyOf
|
108
175
|
if "$defs" in self.schema and "SWMLMethod" in self.schema["$defs"]:
|
109
176
|
swml_method = self.schema["$defs"]["SWMLMethod"]
|
110
|
-
|
177
|
+
self.log.debug("swml_method_found", keys=list(swml_method.keys()))
|
111
178
|
|
112
179
|
if "anyOf" in swml_method:
|
113
|
-
|
180
|
+
self.log.debug("anyof_found", count=len(swml_method['anyOf']))
|
114
181
|
|
115
182
|
for ref in swml_method["anyOf"]:
|
116
183
|
if "$ref" in ref:
|
117
184
|
# Extract the verb name from the reference
|
118
185
|
verb_ref = ref["$ref"]
|
119
186
|
verb_name = verb_ref.split("/")[-1]
|
120
|
-
|
187
|
+
self.log.debug("processing_verb_reference", ref=verb_ref, name=verb_name)
|
121
188
|
|
122
189
|
# Look up the verb definition
|
123
190
|
if verb_name in self.schema["$defs"]:
|
@@ -133,11 +200,11 @@ class SchemaUtils:
|
|
133
200
|
"schema_name": verb_name,
|
134
201
|
"definition": verb_def
|
135
202
|
}
|
136
|
-
|
203
|
+
self.log.debug("verb_added", verb=actual_verb)
|
137
204
|
else:
|
138
|
-
|
205
|
+
self.log.warning("missing_swml_method_or_defs")
|
139
206
|
if "$defs" in self.schema:
|
140
|
-
|
207
|
+
self.log.debug("available_definitions", defs=list(self.schema['$defs'].keys()))
|
141
208
|
|
142
209
|
return verbs
|
143
210
|
|