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.
Files changed (140) hide show
  1. signalwire_agents/__init__.py +130 -4
  2. signalwire_agents/agent_server.py +438 -32
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +18 -0
  5. signalwire_agents/cli/build_search.py +1367 -0
  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/execution/__init__.py +10 -0
  13. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  14. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  15. signalwire_agents/cli/init_project.py +1225 -0
  16. signalwire_agents/cli/output/__init__.py +10 -0
  17. signalwire_agents/cli/output/output_formatter.py +255 -0
  18. signalwire_agents/cli/output/swml_dump.py +186 -0
  19. signalwire_agents/cli/simulation/__init__.py +10 -0
  20. signalwire_agents/cli/simulation/data_generation.py +374 -0
  21. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  22. signalwire_agents/cli/simulation/mock_env.py +282 -0
  23. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  24. signalwire_agents/cli/test_swaig.py +809 -0
  25. signalwire_agents/cli/types.py +81 -0
  26. signalwire_agents/core/__init__.py +2 -2
  27. signalwire_agents/core/agent/__init__.py +12 -0
  28. signalwire_agents/core/agent/config/__init__.py +12 -0
  29. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  30. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  31. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  32. signalwire_agents/core/agent/prompt/manager.py +306 -0
  33. signalwire_agents/core/agent/routing/__init__.py +9 -0
  34. signalwire_agents/core/agent/security/__init__.py +9 -0
  35. signalwire_agents/core/agent/swml/__init__.py +9 -0
  36. signalwire_agents/core/agent/tools/__init__.py +15 -0
  37. signalwire_agents/core/agent/tools/decorator.py +97 -0
  38. signalwire_agents/core/agent/tools/registry.py +210 -0
  39. signalwire_agents/core/agent_base.py +959 -2166
  40. signalwire_agents/core/auth_handler.py +233 -0
  41. signalwire_agents/core/config_loader.py +259 -0
  42. signalwire_agents/core/contexts.py +707 -0
  43. signalwire_agents/core/data_map.py +487 -0
  44. signalwire_agents/core/function_result.py +1150 -1
  45. signalwire_agents/core/logging_config.py +376 -0
  46. signalwire_agents/core/mixins/__init__.py +28 -0
  47. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  48. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  49. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  50. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  51. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  52. signalwire_agents/core/mixins/state_mixin.py +153 -0
  53. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  54. signalwire_agents/core/mixins/web_mixin.py +1134 -0
  55. signalwire_agents/core/security/session_manager.py +174 -86
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +200 -0
  58. signalwire_agents/core/skill_manager.py +244 -0
  59. signalwire_agents/core/swaig_function.py +33 -9
  60. signalwire_agents/core/swml_builder.py +212 -12
  61. signalwire_agents/core/swml_handler.py +43 -13
  62. signalwire_agents/core/swml_renderer.py +123 -297
  63. signalwire_agents/core/swml_service.py +277 -260
  64. signalwire_agents/prefabs/concierge.py +6 -2
  65. signalwire_agents/prefabs/info_gatherer.py +149 -33
  66. signalwire_agents/prefabs/receptionist.py +14 -22
  67. signalwire_agents/prefabs/survey.py +6 -2
  68. signalwire_agents/schema.json +9218 -5489
  69. signalwire_agents/search/__init__.py +137 -0
  70. signalwire_agents/search/document_processor.py +1223 -0
  71. signalwire_agents/search/index_builder.py +804 -0
  72. signalwire_agents/search/migration.py +418 -0
  73. signalwire_agents/search/models.py +30 -0
  74. signalwire_agents/search/pgvector_backend.py +752 -0
  75. signalwire_agents/search/query_processor.py +502 -0
  76. signalwire_agents/search/search_engine.py +1264 -0
  77. signalwire_agents/search/search_service.py +574 -0
  78. signalwire_agents/skills/README.md +452 -0
  79. signalwire_agents/skills/__init__.py +23 -0
  80. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  81. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  82. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  83. signalwire_agents/skills/datasphere/README.md +210 -0
  84. signalwire_agents/skills/datasphere/__init__.py +12 -0
  85. signalwire_agents/skills/datasphere/skill.py +310 -0
  86. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  87. signalwire_agents/skills/datasphere_serverless/__init__.py +10 -0
  88. signalwire_agents/skills/datasphere_serverless/skill.py +237 -0
  89. signalwire_agents/skills/datetime/README.md +132 -0
  90. signalwire_agents/skills/datetime/__init__.py +10 -0
  91. signalwire_agents/skills/datetime/skill.py +126 -0
  92. signalwire_agents/skills/joke/README.md +149 -0
  93. signalwire_agents/skills/joke/__init__.py +10 -0
  94. signalwire_agents/skills/joke/skill.py +109 -0
  95. signalwire_agents/skills/math/README.md +161 -0
  96. signalwire_agents/skills/math/__init__.py +10 -0
  97. signalwire_agents/skills/math/skill.py +105 -0
  98. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  99. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  100. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  101. signalwire_agents/skills/native_vector_search/README.md +210 -0
  102. signalwire_agents/skills/native_vector_search/__init__.py +10 -0
  103. signalwire_agents/skills/native_vector_search/skill.py +820 -0
  104. signalwire_agents/skills/play_background_file/README.md +218 -0
  105. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  106. signalwire_agents/skills/play_background_file/skill.py +242 -0
  107. signalwire_agents/skills/registry.py +459 -0
  108. signalwire_agents/skills/spider/README.md +236 -0
  109. signalwire_agents/skills/spider/__init__.py +13 -0
  110. signalwire_agents/skills/spider/skill.py +598 -0
  111. signalwire_agents/skills/swml_transfer/README.md +395 -0
  112. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  113. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  114. signalwire_agents/skills/weather_api/README.md +178 -0
  115. signalwire_agents/skills/weather_api/__init__.py +12 -0
  116. signalwire_agents/skills/weather_api/skill.py +191 -0
  117. signalwire_agents/skills/web_search/README.md +163 -0
  118. signalwire_agents/skills/web_search/__init__.py +10 -0
  119. signalwire_agents/skills/web_search/skill.py +739 -0
  120. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  121. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  122. signalwire_agents/skills/wikipedia_search/skill.py +210 -0
  123. signalwire_agents/utils/__init__.py +14 -0
  124. signalwire_agents/utils/schema_utils.py +111 -44
  125. signalwire_agents/web/__init__.py +17 -0
  126. signalwire_agents/web/web_service.py +559 -0
  127. signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
  128. signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
  129. signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
  130. signalwire_agents-1.0.7.dist-info/METADATA +992 -0
  131. signalwire_agents-1.0.7.dist-info/RECORD +142 -0
  132. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +1 -1
  133. signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
  134. signalwire_agents/core/state/file_state_manager.py +0 -219
  135. signalwire_agents/core/state/state_manager.py +0 -101
  136. signalwire_agents-0.1.6.data/data/schema.json +0 -5611
  137. signalwire_agents-0.1.6.dist-info/METADATA +0 -199
  138. signalwire_agents-0.1.6.dist-info/RECORD +0 -34
  139. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
  140. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/top_level.txt +0 -0
@@ -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
- __version__ = "0.1.6"
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 signalwire_agents.core.agent_base import AgentBase
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.state import StateManager, FileStateManager
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__ = ["AgentBase", "AgentServer", "SWMLService", "SWMLBuilder", "StateManager", "FileStateManager"]
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 logging
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
- # Set up logging
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) -> None:
302
+ def run(self, event=None, context=None, host: Optional[str] = None, port: Optional[int] = None) -> Any:
306
303
  """
307
- Start the server
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
- host: Optional host to override the default
311
- port: Optional port to override the default
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
- # Print server info
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
- self.logger.info(f"Starting server on {host}:{port}")
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: http://{host}:{port}{route}")
632
+ self.logger.info(f"URL: {agent_url}")
334
633
  self.logger.info(f"Basic Auth: {username}:{password}")
335
-
336
- # Start the server
337
- uvicorn.run(
338
- self.app,
339
- host=host,
340
- port=port,
341
- log_level=self.log_level
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)