signalwire-agents 0.1.32__py3-none-any.whl → 0.1.34__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 +1 -1
- signalwire_agents/cli/core/agent_loader.py +320 -113
- signalwire_agents/cli/core/argparse_helpers.py +7 -1
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/output/output_formatter.py +104 -2
- signalwire_agents/cli/output/swml_dump.py +13 -13
- signalwire_agents/cli/simulation/mock_env.py +2 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +20 -6
- {signalwire_agents-0.1.32.dist-info → signalwire_agents-0.1.34.dist-info}/METADATA +89 -100
- {signalwire_agents-0.1.32.dist-info → signalwire_agents-0.1.34.dist-info}/RECORD +15 -13
- {signalwire_agents-0.1.32.dist-info → signalwire_agents-0.1.34.dist-info}/entry_points.txt +1 -1
- {signalwire_agents-0.1.32.dist-info → signalwire_agents-0.1.34.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.32.dist-info → signalwire_agents-0.1.34.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.32.dist-info → signalwire_agents-0.1.34.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,303 @@
|
|
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
|
+
"""
|
12
|
+
Service discovery and loading functionality - new simplified approach
|
13
|
+
"""
|
14
|
+
|
15
|
+
import importlib.util
|
16
|
+
from pathlib import Path
|
17
|
+
from typing import List, Dict, Any, Optional, Callable
|
18
|
+
import asyncio
|
19
|
+
import sys
|
20
|
+
import io
|
21
|
+
import contextlib
|
22
|
+
|
23
|
+
# Import after checking if available
|
24
|
+
try:
|
25
|
+
from signalwire_agents.core.agent_base import AgentBase
|
26
|
+
from signalwire_agents.core.swml_service import SWMLService
|
27
|
+
from fastapi import Request, Response
|
28
|
+
DEPENDENCIES_AVAILABLE = True
|
29
|
+
except ImportError:
|
30
|
+
AgentBase = None
|
31
|
+
SWMLService = None
|
32
|
+
Request = None
|
33
|
+
Response = None
|
34
|
+
DEPENDENCIES_AVAILABLE = False
|
35
|
+
|
36
|
+
|
37
|
+
class ServiceCapture:
|
38
|
+
"""Captures SWMLService instances when they try to run/serve"""
|
39
|
+
|
40
|
+
def __init__(self):
|
41
|
+
self.captured_services: List[SWMLService] = []
|
42
|
+
self.original_methods = {}
|
43
|
+
|
44
|
+
def capture(self, service_path: str, suppress_output: bool = False) -> List[SWMLService]:
|
45
|
+
"""
|
46
|
+
Execute a service file and capture any services that try to run
|
47
|
+
|
48
|
+
Args:
|
49
|
+
service_path: Path to the Python file
|
50
|
+
suppress_output: If True, suppress stdout during module execution
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
List of captured SWMLService instances
|
54
|
+
"""
|
55
|
+
if not DEPENDENCIES_AVAILABLE:
|
56
|
+
raise ImportError("Required dependencies not available. Please install signalwire-agents package.")
|
57
|
+
|
58
|
+
service_path = Path(service_path).resolve()
|
59
|
+
|
60
|
+
if not service_path.exists():
|
61
|
+
raise FileNotFoundError(f"Service file not found: {service_path}")
|
62
|
+
|
63
|
+
if not service_path.suffix == '.py':
|
64
|
+
raise ValueError(f"Service file must be a Python file (.py): {service_path}")
|
65
|
+
|
66
|
+
# Reset captured services
|
67
|
+
self.captured_services = []
|
68
|
+
|
69
|
+
# Apply patches
|
70
|
+
self._apply_patches()
|
71
|
+
|
72
|
+
# Context manager for optional stdout suppression
|
73
|
+
stdout_context = io.StringIO() if suppress_output else None
|
74
|
+
|
75
|
+
try:
|
76
|
+
with contextlib.redirect_stdout(stdout_context) if suppress_output else contextlib.nullcontext():
|
77
|
+
# Load and execute the module
|
78
|
+
spec = importlib.util.spec_from_file_location("__main__", service_path)
|
79
|
+
module = importlib.util.module_from_spec(spec)
|
80
|
+
|
81
|
+
# Set __name__ to "__main__" to trigger if __name__ == "__main__": blocks
|
82
|
+
module.__name__ = "__main__"
|
83
|
+
|
84
|
+
try:
|
85
|
+
spec.loader.exec_module(module)
|
86
|
+
except Exception as e:
|
87
|
+
# Module might have called run/serve which we intercepted
|
88
|
+
if not self.captured_services:
|
89
|
+
# If we didn't capture anything, the error is real
|
90
|
+
raise ImportError(f"Failed to load service module: {e}")
|
91
|
+
finally:
|
92
|
+
# Always restore original methods
|
93
|
+
self._restore_patches()
|
94
|
+
|
95
|
+
return self.captured_services
|
96
|
+
|
97
|
+
def _apply_patches(self):
|
98
|
+
"""Apply patches to capture services"""
|
99
|
+
|
100
|
+
# Store reference to self for use in closures
|
101
|
+
capture_self = self
|
102
|
+
|
103
|
+
def mock_run(service_instance, *args, **kwargs):
|
104
|
+
"""Capture service when run() is called"""
|
105
|
+
capture_self.captured_services.append(service_instance)
|
106
|
+
# Don't print during stdout suppression
|
107
|
+
# Don't actually run - we're just capturing
|
108
|
+
return service_instance
|
109
|
+
|
110
|
+
def mock_serve(service_instance, *args, **kwargs):
|
111
|
+
"""Capture service when serve() is called"""
|
112
|
+
capture_self.captured_services.append(service_instance)
|
113
|
+
# Don't print during stdout suppression
|
114
|
+
# Don't actually serve - we're just capturing
|
115
|
+
return service_instance
|
116
|
+
|
117
|
+
# Apply patches to both SWMLService and AgentBase
|
118
|
+
for base_class in [SWMLService, AgentBase]:
|
119
|
+
if base_class:
|
120
|
+
if hasattr(base_class, 'run'):
|
121
|
+
self.original_methods[(base_class, 'run')] = base_class.run
|
122
|
+
base_class.run = mock_run
|
123
|
+
|
124
|
+
if hasattr(base_class, 'serve'):
|
125
|
+
self.original_methods[(base_class, 'serve')] = base_class.serve
|
126
|
+
base_class.serve = mock_serve
|
127
|
+
|
128
|
+
def _restore_patches(self):
|
129
|
+
"""Restore original methods"""
|
130
|
+
for (base_class, method_name), original_method in self.original_methods.items():
|
131
|
+
setattr(base_class, method_name, original_method)
|
132
|
+
self.original_methods.clear()
|
133
|
+
|
134
|
+
|
135
|
+
async def simulate_request_to_service(
|
136
|
+
service: SWMLService,
|
137
|
+
method: str = "POST",
|
138
|
+
body: Optional[dict] = None,
|
139
|
+
query_params: Optional[dict] = None,
|
140
|
+
headers: Optional[dict] = None
|
141
|
+
) -> dict:
|
142
|
+
"""
|
143
|
+
Simulate a request to a SWMLService instance
|
144
|
+
|
145
|
+
Args:
|
146
|
+
service: The SWMLService instance
|
147
|
+
method: HTTP method (GET or POST)
|
148
|
+
body: Request body for POST requests
|
149
|
+
query_params: Query parameters
|
150
|
+
headers: Request headers
|
151
|
+
|
152
|
+
Returns:
|
153
|
+
The service's response as a dict
|
154
|
+
"""
|
155
|
+
# Create a mock request
|
156
|
+
from signalwire_agents.cli.simulation.mock_env import create_mock_request
|
157
|
+
|
158
|
+
request = create_mock_request(
|
159
|
+
method=method,
|
160
|
+
headers=headers or {},
|
161
|
+
query_params=query_params or {},
|
162
|
+
body=body or {}
|
163
|
+
)
|
164
|
+
|
165
|
+
# Create a mock response
|
166
|
+
response = Response()
|
167
|
+
|
168
|
+
# Call the service's request handler
|
169
|
+
result = await service._handle_request(request, response)
|
170
|
+
|
171
|
+
# If result is a Response object, extract the content
|
172
|
+
if hasattr(result, 'body'):
|
173
|
+
# FastAPI Response
|
174
|
+
import json
|
175
|
+
return json.loads(result.body.decode())
|
176
|
+
elif isinstance(result, dict):
|
177
|
+
return result
|
178
|
+
else:
|
179
|
+
# Try to get content from response
|
180
|
+
return {"error": "Unable to parse response"}
|
181
|
+
|
182
|
+
|
183
|
+
def load_and_simulate_service(
|
184
|
+
service_path: str,
|
185
|
+
route: Optional[str] = None,
|
186
|
+
method: str = "POST",
|
187
|
+
body: Optional[dict] = None,
|
188
|
+
query_params: Optional[dict] = None,
|
189
|
+
headers: Optional[dict] = None,
|
190
|
+
suppress_output: bool = False
|
191
|
+
) -> dict:
|
192
|
+
"""
|
193
|
+
Load a service file and simulate a request to it
|
194
|
+
|
195
|
+
This is the main entry point that combines loading and request simulation
|
196
|
+
|
197
|
+
Args:
|
198
|
+
service_path: Path to the service file
|
199
|
+
route: Optional route to request (for multi-service files)
|
200
|
+
method: HTTP method
|
201
|
+
body: Request body
|
202
|
+
query_params: Query parameters
|
203
|
+
headers: Request headers
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
The service's response
|
207
|
+
"""
|
208
|
+
# Capture services from the file
|
209
|
+
capturer = ServiceCapture()
|
210
|
+
services = capturer.capture(service_path, suppress_output=suppress_output)
|
211
|
+
|
212
|
+
if not services:
|
213
|
+
raise ValueError(f"No services found in {service_path}")
|
214
|
+
|
215
|
+
# Select the appropriate service
|
216
|
+
if len(services) == 1:
|
217
|
+
service = services[0]
|
218
|
+
else:
|
219
|
+
# Multiple services - need to select by route
|
220
|
+
if not route:
|
221
|
+
# List available routes
|
222
|
+
routes = [s.route for s in services]
|
223
|
+
raise ValueError(
|
224
|
+
f"Multiple services found. Please specify a route.\n"
|
225
|
+
f"Available routes: {', '.join(routes)}"
|
226
|
+
)
|
227
|
+
|
228
|
+
# Find service by route
|
229
|
+
service = None
|
230
|
+
for s in services:
|
231
|
+
if s.route == route:
|
232
|
+
service = s
|
233
|
+
break
|
234
|
+
|
235
|
+
if not service:
|
236
|
+
available = [s.route for s in services]
|
237
|
+
raise ValueError(
|
238
|
+
f"No service found for route '{route}'.\n"
|
239
|
+
f"Available routes: {', '.join(available)}"
|
240
|
+
)
|
241
|
+
|
242
|
+
# Simulate the request
|
243
|
+
return asyncio.run(simulate_request_to_service(
|
244
|
+
service,
|
245
|
+
method=method,
|
246
|
+
body=body,
|
247
|
+
query_params=query_params,
|
248
|
+
headers=headers
|
249
|
+
))
|
250
|
+
|
251
|
+
|
252
|
+
# Backward compatibility
|
253
|
+
def load_agent_from_file(agent_path: str, agent_class_name: Optional[str] = None, suppress_output: bool = False) -> 'AgentBase':
|
254
|
+
"""
|
255
|
+
Backward compatibility wrapper
|
256
|
+
|
257
|
+
Note: This still uses the direct extraction approach for compatibility
|
258
|
+
"""
|
259
|
+
# Use the new service capture but ensure we get an AgentBase
|
260
|
+
capturer = ServiceCapture()
|
261
|
+
services = capturer.capture(agent_path, suppress_output=suppress_output)
|
262
|
+
|
263
|
+
# Filter to only agents
|
264
|
+
agents = [s for s in services if isinstance(s, AgentBase)]
|
265
|
+
|
266
|
+
if not agents:
|
267
|
+
raise ValueError(f"No agents found in {agent_path}")
|
268
|
+
|
269
|
+
if len(agents) == 1:
|
270
|
+
return agents[0]
|
271
|
+
|
272
|
+
# Multiple agents - try to match by class name
|
273
|
+
if agent_class_name:
|
274
|
+
for agent in agents:
|
275
|
+
if agent.__class__.__name__ == agent_class_name:
|
276
|
+
return agent
|
277
|
+
|
278
|
+
# Return first agent
|
279
|
+
return agents[0]
|
280
|
+
|
281
|
+
|
282
|
+
def discover_agents_in_file(agent_path: str) -> List[Dict[str, Any]]:
|
283
|
+
"""
|
284
|
+
Backward compatibility wrapper
|
285
|
+
"""
|
286
|
+
capturer = ServiceCapture()
|
287
|
+
services = capturer.capture(agent_path)
|
288
|
+
|
289
|
+
# Convert to old format
|
290
|
+
agents_found = []
|
291
|
+
for service in services:
|
292
|
+
if isinstance(service, AgentBase):
|
293
|
+
agents_found.append({
|
294
|
+
'name': service.name,
|
295
|
+
'class_name': service.__class__.__name__,
|
296
|
+
'type': 'instance',
|
297
|
+
'agent_name': service.name,
|
298
|
+
'route': service.route,
|
299
|
+
'description': service.__class__.__doc__,
|
300
|
+
'object': service
|
301
|
+
})
|
302
|
+
|
303
|
+
return agents_found
|
@@ -61,7 +61,58 @@ def display_agent_tools(agent: 'AgentBase', verbose: bool = False) -> None:
|
|
61
61
|
param_desc = param_def.get('description', 'No description')
|
62
62
|
is_required = param_name in required_fields
|
63
63
|
required_marker = " (required)" if is_required else ""
|
64
|
-
|
64
|
+
|
65
|
+
# Build constraint details
|
66
|
+
constraints = []
|
67
|
+
|
68
|
+
# Enum values
|
69
|
+
if 'enum' in param_def:
|
70
|
+
constraints.append(f"options: {', '.join(map(str, param_def['enum']))}")
|
71
|
+
|
72
|
+
# Numeric constraints
|
73
|
+
if 'minimum' in param_def:
|
74
|
+
constraints.append(f"min: {param_def['minimum']}")
|
75
|
+
if 'maximum' in param_def:
|
76
|
+
constraints.append(f"max: {param_def['maximum']}")
|
77
|
+
if 'exclusiveMinimum' in param_def:
|
78
|
+
constraints.append(f"min (exclusive): {param_def['exclusiveMinimum']}")
|
79
|
+
if 'exclusiveMaximum' in param_def:
|
80
|
+
constraints.append(f"max (exclusive): {param_def['exclusiveMaximum']}")
|
81
|
+
if 'multipleOf' in param_def:
|
82
|
+
constraints.append(f"multiple of: {param_def['multipleOf']}")
|
83
|
+
|
84
|
+
# String constraints
|
85
|
+
if 'minLength' in param_def:
|
86
|
+
constraints.append(f"min length: {param_def['minLength']}")
|
87
|
+
if 'maxLength' in param_def:
|
88
|
+
constraints.append(f"max length: {param_def['maxLength']}")
|
89
|
+
if 'pattern' in param_def:
|
90
|
+
constraints.append(f"pattern: {param_def['pattern']}")
|
91
|
+
if 'format' in param_def:
|
92
|
+
constraints.append(f"format: {param_def['format']}")
|
93
|
+
|
94
|
+
# Array constraints
|
95
|
+
if param_type == 'array':
|
96
|
+
if 'minItems' in param_def:
|
97
|
+
constraints.append(f"min items: {param_def['minItems']}")
|
98
|
+
if 'maxItems' in param_def:
|
99
|
+
constraints.append(f"max items: {param_def['maxItems']}")
|
100
|
+
if 'uniqueItems' in param_def and param_def['uniqueItems']:
|
101
|
+
constraints.append("unique items")
|
102
|
+
if 'items' in param_def and 'type' in param_def['items']:
|
103
|
+
constraints.append(f"item type: {param_def['items']['type']}")
|
104
|
+
|
105
|
+
# Default value
|
106
|
+
if 'default' in param_def:
|
107
|
+
constraints.append(f"default: {param_def['default']}")
|
108
|
+
|
109
|
+
# Format the type with constraints
|
110
|
+
if constraints:
|
111
|
+
param_type_full = f"{param_type} [{', '.join(constraints)}]"
|
112
|
+
else:
|
113
|
+
param_type_full = param_type
|
114
|
+
|
115
|
+
print(f" {param_name} ({param_type_full}){required_marker}: {param_desc}")
|
65
116
|
else:
|
66
117
|
print(f" Parameters: None")
|
67
118
|
else:
|
@@ -103,7 +154,58 @@ def display_agent_tools(agent: 'AgentBase', verbose: bool = False) -> None:
|
|
103
154
|
param_desc = param_def.get('description', 'No description')
|
104
155
|
is_required = param_name in required_fields
|
105
156
|
required_marker = " (required)" if is_required else ""
|
106
|
-
|
157
|
+
|
158
|
+
# Build constraint details
|
159
|
+
constraints = []
|
160
|
+
|
161
|
+
# Enum values
|
162
|
+
if 'enum' in param_def:
|
163
|
+
constraints.append(f"options: {', '.join(map(str, param_def['enum']))}")
|
164
|
+
|
165
|
+
# Numeric constraints
|
166
|
+
if 'minimum' in param_def:
|
167
|
+
constraints.append(f"min: {param_def['minimum']}")
|
168
|
+
if 'maximum' in param_def:
|
169
|
+
constraints.append(f"max: {param_def['maximum']}")
|
170
|
+
if 'exclusiveMinimum' in param_def:
|
171
|
+
constraints.append(f"min (exclusive): {param_def['exclusiveMinimum']}")
|
172
|
+
if 'exclusiveMaximum' in param_def:
|
173
|
+
constraints.append(f"max (exclusive): {param_def['exclusiveMaximum']}")
|
174
|
+
if 'multipleOf' in param_def:
|
175
|
+
constraints.append(f"multiple of: {param_def['multipleOf']}")
|
176
|
+
|
177
|
+
# String constraints
|
178
|
+
if 'minLength' in param_def:
|
179
|
+
constraints.append(f"min length: {param_def['minLength']}")
|
180
|
+
if 'maxLength' in param_def:
|
181
|
+
constraints.append(f"max length: {param_def['maxLength']}")
|
182
|
+
if 'pattern' in param_def:
|
183
|
+
constraints.append(f"pattern: {param_def['pattern']}")
|
184
|
+
if 'format' in param_def:
|
185
|
+
constraints.append(f"format: {param_def['format']}")
|
186
|
+
|
187
|
+
# Array constraints
|
188
|
+
if param_type == 'array':
|
189
|
+
if 'minItems' in param_def:
|
190
|
+
constraints.append(f"min items: {param_def['minItems']}")
|
191
|
+
if 'maxItems' in param_def:
|
192
|
+
constraints.append(f"max items: {param_def['maxItems']}")
|
193
|
+
if 'uniqueItems' in param_def and param_def['uniqueItems']:
|
194
|
+
constraints.append("unique items")
|
195
|
+
if 'items' in param_def and 'type' in param_def['items']:
|
196
|
+
constraints.append(f"item type: {param_def['items']['type']}")
|
197
|
+
|
198
|
+
# Default value
|
199
|
+
if 'default' in param_def:
|
200
|
+
constraints.append(f"default: {param_def['default']}")
|
201
|
+
|
202
|
+
# Format the type with constraints
|
203
|
+
if constraints:
|
204
|
+
param_type_full = f"{param_type} [{', '.join(constraints)}]"
|
205
|
+
else:
|
206
|
+
param_type_full = param_type
|
207
|
+
|
208
|
+
print(f" {param_name} ({param_type_full}){required_marker}: {param_desc}")
|
107
209
|
else:
|
108
210
|
print(f" Parameters: None")
|
109
211
|
else:
|
@@ -30,17 +30,22 @@ if TYPE_CHECKING:
|
|
30
30
|
original_print = print
|
31
31
|
|
32
32
|
|
33
|
-
def
|
34
|
-
"""Set up output suppression for
|
33
|
+
def setup_output_suppression():
|
34
|
+
"""Set up output suppression for SWML dumping"""
|
35
35
|
# The central logging system is already configured via environment variable
|
36
36
|
# Just suppress any remaining warnings
|
37
37
|
warnings.filterwarnings("ignore")
|
38
38
|
|
39
|
-
# Capture and suppress print statements
|
39
|
+
# Capture and suppress print statements
|
40
40
|
def suppressed_print(*args, **kwargs):
|
41
|
-
|
41
|
+
# If file is specified (like stderr), allow it
|
42
|
+
if 'file' in kwargs and kwargs['file'] is not sys.stdout:
|
43
|
+
original_print(*args, **kwargs)
|
44
|
+
else:
|
45
|
+
# Suppress stdout prints
|
46
|
+
pass
|
42
47
|
|
43
|
-
# Replace print function globally
|
48
|
+
# Replace print function globally
|
44
49
|
import builtins
|
45
50
|
builtins.print = suppressed_print
|
46
51
|
|
@@ -153,21 +158,16 @@ def handle_dump_swml(agent: 'AgentBase', args: argparse.Namespace) -> int:
|
|
153
158
|
swml_doc = agent._render_swml()
|
154
159
|
|
155
160
|
if args.raw:
|
156
|
-
# Temporarily restore print for JSON output
|
157
|
-
if '--raw' in sys.argv and 'original_print' in globals():
|
158
|
-
import builtins
|
159
|
-
builtins.print = original_print
|
160
|
-
|
161
161
|
# Output only the raw JSON for piping to jq/yq
|
162
|
-
|
162
|
+
original_print(swml_doc)
|
163
163
|
else:
|
164
164
|
# Output formatted JSON (like raw but pretty-printed)
|
165
165
|
try:
|
166
166
|
swml_parsed = json.loads(swml_doc)
|
167
|
-
|
167
|
+
original_print(json.dumps(swml_parsed, indent=2))
|
168
168
|
except json.JSONDecodeError:
|
169
169
|
# If not valid JSON, show raw
|
170
|
-
|
170
|
+
original_print(swml_doc)
|
171
171
|
|
172
172
|
return 0
|
173
173
|
|
@@ -109,6 +109,8 @@ class MockRequest:
|
|
109
109
|
self.query_params = MockQueryParams(query_params)
|
110
110
|
self._json_body = json_body or {}
|
111
111
|
self._body = json.dumps(self._json_body).encode('utf-8')
|
112
|
+
# Add state object for request state (used by FastAPI)
|
113
|
+
self.state = type('State', (), {})()
|
112
114
|
|
113
115
|
async def json(self) -> Dict[str, Any]:
|
114
116
|
"""Return the JSON body"""
|
@@ -0,0 +1,52 @@
|
|
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
|
+
"""
|
12
|
+
Wrapper script for swaig-test that sets environment variables before importing any modules.
|
13
|
+
This allows proper control of logging before the logging system is initialized.
|
14
|
+
"""
|
15
|
+
|
16
|
+
import os
|
17
|
+
import sys
|
18
|
+
import subprocess
|
19
|
+
|
20
|
+
|
21
|
+
def main():
|
22
|
+
"""Main entry point for the swaig-test command"""
|
23
|
+
# Determine if we should show logs based on arguments
|
24
|
+
args = sys.argv[1:]
|
25
|
+
|
26
|
+
# Check for verbose flag
|
27
|
+
show_logs = '--verbose' in args or '-v' in args
|
28
|
+
|
29
|
+
# Special cases that always suppress logs
|
30
|
+
force_suppress = '--dump-swml' in args or '--raw' in args
|
31
|
+
|
32
|
+
# Set logging mode
|
33
|
+
if force_suppress:
|
34
|
+
os.environ['SIGNALWIRE_LOG_MODE'] = 'off'
|
35
|
+
elif show_logs:
|
36
|
+
os.environ['SIGNALWIRE_LOG_MODE'] = 'default'
|
37
|
+
else:
|
38
|
+
# Default: suppress logs unless verbose is requested
|
39
|
+
os.environ['SIGNALWIRE_LOG_MODE'] = 'off'
|
40
|
+
|
41
|
+
# Execute the actual implementation
|
42
|
+
# Use sys.executable to ensure we use the same Python interpreter
|
43
|
+
# Add -W ignore::RuntimeWarning to suppress the sys.modules warning
|
44
|
+
cmd = [sys.executable, '-W', 'ignore::RuntimeWarning', '-m', 'signalwire_agents.cli.test_swaig'] + args
|
45
|
+
|
46
|
+
# Run the command and exit with its exit code
|
47
|
+
result = subprocess.run(cmd)
|
48
|
+
sys.exit(result.returncode)
|
49
|
+
|
50
|
+
|
51
|
+
if __name__ == '__main__':
|
52
|
+
main()
|
@@ -34,7 +34,7 @@ from .config import (
|
|
34
34
|
HELP_DESCRIPTION, HELP_EPILOG_SHORT
|
35
35
|
)
|
36
36
|
from .core.argparse_helpers import CustomArgumentParser, parse_function_arguments
|
37
|
-
from .core.agent_loader import discover_agents_in_file, load_agent_from_file
|
37
|
+
from .core.agent_loader import discover_agents_in_file, load_agent_from_file, load_service_from_file
|
38
38
|
from .core.dynamic_config import apply_dynamic_config
|
39
39
|
from .simulation.mock_env import ServerlessSimulator, create_mock_request, load_env_file
|
40
40
|
from .simulation.data_generation import (
|
@@ -44,7 +44,7 @@ from .simulation.data_generation import (
|
|
44
44
|
from .simulation.data_overrides import apply_overrides, apply_convenience_mappings
|
45
45
|
from .execution.datamap_exec import execute_datamap_function
|
46
46
|
from .execution.webhook_exec import execute_external_webhook_function
|
47
|
-
from .output.swml_dump import handle_dump_swml,
|
47
|
+
from .output.swml_dump import handle_dump_swml, setup_output_suppression
|
48
48
|
from .output.output_formatter import display_agent_tools, format_result
|
49
49
|
|
50
50
|
|
@@ -221,9 +221,9 @@ swaig-test agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml
|
|
221
221
|
|
222
222
|
def main():
|
223
223
|
"""Main entry point for the CLI tool"""
|
224
|
-
#
|
225
|
-
if "--
|
226
|
-
|
224
|
+
# Set up suppression early if we're dumping SWML
|
225
|
+
if "--dump-swml" in sys.argv:
|
226
|
+
setup_output_suppression()
|
227
227
|
|
228
228
|
# Check for help sections early
|
229
229
|
if "--help-platforms" in sys.argv:
|
@@ -284,6 +284,10 @@ def main():
|
|
284
284
|
"--agent-class",
|
285
285
|
help="Specify agent class (required if file has multiple agents)"
|
286
286
|
)
|
287
|
+
common.add_argument(
|
288
|
+
"--route",
|
289
|
+
help="Specify service by route (e.g., /healthcare, /finance)"
|
290
|
+
)
|
287
291
|
|
288
292
|
# Actions (choose one)
|
289
293
|
actions = parser.add_argument_group('actions (choose one)')
|
@@ -444,6 +448,9 @@ def main():
|
|
444
448
|
args.tool_name = None
|
445
449
|
|
446
450
|
# Validate arguments
|
451
|
+
if args.route and args.agent_class:
|
452
|
+
parser.error("Cannot specify both --route and --agent-class. Choose one.")
|
453
|
+
|
447
454
|
if not args.list_tools and not args.dump_swml and not args.list_agents:
|
448
455
|
if not args.tool_name:
|
449
456
|
# If no tool_name and no special flags, default to listing tools
|
@@ -556,7 +563,13 @@ def main():
|
|
556
563
|
|
557
564
|
# Load the agent
|
558
565
|
try:
|
559
|
-
|
566
|
+
# Determine which identifier to use
|
567
|
+
service_identifier = args.route if args.route else args.agent_class
|
568
|
+
prefer_route = bool(args.route)
|
569
|
+
|
570
|
+
# Use load_service_from_file which handles both routes and class names
|
571
|
+
from signalwire_agents.cli.core.agent_loader import load_service_from_file
|
572
|
+
agent = load_service_from_file(args.agent_path, service_identifier, prefer_route)
|
560
573
|
except ValueError as e:
|
561
574
|
error_msg = str(e)
|
562
575
|
if "Multiple agent classes found" in error_msg and args.list_tools and not args.agent_class:
|
@@ -615,6 +628,7 @@ def main():
|
|
615
628
|
print("\n" + "=" * 60)
|
616
629
|
print(f"\nTo use a specific agent, run:")
|
617
630
|
print(f" swaig-test {args.agent_path} --agent-class <AgentClassName>")
|
631
|
+
print(f" swaig-test {args.agent_path} --route <route>")
|
618
632
|
return 0
|
619
633
|
except Exception as discover_error:
|
620
634
|
print(f"Error discovering agents: {discover_error}")
|