signalwire-agents 0.1.13__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 (138) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +176 -23
  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/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 +566 -2366
  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 +825 -2916
  40. signalwire_agents/core/auth_handler.py +233 -0
  41. signalwire_agents/core/config_loader.py +259 -0
  42. signalwire_agents/core/contexts.py +418 -0
  43. signalwire_agents/core/data_map.py +3 -15
  44. signalwire_agents/core/function_result.py +116 -44
  45. signalwire_agents/core/logging_config.py +162 -18
  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_config.py +333 -0
  56. signalwire_agents/core/skill_base.py +84 -1
  57. signalwire_agents/core/skill_manager.py +62 -20
  58. signalwire_agents/core/swaig_function.py +18 -5
  59. signalwire_agents/core/swml_builder.py +207 -11
  60. signalwire_agents/core/swml_handler.py +27 -21
  61. signalwire_agents/core/swml_renderer.py +123 -312
  62. signalwire_agents/core/swml_service.py +167 -200
  63. signalwire_agents/prefabs/concierge.py +0 -3
  64. signalwire_agents/prefabs/faq_bot.py +0 -3
  65. signalwire_agents/prefabs/info_gatherer.py +0 -3
  66. signalwire_agents/prefabs/receptionist.py +0 -3
  67. signalwire_agents/prefabs/survey.py +0 -3
  68. signalwire_agents/schema.json +9218 -5489
  69. signalwire_agents/search/__init__.py +7 -1
  70. signalwire_agents/search/document_processor.py +490 -31
  71. signalwire_agents/search/index_builder.py +307 -37
  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 +162 -31
  76. signalwire_agents/search/search_engine.py +916 -35
  77. signalwire_agents/search/search_service.py +376 -53
  78. signalwire_agents/skills/README.md +452 -0
  79. signalwire_agents/skills/__init__.py +10 -1
  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/skill.py +84 -3
  85. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  86. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  87. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  88. signalwire_agents/skills/datetime/README.md +132 -0
  89. signalwire_agents/skills/datetime/__init__.py +9 -0
  90. signalwire_agents/skills/datetime/skill.py +20 -7
  91. signalwire_agents/skills/joke/README.md +149 -0
  92. signalwire_agents/skills/joke/__init__.py +9 -0
  93. signalwire_agents/skills/joke/skill.py +21 -0
  94. signalwire_agents/skills/math/README.md +161 -0
  95. signalwire_agents/skills/math/__init__.py +9 -0
  96. signalwire_agents/skills/math/skill.py +18 -4
  97. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  98. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  99. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  100. signalwire_agents/skills/native_vector_search/README.md +210 -0
  101. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  102. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  103. signalwire_agents/skills/play_background_file/README.md +218 -0
  104. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  105. signalwire_agents/skills/play_background_file/skill.py +242 -0
  106. signalwire_agents/skills/registry.py +395 -40
  107. signalwire_agents/skills/spider/README.md +236 -0
  108. signalwire_agents/skills/spider/__init__.py +13 -0
  109. signalwire_agents/skills/spider/skill.py +598 -0
  110. signalwire_agents/skills/swml_transfer/README.md +395 -0
  111. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  112. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  113. signalwire_agents/skills/weather_api/README.md +178 -0
  114. signalwire_agents/skills/weather_api/__init__.py +12 -0
  115. signalwire_agents/skills/weather_api/skill.py +191 -0
  116. signalwire_agents/skills/web_search/README.md +163 -0
  117. signalwire_agents/skills/web_search/__init__.py +9 -0
  118. signalwire_agents/skills/web_search/skill.py +586 -112
  119. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  120. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  121. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  122. signalwire_agents/web/__init__.py +17 -0
  123. signalwire_agents/web/web_service.py +559 -0
  124. signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
  125. signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
  126. signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
  127. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/METADATA +344 -215
  128. signalwire_agents-1.0.7.dist-info/RECORD +142 -0
  129. signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
  130. signalwire_agents/core/state/file_state_manager.py +0 -219
  131. signalwire_agents/core/state/state_manager.py +0 -101
  132. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  133. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  134. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  135. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  136. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +0 -0
  137. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
  138. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.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.7"
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
 
@@ -548,11 +549,11 @@ class AgentServer:
548
549
  # This is a request to an agent's sub-path
549
550
  relative_path = full_path[len(route.lstrip("/")):]
550
551
  relative_path = relative_path.lstrip("/")
551
-
552
+
552
553
  # Route to appropriate handler based on path
553
554
  if not relative_path or relative_path == "/":
554
555
  return await agent._handle_root_request(request)
555
-
556
+
556
557
  clean_path = relative_path.rstrip("/")
557
558
  if clean_path == "debug":
558
559
  return await agent._handle_debug_request(request)
@@ -563,7 +564,7 @@ class AgentServer:
563
564
  return await agent._handle_post_prompt_request(request)
564
565
  elif clean_path == "check_for_input":
565
566
  return await agent._handle_check_for_input_request(request)
566
-
567
+
567
568
  # Check for custom routing callbacks
568
569
  if hasattr(agent, '_routing_callbacks'):
569
570
  for callback_path, callback_fn in agent._routing_callbacks.items():
@@ -571,36 +572,92 @@ class AgentServer:
571
572
  if clean_path == cb_path_clean:
572
573
  request.state.callback_path = callback_path
573
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")
574
595
 
575
- # No matching agent found
576
- return {"error": "Not Found"}
577
-
578
- # Print server info
596
+ # Set host and port
579
597
  host = host or self.host
580
598
  port = port or self.port
581
599
 
582
- 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}")
583
628
  for route, agent in self.agents.items():
584
629
  username, password = agent.get_basic_auth_credentials()
630
+ agent_url = agent.get_full_url(include_auth=False)
585
631
  self.logger.info(f"Agent '{agent.get_name()}' available at:")
586
- self.logger.info(f"URL: http://{host}:{port}{route}")
632
+ self.logger.info(f"URL: {agent_url}")
587
633
  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
- )
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
+ )
596
653
 
597
- 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]],
598
655
  path: str) -> None:
599
656
  """
600
657
  Register a routing callback across all agents
601
-
658
+
602
659
  This allows you to add unified routing logic to all agents at the same path.
603
-
660
+
604
661
  Args:
605
662
  callback_fn: The callback function to register
606
663
  path: The path to register the callback at
@@ -608,11 +665,107 @@ class AgentServer:
608
665
  # Normalize the path
609
666
  if not path.startswith("/"):
610
667
  path = f"/{path}"
611
-
668
+
612
669
  path = path.rstrip("/")
613
-
670
+
614
671
  # Register with all existing agents
615
672
  for agent in self.agents.values():
616
673
  agent.register_routing_callback(callback_fn, path=path)
617
-
674
+
618
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)
@@ -0,0 +1,296 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ """
11
+ Bedrock Agent - Amazon Bedrock voice-to-voice integration
12
+
13
+ This module provides BedrockAgent, which extends AgentBase to support
14
+ Amazon Bedrock's voice-to-voice model while maintaining compatibility
15
+ with all SignalWire agent features like skills, POM, and SWAIG functions.
16
+ """
17
+
18
+ import json
19
+ from typing import Dict, List, Any, Optional, Union
20
+ from signalwire_agents.core.agent_base import AgentBase
21
+ from signalwire_agents.core.logging_config import get_logger
22
+
23
+ logger = get_logger("bedrock_agent")
24
+
25
+
26
+ class BedrockAgent(AgentBase):
27
+ """
28
+ Agent implementation for Amazon Bedrock voice-to-voice model
29
+
30
+ This agent extends AgentBase to provide full compatibility with
31
+ SignalWire's agent ecosystem while using Amazon Bedrock as the
32
+ AI backend. It supports all standard agent features including:
33
+ - Prompt building with text and POM
34
+ - Skills and SWAIG functions
35
+ - Post-prompt functionality
36
+ - Dynamic configuration
37
+
38
+ The main difference from the standard agent is that it generates
39
+ SWML with the "amazon_bedrock" verb instead of "ai".
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ name: str = "bedrock_agent",
45
+ route: str = "/bedrock",
46
+ system_prompt: Optional[str] = None,
47
+ voice_id: str = "matthew",
48
+ temperature: float = 0.7,
49
+ top_p: float = 0.9,
50
+ max_tokens: int = 1024,
51
+ **kwargs
52
+ ):
53
+ """
54
+ Initialize BedrockAgent
55
+
56
+ Args:
57
+ name: Agent name
58
+ route: HTTP route for the agent
59
+ system_prompt: Initial system prompt (can be overridden with set_prompt)
60
+ voice_id: Bedrock voice ID (default: matthew)
61
+ temperature: Generation temperature (0-1)
62
+ top_p: Nucleus sampling parameter (0-1)
63
+ max_tokens: Maximum tokens to generate
64
+ **kwargs: Additional arguments passed to AgentBase
65
+ """
66
+ # Store Bedrock-specific parameters first
67
+ self._voice_id = voice_id
68
+ self._temperature = temperature
69
+ self._top_p = top_p
70
+ self._max_tokens = max_tokens
71
+
72
+ # Initialize base class
73
+ super().__init__(name=name, route=route, **kwargs)
74
+
75
+ # Set initial prompt if provided (after super init)
76
+ if system_prompt:
77
+ self.set_prompt_text(system_prompt)
78
+
79
+ logger.info(f"BedrockAgent initialized: {name} on route {route}")
80
+
81
+ def _render_swml(self, call_id: str = None, modifications: Optional[dict] = None) -> str:
82
+ """
83
+ Render SWML document with amazon_bedrock verb
84
+
85
+ This method overrides the base implementation to generate
86
+ SWML with the amazon_bedrock verb structure that matches
87
+ the ai verb structure for consistency.
88
+
89
+ Args:
90
+ call_id: Optional call ID for session-specific tokens
91
+ modifications: Optional dict of modifications to apply
92
+
93
+ Returns:
94
+ SWML document as JSON string with amazon_bedrock verb
95
+ """
96
+ # Call parent to build the base SWML with ai verb
97
+ base_swml_json = super()._render_swml(call_id, modifications)
98
+
99
+ # Parse the JSON to modify it
100
+ swml = json.loads(base_swml_json)
101
+
102
+ # Find and transform the ai verb to amazon_bedrock
103
+ sections = swml.get("sections", {})
104
+ main_section = sections.get("main", [])
105
+
106
+ # Look for ai verb and transform it
107
+ for i, verb in enumerate(main_section):
108
+ if "ai" in verb:
109
+ ai_config = verb["ai"]
110
+
111
+ # Build amazon_bedrock verb with same structure
112
+ bedrock_verb = {
113
+ "amazon_bedrock": {
114
+ # Add voice configuration and inference params inside prompt
115
+ # Note: In Bedrock, voice and inference params are part of prompt config
116
+ "prompt": self._add_voice_to_prompt(ai_config.get("prompt", {})),
117
+
118
+ # Copy SWAIG if present
119
+ "SWAIG": ai_config.get("SWAIG", {}),
120
+
121
+ # Include params only if they were explicitly set via set_params()
122
+ # The C++ code ignores params for now (marked for future extensibility)
123
+ "params": ai_config.get("params", {}),
124
+
125
+ # Copy global_data if present
126
+ "global_data": ai_config.get("global_data", {}),
127
+
128
+ # Copy post_prompt if present
129
+ "post_prompt": ai_config.get("post_prompt"),
130
+
131
+ # Copy post_prompt_url if present
132
+ "post_prompt_url": ai_config.get("post_prompt_url")
133
+ }
134
+ }
135
+
136
+ # Remove None values
137
+ bedrock_config = bedrock_verb["amazon_bedrock"]
138
+ bedrock_verb["amazon_bedrock"] = {
139
+ k: v for k, v in bedrock_config.items()
140
+ if v is not None
141
+ }
142
+
143
+ # Replace ai verb with amazon_bedrock verb
144
+ main_section[i] = bedrock_verb
145
+ break
146
+
147
+ # Convert back to JSON string
148
+ return json.dumps(swml)
149
+
150
+ def _add_voice_to_prompt(self, prompt_config: Dict[str, Any]) -> Dict[str, Any]:
151
+ """
152
+ Add voice configuration to the prompt object
153
+
154
+ In Bedrock, voice configuration is part of the prompt object,
155
+ not a separate field like in OpenAI.
156
+
157
+ Args:
158
+ prompt_config: Current prompt configuration
159
+
160
+ Returns:
161
+ Updated prompt configuration with voice
162
+ """
163
+ # Create a clean copy, filtering out text-model-specific parameters
164
+ # that don't apply to Bedrock's voice-to-voice model
165
+ filtered_config = {}
166
+
167
+ # Copy over only the relevant fields
168
+ for key, value in prompt_config.items():
169
+ # Skip text-model-specific parameters
170
+ if key in ['barge_confidence', 'presence_penalty', 'frequency_penalty']:
171
+ continue
172
+ filtered_config[key] = value
173
+
174
+ # Add voice_id to the prompt configuration
175
+ filtered_config["voice_id"] = self._voice_id
176
+
177
+ # Add/override inference parameters (where C code expects them)
178
+ filtered_config["temperature"] = self._temperature
179
+ filtered_config["top_p"] = self._top_p
180
+
181
+ return filtered_config
182
+
183
+ def _build_bedrock_params(self, base_params: Dict[str, Any]) -> Dict[str, Any]:
184
+ """
185
+ Build Bedrock-specific parameters
186
+
187
+ Merges base parameters with Bedrock-specific inference settings.
188
+
189
+ Args:
190
+ base_params: Base parameters from AgentBase
191
+
192
+ Returns:
193
+ Combined parameters for Bedrock
194
+ """
195
+ # Start with base params
196
+ params = base_params.copy()
197
+
198
+ # Add Bedrock inference parameters
199
+ params.update({
200
+ "temperature": self._temperature,
201
+ "top_p": self._top_p,
202
+ "max_tokens": self._max_tokens
203
+ })
204
+
205
+ return params
206
+
207
+ def set_voice(self, voice_id: str) -> None:
208
+ """
209
+ Set the Bedrock voice ID
210
+
211
+ Args:
212
+ voice_id: Bedrock voice identifier (e.g., 'matthew', 'joanna')
213
+ """
214
+ self._voice_id = voice_id
215
+ logger.debug(f"Voice set to: {voice_id}")
216
+
217
+ def set_inference_params(
218
+ self,
219
+ temperature: Optional[float] = None,
220
+ top_p: Optional[float] = None,
221
+ max_tokens: Optional[int] = None
222
+ ) -> None:
223
+ """
224
+ Update Bedrock inference parameters
225
+
226
+ Args:
227
+ temperature: Generation temperature (0-1)
228
+ top_p: Nucleus sampling parameter (0-1)
229
+ max_tokens: Maximum tokens to generate
230
+ """
231
+ if temperature is not None:
232
+ self._temperature = temperature
233
+ if top_p is not None:
234
+ self._top_p = top_p
235
+ if max_tokens is not None:
236
+ self._max_tokens = max_tokens
237
+
238
+ logger.debug(f"Inference params updated: temp={self._temperature}, "
239
+ f"top_p={self._top_p}, max_tokens={self._max_tokens}")
240
+
241
+ # Methods that may not be relevant to Bedrock
242
+ # These are overridden to provide appropriate behavior or warnings
243
+
244
+ def set_llm_model(self, model: str) -> None:
245
+ """
246
+ Set LLM model - not applicable for Bedrock
247
+
248
+ Bedrock uses a fixed voice-to-voice model, so this method
249
+ logs a warning and does nothing.
250
+
251
+ Args:
252
+ model: Model name (ignored)
253
+ """
254
+ logger.warning(f"set_llm_model('{model}') called but Bedrock uses a fixed voice-to-voice model")
255
+
256
+ def set_llm_temperature(self, temperature: float) -> None:
257
+ """
258
+ Set LLM temperature - redirects to set_inference_params
259
+
260
+ Args:
261
+ temperature: Temperature value
262
+ """
263
+ self.set_inference_params(temperature=temperature)
264
+
265
+ def set_post_prompt_llm_params(self, **params) -> None:
266
+ """
267
+ Set post-prompt LLM parameters - not applicable for Bedrock
268
+
269
+ Bedrock uses OpenAI for post-prompt summarization, but those
270
+ parameters are configured in the C code.
271
+
272
+ Args:
273
+ **params: Ignored parameters
274
+ """
275
+ logger.warning("set_post_prompt_llm_params() called but Bedrock post-prompt uses OpenAI configured in C code")
276
+
277
+ def set_prompt_llm_params(self, **params) -> None:
278
+ """
279
+ Set prompt LLM parameters - use set_inference_params instead
280
+
281
+ For Bedrock, use set_inference_params() to configure temperature,
282
+ top_p, and max_tokens.
283
+
284
+ Args:
285
+ **params: Parameters (ignored, use set_inference_params)
286
+ """
287
+ logger.warning("set_prompt_llm_params() called - use set_inference_params() for Bedrock")
288
+
289
+ # Note: We don't override prompt methods like set_prompt_text, set_prompt_pom
290
+ # because those work fine - they just build the prompt structure that we
291
+ # transform in _render_swml()
292
+
293
+ def __repr__(self) -> str:
294
+ """String representation of the agent"""
295
+ return (f"BedrockAgent(name='{self.name}', route='{self.route}', "
296
+ f"voice='{self._voice_id}')")