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,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,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
|