signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -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.1.13"
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 skills to trigger discovery
35
- import signalwire_agents.skills
34
+ # Import WebService for static file serving
35
+ from signalwire_agents.web import WebService
36
36
 
37
- # Import convenience functions from the CLI (if available)
38
- try:
39
- from signalwire_agents.cli.helpers import start_agent, run_agent, list_skills
40
- except ImportError:
41
- # CLI helpers not available, define minimal versions
42
- def start_agent(*args, **kwargs):
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
- def run_agent(*args, **kwargs):
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
- def list_skills(*args, **kwargs):
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.1.2",
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 _run_server(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
523
- """Original server mode logic"""
524
- if not self.agents:
525
- self.logger.warning("Starting server with no registered agents")
526
-
527
- # Add a health check endpoint
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
- # Add catch-all route handler to handle both trailing slash and non-trailing slash versions
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
- def register_global_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]],
598
- path: str) -> None:
599
- """
600
- Register a routing callback across all agents
601
-
602
- This allows you to add unified routing logic to all agents at the same path.
603
-
604
- Args:
605
- callback_fn: The callback function to register
606
- path: The path to register the callback at
607
- """
608
- # Normalize the path
609
- if not path.startswith("/"):
610
- path = f"/{path}"
611
-
612
- path = path.rstrip("/")
613
-
614
- # Register with all existing agents
615
- for agent in self.agents.values():
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")