signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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 +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
Custom argument parsing and function argument parsing
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
import argparse
|
|
17
|
+
from typing import List, Dict, Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CustomArgumentParser(argparse.ArgumentParser):
|
|
21
|
+
"""Custom ArgumentParser with better error handling"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self._suppress_usage = False
|
|
26
|
+
|
|
27
|
+
def _print_message(self, message, file=None):
|
|
28
|
+
"""Override to suppress usage output for specific errors"""
|
|
29
|
+
if self._suppress_usage:
|
|
30
|
+
return
|
|
31
|
+
super()._print_message(message, file)
|
|
32
|
+
|
|
33
|
+
def error(self, message):
|
|
34
|
+
"""Override error method to provide user-friendly error messages"""
|
|
35
|
+
if "required" in message.lower() and "agent_path" in message:
|
|
36
|
+
self._suppress_usage = True
|
|
37
|
+
print("Error: Missing required argument.")
|
|
38
|
+
print()
|
|
39
|
+
print(f"Usage: {self.prog} <agent_path> [options]")
|
|
40
|
+
print()
|
|
41
|
+
print("Examples:")
|
|
42
|
+
print(f" {self.prog} examples/my_agent.py --list-tools")
|
|
43
|
+
print(f" {self.prog} examples/my_agent.py --dump-swml")
|
|
44
|
+
print(f" {self.prog} examples/my_agent.py --exec my_function --param value")
|
|
45
|
+
print()
|
|
46
|
+
print(f"For full help: {self.prog} --help")
|
|
47
|
+
sys.exit(2)
|
|
48
|
+
else:
|
|
49
|
+
# For other errors, use the default behavior
|
|
50
|
+
super().error(message)
|
|
51
|
+
|
|
52
|
+
def print_usage(self, file=None):
|
|
53
|
+
"""Override print_usage to suppress output when we want custom error handling"""
|
|
54
|
+
if self._suppress_usage:
|
|
55
|
+
return
|
|
56
|
+
super().print_usage(file)
|
|
57
|
+
|
|
58
|
+
def parse_args(self, args=None, namespace=None):
|
|
59
|
+
"""Override parse_args to provide custom error handling for missing arguments"""
|
|
60
|
+
# Check if no arguments provided (just the program name)
|
|
61
|
+
if args is None:
|
|
62
|
+
args = sys.argv[1:]
|
|
63
|
+
|
|
64
|
+
# If no arguments provided, show custom error
|
|
65
|
+
if not args:
|
|
66
|
+
print("Error: Missing required argument.")
|
|
67
|
+
print()
|
|
68
|
+
print(f"Usage: {self.prog} <agent_path> [options]")
|
|
69
|
+
print()
|
|
70
|
+
print("Examples:")
|
|
71
|
+
print(f" {self.prog} examples/my_agent.py --list-tools")
|
|
72
|
+
print(f" {self.prog} examples/my_agent.py --dump-swml")
|
|
73
|
+
print(f" {self.prog} examples/my_agent.py --exec my_function --param value")
|
|
74
|
+
print()
|
|
75
|
+
print(f"For full help: {self.prog} --help")
|
|
76
|
+
sys.exit(2)
|
|
77
|
+
|
|
78
|
+
# Otherwise, use default parsing
|
|
79
|
+
return super().parse_args(args, namespace)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_function_arguments(function_args_list: List[str], func_schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
83
|
+
"""
|
|
84
|
+
Parse function arguments from command line with type coercion based on schema
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
function_args_list: List of command line arguments after --args
|
|
88
|
+
func_schema: Function schema with parameter definitions
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary of parsed function arguments
|
|
92
|
+
"""
|
|
93
|
+
parsed_args = {}
|
|
94
|
+
i = 0
|
|
95
|
+
|
|
96
|
+
# Get parameter schema
|
|
97
|
+
parameters = {}
|
|
98
|
+
required_params = []
|
|
99
|
+
|
|
100
|
+
if isinstance(func_schema, dict):
|
|
101
|
+
# DataMap function
|
|
102
|
+
if 'parameters' in func_schema:
|
|
103
|
+
params = func_schema['parameters']
|
|
104
|
+
if 'properties' in params:
|
|
105
|
+
parameters = params['properties']
|
|
106
|
+
required_params = params.get('required', [])
|
|
107
|
+
else:
|
|
108
|
+
parameters = params
|
|
109
|
+
else:
|
|
110
|
+
parameters = func_schema
|
|
111
|
+
else:
|
|
112
|
+
# Regular SWAIG function
|
|
113
|
+
if hasattr(func_schema, 'parameters') and func_schema.parameters:
|
|
114
|
+
params = func_schema.parameters
|
|
115
|
+
if 'properties' in params:
|
|
116
|
+
parameters = params['properties']
|
|
117
|
+
required_params = params.get('required', [])
|
|
118
|
+
else:
|
|
119
|
+
parameters = params
|
|
120
|
+
|
|
121
|
+
# Parse arguments
|
|
122
|
+
while i < len(function_args_list):
|
|
123
|
+
arg = function_args_list[i]
|
|
124
|
+
|
|
125
|
+
if arg.startswith('--'):
|
|
126
|
+
param_name = arg[2:] # Remove --
|
|
127
|
+
|
|
128
|
+
# Convert kebab-case to snake_case for parameter lookup
|
|
129
|
+
param_key = param_name.replace('-', '_')
|
|
130
|
+
|
|
131
|
+
# Check if this parameter exists in schema
|
|
132
|
+
param_schema = parameters.get(param_key, {})
|
|
133
|
+
param_type = param_schema.get('type', 'string')
|
|
134
|
+
|
|
135
|
+
if param_type == 'boolean':
|
|
136
|
+
# Check if next arg is a boolean value or if this is a flag
|
|
137
|
+
if i + 1 < len(function_args_list) and function_args_list[i + 1].lower() in ['true', 'false']:
|
|
138
|
+
parsed_args[param_key] = function_args_list[i + 1].lower() == 'true'
|
|
139
|
+
i += 2
|
|
140
|
+
else:
|
|
141
|
+
# Treat as flag (present = true)
|
|
142
|
+
parsed_args[param_key] = True
|
|
143
|
+
i += 1
|
|
144
|
+
else:
|
|
145
|
+
# Need a value
|
|
146
|
+
if i + 1 >= len(function_args_list):
|
|
147
|
+
# Check if this looks like a CLI flag that was misplaced
|
|
148
|
+
if param_name in ['verbose', 'raw', 'help', 'list-tools', 'list-agents', 'dump-swml',
|
|
149
|
+
'minimal', 'fake-full-data', 'simulate-serverless', 'agent-class', 'route']:
|
|
150
|
+
raise ValueError(f"CLI flag --{param_name} must come BEFORE --exec, not after.\n"
|
|
151
|
+
f"Example: swaig-test file.py --{param_name} --exec function_name")
|
|
152
|
+
else:
|
|
153
|
+
raise ValueError(f"Parameter --{param_name} requires a value")
|
|
154
|
+
|
|
155
|
+
value = function_args_list[i + 1]
|
|
156
|
+
|
|
157
|
+
# Type coercion
|
|
158
|
+
if param_type == 'integer':
|
|
159
|
+
try:
|
|
160
|
+
parsed_args[param_key] = int(value)
|
|
161
|
+
except ValueError:
|
|
162
|
+
raise ValueError(f"Parameter --{param_name} must be an integer, got: {value}")
|
|
163
|
+
elif param_type == 'number':
|
|
164
|
+
try:
|
|
165
|
+
parsed_args[param_key] = float(value)
|
|
166
|
+
except ValueError:
|
|
167
|
+
raise ValueError(f"Parameter --{param_name} must be a number, got: {value}")
|
|
168
|
+
elif param_type == 'array':
|
|
169
|
+
# Handle comma-separated arrays
|
|
170
|
+
parsed_args[param_key] = [item.strip() for item in value.split(',')]
|
|
171
|
+
else:
|
|
172
|
+
# String (default)
|
|
173
|
+
parsed_args[param_key] = value
|
|
174
|
+
|
|
175
|
+
i += 2
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError(f"Expected parameter name starting with --, got: {arg}")
|
|
178
|
+
|
|
179
|
+
return parsed_args
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
Apply dynamic configuration to agents
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Optional, TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from signalwire_agents.core.agent_base import AgentBase
|
|
19
|
+
from ..simulation.mock_env import MockRequest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def apply_dynamic_config(agent: 'AgentBase', mock_request: Optional['MockRequest'] = None, verbose: bool = False) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Apply dynamic configuration callback if the agent has one
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
agent: The agent instance
|
|
28
|
+
mock_request: Optional mock request object
|
|
29
|
+
verbose: Whether to print verbose output
|
|
30
|
+
"""
|
|
31
|
+
# Check if dynamic config has already been applied to this agent
|
|
32
|
+
if hasattr(agent, '_dynamic_config_applied') and agent._dynamic_config_applied:
|
|
33
|
+
if verbose:
|
|
34
|
+
print("Dynamic configuration already applied, skipping...")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# Check if agent has dynamic config callback
|
|
38
|
+
if hasattr(agent, '_dynamic_config_callback') and agent._dynamic_config_callback:
|
|
39
|
+
try:
|
|
40
|
+
# Create mock request data if not provided
|
|
41
|
+
if mock_request is None:
|
|
42
|
+
from ..simulation.mock_env import create_mock_request
|
|
43
|
+
mock_request = create_mock_request()
|
|
44
|
+
|
|
45
|
+
# Extract request data
|
|
46
|
+
query_params = dict(mock_request.query_params)
|
|
47
|
+
body_params = {} # Empty for GET requests
|
|
48
|
+
headers = dict(mock_request.headers)
|
|
49
|
+
|
|
50
|
+
if verbose:
|
|
51
|
+
print("Applying dynamic configuration callback...")
|
|
52
|
+
|
|
53
|
+
# Call the user's configuration callback directly with the agent
|
|
54
|
+
# This is what pc_builder_service.py expects - to get the agent itself
|
|
55
|
+
agent._dynamic_config_callback(query_params, body_params, headers, agent)
|
|
56
|
+
|
|
57
|
+
# Mark that dynamic config has been applied to prevent duplicate application
|
|
58
|
+
agent._dynamic_config_applied = True
|
|
59
|
+
|
|
60
|
+
if verbose:
|
|
61
|
+
print("Dynamic configuration callback applied successfully")
|
|
62
|
+
# Show loaded skills after dynamic config
|
|
63
|
+
skills = agent.list_skills()
|
|
64
|
+
if skills:
|
|
65
|
+
print(f"Skills loaded by dynamic config: {', '.join(skills)}")
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
if verbose:
|
|
69
|
+
print(f"Warning: Failed to apply dynamic configuration: {e}")
|
|
70
|
+
import traceback
|
|
71
|
+
traceback.print_exc()
|
|
@@ -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
|