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,80 @@
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
+ Configuration constants for the CLI tools
13
+ """
14
+
15
+ # Default values for call configuration
16
+ DEFAULT_CALL_TYPE = "webrtc"
17
+ DEFAULT_CALL_DIRECTION = "inbound"
18
+ DEFAULT_CALL_STATE = "created"
19
+ DEFAULT_HTTP_METHOD = "POST"
20
+ DEFAULT_TOKEN_EXPIRY = 3600
21
+
22
+ # Default fake data values
23
+ DEFAULT_PROJECT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
24
+ DEFAULT_SPACE_ID = "zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv"
25
+ DEFAULT_SPACE_NAME = "example-space"
26
+ DEFAULT_ENVIRONMENT = "production"
27
+
28
+ # Request timeouts
29
+ HTTP_REQUEST_TIMEOUT = 30
30
+
31
+ # Output formatting
32
+ RESULT_LINE_SEP = "-" * 60
33
+
34
+ # Platform-specific constants
35
+ SERVERLESS_PLATFORMS = ["lambda", "cgi", "cloud_function", "azure_function"]
36
+
37
+ # AWS Lambda defaults
38
+ AWS_DEFAULT_REGION = "us-east-1"
39
+ AWS_DEFAULT_STAGE = "prod"
40
+
41
+ # Google Cloud defaults
42
+ GCP_DEFAULT_REGION = "us-central1"
43
+
44
+ # Error messages
45
+ ERROR_MISSING_AGENT = "Error: Missing required argument."
46
+ ERROR_MULTIPLE_AGENTS = "Multiple agents found in file. Please specify --agent-class"
47
+ ERROR_NO_AGENTS = "No agents found in file: {file_path}"
48
+ ERROR_AGENT_NOT_FOUND = "Agent class '{class_name}' not found in file: {file_path}"
49
+ ERROR_FUNCTION_NOT_FOUND = "Function '{function_name}' not found in agent"
50
+ ERROR_CGI_HOST_REQUIRED = "CGI simulation requires --cgi-host"
51
+
52
+ # Help messages
53
+ HELP_DESCRIPTION = """Test SWAIG functions and generate SWML documents for SignalWire AI agents
54
+
55
+ IMPORTANT: When using --exec, ALL options (like --call-id, --verbose, etc.) must come BEFORE --exec.
56
+ Everything after --exec <function_name> is treated as arguments to the function."""
57
+
58
+ HELP_EPILOG_SHORT = """
59
+ examples:
60
+ # Execute a function
61
+ %(prog)s agent.py --exec search --query "test" --limit 5
62
+
63
+ # Execute with persistent session (--call-id MUST come BEFORE --exec)
64
+ %(prog)s agent.py --call-id my-session --exec add_todo --text "Buy milk"
65
+
66
+ # WRONG: This won't work! --call-id is treated as a function argument
67
+ %(prog)s agent.py --exec add_todo --text "Buy milk" --call-id my-session
68
+
69
+ # Generate SWML
70
+ %(prog)s agent.py --dump-swml --raw | jq '.'
71
+
72
+ # Test with specific agent
73
+ %(prog)s multi_agent.py --agent-class MattiAgent --list-tools
74
+
75
+ # Simulate serverless
76
+ %(prog)s agent.py --simulate-serverless lambda --exec my_function
77
+
78
+ For platform-specific options: %(prog)s --help-platforms
79
+ For more examples: %(prog)s --help-examples
80
+ """
@@ -0,0 +1,10 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ """Core functionality for CLI tools"""
@@ -0,0 +1,470 @@
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
+ Agent discovery and loading functionality
13
+ """
14
+
15
+ import importlib.util
16
+ from pathlib import Path
17
+ from typing import List, Dict, Any, Optional
18
+
19
+ # Import after checking if available
20
+ try:
21
+ from signalwire_agents.core.agent_base import AgentBase
22
+ from signalwire_agents.core.swml_service import SWMLService
23
+ # Import the new service loader
24
+ from .service_loader import ServiceCapture, load_agent_from_file as new_load_agent
25
+ AGENT_BASE_AVAILABLE = True
26
+ SWML_SERVICE_AVAILABLE = True
27
+ NEW_LOADER_AVAILABLE = True
28
+ except ImportError:
29
+ AgentBase = None
30
+ SWMLService = None
31
+ ServiceCapture = None
32
+ new_load_agent = None
33
+ AGENT_BASE_AVAILABLE = False
34
+ SWML_SERVICE_AVAILABLE = False
35
+ NEW_LOADER_AVAILABLE = False
36
+
37
+
38
+ def discover_services_in_file(service_path: str) -> List[Dict[str, Any]]:
39
+ """
40
+ Discover all available SWML services (including agents) in a Python file without instantiating them
41
+
42
+ Args:
43
+ service_path: Path to the Python file containing services
44
+
45
+ Returns:
46
+ List of dictionaries with service information
47
+
48
+ Raises:
49
+ ImportError: If the file cannot be imported
50
+ FileNotFoundError: If the file doesn't exist
51
+ """
52
+ if not SWML_SERVICE_AVAILABLE:
53
+ raise ImportError("SWMLService not available. Please install signalwire-agents package.")
54
+
55
+ # Keep backward compatibility
56
+ return _discover_services_impl(service_path)
57
+
58
+
59
+ def discover_agents_in_file(agent_path: str) -> List[Dict[str, Any]]:
60
+ """
61
+ Backward compatibility wrapper - discovers agents in a file
62
+
63
+ Args:
64
+ agent_path: Path to the Python file containing agents
65
+
66
+ Returns:
67
+ List of dictionaries with agent information
68
+ """
69
+ # Filter to only return AgentBase instances/classes
70
+ all_services = discover_services_in_file(agent_path)
71
+ return [s for s in all_services if s.get('is_agent', False)]
72
+
73
+
74
+ def _discover_services_impl(service_path: str) -> List[Dict[str, Any]]:
75
+ """
76
+ Internal implementation for discovering services
77
+ """
78
+ service_path = Path(service_path).resolve()
79
+
80
+ if not service_path.exists():
81
+ raise FileNotFoundError(f"Service file not found: {service_path}")
82
+
83
+ if not service_path.suffix == '.py':
84
+ raise ValueError(f"Service file must be a Python file (.py): {service_path}")
85
+
86
+ # Add the module's directory to sys.path temporarily to allow local imports
87
+ import sys
88
+ module_dir = str(service_path.parent)
89
+ sys_path_added = False
90
+ if module_dir not in sys.path:
91
+ sys.path.insert(0, module_dir)
92
+ sys_path_added = True
93
+
94
+ # Load the module, but prevent main() execution by setting __name__ to something other than "__main__"
95
+ spec = importlib.util.spec_from_file_location("service_module", service_path)
96
+ module = importlib.util.module_from_spec(spec)
97
+
98
+ try:
99
+ # Set __name__ to prevent if __name__ == "__main__": blocks from running
100
+ module.__name__ = "service_module"
101
+ spec.loader.exec_module(module)
102
+ except Exception as e:
103
+ # Clean up sys.path if we added to it
104
+ if sys_path_added:
105
+ sys.path.remove(module_dir)
106
+ raise ImportError(f"Failed to load service module: {e}")
107
+ finally:
108
+ # Clean up sys.path after successful load too
109
+ if sys_path_added and module_dir in sys.path:
110
+ sys.path.remove(module_dir)
111
+
112
+ services_found = []
113
+
114
+ # Look for SWMLService instances (including AgentBase which inherits from it)
115
+ for name, obj in vars(module).items():
116
+ if isinstance(obj, SWMLService):
117
+ is_agent = isinstance(obj, AgentBase) if AgentBase else False
118
+ services_found.append({
119
+ 'name': name,
120
+ 'class_name': obj.__class__.__name__,
121
+ 'type': 'instance',
122
+ 'service_name': getattr(obj, 'name', 'Unknown'),
123
+ 'route': getattr(obj, 'route', 'Unknown'),
124
+ 'description': obj.__class__.__doc__,
125
+ 'object': obj,
126
+ 'is_agent': is_agent,
127
+ 'has_tools': is_agent and hasattr(obj, '_tool_registry')
128
+ })
129
+
130
+ # Look for SWMLService subclasses (that could be instantiated)
131
+ for name, obj in vars(module).items():
132
+ if (isinstance(obj, type) and
133
+ issubclass(obj, SWMLService) and
134
+ obj not in (SWMLService, AgentBase)): # Don't include base classes
135
+ # Check if we already found an instance of this class
136
+ instance_found = any(service['class_name'] == name for service in services_found)
137
+ if not instance_found:
138
+ is_agent = AgentBase and issubclass(obj, AgentBase)
139
+ try:
140
+ # Try to get class information without instantiating
141
+ service_info = {
142
+ 'name': name,
143
+ 'class_name': name,
144
+ 'type': 'class',
145
+ 'service_name': 'Unknown (not instantiated)',
146
+ 'route': 'Unknown (not instantiated)',
147
+ 'description': obj.__doc__,
148
+ 'object': obj,
149
+ 'is_agent': is_agent,
150
+ 'has_tools': False # Can't determine without instantiation
151
+ }
152
+ services_found.append(service_info)
153
+ except Exception:
154
+ # If we can't get info, still record that the class exists
155
+ services_found.append({
156
+ 'name': name,
157
+ 'class_name': name,
158
+ 'type': 'class',
159
+ 'service_name': 'Unknown (not instantiated)',
160
+ 'route': 'Unknown (not instantiated)',
161
+ 'description': obj.__doc__ or 'No description available',
162
+ 'object': obj,
163
+ 'is_agent': is_agent,
164
+ 'has_tools': False
165
+ })
166
+
167
+ return services_found
168
+
169
+
170
+ def load_service_from_file(service_path: str, service_identifier: Optional[str] = None, prefer_route: bool = True) -> 'SWMLService':
171
+ """
172
+ Load a SWML service from a Python file
173
+
174
+ Args:
175
+ service_path: Path to the Python file containing the service
176
+ service_identifier: Optional service identifier - can be class name or route
177
+ prefer_route: If True, interpret identifier as route first, then class name
178
+
179
+ Returns:
180
+ SWMLService instance (could be AgentBase or basic SWMLService)
181
+
182
+ Raises:
183
+ ImportError: If the file cannot be imported
184
+ ValueError: If no service is found in the file
185
+ """
186
+ if not SWML_SERVICE_AVAILABLE:
187
+ raise ImportError("SWMLService not available. Please install signalwire-agents package.")
188
+
189
+ # Use the main implementation
190
+ return _load_service_impl(service_path, service_identifier, prefer_route)
191
+
192
+
193
+ def load_agent_from_file(agent_path: str, agent_class_name: Optional[str] = None) -> 'AgentBase':
194
+ """
195
+ Load an agent from a Python file
196
+
197
+ Args:
198
+ agent_path: Path to the Python file containing the agent
199
+ agent_class_name: Optional name of the agent class to instantiate
200
+
201
+ Returns:
202
+ AgentBase instance
203
+
204
+ Raises:
205
+ ImportError: If the file cannot be imported
206
+ ValueError: If no agent is found in the file
207
+ """
208
+ if not AGENT_BASE_AVAILABLE:
209
+ raise ImportError("AgentBase not available. Please install signalwire-agents package.")
210
+
211
+ # Check if we're being called from swaig-test --dump-swml (or similar)
212
+ # We can detect this by checking the call stack or environment
213
+ import inspect
214
+ suppress_output = False
215
+
216
+ # Check if we're in a context where output should be suppressed
217
+ frame = inspect.currentframe()
218
+ try:
219
+ # Walk up the call stack to see if we're being called from dump_swml
220
+ while frame:
221
+ if 'dump_swml' in frame.f_code.co_filename or 'swml_dump' in frame.f_code.co_filename:
222
+ suppress_output = True
223
+ break
224
+ frame = frame.f_back
225
+ finally:
226
+ del frame # Avoid reference cycles
227
+
228
+ # Use the old implementation but with a fix for the ordering
229
+ return _load_service_impl(agent_path, agent_class_name, prefer_route=False)
230
+
231
+
232
+ def _load_service_impl(service_path: str, service_identifier: Optional[str] = None, prefer_route: bool = True) -> 'SWMLService':
233
+ """
234
+ Internal implementation for loading services with smart detection
235
+ """
236
+ service_path = Path(service_path).resolve()
237
+
238
+ if not service_path.exists():
239
+ raise FileNotFoundError(f"Service file not found: {service_path}")
240
+
241
+ if not service_path.suffix == '.py':
242
+ raise ValueError(f"Service file must be a Python file (.py): {service_path}")
243
+
244
+ # Add the module's directory to sys.path temporarily to allow local imports
245
+ import sys
246
+ module_dir = str(service_path.parent)
247
+ sys_path_added = False
248
+ if module_dir not in sys.path:
249
+ sys.path.insert(0, module_dir)
250
+ sys_path_added = True
251
+
252
+ # Load the module, but prevent main() execution by setting __name__ to something other than "__main__"
253
+ spec = importlib.util.spec_from_file_location("service_module", service_path)
254
+ module = importlib.util.module_from_spec(spec)
255
+
256
+ try:
257
+ # Set __name__ to prevent if __name__ == "__main__": blocks from running
258
+ module.__name__ = "service_module"
259
+ spec.loader.exec_module(module)
260
+ except Exception as e:
261
+ # Clean up sys.path if we added to it
262
+ if sys_path_added:
263
+ sys.path.remove(module_dir)
264
+ raise ImportError(f"Failed to load service module: {e}")
265
+ finally:
266
+ # Clean up sys.path after successful load too
267
+ if sys_path_added and module_dir in sys.path:
268
+ sys.path.remove(module_dir)
269
+
270
+ # Find the service instance
271
+ service = None
272
+
273
+ # If service_identifier is specified and prefer_route is True, try to find by route first
274
+ if service_identifier and prefer_route:
275
+ # First, try to find an existing instance with matching route
276
+ for name, obj in vars(module).items():
277
+ if isinstance(obj, SWMLService) and hasattr(obj, 'route'):
278
+ if obj.route == service_identifier:
279
+ service = obj
280
+ break
281
+
282
+ # If not found, try instantiating classes to check their routes
283
+ if service is None:
284
+ for name, obj in vars(module).items():
285
+ if (isinstance(obj, type) and
286
+ issubclass(obj, SWMLService) and
287
+ obj not in (SWMLService, AgentBase)):
288
+ try:
289
+ temp_instance = obj()
290
+ if hasattr(temp_instance, 'route') and temp_instance.route == service_identifier:
291
+ service = temp_instance
292
+ break
293
+ except Exception:
294
+ # Skip classes that can't be instantiated
295
+ pass
296
+
297
+ # If still not found and service_identifier looks like a class name, fall back to class name
298
+ if service is None and hasattr(module, service_identifier):
299
+ obj = getattr(module, service_identifier)
300
+ if isinstance(obj, type) and issubclass(obj, SWMLService):
301
+ try:
302
+ service = obj()
303
+ except Exception as e:
304
+ raise ValueError(f"No service found with route '{service_identifier}' and failed to instantiate class '{service_identifier}': {e}")
305
+ elif isinstance(obj, SWMLService):
306
+ service = obj
307
+
308
+ if service is None:
309
+ raise ValueError(f"No service found with route '{service_identifier}'")
310
+
311
+ # If service_identifier is specified as a class name, try to instantiate that specific class first
312
+ elif service_identifier and not prefer_route:
313
+ if hasattr(module, service_identifier):
314
+ obj = getattr(module, service_identifier)
315
+ if isinstance(obj, type) and issubclass(obj, SWMLService):
316
+ try:
317
+ service = obj()
318
+ if service and not service.route.endswith('dummy'): # Avoid test services with dummy routes
319
+ pass # Successfully created specific service
320
+ else:
321
+ service = obj() # Create anyway if requested specifically
322
+ except Exception as e:
323
+ raise ValueError(f"Failed to instantiate service class '{service_identifier}': {e}")
324
+ elif isinstance(obj, SWMLService):
325
+ # It's already an instance
326
+ service = obj
327
+ else:
328
+ raise ValueError(f"'{service_identifier}' is not a valid SWMLService class or instance")
329
+ else:
330
+ raise ValueError(f"Service class '{service_identifier}' not found in {service_path}")
331
+
332
+ # Strategy 1: Look for 'agent' or 'service' variable (most common pattern)
333
+ if service is None:
334
+ if hasattr(module, 'agent') and isinstance(module.agent, SWMLService):
335
+ service = module.agent
336
+ elif hasattr(module, 'service') and isinstance(module.service, SWMLService):
337
+ service = module.service
338
+
339
+ # Strategy 2: Look for any SWMLService instance in module globals
340
+ if service is None:
341
+ services_found = []
342
+ for name, obj in vars(module).items():
343
+ if isinstance(obj, SWMLService):
344
+ services_found.append((name, obj))
345
+
346
+ if len(services_found) == 1:
347
+ service = services_found[0][1]
348
+ elif len(services_found) > 1:
349
+ # Multiple services found, prefer one named 'agent' or 'service'
350
+ for name, obj in services_found:
351
+ if name in ('agent', 'service'):
352
+ service = obj
353
+ break
354
+ # If no preferred variable, use the first one
355
+ if service is None:
356
+ service = services_found[0][1]
357
+ print(f"Warning: Multiple services found, using '{services_found[0][0]}'")
358
+ print(f"Hint: Use --route or service identifier to choose specific service")
359
+
360
+ # Strategy 3: Look for SWMLService subclass and try to instantiate it
361
+ if service is None and not hasattr(module, 'main'):
362
+ service_classes_found = []
363
+ for name, obj in vars(module).items():
364
+ if (isinstance(obj, type) and
365
+ issubclass(obj, SWMLService) and
366
+ obj not in (SWMLService, AgentBase)):
367
+ service_classes_found.append((name, obj))
368
+
369
+ if len(service_classes_found) == 1:
370
+ try:
371
+ service = service_classes_found[0][1]()
372
+ except Exception as e:
373
+ print(f"Warning: Failed to instantiate {service_classes_found[0][0]}: {e}")
374
+ elif len(service_classes_found) > 1:
375
+ # Multiple service classes found - get detailed info
376
+ service_info = []
377
+ for name, cls in service_classes_found:
378
+ try:
379
+ # Try to instantiate temporarily to get route
380
+ temp_instance = cls()
381
+ route = getattr(temp_instance, 'route', 'Unknown')
382
+ service_name = getattr(temp_instance, 'name', 'Unknown')
383
+ service_info.append({
384
+ 'class_name': name,
385
+ 'route': route,
386
+ 'service_name': service_name
387
+ })
388
+ except Exception:
389
+ # If instantiation fails, still include the class
390
+ service_info.append({
391
+ 'class_name': name,
392
+ 'route': 'Unknown (instantiation failed)',
393
+ 'service_name': 'Unknown'
394
+ })
395
+
396
+ # Format error message with service details
397
+ error_lines = ["Multiple service classes found:"]
398
+ error_lines.append("")
399
+ for info in service_info:
400
+ error_lines.append(f" {info['class_name']}:")
401
+ error_lines.append(f" Route: {info['route']}")
402
+ error_lines.append(f" Name: {info['service_name']}")
403
+ error_lines.append("")
404
+ error_lines.append("Please specify which service to use:")
405
+ error_lines.append(f" swaig-test {service_path} --agent-class <ClassName>")
406
+ error_lines.append(f" swaig-test {service_path} --route <route>")
407
+
408
+ raise ValueError("\n".join(error_lines))
409
+ else:
410
+ # Try instantiating any SWMLService class we can find
411
+ for name, obj in vars(module).items():
412
+ if (isinstance(obj, type) and
413
+ issubclass(obj, SWMLService) and
414
+ obj not in (SWMLService, AgentBase)):
415
+ try:
416
+ service = obj()
417
+ break
418
+ except Exception as e:
419
+ print(f"Warning: Failed to instantiate {name}: {e}")
420
+
421
+ # Strategy 4: Try calling a modified main() function that doesn't start the server
422
+ if service is None and hasattr(module, 'main'):
423
+ print("Warning: No agent instance found, attempting to call main() without server startup")
424
+ try:
425
+ # Temporarily patch serve() and run() on both SWMLService and AgentBase
426
+ captured_services = []
427
+ patches_applied = []
428
+
429
+ def mock_serve(self, *args, **kwargs):
430
+ captured_services.append(self)
431
+ print(f" (Intercepted serve() call, service captured for testing)")
432
+ return self
433
+
434
+ def mock_run(self, *args, **kwargs):
435
+ captured_services.append(self)
436
+ print(f" (Intercepted run() call, service captured for testing)")
437
+ return self
438
+
439
+ # Apply patches to both base classes
440
+ for base_class in [SWMLService, AgentBase]:
441
+ if base_class:
442
+ if hasattr(base_class, 'serve'):
443
+ patches_applied.append((base_class, 'serve', base_class.serve))
444
+ base_class.serve = mock_serve
445
+ if hasattr(base_class, 'run'):
446
+ patches_applied.append((base_class, 'run', base_class.run))
447
+ base_class.run = mock_run
448
+
449
+ try:
450
+ result = module.main()
451
+ if isinstance(result, SWMLService):
452
+ service = result
453
+ elif captured_services:
454
+ # Use the last captured service (most likely the configured one)
455
+ service = captured_services[-1]
456
+ finally:
457
+ # Restore original methods
458
+ for base_class, method_name, original_method in patches_applied:
459
+ setattr(base_class, method_name, original_method)
460
+
461
+ except Exception as e:
462
+ print(f"Warning: Failed to call main() function: {e}")
463
+
464
+ if service is None:
465
+ raise ValueError(f"No service found in {service_path}. The file must contain either:\n"
466
+ f"- A SWMLService instance (e.g., agent = MyAgent() or service = MyService())\n"
467
+ f"- A SWMLService subclass that can be instantiated\n"
468
+ f"- A main() function that creates and returns a service")
469
+
470
+ return service