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
signalwire_agents/__init__.py
CHANGED
|
@@ -18,41 +18,120 @@ A package for building AI agents using SignalWire's AI and SWML capabilities.
|
|
|
18
18
|
from .core.logging_config import configure_logging
|
|
19
19
|
configure_logging()
|
|
20
20
|
|
|
21
|
-
__version__ = "0.
|
|
21
|
+
__version__ = "1.0.17.dev4"
|
|
22
22
|
|
|
23
23
|
# Import core classes for easier access
|
|
24
24
|
from .core.agent_base import AgentBase
|
|
25
25
|
from .core.contexts import ContextBuilder, Context, Step, create_simple_context
|
|
26
26
|
from .core.data_map import DataMap, create_simple_api_tool, create_expression_tool
|
|
27
|
-
from .core.state import StateManager, FileStateManager
|
|
28
27
|
from signalwire_agents.agent_server import AgentServer
|
|
29
28
|
from signalwire_agents.core.swml_service import SWMLService
|
|
30
29
|
from signalwire_agents.core.swml_builder import SWMLBuilder
|
|
31
30
|
from signalwire_agents.core.function_result import SwaigFunctionResult
|
|
32
31
|
from signalwire_agents.core.swaig_function import SWAIGFunction
|
|
32
|
+
from signalwire_agents.agents.bedrock import BedrockAgent
|
|
33
33
|
|
|
34
|
-
# Import
|
|
35
|
-
|
|
34
|
+
# Import WebService for static file serving
|
|
35
|
+
from signalwire_agents.web import WebService
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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:
|
|
43
51
|
raise NotImplementedError("CLI helpers not available")
|
|
44
|
-
|
|
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:
|
|
45
59
|
raise NotImplementedError("CLI helpers not available")
|
|
46
|
-
|
|
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:
|
|
47
67
|
raise NotImplementedError("CLI helpers not available")
|
|
48
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)
|
|
129
|
+
|
|
49
130
|
__all__ = [
|
|
50
131
|
"AgentBase",
|
|
51
132
|
"AgentServer",
|
|
52
133
|
"SWMLService",
|
|
53
134
|
"SWMLBuilder",
|
|
54
|
-
"StateManager",
|
|
55
|
-
"FileStateManager",
|
|
56
135
|
"SwaigFunctionResult",
|
|
57
136
|
"SWAIGFunction",
|
|
58
137
|
"DataMap",
|
|
@@ -62,7 +141,12 @@ __all__ = [
|
|
|
62
141
|
"Context",
|
|
63
142
|
"Step",
|
|
64
143
|
"create_simple_context",
|
|
144
|
+
"WebService",
|
|
65
145
|
"start_agent",
|
|
66
146
|
"run_agent",
|
|
67
|
-
"list_skills"
|
|
147
|
+
"list_skills",
|
|
148
|
+
"list_skills_with_params",
|
|
149
|
+
"register_skill",
|
|
150
|
+
"add_skill_directory",
|
|
151
|
+
"BedrockAgent"
|
|
68
152
|
]
|
|
@@ -11,6 +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 os
|
|
14
15
|
import re
|
|
15
16
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
|
16
17
|
|
|
@@ -60,17 +61,28 @@ class AgentServer:
|
|
|
60
61
|
self.app = FastAPI(
|
|
61
62
|
title="SignalWire AI Agents",
|
|
62
63
|
description="Hosted SignalWire AI Agents",
|
|
63
|
-
version="0.
|
|
64
|
+
version="1.0.17.dev4",
|
|
64
65
|
redirect_slashes=False
|
|
65
66
|
)
|
|
66
67
|
|
|
67
68
|
# Keep track of registered agents
|
|
68
69
|
self.agents: Dict[str, AgentBase] = {}
|
|
69
|
-
|
|
70
|
+
|
|
70
71
|
# Keep track of SIP routing configuration
|
|
71
72
|
self._sip_routing_enabled = False
|
|
72
73
|
self._sip_route = None
|
|
73
74
|
self._sip_username_mapping: Dict[str, str] = {} # Maps SIP usernames to routes
|
|
75
|
+
|
|
76
|
+
# Register health endpoints immediately so they're available
|
|
77
|
+
# whether using server.run() or server.app with gunicorn
|
|
78
|
+
self._register_health_endpoints()
|
|
79
|
+
|
|
80
|
+
# Register catch-all handler on startup (not in __init__) so it runs AFTER
|
|
81
|
+
# all other routes are registered. This ensures custom routes like /get_token
|
|
82
|
+
# don't get overshadowed by the catch-all /{full_path:path} route.
|
|
83
|
+
@self.app.on_event("startup")
|
|
84
|
+
async def _setup_catch_all():
|
|
85
|
+
self._register_catch_all_handler()
|
|
74
86
|
|
|
75
87
|
def register(self, agent: AgentBase, route: Optional[str] = None) -> None:
|
|
76
88
|
"""
|
|
@@ -99,12 +111,12 @@ class AgentServer:
|
|
|
99
111
|
|
|
100
112
|
# Store the agent
|
|
101
113
|
self.agents[route] = agent
|
|
102
|
-
|
|
114
|
+
|
|
103
115
|
# Get the router and register it using the standard approach
|
|
104
116
|
# The agent's router already handles both trailing slash versions properly
|
|
105
117
|
router = agent.as_router()
|
|
106
118
|
self.app.include_router(router, prefix=route)
|
|
107
|
-
|
|
119
|
+
|
|
108
120
|
self.logger.info(f"Registered agent '{agent.get_name()}' at route '{route}'")
|
|
109
121
|
|
|
110
122
|
# If SIP routing is enabled and auto-mapping is on, register SIP usernames for this agent
|
|
@@ -518,13 +530,13 @@ class AgentServer:
|
|
|
518
530
|
sys.stdout.flush()
|
|
519
531
|
|
|
520
532
|
return response
|
|
521
|
-
|
|
522
|
-
def
|
|
523
|
-
"""
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
533
|
+
|
|
534
|
+
def _register_health_endpoints(self) -> None:
|
|
535
|
+
"""Register health and readiness endpoints.
|
|
536
|
+
|
|
537
|
+
Called during __init__ so endpoints are available whether using
|
|
538
|
+
server.run() or accessing server.app directly with gunicorn.
|
|
539
|
+
"""
|
|
528
540
|
@self.app.get("/health")
|
|
529
541
|
def health_check():
|
|
530
542
|
return {
|
|
@@ -532,8 +544,209 @@ class AgentServer:
|
|
|
532
544
|
"agents": len(self.agents),
|
|
533
545
|
"routes": list(self.agents.keys())
|
|
534
546
|
}
|
|
535
|
-
|
|
536
|
-
|
|
547
|
+
|
|
548
|
+
@self.app.get("/ready")
|
|
549
|
+
def readiness_check():
|
|
550
|
+
return {
|
|
551
|
+
"status": "ready",
|
|
552
|
+
"agents": len(self.agents)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
def _run_server(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
|
|
556
|
+
"""Original server mode logic"""
|
|
557
|
+
if not self.agents:
|
|
558
|
+
self.logger.warning("Starting server with no registered agents")
|
|
559
|
+
|
|
560
|
+
# Set host and port
|
|
561
|
+
host = host or self.host
|
|
562
|
+
port = port or self.port
|
|
563
|
+
|
|
564
|
+
# Check for SSL configuration from environment variables
|
|
565
|
+
ssl_enabled_env = os.environ.get('SWML_SSL_ENABLED', '').lower()
|
|
566
|
+
ssl_enabled = ssl_enabled_env in ('true', '1', 'yes')
|
|
567
|
+
ssl_cert_path = os.environ.get('SWML_SSL_CERT_PATH')
|
|
568
|
+
ssl_key_path = os.environ.get('SWML_SSL_KEY_PATH')
|
|
569
|
+
domain = os.environ.get('SWML_DOMAIN')
|
|
570
|
+
|
|
571
|
+
# Validate SSL configuration if enabled
|
|
572
|
+
if ssl_enabled:
|
|
573
|
+
if not ssl_cert_path or not os.path.exists(ssl_cert_path):
|
|
574
|
+
self.logger.warning(f"SSL cert not found: {ssl_cert_path}")
|
|
575
|
+
ssl_enabled = False
|
|
576
|
+
elif not ssl_key_path or not os.path.exists(ssl_key_path):
|
|
577
|
+
self.logger.warning(f"SSL key not found: {ssl_key_path}")
|
|
578
|
+
ssl_enabled = False
|
|
579
|
+
|
|
580
|
+
# Update server info display with correct protocol
|
|
581
|
+
protocol = "https" if ssl_enabled else "http"
|
|
582
|
+
|
|
583
|
+
# Determine display host - include port unless it's the standard port for the protocol
|
|
584
|
+
if ssl_enabled and domain:
|
|
585
|
+
# Use domain, but include port if it's not the standard HTTPS port (443)
|
|
586
|
+
display_host = f"{domain}:{port}" if port != 443 else domain
|
|
587
|
+
else:
|
|
588
|
+
# Use host:port for HTTP or when no domain is specified
|
|
589
|
+
display_host = f"{host}:{port}"
|
|
590
|
+
|
|
591
|
+
self.logger.info(f"Starting server on {protocol}://{display_host}")
|
|
592
|
+
for route, agent in self.agents.items():
|
|
593
|
+
username, password = agent.get_basic_auth_credentials()
|
|
594
|
+
agent_url = agent.get_full_url(include_auth=False)
|
|
595
|
+
self.logger.info(f"Agent '{agent.get_name()}' available at:")
|
|
596
|
+
self.logger.info(f"URL: {agent_url}")
|
|
597
|
+
self.logger.info(f"Basic Auth: {username}:{password}")
|
|
598
|
+
|
|
599
|
+
# Start the server with or without SSL
|
|
600
|
+
if ssl_enabled and ssl_cert_path and ssl_key_path:
|
|
601
|
+
self.logger.info(f"Starting with SSL - cert: {ssl_cert_path}, key: {ssl_key_path}")
|
|
602
|
+
uvicorn.run(
|
|
603
|
+
self.app,
|
|
604
|
+
host=host,
|
|
605
|
+
port=port,
|
|
606
|
+
log_level=self.log_level,
|
|
607
|
+
ssl_certfile=ssl_cert_path,
|
|
608
|
+
ssl_keyfile=ssl_key_path
|
|
609
|
+
)
|
|
610
|
+
else:
|
|
611
|
+
uvicorn.run(
|
|
612
|
+
self.app,
|
|
613
|
+
host=host,
|
|
614
|
+
port=port,
|
|
615
|
+
log_level=self.log_level
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
def register_global_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]],
|
|
619
|
+
path: str) -> None:
|
|
620
|
+
"""
|
|
621
|
+
Register a routing callback across all agents
|
|
622
|
+
|
|
623
|
+
This allows you to add unified routing logic to all agents at the same path.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
callback_fn: The callback function to register
|
|
627
|
+
path: The path to register the callback at
|
|
628
|
+
"""
|
|
629
|
+
# Normalize the path
|
|
630
|
+
if not path.startswith("/"):
|
|
631
|
+
path = f"/{path}"
|
|
632
|
+
|
|
633
|
+
path = path.rstrip("/")
|
|
634
|
+
|
|
635
|
+
# Register with all existing agents
|
|
636
|
+
for agent in self.agents.values():
|
|
637
|
+
agent.register_routing_callback(callback_fn, path=path)
|
|
638
|
+
|
|
639
|
+
self.logger.info(f"Registered global routing callback at {path} on all agents")
|
|
640
|
+
|
|
641
|
+
def serve_static_files(self, directory: str, route: str = "/") -> None:
|
|
642
|
+
"""
|
|
643
|
+
Serve static files from a directory.
|
|
644
|
+
|
|
645
|
+
This method properly integrates static file serving with agent routes,
|
|
646
|
+
ensuring that agent routes take priority over static files.
|
|
647
|
+
|
|
648
|
+
Unlike using StaticFiles.mount("/", ...) directly on self.app, this method
|
|
649
|
+
uses explicit route handlers that work correctly with agent routes.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
directory: Path to the directory containing static files
|
|
653
|
+
route: URL path prefix for static files (default: "/" for root)
|
|
654
|
+
|
|
655
|
+
Example:
|
|
656
|
+
server = AgentServer()
|
|
657
|
+
server.register(SupportAgent(), "/support")
|
|
658
|
+
server.serve_static_files("./web") # Serves at /
|
|
659
|
+
# /support -> SupportAgent
|
|
660
|
+
# /index.html -> ./web/index.html
|
|
661
|
+
# / -> ./web/index.html
|
|
662
|
+
"""
|
|
663
|
+
from pathlib import Path
|
|
664
|
+
from fastapi.responses import FileResponse
|
|
665
|
+
from fastapi import HTTPException
|
|
666
|
+
|
|
667
|
+
# Normalize directory path
|
|
668
|
+
static_dir = Path(directory).resolve()
|
|
669
|
+
|
|
670
|
+
if not static_dir.exists():
|
|
671
|
+
raise ValueError(f"Directory does not exist: {directory}")
|
|
672
|
+
|
|
673
|
+
if not static_dir.is_dir():
|
|
674
|
+
raise ValueError(f"Path is not a directory: {directory}")
|
|
675
|
+
|
|
676
|
+
# Normalize route
|
|
677
|
+
if not route.startswith("/"):
|
|
678
|
+
route = f"/{route}"
|
|
679
|
+
route = route.rstrip("/")
|
|
680
|
+
|
|
681
|
+
# Store static directory config for use by catch-all handler
|
|
682
|
+
if not hasattr(self, '_static_directories'):
|
|
683
|
+
self._static_directories = {}
|
|
684
|
+
|
|
685
|
+
self._static_directories[route] = static_dir
|
|
686
|
+
|
|
687
|
+
self.logger.info(f"Serving static files from '{directory}' at route '{route or '/'}'")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _serve_static_file(self, file_path: str, route: str = "/") -> Optional[Response]:
|
|
691
|
+
"""
|
|
692
|
+
Internal method to serve a static file.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
file_path: The requested file path
|
|
696
|
+
route: The route prefix
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
FileResponse if file exists, None otherwise
|
|
700
|
+
"""
|
|
701
|
+
from pathlib import Path
|
|
702
|
+
from fastapi.responses import FileResponse
|
|
703
|
+
|
|
704
|
+
if not hasattr(self, '_static_directories'):
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
static_dir = self._static_directories.get(route)
|
|
708
|
+
if not static_dir:
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
# Default to index.html for empty path
|
|
712
|
+
if not file_path:
|
|
713
|
+
file_path = "index.html"
|
|
714
|
+
|
|
715
|
+
full_path = static_dir / file_path
|
|
716
|
+
|
|
717
|
+
# Security: prevent path traversal
|
|
718
|
+
try:
|
|
719
|
+
full_path = full_path.resolve()
|
|
720
|
+
if not str(full_path).startswith(str(static_dir)):
|
|
721
|
+
return None
|
|
722
|
+
except Exception:
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
# Handle directory requests
|
|
726
|
+
if full_path.is_dir():
|
|
727
|
+
index_path = full_path / "index.html"
|
|
728
|
+
if index_path.exists():
|
|
729
|
+
full_path = index_path
|
|
730
|
+
else:
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
if not full_path.exists():
|
|
734
|
+
return None
|
|
735
|
+
|
|
736
|
+
return FileResponse(full_path)
|
|
737
|
+
|
|
738
|
+
def _register_catch_all_handler(self) -> None:
|
|
739
|
+
"""
|
|
740
|
+
Register catch-all route handler for agent routing and static files.
|
|
741
|
+
|
|
742
|
+
This handler is needed for:
|
|
743
|
+
1. Routing requests without trailing slashes to agents (e.g., /santa instead of /santa/)
|
|
744
|
+
2. Serving static files from directories registered with serve_static_files()
|
|
745
|
+
|
|
746
|
+
Called via startup event (not __init__) to ensure it runs AFTER all other routes
|
|
747
|
+
are registered. This prevents the catch-all from overshadowing custom routes
|
|
748
|
+
like /get_token that users may add to server.app.
|
|
749
|
+
"""
|
|
537
750
|
@self.app.get("/{full_path:path}")
|
|
538
751
|
@self.app.post("/{full_path:path}")
|
|
539
752
|
async def handle_all_routes(request: Request, full_path: str):
|
|
@@ -548,11 +761,11 @@ class AgentServer:
|
|
|
548
761
|
# This is a request to an agent's sub-path
|
|
549
762
|
relative_path = full_path[len(route.lstrip("/")):]
|
|
550
763
|
relative_path = relative_path.lstrip("/")
|
|
551
|
-
|
|
764
|
+
|
|
552
765
|
# Route to appropriate handler based on path
|
|
553
766
|
if not relative_path or relative_path == "/":
|
|
554
767
|
return await agent._handle_root_request(request)
|
|
555
|
-
|
|
768
|
+
|
|
556
769
|
clean_path = relative_path.rstrip("/")
|
|
557
770
|
if clean_path == "debug":
|
|
558
771
|
return await agent._handle_debug_request(request)
|
|
@@ -563,7 +776,7 @@ class AgentServer:
|
|
|
563
776
|
return await agent._handle_post_prompt_request(request)
|
|
564
777
|
elif clean_path == "check_for_input":
|
|
565
778
|
return await agent._handle_check_for_input_request(request)
|
|
566
|
-
|
|
779
|
+
|
|
567
780
|
# Check for custom routing callbacks
|
|
568
781
|
if hasattr(agent, '_routing_callbacks'):
|
|
569
782
|
for callback_path, callback_fn in agent._routing_callbacks.items():
|
|
@@ -571,48 +784,23 @@ class AgentServer:
|
|
|
571
784
|
if clean_path == cb_path_clean:
|
|
572
785
|
request.state.callback_path = callback_path
|
|
573
786
|
return await agent._handle_root_request(request)
|
|
574
|
-
|
|
575
|
-
# No matching agent found
|
|
576
|
-
return {"error": "Not Found"}
|
|
577
|
-
|
|
578
|
-
# Print server info
|
|
579
|
-
host = host or self.host
|
|
580
|
-
port = port or self.port
|
|
581
|
-
|
|
582
|
-
self.logger.info(f"Starting server on {host}:{port}")
|
|
583
|
-
for route, agent in self.agents.items():
|
|
584
|
-
username, password = agent.get_basic_auth_credentials()
|
|
585
|
-
self.logger.info(f"Agent '{agent.get_name()}' available at:")
|
|
586
|
-
self.logger.info(f"URL: http://{host}:{port}{route}")
|
|
587
|
-
self.logger.info(f"Basic Auth: {username}:{password}")
|
|
588
|
-
|
|
589
|
-
# Start the server
|
|
590
|
-
uvicorn.run(
|
|
591
|
-
self.app,
|
|
592
|
-
host=host,
|
|
593
|
-
port=port,
|
|
594
|
-
log_level=self.log_level
|
|
595
|
-
)
|
|
596
787
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
agent.register_routing_callback(callback_fn, path=path)
|
|
617
|
-
|
|
618
|
-
self.logger.info(f"Registered global routing callback at {path} on all agents")
|
|
788
|
+
# No matching agent - check for static files
|
|
789
|
+
if hasattr(self, '_static_directories'):
|
|
790
|
+
# Check each static directory route
|
|
791
|
+
for static_route, static_dir in self._static_directories.items():
|
|
792
|
+
# For root static route, serve any unmatched path
|
|
793
|
+
if static_route == "" or static_route == "/":
|
|
794
|
+
response = self._serve_static_file(full_path, "")
|
|
795
|
+
if response:
|
|
796
|
+
return response
|
|
797
|
+
# For prefixed static routes, check if path matches
|
|
798
|
+
elif full_path.startswith(static_route.lstrip("/") + "/") or full_path == static_route.lstrip("/"):
|
|
799
|
+
relative_path = full_path[len(static_route.lstrip("/")):].lstrip("/")
|
|
800
|
+
response = self._serve_static_file(relative_path, static_route)
|
|
801
|
+
if response:
|
|
802
|
+
return response
|
|
803
|
+
|
|
804
|
+
# No matching agent or static file found
|
|
805
|
+
from fastapi import HTTPException
|
|
806
|
+
raise HTTPException(status_code=404, detail="Not Found")
|