signalwire-agents 0.1.10__py3-none-any.whl → 0.1.11__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 +39 -4
- signalwire_agents/agent_server.py +46 -2
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/test_swaig.py +2545 -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/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/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/registry.py +23 -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 +2 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents/utils/serverless.py +38 -0
- signalwire_agents-0.1.11.dist-info/METADATA +756 -0
- signalwire_agents-0.1.11.dist-info/RECORD +58 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/WHEEL +1 -1
- signalwire_agents-0.1.11.dist-info/entry_points.txt +2 -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.11.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/top_level.txt +0 -0
@@ -21,9 +21,10 @@ from signalwire_agents.core.function_result import SwaigFunctionResult
|
|
21
21
|
class GoogleSearchScraper:
|
22
22
|
"""Google Search and Web Scraping functionality"""
|
23
23
|
|
24
|
-
def __init__(self, api_key: str, search_engine_id: str):
|
24
|
+
def __init__(self, api_key: str, search_engine_id: str, max_content_length: int = 2000):
|
25
25
|
self.api_key = api_key
|
26
26
|
self.search_engine_id = search_engine_id
|
27
|
+
self.max_content_length = max_content_length
|
27
28
|
self.session = requests.Session()
|
28
29
|
self.session.headers.update({
|
29
30
|
'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'
|
@@ -81,8 +82,8 @@ class GoogleSearchScraper:
|
|
81
82
|
text = ' '.join(chunk for chunk in chunks if chunk)
|
82
83
|
|
83
84
|
# Limit text length
|
84
|
-
if len(text) >
|
85
|
-
text = text[:
|
85
|
+
if len(text) > self.max_content_length:
|
86
|
+
text = text[:self.max_content_length] + "... [Content truncated]"
|
86
87
|
|
87
88
|
return text
|
88
89
|
|
@@ -128,29 +129,63 @@ class WebSearchSkill(SkillBase):
|
|
128
129
|
SKILL_DESCRIPTION = "Search the web for information using Google Custom Search API"
|
129
130
|
SKILL_VERSION = "1.0.0"
|
130
131
|
REQUIRED_PACKAGES = ["bs4", "requests"]
|
131
|
-
REQUIRED_ENV_VARS = [
|
132
|
+
REQUIRED_ENV_VARS = [] # No required env vars since all config comes from params
|
133
|
+
|
134
|
+
# Enable multiple instances support
|
135
|
+
SUPPORTS_MULTIPLE_INSTANCES = True
|
136
|
+
|
137
|
+
def get_instance_key(self) -> str:
|
138
|
+
"""
|
139
|
+
Get the key used to track this skill instance
|
140
|
+
|
141
|
+
For web search, we use the search_engine_id to differentiate instances
|
142
|
+
"""
|
143
|
+
search_engine_id = self.params.get('search_engine_id', 'default')
|
144
|
+
tool_name = self.params.get('tool_name', 'web_search')
|
145
|
+
return f"{self.SKILL_NAME}_{search_engine_id}_{tool_name}"
|
132
146
|
|
133
147
|
def setup(self) -> bool:
|
134
148
|
"""Setup the web search skill"""
|
135
|
-
|
149
|
+
# Validate required parameters
|
150
|
+
required_params = ['api_key', 'search_engine_id']
|
151
|
+
missing_params = [param for param in required_params if not self.params.get(param)]
|
152
|
+
if missing_params:
|
153
|
+
self.logger.error(f"Missing required parameters: {missing_params}")
|
154
|
+
return False
|
155
|
+
|
156
|
+
if not self.validate_packages():
|
136
157
|
return False
|
137
158
|
|
159
|
+
# Set parameters from config
|
160
|
+
self.api_key = self.params['api_key']
|
161
|
+
self.search_engine_id = self.params['search_engine_id']
|
162
|
+
|
138
163
|
# Set default parameters
|
139
164
|
self.default_num_results = self.params.get('num_results', 1)
|
140
165
|
self.default_delay = self.params.get('delay', 0)
|
166
|
+
self.max_content_length = self.params.get('max_content_length', 2000)
|
167
|
+
self.no_results_message = self.params.get('no_results_message',
|
168
|
+
"I couldn't find any results for '{query}'. "
|
169
|
+
"This might be due to a very specific query or temporary issues. "
|
170
|
+
"Try rephrasing your search or asking about a different topic."
|
171
|
+
)
|
172
|
+
|
173
|
+
# Tool name (for multiple instances)
|
174
|
+
self.tool_name = self.params.get('tool_name', 'web_search')
|
141
175
|
|
142
176
|
# Initialize the search scraper
|
143
177
|
self.search_scraper = GoogleSearchScraper(
|
144
|
-
api_key=
|
145
|
-
search_engine_id=
|
178
|
+
api_key=self.api_key,
|
179
|
+
search_engine_id=self.search_engine_id,
|
180
|
+
max_content_length=self.max_content_length
|
146
181
|
)
|
147
182
|
|
148
183
|
return True
|
149
184
|
|
150
185
|
def register_tools(self) -> None:
|
151
186
|
"""Register web search tool with the agent"""
|
152
|
-
self.
|
153
|
-
name=
|
187
|
+
self.agent.define_tool(
|
188
|
+
name=self.tool_name,
|
154
189
|
description="Search the web for information on any topic and return detailed results with content from multiple sources",
|
155
190
|
parameters={
|
156
191
|
"query": {
|
@@ -158,7 +193,8 @@ class WebSearchSkill(SkillBase):
|
|
158
193
|
"description": "The search query - what you want to find information about"
|
159
194
|
}
|
160
195
|
},
|
161
|
-
handler=self._web_search_handler
|
196
|
+
handler=self._web_search_handler,
|
197
|
+
**self.swaig_fields
|
162
198
|
)
|
163
199
|
|
164
200
|
def _web_search_handler(self, args, raw_data):
|
@@ -184,11 +220,9 @@ class WebSearchSkill(SkillBase):
|
|
184
220
|
)
|
185
221
|
|
186
222
|
if not search_results or "No search results found" in search_results:
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
"Try rephrasing your search or asking about a different topic."
|
191
|
-
)
|
223
|
+
# Format the no results message with the query if it contains a placeholder
|
224
|
+
formatted_message = self.no_results_message.format(query=query) if '{query}' in self.no_results_message else self.no_results_message
|
225
|
+
return SwaigFunctionResult(formatted_message)
|
192
226
|
|
193
227
|
response = f"I found {num_results} results for '{query}':\n\n{search_results}"
|
194
228
|
return SwaigFunctionResult(response)
|
@@ -201,10 +235,12 @@ class WebSearchSkill(SkillBase):
|
|
201
235
|
|
202
236
|
def get_hints(self) -> List[str]:
|
203
237
|
"""Return speech recognition hints"""
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
238
|
+
# Currently no hints provided, but you could add them like:
|
239
|
+
# return [
|
240
|
+
# "Google", "search", "internet", "web", "information",
|
241
|
+
# "find", "look up", "research", "query", "results"
|
242
|
+
# ]
|
243
|
+
return []
|
208
244
|
|
209
245
|
def get_global_data(self) -> Dict[str, Any]:
|
210
246
|
"""Return global data for agent context"""
|
@@ -218,9 +254,9 @@ class WebSearchSkill(SkillBase):
|
|
218
254
|
return [
|
219
255
|
{
|
220
256
|
"title": "Web Search Capability",
|
221
|
-
"body": "You can search the internet for current, accurate information on any topic.",
|
257
|
+
"body": f"You can search the internet for current, accurate information on any topic using the {self.tool_name} tool.",
|
222
258
|
"bullets": [
|
223
|
-
"Use the
|
259
|
+
f"Use the {self.tool_name} tool when users ask for information you need to look up",
|
224
260
|
"Search for news, current events, product information, or any current data",
|
225
261
|
"Summarize search results in a clear, helpful way",
|
226
262
|
"Include relevant URLs so users can read more if interested"
|
@@ -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 []
|
@@ -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
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Copyright (c) 2025 SignalWire
|
4
|
+
|
5
|
+
This file is part of the SignalWire AI Agents SDK.
|
6
|
+
|
7
|
+
Licensed under the MIT License.
|
8
|
+
See LICENSE file in the project root for full license information.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import os
|
12
|
+
|
13
|
+
def get_execution_mode() -> str:
|
14
|
+
"""
|
15
|
+
Detect the current execution environment.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
str: One of 'cgi', 'lambda', 'cloud_function', 'azure_function', or 'server'
|
19
|
+
"""
|
20
|
+
if os.getenv('GATEWAY_INTERFACE'):
|
21
|
+
return 'cgi'
|
22
|
+
elif os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
23
|
+
return 'lambda'
|
24
|
+
elif os.getenv('GOOGLE_CLOUD_PROJECT'):
|
25
|
+
return 'cloud_function'
|
26
|
+
elif os.getenv('AZURE_FUNCTIONS_ENVIRONMENT'):
|
27
|
+
return 'azure_function'
|
28
|
+
else:
|
29
|
+
return 'server'
|
30
|
+
|
31
|
+
def is_serverless_mode() -> bool:
|
32
|
+
"""
|
33
|
+
Check if running in any serverless environment.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
bool: True if in serverless mode, False if in server mode
|
37
|
+
"""
|
38
|
+
return get_execution_mode() != 'server'
|