signalwire-agents 0.1.23__py3-none-any.whl → 0.1.25__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 (64) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +2 -1
  3. signalwire_agents/cli/config.py +61 -0
  4. signalwire_agents/cli/core/__init__.py +1 -0
  5. signalwire_agents/cli/core/agent_loader.py +254 -0
  6. signalwire_agents/cli/core/argparse_helpers.py +164 -0
  7. signalwire_agents/cli/core/dynamic_config.py +62 -0
  8. signalwire_agents/cli/execution/__init__.py +1 -0
  9. signalwire_agents/cli/execution/datamap_exec.py +437 -0
  10. signalwire_agents/cli/execution/webhook_exec.py +125 -0
  11. signalwire_agents/cli/output/__init__.py +1 -0
  12. signalwire_agents/cli/output/output_formatter.py +132 -0
  13. signalwire_agents/cli/output/swml_dump.py +177 -0
  14. signalwire_agents/cli/simulation/__init__.py +1 -0
  15. signalwire_agents/cli/simulation/data_generation.py +365 -0
  16. signalwire_agents/cli/simulation/data_overrides.py +187 -0
  17. signalwire_agents/cli/simulation/mock_env.py +271 -0
  18. signalwire_agents/cli/test_swaig.py +522 -2539
  19. signalwire_agents/cli/types.py +72 -0
  20. signalwire_agents/core/agent/__init__.py +1 -3
  21. signalwire_agents/core/agent/config/__init__.py +1 -3
  22. signalwire_agents/core/agent/prompt/manager.py +25 -7
  23. signalwire_agents/core/agent/tools/decorator.py +2 -0
  24. signalwire_agents/core/agent/tools/registry.py +8 -0
  25. signalwire_agents/core/agent_base.py +492 -3053
  26. signalwire_agents/core/function_result.py +31 -42
  27. signalwire_agents/core/mixins/__init__.py +28 -0
  28. signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
  29. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  30. signalwire_agents/core/mixins/prompt_mixin.py +345 -0
  31. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  32. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  33. signalwire_agents/core/mixins/state_mixin.py +219 -0
  34. signalwire_agents/core/mixins/tool_mixin.py +295 -0
  35. signalwire_agents/core/mixins/web_mixin.py +1130 -0
  36. signalwire_agents/core/skill_manager.py +3 -1
  37. signalwire_agents/core/swaig_function.py +10 -1
  38. signalwire_agents/core/swml_service.py +140 -58
  39. signalwire_agents/skills/README.md +452 -0
  40. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  41. signalwire_agents/skills/datasphere/README.md +210 -0
  42. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  43. signalwire_agents/skills/datetime/README.md +132 -0
  44. signalwire_agents/skills/joke/README.md +149 -0
  45. signalwire_agents/skills/math/README.md +161 -0
  46. signalwire_agents/skills/native_vector_search/skill.py +33 -13
  47. signalwire_agents/skills/play_background_file/README.md +218 -0
  48. signalwire_agents/skills/spider/README.md +236 -0
  49. signalwire_agents/skills/spider/__init__.py +4 -0
  50. signalwire_agents/skills/spider/skill.py +479 -0
  51. signalwire_agents/skills/swml_transfer/README.md +395 -0
  52. signalwire_agents/skills/swml_transfer/__init__.py +1 -0
  53. signalwire_agents/skills/swml_transfer/skill.py +257 -0
  54. signalwire_agents/skills/weather_api/README.md +178 -0
  55. signalwire_agents/skills/web_search/README.md +163 -0
  56. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  57. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/METADATA +47 -2
  58. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/RECORD +62 -22
  59. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/entry_points.txt +1 -1
  60. signalwire_agents/core/agent/config/ephemeral.py +0 -176
  61. signalwire_agents-0.1.23.data/data/schema.json +0 -5611
  62. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/WHEEL +0 -0
  63. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/licenses/LICENSE +0 -0
  64. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Handle CLI overrides and mapping to nested data
4
+ """
5
+
6
+ import json
7
+ import uuid
8
+ import argparse
9
+ from typing import Dict, Any, List
10
+
11
+
12
+ def set_nested_value(data: Dict[str, Any], path: str, value: Any) -> None:
13
+ """
14
+ Set a nested value using dot notation path
15
+
16
+ Args:
17
+ data: Dictionary to modify
18
+ path: Dot-notation path (e.g., "call.call_id" or "vars.userVariables.custom")
19
+ value: Value to set
20
+ """
21
+ keys = path.split('.')
22
+ current = data
23
+
24
+ # Navigate to the parent of the target key
25
+ for key in keys[:-1]:
26
+ if key not in current:
27
+ current[key] = {}
28
+ current = current[key]
29
+
30
+ # Set the final value
31
+ current[keys[-1]] = value
32
+
33
+
34
+ def parse_value(value_str: str) -> Any:
35
+ """
36
+ Parse a string value into appropriate Python type
37
+
38
+ Args:
39
+ value_str: String representation of value
40
+
41
+ Returns:
42
+ Parsed value (str, int, float, bool, None, or JSON object)
43
+ """
44
+ # Handle special values
45
+ if value_str.lower() == 'null':
46
+ return None
47
+ elif value_str.lower() == 'true':
48
+ return True
49
+ elif value_str.lower() == 'false':
50
+ return False
51
+
52
+ # Try parsing as number
53
+ try:
54
+ if '.' in value_str:
55
+ return float(value_str)
56
+ else:
57
+ return int(value_str)
58
+ except ValueError:
59
+ pass
60
+
61
+ # Try parsing as JSON (for objects/arrays)
62
+ try:
63
+ return json.loads(value_str)
64
+ except json.JSONDecodeError:
65
+ pass
66
+
67
+ # Return as string
68
+ return value_str
69
+
70
+
71
+ def apply_overrides(data: Dict[str, Any], overrides: List[str],
72
+ json_overrides: List[str]) -> Dict[str, Any]:
73
+ """
74
+ Apply override values to data using dot notation paths
75
+
76
+ Args:
77
+ data: Data dictionary to modify
78
+ overrides: List of "path=value" strings
79
+ json_overrides: List of "path=json_value" strings
80
+
81
+ Returns:
82
+ Modified data dictionary
83
+ """
84
+ data = data.copy()
85
+
86
+ # Apply simple overrides
87
+ for override in overrides:
88
+ if '=' not in override:
89
+ continue
90
+ path, value_str = override.split('=', 1)
91
+ value = parse_value(value_str)
92
+ set_nested_value(data, path, value)
93
+
94
+ # Apply JSON overrides
95
+ for json_override in json_overrides:
96
+ if '=' not in json_override:
97
+ continue
98
+ path, json_str = json_override.split('=', 1)
99
+ try:
100
+ value = json.loads(json_str)
101
+ set_nested_value(data, path, value)
102
+ except json.JSONDecodeError as e:
103
+ print(f"Warning: Invalid JSON in override '{json_override}': {e}")
104
+
105
+ return data
106
+
107
+
108
+ def apply_convenience_mappings(data: Dict[str, Any], args: argparse.Namespace) -> Dict[str, Any]:
109
+ """
110
+ Apply convenience CLI arguments to data structure
111
+
112
+ Args:
113
+ data: Data dictionary to modify
114
+ args: Parsed CLI arguments
115
+
116
+ Returns:
117
+ Modified data dictionary
118
+ """
119
+ data = data.copy()
120
+
121
+ # Map high-level arguments to specific paths
122
+ if hasattr(args, 'call_id') and args.call_id:
123
+ set_nested_value(data, "call.call_id", args.call_id)
124
+ set_nested_value(data, "call.tag", args.call_id) # tag often matches call_id
125
+
126
+ if hasattr(args, 'project_id') and args.project_id:
127
+ set_nested_value(data, "call.project_id", args.project_id)
128
+
129
+ if hasattr(args, 'space_id') and args.space_id:
130
+ set_nested_value(data, "call.space_id", args.space_id)
131
+
132
+ if hasattr(args, 'call_state') and args.call_state:
133
+ set_nested_value(data, "call.state", args.call_state)
134
+
135
+ if hasattr(args, 'call_direction') and args.call_direction:
136
+ set_nested_value(data, "call.direction", args.call_direction)
137
+
138
+ # Handle from/to addresses with fake generation if needed
139
+ if hasattr(args, 'from_number') and args.from_number:
140
+ # If looks like phone number, use as-is, otherwise generate fake
141
+ if args.from_number.startswith('+') or args.from_number.isdigit():
142
+ set_nested_value(data, "call.from", args.from_number)
143
+ else:
144
+ # Generate fake phone number or SIP address
145
+ call_type = getattr(args, 'call_type', 'webrtc')
146
+ if call_type == 'sip':
147
+ set_nested_value(data, "call.from", f"+1555{uuid.uuid4().hex[:7]}")
148
+ else:
149
+ set_nested_value(data, "call.from", f"{args.from_number}@test.domain")
150
+
151
+ if hasattr(args, 'to_extension') and args.to_extension:
152
+ # Similar logic for 'to' address
153
+ if args.to_extension.startswith('+') or args.to_extension.isdigit():
154
+ set_nested_value(data, "call.to", args.to_extension)
155
+ else:
156
+ call_type = getattr(args, 'call_type', 'webrtc')
157
+ if call_type == 'sip':
158
+ set_nested_value(data, "call.to", f"+1444{uuid.uuid4().hex[:7]}")
159
+ else:
160
+ set_nested_value(data, "call.to", f"{args.to_extension}@test.domain")
161
+
162
+ # Merge user variables
163
+ user_vars = {}
164
+
165
+ # Add user_vars if provided
166
+ if hasattr(args, 'user_vars') and args.user_vars:
167
+ try:
168
+ user_vars.update(json.loads(args.user_vars))
169
+ except json.JSONDecodeError as e:
170
+ print(f"Warning: Invalid JSON in --user-vars: {e}")
171
+
172
+ # Add query_params if provided (merged into userVariables)
173
+ if hasattr(args, 'query_params') and args.query_params:
174
+ try:
175
+ user_vars.update(json.loads(args.query_params))
176
+ except json.JSONDecodeError as e:
177
+ print(f"Warning: Invalid JSON in --query-params: {e}")
178
+
179
+ # Apply user variables
180
+ if user_vars:
181
+ if "vars" not in data:
182
+ data["vars"] = {}
183
+ if "userVariables" not in data["vars"]:
184
+ data["vars"]["userVariables"] = {}
185
+ data["vars"]["userVariables"].update(user_vars)
186
+
187
+ return data
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Mock environment and serverless simulation functionality
4
+ """
5
+
6
+ import os
7
+ import json
8
+ from typing import Optional, Dict, Any
9
+ from ..types import PostData
10
+
11
+
12
+ class MockQueryParams:
13
+ """Mock FastAPI QueryParams (simple dict-like)"""
14
+ def __init__(self, params: Optional[Dict[str, str]] = None):
15
+ self._params = params or {}
16
+
17
+ def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
18
+ return self._params.get(key, default)
19
+
20
+ def __getitem__(self, key: str) -> str:
21
+ return self._params[key]
22
+
23
+ def __contains__(self, key: str) -> bool:
24
+ return key in self._params
25
+
26
+ def items(self):
27
+ return self._params.items()
28
+
29
+ def keys(self):
30
+ return self._params.keys()
31
+
32
+ def values(self):
33
+ return self._params.values()
34
+
35
+
36
+ class MockHeaders:
37
+ """Mock FastAPI Headers (case-insensitive dict-like)"""
38
+ def __init__(self, headers: Optional[Dict[str, str]] = None):
39
+ # Store headers with lowercase keys for case-insensitive lookup
40
+ self._headers = {}
41
+ if headers:
42
+ for k, v in headers.items():
43
+ self._headers[k.lower()] = v
44
+
45
+ def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
46
+ return self._headers.get(key.lower(), default)
47
+
48
+ def __getitem__(self, key: str) -> str:
49
+ return self._headers[key.lower()]
50
+
51
+ def __contains__(self, key: str) -> bool:
52
+ return key.lower() in self._headers
53
+
54
+ def items(self):
55
+ return self._headers.items()
56
+
57
+ def keys(self):
58
+ return self._headers.keys()
59
+
60
+ def values(self):
61
+ return self._headers.values()
62
+
63
+
64
+ class MockURL:
65
+ """Mock FastAPI URL object"""
66
+ def __init__(self, url: str = "http://localhost:8080/swml"):
67
+ self._url = url
68
+ # Parse basic components
69
+ if "?" in url:
70
+ self.path, query_string = url.split("?", 1)
71
+ self.query = query_string
72
+ else:
73
+ self.path = url
74
+ self.query = ""
75
+
76
+ # Extract scheme and netloc
77
+ if "://" in url:
78
+ self.scheme, rest = url.split("://", 1)
79
+ if "/" in rest:
80
+ self.netloc = rest.split("/", 1)[0]
81
+ else:
82
+ self.netloc = rest
83
+ else:
84
+ self.scheme = "http"
85
+ self.netloc = "localhost:8080"
86
+
87
+ def __str__(self):
88
+ return self._url
89
+
90
+
91
+ class MockRequest:
92
+ """Mock FastAPI Request object for dynamic agent testing"""
93
+ def __init__(self, method: str = "POST", url: str = "http://localhost:8080/swml",
94
+ headers: Optional[Dict[str, str]] = None,
95
+ query_params: Optional[Dict[str, str]] = None,
96
+ json_body: Optional[Dict[str, Any]] = None):
97
+ self.method = method
98
+ self.url = MockURL(url)
99
+ self.headers = MockHeaders(headers)
100
+ self.query_params = MockQueryParams(query_params)
101
+ self._json_body = json_body or {}
102
+ self._body = json.dumps(self._json_body).encode('utf-8')
103
+
104
+ async def json(self) -> Dict[str, Any]:
105
+ """Return the JSON body"""
106
+ return self._json_body
107
+
108
+ async def body(self) -> bytes:
109
+ """Return the raw body bytes"""
110
+ return self._body
111
+
112
+ def client(self):
113
+ """Mock client property"""
114
+ return type('MockClient', (), {'host': '127.0.0.1', 'port': 0})()
115
+
116
+
117
+ def create_mock_request(method: str = "POST", url: str = "http://localhost:8080/swml",
118
+ headers: Optional[Dict[str, str]] = None,
119
+ query_params: Optional[Dict[str, str]] = None,
120
+ body: Optional[Dict[str, Any]] = None) -> MockRequest:
121
+ """
122
+ Factory function to create a mock FastAPI Request object
123
+ """
124
+ return MockRequest(method=method, url=url, headers=headers,
125
+ query_params=query_params, json_body=body)
126
+
127
+
128
+ class ServerlessSimulator:
129
+ """Manages serverless environment simulation for different platforms"""
130
+
131
+ # Default environment presets for each platform
132
+ PLATFORM_PRESETS = {
133
+ 'lambda': {
134
+ 'AWS_LAMBDA_FUNCTION_NAME': 'test-agent-function',
135
+ 'AWS_LAMBDA_FUNCTION_URL': 'https://abc123.lambda-url.us-east-1.on.aws/',
136
+ 'AWS_REGION': 'us-east-1',
137
+ '_HANDLER': 'lambda_function.lambda_handler'
138
+ },
139
+ 'cgi': {
140
+ 'GATEWAY_INTERFACE': 'CGI/1.1',
141
+ 'HTTP_HOST': 'example.com',
142
+ 'SCRIPT_NAME': '/cgi-bin/agent.cgi',
143
+ 'HTTPS': 'on',
144
+ 'SERVER_NAME': 'example.com'
145
+ },
146
+ 'cloud_function': {
147
+ 'GOOGLE_CLOUD_PROJECT': 'test-project',
148
+ 'FUNCTION_URL': 'https://my-function-abc123.cloudfunctions.net',
149
+ 'GOOGLE_CLOUD_REGION': 'us-central1',
150
+ 'K_SERVICE': 'agent'
151
+ },
152
+ 'azure_function': {
153
+ 'AZURE_FUNCTIONS_ENVIRONMENT': 'Development',
154
+ 'FUNCTIONS_WORKER_RUNTIME': 'python',
155
+ 'WEBSITE_SITE_NAME': 'my-function-app'
156
+ }
157
+ }
158
+
159
+ def __init__(self, platform: str, overrides: Optional[Dict[str, str]] = None):
160
+ self.platform = platform
161
+ self.original_env = dict(os.environ)
162
+ self.preset_env = self.PLATFORM_PRESETS.get(platform, {}).copy()
163
+ self.overrides = overrides or {}
164
+ self.active = False
165
+ self._cleared_vars = {}
166
+
167
+ def activate(self, verbose: bool = False):
168
+ """Apply serverless environment simulation"""
169
+ if self.active:
170
+ return
171
+
172
+ # Clear conflicting environment variables
173
+ self._clear_conflicting_env()
174
+
175
+ # Apply preset environment
176
+ os.environ.update(self.preset_env)
177
+
178
+ # Apply user overrides
179
+ os.environ.update(self.overrides)
180
+
181
+ # Set appropriate logging mode for serverless simulation
182
+ if self.platform == 'cgi' and 'SIGNALWIRE_LOG_MODE' not in self.overrides:
183
+ # CGI mode should default to 'off' unless explicitly overridden
184
+ os.environ['SIGNALWIRE_LOG_MODE'] = 'off'
185
+
186
+ self.active = True
187
+
188
+ if verbose:
189
+ print(f"✓ Activated {self.platform} environment simulation")
190
+
191
+ # Debug: Show key environment variables
192
+ if self.platform == 'lambda':
193
+ print(f" AWS_LAMBDA_FUNCTION_NAME: {os.environ.get('AWS_LAMBDA_FUNCTION_NAME')}")
194
+ print(f" AWS_LAMBDA_FUNCTION_URL: {os.environ.get('AWS_LAMBDA_FUNCTION_URL')}")
195
+ print(f" AWS_REGION: {os.environ.get('AWS_REGION')}")
196
+ elif self.platform == 'cgi':
197
+ print(f" GATEWAY_INTERFACE: {os.environ.get('GATEWAY_INTERFACE')}")
198
+ print(f" HTTP_HOST: {os.environ.get('HTTP_HOST')}")
199
+ print(f" SCRIPT_NAME: {os.environ.get('SCRIPT_NAME')}")
200
+ print(f" SIGNALWIRE_LOG_MODE: {os.environ.get('SIGNALWIRE_LOG_MODE')}")
201
+ elif self.platform == 'cloud_function':
202
+ print(f" GOOGLE_CLOUD_PROJECT: {os.environ.get('GOOGLE_CLOUD_PROJECT')}")
203
+ print(f" FUNCTION_URL: {os.environ.get('FUNCTION_URL')}")
204
+ print(f" GOOGLE_CLOUD_REGION: {os.environ.get('GOOGLE_CLOUD_REGION')}")
205
+ elif self.platform == 'azure_function':
206
+ print(f" AZURE_FUNCTIONS_ENVIRONMENT: {os.environ.get('AZURE_FUNCTIONS_ENVIRONMENT')}")
207
+ print(f" WEBSITE_SITE_NAME: {os.environ.get('WEBSITE_SITE_NAME')}")
208
+
209
+ # Debug: Confirm SWML_PROXY_URL_BASE is cleared
210
+ proxy_url = os.environ.get('SWML_PROXY_URL_BASE')
211
+ if proxy_url:
212
+ print(f" WARNING: SWML_PROXY_URL_BASE still set: {proxy_url}")
213
+ else:
214
+ print(f" ✓ SWML_PROXY_URL_BASE cleared successfully")
215
+
216
+ def deactivate(self, verbose: bool = False):
217
+ """Restore original environment"""
218
+ if not self.active:
219
+ return
220
+
221
+ os.environ.clear()
222
+ os.environ.update(self.original_env)
223
+ self.active = False
224
+
225
+ if verbose:
226
+ print(f"✓ Deactivated {self.platform} environment simulation")
227
+
228
+ def _clear_conflicting_env(self):
229
+ """Clear environment variables that might conflict with simulation"""
230
+ # Remove variables from other platforms
231
+ conflicting_vars = []
232
+ for platform, preset in self.PLATFORM_PRESETS.items():
233
+ if platform != self.platform:
234
+ conflicting_vars.extend(preset.keys())
235
+
236
+ # Always clear SWML_PROXY_URL_BASE during serverless simulation
237
+ # so that platform-specific URL generation takes precedence
238
+ conflicting_vars.append('SWML_PROXY_URL_BASE')
239
+
240
+ for var in conflicting_vars:
241
+ if var in os.environ:
242
+ self._cleared_vars[var] = os.environ[var]
243
+ os.environ.pop(var)
244
+
245
+ def add_override(self, key: str, value: str):
246
+ """Add an environment variable override"""
247
+ self.overrides[key] = value
248
+ if self.active:
249
+ os.environ[key] = value
250
+
251
+ def get_current_env(self) -> Dict[str, str]:
252
+ """Get the current environment that would be applied"""
253
+ env = self.preset_env.copy()
254
+ env.update(self.overrides)
255
+ return env
256
+
257
+
258
+ def load_env_file(env_file_path: str) -> Dict[str, str]:
259
+ """Load environment variables from a file"""
260
+ env_vars = {}
261
+ if not os.path.exists(env_file_path):
262
+ raise FileNotFoundError(f"Environment file not found: {env_file_path}")
263
+
264
+ with open(env_file_path, 'r') as f:
265
+ for line in f:
266
+ line = line.strip()
267
+ if line and not line.startswith('#') and '=' in line:
268
+ key, value = line.split('=', 1)
269
+ env_vars[key.strip()] = value.strip()
270
+
271
+ return env_vars