signalwire-agents 0.1.6__py3-none-any.whl → 1.0.7__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 +130 -4
- signalwire_agents/agent_server.py +438 -32
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +18 -0
- signalwire_agents/cli/build_search.py +1367 -0
- 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/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 +1225 -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 +809 -0
- 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 +959 -2166
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +707 -0
- signalwire_agents/core/data_map.py +487 -0
- signalwire_agents/core/function_result.py +1150 -1
- signalwire_agents/core/logging_config.py +376 -0
- 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 +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -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 +1134 -0
- signalwire_agents/core/security/session_manager.py +174 -86
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +200 -0
- signalwire_agents/core/skill_manager.py +244 -0
- signalwire_agents/core/swaig_function.py +33 -9
- signalwire_agents/core/swml_builder.py +212 -12
- signalwire_agents/core/swml_handler.py +43 -13
- signalwire_agents/core/swml_renderer.py +123 -297
- signalwire_agents/core/swml_service.py +277 -260
- signalwire_agents/prefabs/concierge.py +6 -2
- signalwire_agents/prefabs/info_gatherer.py +149 -33
- signalwire_agents/prefabs/receptionist.py +14 -22
- signalwire_agents/prefabs/survey.py +6 -2
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +137 -0
- signalwire_agents/search/document_processor.py +1223 -0
- signalwire_agents/search/index_builder.py +804 -0
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +752 -0
- signalwire_agents/search/query_processor.py +502 -0
- signalwire_agents/search/search_engine.py +1264 -0
- signalwire_agents/search/search_service.py +574 -0
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +23 -0
- 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/__init__.py +12 -0
- signalwire_agents/skills/datasphere/skill.py +310 -0
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +10 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +237 -0
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +10 -0
- signalwire_agents/skills/datetime/skill.py +126 -0
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +10 -0
- signalwire_agents/skills/joke/skill.py +109 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +10 -0
- signalwire_agents/skills/math/skill.py +105 -0
- 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 +10 -0
- signalwire_agents/skills/native_vector_search/skill.py +820 -0
- 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 +459 -0
- 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 +10 -0
- signalwire_agents/skills/web_search/skill.py +739 -0
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/wikipedia_search/skill.py +210 -0
- signalwire_agents/utils/__init__.py +14 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
- signalwire_agents-1.0.7.dist-info/METADATA +992 -0
- signalwire_agents-1.0.7.dist-info/RECORD +142 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +1 -1
- signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents-0.1.6.data/data/schema.json +0 -5611
- signalwire_agents-0.1.6.dist-info/METADATA +0 -199
- signalwire_agents-0.1.6.dist-info/RECORD +0 -34
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/top_level.txt +0 -0
signalwire_agents/__init__.py
CHANGED
|
@@ -14,13 +14,139 @@ SignalWire AI Agents SDK
|
|
|
14
14
|
A package for building AI agents using SignalWire's AI and SWML capabilities.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Configure logging before any other imports to ensure early initialization
|
|
18
|
+
from .core.logging_config import configure_logging
|
|
19
|
+
configure_logging()
|
|
20
|
+
|
|
21
|
+
__version__ = "1.0.7"
|
|
18
22
|
|
|
19
23
|
# Import core classes for easier access
|
|
20
|
-
from
|
|
24
|
+
from .core.agent_base import AgentBase
|
|
25
|
+
from .core.contexts import ContextBuilder, Context, Step, create_simple_context
|
|
26
|
+
from .core.data_map import DataMap, create_simple_api_tool, create_expression_tool
|
|
21
27
|
from signalwire_agents.agent_server import AgentServer
|
|
22
28
|
from signalwire_agents.core.swml_service import SWMLService
|
|
23
29
|
from signalwire_agents.core.swml_builder import SWMLBuilder
|
|
24
|
-
from signalwire_agents.core.
|
|
30
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
|
31
|
+
from signalwire_agents.core.swaig_function import SWAIGFunction
|
|
32
|
+
from signalwire_agents.agents.bedrock import BedrockAgent
|
|
33
|
+
|
|
34
|
+
# Import WebService for static file serving
|
|
35
|
+
from signalwire_agents.web import WebService
|
|
36
|
+
|
|
37
|
+
# Lazy import skills to avoid slow startup for CLI tools
|
|
38
|
+
# Skills are now loaded on-demand when requested
|
|
39
|
+
def _get_skill_registry():
|
|
40
|
+
"""Lazy import and return skill registry"""
|
|
41
|
+
import signalwire_agents.skills
|
|
42
|
+
return signalwire_agents.skills.skill_registry
|
|
43
|
+
|
|
44
|
+
# Lazy import convenience functions from the CLI (if available)
|
|
45
|
+
def start_agent(*args, **kwargs):
|
|
46
|
+
"""Start an agent (lazy import)"""
|
|
47
|
+
try:
|
|
48
|
+
from signalwire_agents.cli.helpers import start_agent as _start_agent
|
|
49
|
+
return _start_agent(*args, **kwargs)
|
|
50
|
+
except ImportError:
|
|
51
|
+
raise NotImplementedError("CLI helpers not available")
|
|
52
|
+
|
|
53
|
+
def run_agent(*args, **kwargs):
|
|
54
|
+
"""Run an agent (lazy import)"""
|
|
55
|
+
try:
|
|
56
|
+
from signalwire_agents.cli.helpers import run_agent as _run_agent
|
|
57
|
+
return _run_agent(*args, **kwargs)
|
|
58
|
+
except ImportError:
|
|
59
|
+
raise NotImplementedError("CLI helpers not available")
|
|
60
|
+
|
|
61
|
+
def list_skills(*args, **kwargs):
|
|
62
|
+
"""List available skills (lazy import)"""
|
|
63
|
+
try:
|
|
64
|
+
from signalwire_agents.cli.helpers import list_skills as _list_skills
|
|
65
|
+
return _list_skills(*args, **kwargs)
|
|
66
|
+
except ImportError:
|
|
67
|
+
raise NotImplementedError("CLI helpers not available")
|
|
68
|
+
|
|
69
|
+
def list_skills_with_params():
|
|
70
|
+
"""
|
|
71
|
+
Get complete schema for all available skills including parameter metadata
|
|
72
|
+
|
|
73
|
+
This function returns a comprehensive schema for all available skills,
|
|
74
|
+
including their metadata and parameter definitions. This is useful for
|
|
75
|
+
GUI configuration tools, API documentation, or programmatic skill discovery.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dict[str, Dict[str, Any]]: Complete skill schema where keys are skill names
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> schema = list_skills_with_params()
|
|
82
|
+
>>> print(schema['web_search']['parameters']['api_key'])
|
|
83
|
+
{
|
|
84
|
+
'type': 'string',
|
|
85
|
+
'description': 'Google Custom Search API key',
|
|
86
|
+
'required': True,
|
|
87
|
+
'hidden': True,
|
|
88
|
+
'env_var': 'GOOGLE_SEARCH_API_KEY'
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
from signalwire_agents.skills.registry import skill_registry
|
|
92
|
+
return skill_registry.get_all_skills_schema()
|
|
93
|
+
|
|
94
|
+
def register_skill(skill_class):
|
|
95
|
+
"""
|
|
96
|
+
Register a custom skill class
|
|
97
|
+
|
|
98
|
+
This allows third-party code to register skill classes directly without
|
|
99
|
+
requiring them to be in a specific directory structure.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
skill_class: A class that inherits from SkillBase
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> from my_custom_skills import MyWeatherSkill
|
|
106
|
+
>>> register_skill(MyWeatherSkill)
|
|
107
|
+
>>> # Now you can use it in agents:
|
|
108
|
+
>>> agent.add_skill('my_weather')
|
|
109
|
+
"""
|
|
110
|
+
from signalwire_agents.skills.registry import skill_registry
|
|
111
|
+
return skill_registry.register_skill(skill_class)
|
|
112
|
+
|
|
113
|
+
def add_skill_directory(path):
|
|
114
|
+
"""
|
|
115
|
+
Add a directory to search for skills
|
|
116
|
+
|
|
117
|
+
This allows third-party skill collections to be registered by path.
|
|
118
|
+
Skills in these directories should follow the same structure as built-in skills.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
path: Path to directory containing skill subdirectories
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> add_skill_directory('/opt/custom_skills')
|
|
125
|
+
>>> # Now agent.add_skill('my_custom_skill') will search in this directory
|
|
126
|
+
"""
|
|
127
|
+
from signalwire_agents.skills.registry import skill_registry
|
|
128
|
+
return skill_registry.add_skill_directory(path)
|
|
25
129
|
|
|
26
|
-
__all__ = [
|
|
130
|
+
__all__ = [
|
|
131
|
+
"AgentBase",
|
|
132
|
+
"AgentServer",
|
|
133
|
+
"SWMLService",
|
|
134
|
+
"SWMLBuilder",
|
|
135
|
+
"SwaigFunctionResult",
|
|
136
|
+
"SWAIGFunction",
|
|
137
|
+
"DataMap",
|
|
138
|
+
"create_simple_api_tool",
|
|
139
|
+
"create_expression_tool",
|
|
140
|
+
"ContextBuilder",
|
|
141
|
+
"Context",
|
|
142
|
+
"Step",
|
|
143
|
+
"create_simple_context",
|
|
144
|
+
"WebService",
|
|
145
|
+
"start_agent",
|
|
146
|
+
"run_agent",
|
|
147
|
+
"list_skills",
|
|
148
|
+
"list_skills_with_params",
|
|
149
|
+
"register_skill",
|
|
150
|
+
"add_skill_directory",
|
|
151
|
+
"BedrockAgent"
|
|
152
|
+
]
|
|
@@ -11,7 +11,7 @@ See LICENSE file in the project root for full license information.
|
|
|
11
11
|
AgentServer - Class for hosting multiple SignalWire AI Agents in a single server
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import os
|
|
15
15
|
import re
|
|
16
16
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
|
17
17
|
|
|
@@ -25,6 +25,7 @@ except ImportError:
|
|
|
25
25
|
|
|
26
26
|
from signalwire_agents.core.agent_base import AgentBase
|
|
27
27
|
from signalwire_agents.core.swml_service import SWMLService
|
|
28
|
+
from signalwire_agents.core.logging_config import get_logger, get_execution_mode
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class AgentServer:
|
|
@@ -48,25 +49,20 @@ class AgentServer:
|
|
|
48
49
|
Args:
|
|
49
50
|
host: Host to bind the server to
|
|
50
51
|
port: Port to bind the server to
|
|
51
|
-
log_level: Logging level (debug, info, warning, error)
|
|
52
|
+
log_level: Logging level (debug, info, warning, error, critical)
|
|
52
53
|
"""
|
|
53
54
|
self.host = host
|
|
54
55
|
self.port = port
|
|
55
56
|
self.log_level = log_level.lower()
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
numeric_level = getattr(logging, self.log_level.upper(), logging.INFO)
|
|
59
|
-
logging.basicConfig(
|
|
60
|
-
level=numeric_level,
|
|
61
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
62
|
-
)
|
|
63
|
-
self.logger = logging.getLogger("AgentServer")
|
|
58
|
+
self.logger = get_logger("AgentServer")
|
|
64
59
|
|
|
65
60
|
# Create FastAPI app
|
|
66
61
|
self.app = FastAPI(
|
|
67
62
|
title="SignalWire AI Agents",
|
|
68
63
|
description="Hosted SignalWire AI Agents",
|
|
69
|
-
version="0.1.2"
|
|
64
|
+
version="0.1.2",
|
|
65
|
+
redirect_slashes=False
|
|
70
66
|
)
|
|
71
67
|
|
|
72
68
|
# Keep track of registered agents
|
|
@@ -105,7 +101,8 @@ class AgentServer:
|
|
|
105
101
|
# Store the agent
|
|
106
102
|
self.agents[route] = agent
|
|
107
103
|
|
|
108
|
-
# Get the router and register it
|
|
104
|
+
# Get the router and register it using the standard approach
|
|
105
|
+
# The agent's router already handles both trailing slash versions properly
|
|
109
106
|
router = agent.as_router()
|
|
110
107
|
self.app.include_router(router, prefix=route)
|
|
111
108
|
|
|
@@ -302,14 +299,229 @@ class AgentServer:
|
|
|
302
299
|
|
|
303
300
|
return self.agents.get(route)
|
|
304
301
|
|
|
305
|
-
def run(self, host: Optional[str] = None, port: Optional[int] = None) ->
|
|
302
|
+
def run(self, event=None, context=None, host: Optional[str] = None, port: Optional[int] = None) -> Any:
|
|
306
303
|
"""
|
|
307
|
-
|
|
304
|
+
Universal run method that automatically detects environment and handles accordingly
|
|
305
|
+
|
|
306
|
+
Detects execution mode and routes appropriately:
|
|
307
|
+
- Server mode: Starts uvicorn server with FastAPI
|
|
308
|
+
- CGI mode: Uses same routing logic but outputs CGI headers
|
|
309
|
+
- Lambda mode: Uses same routing logic but returns Lambda response
|
|
308
310
|
|
|
309
311
|
Args:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
+
event: Serverless event object (Lambda, Cloud Functions)
|
|
313
|
+
context: Serverless context object (Lambda, Cloud Functions)
|
|
314
|
+
host: Optional host to override the default (server mode only)
|
|
315
|
+
port: Optional port to override the default (server mode only)
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Response for serverless modes, None for server mode
|
|
312
319
|
"""
|
|
320
|
+
from signalwire_agents.core.logging_config import get_execution_mode
|
|
321
|
+
import os
|
|
322
|
+
import json
|
|
323
|
+
|
|
324
|
+
# Detect execution mode
|
|
325
|
+
mode = get_execution_mode()
|
|
326
|
+
|
|
327
|
+
if mode == 'cgi':
|
|
328
|
+
return self._handle_cgi_request()
|
|
329
|
+
elif mode == 'lambda':
|
|
330
|
+
return self._handle_lambda_request(event, context)
|
|
331
|
+
else:
|
|
332
|
+
# Server mode - use existing logic
|
|
333
|
+
return self._run_server(host, port)
|
|
334
|
+
|
|
335
|
+
def _handle_cgi_request(self) -> str:
|
|
336
|
+
"""Handle CGI request using same routing logic as server"""
|
|
337
|
+
import os
|
|
338
|
+
import sys
|
|
339
|
+
import json
|
|
340
|
+
|
|
341
|
+
# Get PATH_INFO to determine routing
|
|
342
|
+
path_info = os.getenv('PATH_INFO', '').strip('/')
|
|
343
|
+
|
|
344
|
+
# Use same routing logic as the server
|
|
345
|
+
if not path_info:
|
|
346
|
+
# Root request - return basic info or 404
|
|
347
|
+
response = {"error": "No agent specified in path"}
|
|
348
|
+
return self._format_cgi_response(response, status="404 Not Found")
|
|
349
|
+
|
|
350
|
+
# Find matching agent using same logic as server
|
|
351
|
+
for route, agent in self.agents.items():
|
|
352
|
+
route_clean = route.lstrip("/")
|
|
353
|
+
|
|
354
|
+
if path_info == route_clean:
|
|
355
|
+
# Request to agent root - return SWML
|
|
356
|
+
try:
|
|
357
|
+
swml = agent._render_swml()
|
|
358
|
+
return self._format_cgi_response(swml, content_type="application/json")
|
|
359
|
+
except Exception as e:
|
|
360
|
+
error_response = {"error": f"Failed to generate SWML: {str(e)}"}
|
|
361
|
+
return self._format_cgi_response(error_response, status="500 Internal Server Error")
|
|
362
|
+
|
|
363
|
+
elif path_info.startswith(route_clean + "/"):
|
|
364
|
+
# Request to agent sub-path
|
|
365
|
+
relative_path = path_info[len(route_clean):].lstrip("/")
|
|
366
|
+
|
|
367
|
+
if relative_path == "swaig":
|
|
368
|
+
# SWAIG function call - parse stdin for POST data
|
|
369
|
+
try:
|
|
370
|
+
# Read POST data from stdin
|
|
371
|
+
content_length = os.getenv('CONTENT_LENGTH')
|
|
372
|
+
if content_length:
|
|
373
|
+
raw_data = sys.stdin.buffer.read(int(content_length))
|
|
374
|
+
try:
|
|
375
|
+
post_data = json.loads(raw_data.decode('utf-8'))
|
|
376
|
+
except:
|
|
377
|
+
post_data = {}
|
|
378
|
+
else:
|
|
379
|
+
post_data = {}
|
|
380
|
+
|
|
381
|
+
# Execute SWAIG function
|
|
382
|
+
result = agent._execute_swaig_function("", post_data, None, None)
|
|
383
|
+
return self._format_cgi_response(result, content_type="application/json")
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
error_response = {"error": f"SWAIG function failed: {str(e)}"}
|
|
387
|
+
return self._format_cgi_response(error_response, status="500 Internal Server Error")
|
|
388
|
+
|
|
389
|
+
elif relative_path.startswith("swaig/"):
|
|
390
|
+
# Direct function call like /matti/swaig/function_name
|
|
391
|
+
function_name = relative_path[6:] # Remove "swaig/"
|
|
392
|
+
try:
|
|
393
|
+
# Read POST data from stdin
|
|
394
|
+
content_length = os.getenv('CONTENT_LENGTH')
|
|
395
|
+
if content_length:
|
|
396
|
+
raw_data = sys.stdin.buffer.read(int(content_length))
|
|
397
|
+
try:
|
|
398
|
+
post_data = json.loads(raw_data.decode('utf-8'))
|
|
399
|
+
except:
|
|
400
|
+
post_data = {}
|
|
401
|
+
else:
|
|
402
|
+
post_data = {}
|
|
403
|
+
|
|
404
|
+
result = agent._execute_swaig_function(function_name, post_data, None, None)
|
|
405
|
+
return self._format_cgi_response(result, content_type="application/json")
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
error_response = {"error": f"Function call failed: {str(e)}"}
|
|
409
|
+
return self._format_cgi_response(error_response, status="500 Internal Server Error")
|
|
410
|
+
|
|
411
|
+
# No matching agent found
|
|
412
|
+
error_response = {"error": "Not Found"}
|
|
413
|
+
return self._format_cgi_response(error_response, status="404 Not Found")
|
|
414
|
+
|
|
415
|
+
def _handle_lambda_request(self, event, context) -> dict:
|
|
416
|
+
"""Handle Lambda request using same routing logic as server"""
|
|
417
|
+
import json
|
|
418
|
+
|
|
419
|
+
# Extract path from Lambda event
|
|
420
|
+
path = ""
|
|
421
|
+
if event and 'pathParameters' in event and event['pathParameters']:
|
|
422
|
+
path = event['pathParameters'].get('proxy', '')
|
|
423
|
+
elif event and 'path' in event:
|
|
424
|
+
path = event['path']
|
|
425
|
+
|
|
426
|
+
path = path.strip('/')
|
|
427
|
+
|
|
428
|
+
# Use same routing logic as server
|
|
429
|
+
if not path:
|
|
430
|
+
return {
|
|
431
|
+
"statusCode": 404,
|
|
432
|
+
"headers": {"Content-Type": "application/json"},
|
|
433
|
+
"body": json.dumps({"error": "No agent specified in path"})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Find matching agent
|
|
437
|
+
for route, agent in self.agents.items():
|
|
438
|
+
route_clean = route.lstrip("/")
|
|
439
|
+
|
|
440
|
+
if path == route_clean:
|
|
441
|
+
# Request to agent root - return SWML
|
|
442
|
+
try:
|
|
443
|
+
swml = agent._render_swml()
|
|
444
|
+
return {
|
|
445
|
+
"statusCode": 200,
|
|
446
|
+
"headers": {"Content-Type": "application/json"},
|
|
447
|
+
"body": json.dumps(swml) if isinstance(swml, dict) else swml
|
|
448
|
+
}
|
|
449
|
+
except Exception as e:
|
|
450
|
+
return {
|
|
451
|
+
"statusCode": 500,
|
|
452
|
+
"headers": {"Content-Type": "application/json"},
|
|
453
|
+
"body": json.dumps({"error": f"Failed to generate SWML: {str(e)}"})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
elif path.startswith(route_clean + "/"):
|
|
457
|
+
# Request to agent sub-path
|
|
458
|
+
relative_path = path[len(route_clean):].lstrip("/")
|
|
459
|
+
|
|
460
|
+
if relative_path == "swaig" or relative_path.startswith("swaig/"):
|
|
461
|
+
# SWAIG function call
|
|
462
|
+
try:
|
|
463
|
+
# Parse function name and body from event
|
|
464
|
+
function_name = relative_path[6:] if relative_path.startswith("swaig/") else ""
|
|
465
|
+
|
|
466
|
+
# Get POST data from Lambda event body
|
|
467
|
+
post_data = {}
|
|
468
|
+
if event and 'body' in event and event['body']:
|
|
469
|
+
try:
|
|
470
|
+
post_data = json.loads(event['body'])
|
|
471
|
+
except:
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
result = agent._execute_swaig_function(function_name, post_data, None, None)
|
|
475
|
+
return {
|
|
476
|
+
"statusCode": 200,
|
|
477
|
+
"headers": {"Content-Type": "application/json"},
|
|
478
|
+
"body": json.dumps(result) if isinstance(result, dict) else result
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
except Exception as e:
|
|
482
|
+
return {
|
|
483
|
+
"statusCode": 500,
|
|
484
|
+
"headers": {"Content-Type": "application/json"},
|
|
485
|
+
"body": json.dumps({"error": f"Function call failed: {str(e)}"})
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# No matching agent found
|
|
489
|
+
return {
|
|
490
|
+
"statusCode": 404,
|
|
491
|
+
"headers": {"Content-Type": "application/json"},
|
|
492
|
+
"body": json.dumps({"error": "Not Found"})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
def _format_cgi_response(self, data, content_type: str = "application/json", status: str = "200 OK") -> str:
|
|
496
|
+
"""Format response for CGI output"""
|
|
497
|
+
import json
|
|
498
|
+
import sys
|
|
499
|
+
|
|
500
|
+
# Format the body
|
|
501
|
+
if isinstance(data, dict):
|
|
502
|
+
body = json.dumps(data)
|
|
503
|
+
else:
|
|
504
|
+
body = str(data)
|
|
505
|
+
|
|
506
|
+
# Build CGI response with headers
|
|
507
|
+
response_lines = [
|
|
508
|
+
f"Status: {status}",
|
|
509
|
+
f"Content-Type: {content_type}",
|
|
510
|
+
f"Content-Length: {len(body.encode('utf-8'))}",
|
|
511
|
+
"", # Empty line separates headers from body
|
|
512
|
+
body
|
|
513
|
+
]
|
|
514
|
+
|
|
515
|
+
response = "\n".join(response_lines)
|
|
516
|
+
|
|
517
|
+
# Write directly to stdout and flush to ensure immediate output
|
|
518
|
+
sys.stdout.write(response)
|
|
519
|
+
sys.stdout.flush()
|
|
520
|
+
|
|
521
|
+
return response
|
|
522
|
+
|
|
523
|
+
def _run_server(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
|
|
524
|
+
"""Original server mode logic"""
|
|
313
525
|
if not self.agents:
|
|
314
526
|
self.logger.warning("Starting server with no registered agents")
|
|
315
527
|
|
|
@@ -322,32 +534,130 @@ class AgentServer:
|
|
|
322
534
|
"routes": list(self.agents.keys())
|
|
323
535
|
}
|
|
324
536
|
|
|
325
|
-
#
|
|
537
|
+
# Add catch-all route handler to handle both trailing slash and non-trailing slash versions
|
|
538
|
+
@self.app.get("/{full_path:path}")
|
|
539
|
+
@self.app.post("/{full_path:path}")
|
|
540
|
+
async def handle_all_routes(request: Request, full_path: str):
|
|
541
|
+
"""Handle requests that don't match registered routes (e.g. /matti instead of /matti/)"""
|
|
542
|
+
# Check if this path maps to one of our registered agents
|
|
543
|
+
for route, agent in self.agents.items():
|
|
544
|
+
# Check for exact match with registered route
|
|
545
|
+
if full_path == route.lstrip("/"):
|
|
546
|
+
# This is a request to an agent's root without trailing slash
|
|
547
|
+
return await agent._handle_root_request(request)
|
|
548
|
+
elif full_path.startswith(route.lstrip("/") + "/"):
|
|
549
|
+
# This is a request to an agent's sub-path
|
|
550
|
+
relative_path = full_path[len(route.lstrip("/")):]
|
|
551
|
+
relative_path = relative_path.lstrip("/")
|
|
552
|
+
|
|
553
|
+
# Route to appropriate handler based on path
|
|
554
|
+
if not relative_path or relative_path == "/":
|
|
555
|
+
return await agent._handle_root_request(request)
|
|
556
|
+
|
|
557
|
+
clean_path = relative_path.rstrip("/")
|
|
558
|
+
if clean_path == "debug":
|
|
559
|
+
return await agent._handle_debug_request(request)
|
|
560
|
+
elif clean_path == "swaig":
|
|
561
|
+
from fastapi import Response
|
|
562
|
+
return await agent._handle_swaig_request(request, Response())
|
|
563
|
+
elif clean_path == "post_prompt":
|
|
564
|
+
return await agent._handle_post_prompt_request(request)
|
|
565
|
+
elif clean_path == "check_for_input":
|
|
566
|
+
return await agent._handle_check_for_input_request(request)
|
|
567
|
+
|
|
568
|
+
# Check for custom routing callbacks
|
|
569
|
+
if hasattr(agent, '_routing_callbacks'):
|
|
570
|
+
for callback_path, callback_fn in agent._routing_callbacks.items():
|
|
571
|
+
cb_path_clean = callback_path.strip("/")
|
|
572
|
+
if clean_path == cb_path_clean:
|
|
573
|
+
request.state.callback_path = callback_path
|
|
574
|
+
return await agent._handle_root_request(request)
|
|
575
|
+
|
|
576
|
+
# No matching agent - check for static files
|
|
577
|
+
if hasattr(self, '_static_directories'):
|
|
578
|
+
# Check each static directory route
|
|
579
|
+
for static_route, static_dir in self._static_directories.items():
|
|
580
|
+
# For root static route, serve any unmatched path
|
|
581
|
+
if static_route == "" or static_route == "/":
|
|
582
|
+
response = self._serve_static_file(full_path, "")
|
|
583
|
+
if response:
|
|
584
|
+
return response
|
|
585
|
+
# For prefixed static routes, check if path matches
|
|
586
|
+
elif full_path.startswith(static_route.lstrip("/") + "/") or full_path == static_route.lstrip("/"):
|
|
587
|
+
relative_path = full_path[len(static_route.lstrip("/")):].lstrip("/")
|
|
588
|
+
response = self._serve_static_file(relative_path, static_route)
|
|
589
|
+
if response:
|
|
590
|
+
return response
|
|
591
|
+
|
|
592
|
+
# No matching agent or static file found
|
|
593
|
+
from fastapi import HTTPException
|
|
594
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
595
|
+
|
|
596
|
+
# Set host and port
|
|
326
597
|
host = host or self.host
|
|
327
598
|
port = port or self.port
|
|
328
599
|
|
|
329
|
-
|
|
600
|
+
# Check for SSL configuration from environment variables
|
|
601
|
+
ssl_enabled_env = os.environ.get('SWML_SSL_ENABLED', '').lower()
|
|
602
|
+
ssl_enabled = ssl_enabled_env in ('true', '1', 'yes')
|
|
603
|
+
ssl_cert_path = os.environ.get('SWML_SSL_CERT_PATH')
|
|
604
|
+
ssl_key_path = os.environ.get('SWML_SSL_KEY_PATH')
|
|
605
|
+
domain = os.environ.get('SWML_DOMAIN')
|
|
606
|
+
|
|
607
|
+
# Validate SSL configuration if enabled
|
|
608
|
+
if ssl_enabled:
|
|
609
|
+
if not ssl_cert_path or not os.path.exists(ssl_cert_path):
|
|
610
|
+
self.logger.warning(f"SSL cert not found: {ssl_cert_path}")
|
|
611
|
+
ssl_enabled = False
|
|
612
|
+
elif not ssl_key_path or not os.path.exists(ssl_key_path):
|
|
613
|
+
self.logger.warning(f"SSL key not found: {ssl_key_path}")
|
|
614
|
+
ssl_enabled = False
|
|
615
|
+
|
|
616
|
+
# Update server info display with correct protocol
|
|
617
|
+
protocol = "https" if ssl_enabled else "http"
|
|
618
|
+
|
|
619
|
+
# Determine display host - include port unless it's the standard port for the protocol
|
|
620
|
+
if ssl_enabled and domain:
|
|
621
|
+
# Use domain, but include port if it's not the standard HTTPS port (443)
|
|
622
|
+
display_host = f"{domain}:{port}" if port != 443 else domain
|
|
623
|
+
else:
|
|
624
|
+
# Use host:port for HTTP or when no domain is specified
|
|
625
|
+
display_host = f"{host}:{port}"
|
|
626
|
+
|
|
627
|
+
self.logger.info(f"Starting server on {protocol}://{display_host}")
|
|
330
628
|
for route, agent in self.agents.items():
|
|
331
629
|
username, password = agent.get_basic_auth_credentials()
|
|
630
|
+
agent_url = agent.get_full_url(include_auth=False)
|
|
332
631
|
self.logger.info(f"Agent '{agent.get_name()}' available at:")
|
|
333
|
-
self.logger.info(f"URL:
|
|
632
|
+
self.logger.info(f"URL: {agent_url}")
|
|
334
633
|
self.logger.info(f"Basic Auth: {username}:{password}")
|
|
335
|
-
|
|
336
|
-
# Start the server
|
|
337
|
-
|
|
338
|
-
self.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
634
|
+
|
|
635
|
+
# Start the server with or without SSL
|
|
636
|
+
if ssl_enabled and ssl_cert_path and ssl_key_path:
|
|
637
|
+
self.logger.info(f"Starting with SSL - cert: {ssl_cert_path}, key: {ssl_key_path}")
|
|
638
|
+
uvicorn.run(
|
|
639
|
+
self.app,
|
|
640
|
+
host=host,
|
|
641
|
+
port=port,
|
|
642
|
+
log_level=self.log_level,
|
|
643
|
+
ssl_certfile=ssl_cert_path,
|
|
644
|
+
ssl_keyfile=ssl_key_path
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
uvicorn.run(
|
|
648
|
+
self.app,
|
|
649
|
+
host=host,
|
|
650
|
+
port=port,
|
|
651
|
+
log_level=self.log_level
|
|
652
|
+
)
|
|
343
653
|
|
|
344
|
-
def register_global_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]],
|
|
654
|
+
def register_global_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]],
|
|
345
655
|
path: str) -> None:
|
|
346
656
|
"""
|
|
347
657
|
Register a routing callback across all agents
|
|
348
|
-
|
|
658
|
+
|
|
349
659
|
This allows you to add unified routing logic to all agents at the same path.
|
|
350
|
-
|
|
660
|
+
|
|
351
661
|
Args:
|
|
352
662
|
callback_fn: The callback function to register
|
|
353
663
|
path: The path to register the callback at
|
|
@@ -355,11 +665,107 @@ class AgentServer:
|
|
|
355
665
|
# Normalize the path
|
|
356
666
|
if not path.startswith("/"):
|
|
357
667
|
path = f"/{path}"
|
|
358
|
-
|
|
668
|
+
|
|
359
669
|
path = path.rstrip("/")
|
|
360
|
-
|
|
670
|
+
|
|
361
671
|
# Register with all existing agents
|
|
362
672
|
for agent in self.agents.values():
|
|
363
673
|
agent.register_routing_callback(callback_fn, path=path)
|
|
364
|
-
|
|
674
|
+
|
|
365
675
|
self.logger.info(f"Registered global routing callback at {path} on all agents")
|
|
676
|
+
|
|
677
|
+
def serve_static_files(self, directory: str, route: str = "/") -> None:
|
|
678
|
+
"""
|
|
679
|
+
Serve static files from a directory.
|
|
680
|
+
|
|
681
|
+
This method properly integrates static file serving with agent routes,
|
|
682
|
+
ensuring that agent routes take priority over static files.
|
|
683
|
+
|
|
684
|
+
Unlike using StaticFiles.mount("/", ...) directly on self.app, this method
|
|
685
|
+
uses explicit route handlers that work correctly with agent routes.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
directory: Path to the directory containing static files
|
|
689
|
+
route: URL path prefix for static files (default: "/" for root)
|
|
690
|
+
|
|
691
|
+
Example:
|
|
692
|
+
server = AgentServer()
|
|
693
|
+
server.register(SupportAgent(), "/support")
|
|
694
|
+
server.serve_static_files("./web") # Serves at /
|
|
695
|
+
# /support -> SupportAgent
|
|
696
|
+
# /index.html -> ./web/index.html
|
|
697
|
+
# / -> ./web/index.html
|
|
698
|
+
"""
|
|
699
|
+
from pathlib import Path
|
|
700
|
+
from fastapi.responses import FileResponse
|
|
701
|
+
from fastapi import HTTPException
|
|
702
|
+
|
|
703
|
+
# Normalize directory path
|
|
704
|
+
static_dir = Path(directory).resolve()
|
|
705
|
+
|
|
706
|
+
if not static_dir.exists():
|
|
707
|
+
raise ValueError(f"Directory does not exist: {directory}")
|
|
708
|
+
|
|
709
|
+
if not static_dir.is_dir():
|
|
710
|
+
raise ValueError(f"Path is not a directory: {directory}")
|
|
711
|
+
|
|
712
|
+
# Normalize route
|
|
713
|
+
if not route.startswith("/"):
|
|
714
|
+
route = f"/{route}"
|
|
715
|
+
route = route.rstrip("/")
|
|
716
|
+
|
|
717
|
+
# Store static directory config for use by catch-all handler
|
|
718
|
+
if not hasattr(self, '_static_directories'):
|
|
719
|
+
self._static_directories = {}
|
|
720
|
+
|
|
721
|
+
self._static_directories[route] = static_dir
|
|
722
|
+
|
|
723
|
+
self.logger.info(f"Serving static files from '{directory}' at route '{route or '/'}'")
|
|
724
|
+
|
|
725
|
+
def _serve_static_file(self, file_path: str, route: str = "/") -> Optional[Response]:
|
|
726
|
+
"""
|
|
727
|
+
Internal method to serve a static file.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
file_path: The requested file path
|
|
731
|
+
route: The route prefix
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
FileResponse if file exists, None otherwise
|
|
735
|
+
"""
|
|
736
|
+
from pathlib import Path
|
|
737
|
+
from fastapi.responses import FileResponse
|
|
738
|
+
|
|
739
|
+
if not hasattr(self, '_static_directories'):
|
|
740
|
+
return None
|
|
741
|
+
|
|
742
|
+
static_dir = self._static_directories.get(route)
|
|
743
|
+
if not static_dir:
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
# Default to index.html for empty path
|
|
747
|
+
if not file_path:
|
|
748
|
+
file_path = "index.html"
|
|
749
|
+
|
|
750
|
+
full_path = static_dir / file_path
|
|
751
|
+
|
|
752
|
+
# Security: prevent path traversal
|
|
753
|
+
try:
|
|
754
|
+
full_path = full_path.resolve()
|
|
755
|
+
if not str(full_path).startswith(str(static_dir)):
|
|
756
|
+
return None
|
|
757
|
+
except Exception:
|
|
758
|
+
return None
|
|
759
|
+
|
|
760
|
+
# Handle directory requests
|
|
761
|
+
if full_path.is_dir():
|
|
762
|
+
index_path = full_path / "index.html"
|
|
763
|
+
if index_path.exists():
|
|
764
|
+
full_path = index_path
|
|
765
|
+
else:
|
|
766
|
+
return None
|
|
767
|
+
|
|
768
|
+
if not full_path.exists():
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
return FileResponse(full_path)
|