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.
Files changed (37) hide show
  1. signalwire_agents/__init__.py +39 -4
  2. signalwire_agents/agent_server.py +46 -2
  3. signalwire_agents/cli/__init__.py +9 -0
  4. signalwire_agents/cli/test_swaig.py +2545 -0
  5. signalwire_agents/core/agent_base.py +691 -82
  6. signalwire_agents/core/contexts.py +289 -0
  7. signalwire_agents/core/data_map.py +499 -0
  8. signalwire_agents/core/function_result.py +57 -10
  9. signalwire_agents/core/skill_base.py +27 -37
  10. signalwire_agents/core/skill_manager.py +89 -23
  11. signalwire_agents/core/swaig_function.py +13 -1
  12. signalwire_agents/core/swml_handler.py +37 -13
  13. signalwire_agents/core/swml_service.py +37 -28
  14. signalwire_agents/skills/datasphere/__init__.py +12 -0
  15. signalwire_agents/skills/datasphere/skill.py +229 -0
  16. signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
  17. signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
  18. signalwire_agents/skills/datetime/skill.py +9 -5
  19. signalwire_agents/skills/joke/__init__.py +1 -0
  20. signalwire_agents/skills/joke/skill.py +88 -0
  21. signalwire_agents/skills/math/skill.py +9 -6
  22. signalwire_agents/skills/registry.py +23 -4
  23. signalwire_agents/skills/web_search/skill.py +57 -21
  24. signalwire_agents/skills/wikipedia/__init__.py +9 -0
  25. signalwire_agents/skills/wikipedia/skill.py +180 -0
  26. signalwire_agents/utils/__init__.py +2 -0
  27. signalwire_agents/utils/schema_utils.py +111 -44
  28. signalwire_agents/utils/serverless.py +38 -0
  29. signalwire_agents-0.1.11.dist-info/METADATA +756 -0
  30. signalwire_agents-0.1.11.dist-info/RECORD +58 -0
  31. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/WHEEL +1 -1
  32. signalwire_agents-0.1.11.dist-info/entry_points.txt +2 -0
  33. signalwire_agents-0.1.10.dist-info/METADATA +0 -319
  34. signalwire_agents-0.1.10.dist-info/RECORD +0 -44
  35. {signalwire_agents-0.1.10.data → signalwire_agents-0.1.11.data}/data/schema.json +0 -0
  36. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.11.dist-info}/licenses/LICENSE +0 -0
  37. {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) > 2000:
85
- text = text[:2000] + "... [Content truncated]"
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 = ["GOOGLE_SEARCH_API_KEY", "GOOGLE_SEARCH_ENGINE_ID"]
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
- if not self.validate_env_vars() or not self.validate_packages():
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=os.getenv('GOOGLE_SEARCH_API_KEY'),
145
- search_engine_id=os.getenv('GOOGLE_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.define_tool_with_swaig_fields(
153
- name="web_search",
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
- return SwaigFunctionResult(
188
- f"I couldn't find any results for '{query}'. "
189
- "This might be due to a very specific query or temporary issues. "
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
- return [
205
- "Google", "search", "internet", "web", "information",
206
- "find", "look up", "research", "query", "results"
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 web_search tool when users ask for information you need to look up",
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,9 @@
1
+ """
2
+ Wikipedia Search Skill
3
+
4
+ This skill provides Wikipedia search capabilities using the Wikipedia API.
5
+ """
6
+
7
+ from .skill import WikipediaSearchSkill
8
+
9
+ __all__ = ['WikipediaSearchSkill']
@@ -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,5 @@ Licensed under the MIT License.
7
7
  See LICENSE file in the project root for full license information.
8
8
  """
9
9
 
10
+ from .serverless import get_execution_mode, is_serverless_mode
11
+
@@ -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
- Utilities for working with the SWML JSON schema.
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 re
20
- from typing import Dict, List, Any, Optional, Set, Tuple, Callable
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
- Utilities for working with SWML JSON schema.
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 SchemaUtils with the provided schema
57
+ Initialize the schema utilities
37
58
 
38
59
  Args:
39
- schema_path: Path to the schema file (optional)
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
- print(f"No schema_path provided, using default: {self.schema_path}")
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
- print(f"Extracted {len(self.verbs)} verbs from schema")
71
+ self.log.debug("schema_initialized", verb_count=len(self.verbs))
49
72
  if self.verbs:
50
- print(f"First few verbs: {list(self.verbs.keys())[:5]}")
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
- # Default path is the schema.json in the root directory
60
- package_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
61
- default_path = os.path.join(package_dir, "schema.json")
62
- print(f"Default schema path: {default_path}")
63
- print(f"Path exists: {os.path.exists(default_path)}")
64
-
65
- return default_path
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
- print(f"Warning: No schema path provided. Using empty schema.")
143
+ self.log.warning("no_schema_path_provided")
76
144
  return {}
77
145
 
78
146
  try:
79
- print(f"Attempting to load schema from: {self.schema_path}")
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
- print(f"Successfully loaded schema from {self.schema_path}")
86
- print(f"Schema has {len(schema.keys()) if schema else 0} top-level keys")
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
- print(f"Schema has {len(schema['$defs'])} definitions")
156
+ self.log.debug("schema_definitions_found", count=len(schema['$defs']))
89
157
  return schema
90
158
  else:
91
- print(f"Schema file not found at {self.schema_path}")
159
+ self.log.error("schema_file_not_found", path=self.schema_path)
92
160
  return {}
93
161
  except (FileNotFoundError, json.JSONDecodeError) as e:
94
- print(f"Error loading schema: {e}")
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
- print(f"Found SWMLMethod in schema with keys: {swml_method.keys()}")
177
+ self.log.debug("swml_method_found", keys=list(swml_method.keys()))
111
178
 
112
179
  if "anyOf" in swml_method:
113
- print(f"Found anyOf in SWMLMethod with {len(swml_method['anyOf'])} items")
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
- print(f"Processing verb reference: {verb_ref} -> {verb_name}")
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
- print(f"Added verb: {actual_verb}")
203
+ self.log.debug("verb_added", verb=actual_verb)
137
204
  else:
138
- print(f"Missing $defs or SWMLMethod in schema")
205
+ self.log.warning("missing_swml_method_or_defs")
139
206
  if "$defs" in self.schema:
140
- print(f"Available definitions: {list(self.schema['$defs'].keys())}")
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'