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.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +176 -23
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +1225 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +825 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1134 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +167 -200
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +752 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +10 -1
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/METADATA +344 -215
- signalwire_agents-1.0.7.dist-info/RECORD +142 -0
- signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/top_level.txt +0 -0
signalwire_agents/__init__.py
CHANGED
|
@@ -18,41 +18,120 @@ A package for building AI agents using SignalWire's AI and SWML capabilities.
|
|
|
18
18
|
from .core.logging_config import configure_logging
|
|
19
19
|
configure_logging()
|
|
20
20
|
|
|
21
|
-
__version__ = "0.
|
|
21
|
+
__version__ = "1.0.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
|
|
35
|
-
|
|
34
|
+
# Import WebService for static file serving
|
|
35
|
+
from signalwire_agents.web import WebService
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
# Lazy import skills to avoid slow startup for CLI tools
|
|
38
|
+
# Skills are now loaded on-demand when requested
|
|
39
|
+
def _get_skill_registry():
|
|
40
|
+
"""Lazy import and return skill registry"""
|
|
41
|
+
import signalwire_agents.skills
|
|
42
|
+
return signalwire_agents.skills.skill_registry
|
|
43
|
+
|
|
44
|
+
# Lazy import convenience functions from the CLI (if available)
|
|
45
|
+
def start_agent(*args, **kwargs):
|
|
46
|
+
"""Start an agent (lazy import)"""
|
|
47
|
+
try:
|
|
48
|
+
from signalwire_agents.cli.helpers import start_agent as _start_agent
|
|
49
|
+
return _start_agent(*args, **kwargs)
|
|
50
|
+
except ImportError:
|
|
43
51
|
raise NotImplementedError("CLI helpers not available")
|
|
44
|
-
|
|
52
|
+
|
|
53
|
+
def run_agent(*args, **kwargs):
|
|
54
|
+
"""Run an agent (lazy import)"""
|
|
55
|
+
try:
|
|
56
|
+
from signalwire_agents.cli.helpers import run_agent as _run_agent
|
|
57
|
+
return _run_agent(*args, **kwargs)
|
|
58
|
+
except ImportError:
|
|
45
59
|
raise NotImplementedError("CLI helpers not available")
|
|
46
|
-
|
|
60
|
+
|
|
61
|
+
def list_skills(*args, **kwargs):
|
|
62
|
+
"""List available skills (lazy import)"""
|
|
63
|
+
try:
|
|
64
|
+
from signalwire_agents.cli.helpers import list_skills as _list_skills
|
|
65
|
+
return _list_skills(*args, **kwargs)
|
|
66
|
+
except ImportError:
|
|
47
67
|
raise NotImplementedError("CLI helpers not available")
|
|
48
68
|
|
|
69
|
+
def list_skills_with_params():
|
|
70
|
+
"""
|
|
71
|
+
Get complete schema for all available skills including parameter metadata
|
|
72
|
+
|
|
73
|
+
This function returns a comprehensive schema for all available skills,
|
|
74
|
+
including their metadata and parameter definitions. This is useful for
|
|
75
|
+
GUI configuration tools, API documentation, or programmatic skill discovery.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dict[str, Dict[str, Any]]: Complete skill schema where keys are skill names
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> schema = list_skills_with_params()
|
|
82
|
+
>>> print(schema['web_search']['parameters']['api_key'])
|
|
83
|
+
{
|
|
84
|
+
'type': 'string',
|
|
85
|
+
'description': 'Google Custom Search API key',
|
|
86
|
+
'required': True,
|
|
87
|
+
'hidden': True,
|
|
88
|
+
'env_var': 'GOOGLE_SEARCH_API_KEY'
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
from signalwire_agents.skills.registry import skill_registry
|
|
92
|
+
return skill_registry.get_all_skills_schema()
|
|
93
|
+
|
|
94
|
+
def register_skill(skill_class):
|
|
95
|
+
"""
|
|
96
|
+
Register a custom skill class
|
|
97
|
+
|
|
98
|
+
This allows third-party code to register skill classes directly without
|
|
99
|
+
requiring them to be in a specific directory structure.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
skill_class: A class that inherits from SkillBase
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> from my_custom_skills import MyWeatherSkill
|
|
106
|
+
>>> register_skill(MyWeatherSkill)
|
|
107
|
+
>>> # Now you can use it in agents:
|
|
108
|
+
>>> agent.add_skill('my_weather')
|
|
109
|
+
"""
|
|
110
|
+
from signalwire_agents.skills.registry import skill_registry
|
|
111
|
+
return skill_registry.register_skill(skill_class)
|
|
112
|
+
|
|
113
|
+
def add_skill_directory(path):
|
|
114
|
+
"""
|
|
115
|
+
Add a directory to search for skills
|
|
116
|
+
|
|
117
|
+
This allows third-party skill collections to be registered by path.
|
|
118
|
+
Skills in these directories should follow the same structure as built-in skills.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
path: Path to directory containing skill subdirectories
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> add_skill_directory('/opt/custom_skills')
|
|
125
|
+
>>> # Now agent.add_skill('my_custom_skill') will search in this directory
|
|
126
|
+
"""
|
|
127
|
+
from signalwire_agents.skills.registry import skill_registry
|
|
128
|
+
return skill_registry.add_skill_directory(path)
|
|
129
|
+
|
|
49
130
|
__all__ = [
|
|
50
131
|
"AgentBase",
|
|
51
132
|
"AgentServer",
|
|
52
133
|
"SWMLService",
|
|
53
134
|
"SWMLBuilder",
|
|
54
|
-
"StateManager",
|
|
55
|
-
"FileStateManager",
|
|
56
135
|
"SwaigFunctionResult",
|
|
57
136
|
"SWAIGFunction",
|
|
58
137
|
"DataMap",
|
|
@@ -62,7 +141,12 @@ __all__ = [
|
|
|
62
141
|
"Context",
|
|
63
142
|
"Step",
|
|
64
143
|
"create_simple_context",
|
|
144
|
+
"WebService",
|
|
65
145
|
"start_agent",
|
|
66
146
|
"run_agent",
|
|
67
|
-
"list_skills"
|
|
147
|
+
"list_skills",
|
|
148
|
+
"list_skills_with_params",
|
|
149
|
+
"register_skill",
|
|
150
|
+
"add_skill_directory",
|
|
151
|
+
"BedrockAgent"
|
|
68
152
|
]
|
|
@@ -11,6 +11,7 @@ See LICENSE file in the project root for full license information.
|
|
|
11
11
|
AgentServer - Class for hosting multiple SignalWire AI Agents in a single server
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import os
|
|
14
15
|
import re
|
|
15
16
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
|
16
17
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
632
|
+
self.logger.info(f"URL: {agent_url}")
|
|
587
633
|
self.logger.info(f"Basic Auth: {username}:{password}")
|
|
588
|
-
|
|
589
|
-
# Start the server
|
|
590
|
-
|
|
591
|
-
self.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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}')")
|