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.
Files changed (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {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