signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
|
@@ -24,51 +24,11 @@ import types
|
|
|
24
24
|
from typing import Dict, List, Any, Optional, Union, Callable, Tuple, Type
|
|
25
25
|
from urllib.parse import urlparse
|
|
26
26
|
|
|
27
|
-
# Import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if not hasattr(structlog, "_configured") or not structlog._configured:
|
|
33
|
-
structlog.configure(
|
|
34
|
-
processors=[
|
|
35
|
-
structlog.stdlib.filter_by_level,
|
|
36
|
-
structlog.stdlib.add_logger_name,
|
|
37
|
-
structlog.stdlib.add_log_level,
|
|
38
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
39
|
-
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
|
40
|
-
structlog.processors.StackInfoRenderer(),
|
|
41
|
-
structlog.processors.format_exc_info,
|
|
42
|
-
structlog.processors.UnicodeDecoder(),
|
|
43
|
-
structlog.dev.ConsoleRenderer()
|
|
44
|
-
],
|
|
45
|
-
context_class=dict,
|
|
46
|
-
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
47
|
-
wrapper_class=structlog.stdlib.BoundLogger,
|
|
48
|
-
cache_logger_on_first_use=True,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
# Set up root logger with structlog
|
|
52
|
-
logging.basicConfig(
|
|
53
|
-
format="%(message)s",
|
|
54
|
-
stream=sys.stdout,
|
|
55
|
-
level=logging.INFO,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# Mark as configured to avoid duplicate configuration
|
|
59
|
-
structlog._configured = True
|
|
60
|
-
|
|
61
|
-
# Create the module logger
|
|
62
|
-
logger = structlog.get_logger("swml_service")
|
|
63
|
-
|
|
64
|
-
except ImportError:
|
|
65
|
-
# Fallback to standard logging if structlog is not available
|
|
66
|
-
logging.basicConfig(
|
|
67
|
-
level=logging.INFO,
|
|
68
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
69
|
-
stream=sys.stdout
|
|
70
|
-
)
|
|
71
|
-
logger = logging.getLogger("swml_service")
|
|
27
|
+
# Import centralized logging system
|
|
28
|
+
from signalwire_agents.core.logging_config import get_logger
|
|
29
|
+
|
|
30
|
+
# Create the module logger using centralized system
|
|
31
|
+
logger = get_logger("swml_service")
|
|
72
32
|
|
|
73
33
|
try:
|
|
74
34
|
import fastapi
|
|
@@ -82,6 +42,7 @@ except ImportError:
|
|
|
82
42
|
|
|
83
43
|
from signalwire_agents.utils.schema_utils import SchemaUtils
|
|
84
44
|
from signalwire_agents.core.swml_handler import VerbHandlerRegistry, SWMLVerbHandler
|
|
45
|
+
from signalwire_agents.core.security_config import SecurityConfig
|
|
85
46
|
|
|
86
47
|
|
|
87
48
|
class SWMLService:
|
|
@@ -103,9 +64,10 @@ class SWMLService:
|
|
|
103
64
|
name: str,
|
|
104
65
|
route: str = "/",
|
|
105
66
|
host: str = "0.0.0.0",
|
|
106
|
-
port: int =
|
|
67
|
+
port: Optional[int] = None,
|
|
107
68
|
basic_auth: Optional[Tuple[str, str]] = None,
|
|
108
|
-
schema_path: Optional[str] = None
|
|
69
|
+
schema_path: Optional[str] = None,
|
|
70
|
+
config_file: Optional[str] = None
|
|
109
71
|
):
|
|
110
72
|
"""
|
|
111
73
|
Initialize a new SWML service
|
|
@@ -117,40 +79,44 @@ class SWMLService:
|
|
|
117
79
|
port: Port to bind the web server to
|
|
118
80
|
basic_auth: Optional (username, password) tuple for basic auth
|
|
119
81
|
schema_path: Optional path to the schema file
|
|
82
|
+
config_file: Optional path to configuration file
|
|
120
83
|
"""
|
|
121
84
|
self.name = name
|
|
122
85
|
self.route = route.rstrip("/") # Ensure no trailing slash
|
|
123
86
|
self.host = host
|
|
124
|
-
|
|
125
|
-
self.
|
|
126
|
-
|
|
87
|
+
# Use provided port, or PORT env var, or default to 3000
|
|
88
|
+
self.port = port if port is not None else int(os.environ.get("PORT", 3000))
|
|
89
|
+
|
|
90
|
+
# Initialize logger for this instance FIRST before using it
|
|
91
|
+
self.log = logger.bind(service=name)
|
|
92
|
+
|
|
93
|
+
# Load unified security configuration with optional config file
|
|
94
|
+
self.security = SecurityConfig(config_file=config_file, service_name=name)
|
|
95
|
+
self.security.log_config("SWMLService")
|
|
96
|
+
|
|
97
|
+
# For backward compatibility, expose SSL settings as instance attributes
|
|
98
|
+
self.ssl_enabled = self.security.ssl_enabled
|
|
99
|
+
self.domain = self.security.domain
|
|
100
|
+
self.ssl_cert_path = self.security.ssl_cert_path
|
|
101
|
+
self.ssl_key_path = self.security.ssl_key_path
|
|
127
102
|
|
|
128
103
|
# Initialize proxy detection attributes
|
|
129
104
|
self._proxy_url_base = os.environ.get('SWML_PROXY_URL_BASE')
|
|
105
|
+
self._proxy_url_base_from_env = bool(self._proxy_url_base) # Track if it came from environment
|
|
106
|
+
if self._proxy_url_base:
|
|
107
|
+
self.log.warning("SWML_PROXY_URL_BASE is set in environment - This overrides SSL configuration and port settings. Remove this variable to use automatic detection.",
|
|
108
|
+
proxy_url_base=self._proxy_url_base)
|
|
130
109
|
self._proxy_detection_done = False
|
|
131
110
|
self._proxy_debug = os.environ.get('SWML_PROXY_DEBUG', '').lower() in ('true', '1', 'yes')
|
|
132
|
-
|
|
133
|
-
# Initialize logger for this instance
|
|
134
|
-
self.log = logger.bind(service=name)
|
|
135
|
-
self.log.info("service_initializing", route=self.route, host=host, port=port)
|
|
111
|
+
self.log.info("service_initializing", route=self.route, host=self.host, port=self.port)
|
|
136
112
|
|
|
137
113
|
# Set basic auth credentials
|
|
138
114
|
if basic_auth is not None:
|
|
139
115
|
# Use provided credentials
|
|
140
116
|
self._basic_auth = basic_auth
|
|
141
117
|
else:
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
env_pass = os.environ.get('SWML_BASIC_AUTH_PASSWORD')
|
|
145
|
-
|
|
146
|
-
if env_user and env_pass:
|
|
147
|
-
# Use environment variables
|
|
148
|
-
self._basic_auth = (env_user, env_pass)
|
|
149
|
-
else:
|
|
150
|
-
# Generate random credentials as fallback
|
|
151
|
-
username = f"user_{secrets.token_hex(4)}"
|
|
152
|
-
password = secrets.token_urlsafe(16)
|
|
153
|
-
self._basic_auth = (username, password)
|
|
118
|
+
# Use unified security config for auth credentials
|
|
119
|
+
self._basic_auth = self.security.get_basic_auth()
|
|
154
120
|
|
|
155
121
|
# Find the schema file if not provided
|
|
156
122
|
if schema_path is None:
|
|
@@ -687,10 +653,8 @@ class SWMLService:
|
|
|
687
653
|
Returns:
|
|
688
654
|
Response with SWML document or error
|
|
689
655
|
"""
|
|
690
|
-
#
|
|
691
|
-
|
|
692
|
-
self._detect_proxy_from_request(request)
|
|
693
|
-
self._proxy_detection_done = True
|
|
656
|
+
# Always detect proxy from current request - allows mixing direct and proxied access
|
|
657
|
+
self._detect_proxy_from_request(request)
|
|
694
658
|
|
|
695
659
|
# Check auth
|
|
696
660
|
if not self._check_basic_auth(request):
|
|
@@ -789,21 +753,21 @@ class SWMLService:
|
|
|
789
753
|
"""
|
|
790
754
|
import uvicorn
|
|
791
755
|
|
|
792
|
-
# Store SSL configuration
|
|
793
|
-
|
|
794
|
-
|
|
756
|
+
# Store SSL configuration (override environment if explicitly provided)
|
|
757
|
+
if ssl_enabled is not None:
|
|
758
|
+
self.ssl_enabled = ssl_enabled
|
|
759
|
+
if domain is not None:
|
|
760
|
+
self.domain = domain
|
|
795
761
|
|
|
796
|
-
# Set SSL paths
|
|
797
|
-
ssl_cert_path = ssl_cert
|
|
798
|
-
ssl_key_path = ssl_key
|
|
762
|
+
# Set SSL paths (use provided paths or fall back to environment)
|
|
763
|
+
ssl_cert_path = ssl_cert or getattr(self, 'ssl_cert_path', None)
|
|
764
|
+
ssl_key_path = ssl_key or getattr(self, 'ssl_key_path', None)
|
|
799
765
|
|
|
800
766
|
# Validate SSL configuration if enabled
|
|
801
767
|
if self.ssl_enabled:
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
self.
|
|
805
|
-
elif not ssl_key_path or not os.path.exists(ssl_key_path):
|
|
806
|
-
self.log.warning("ssl_key_not_found", path=ssl_key_path)
|
|
768
|
+
is_valid, error = self.security.validate_ssl_config()
|
|
769
|
+
if not is_valid:
|
|
770
|
+
self.log.warning("ssl_config_invalid", error=error)
|
|
807
771
|
self.ssl_enabled = False
|
|
808
772
|
elif not self.domain:
|
|
809
773
|
self.log.warning("ssl_domain_not_specified")
|
|
@@ -818,8 +782,11 @@ class SWMLService:
|
|
|
818
782
|
# This avoids the FastAPI error about prefixes ending with slashes
|
|
819
783
|
normalized_route = "/" + self.route.strip("/")
|
|
820
784
|
|
|
821
|
-
# Include router with the normalized prefix
|
|
822
|
-
|
|
785
|
+
# Include router with the normalized prefix (handle root route special case)
|
|
786
|
+
if normalized_route == "/":
|
|
787
|
+
app.include_router(router)
|
|
788
|
+
else:
|
|
789
|
+
app.include_router(router, prefix=normalized_route)
|
|
823
790
|
|
|
824
791
|
# Add a catch-all route handler that will handle both /path and /path/ formats
|
|
825
792
|
# This provides the same behavior without using a trailing slash in the prefix
|
|
@@ -876,27 +843,27 @@ class SWMLService:
|
|
|
876
843
|
# Get the auth credentials
|
|
877
844
|
username, password = self._basic_auth
|
|
878
845
|
|
|
879
|
-
#
|
|
880
|
-
|
|
881
|
-
display_host = self.domain if self.ssl_enabled and self.domain else f"{host}:{port}"
|
|
846
|
+
# Get the proper URL using unified URL building
|
|
847
|
+
startup_url = self._build_full_url(include_auth=False)
|
|
882
848
|
|
|
883
849
|
self.log.info("starting_server",
|
|
884
|
-
url=
|
|
850
|
+
url=startup_url,
|
|
885
851
|
ssl_enabled=self.ssl_enabled,
|
|
886
852
|
username=username,
|
|
887
853
|
password_length=len(password))
|
|
888
854
|
|
|
889
855
|
# Print user-friendly startup message (keep for UX)
|
|
890
856
|
print(f"Service '{self.name}' is available at:")
|
|
891
|
-
print(f"URL: {
|
|
892
|
-
print(f"URL with trailing slash: {
|
|
857
|
+
print(f"URL: {startup_url}")
|
|
858
|
+
print(f"URL with trailing slash: {startup_url}/")
|
|
893
859
|
print(f"Basic Auth: {username}:{password}")
|
|
894
860
|
|
|
895
861
|
# Check if SIP routing is enabled and log additional info
|
|
896
862
|
if self._routing_callbacks:
|
|
897
863
|
print(f"Callback endpoints:")
|
|
898
864
|
for path in self._routing_callbacks:
|
|
899
|
-
|
|
865
|
+
callback_url = self._build_full_url(endpoint=path.lstrip('/'), include_auth=False)
|
|
866
|
+
print(f" {callback_url}")
|
|
900
867
|
|
|
901
868
|
# Start uvicorn with or without SSL
|
|
902
869
|
if self.ssl_enabled and ssl_cert_path and ssl_key_path:
|
|
@@ -972,149 +939,146 @@ class SWMLService:
|
|
|
972
939
|
|
|
973
940
|
return username, password
|
|
974
941
|
|
|
975
|
-
# Keep the existing methods for backward compatibility
|
|
976
|
-
|
|
977
|
-
def add_answer_verb(self, max_duration: Optional[int] = None, codecs: Optional[str] = None) -> bool:
|
|
978
|
-
"""
|
|
979
|
-
Add an answer verb to the current document
|
|
980
942
|
|
|
981
|
-
|
|
982
|
-
max_duration: Maximum duration in seconds
|
|
983
|
-
codecs: Comma-separated list of codecs
|
|
984
|
-
|
|
985
|
-
Returns:
|
|
986
|
-
True if added successfully, False otherwise
|
|
943
|
+
def _get_base_url(self, include_auth: bool = True) -> str:
|
|
987
944
|
"""
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
config["codecs"] = codecs
|
|
993
|
-
|
|
994
|
-
return self.add_verb("answer", config)
|
|
995
|
-
|
|
996
|
-
def add_hangup_verb(self, reason: Optional[str] = None) -> bool:
|
|
997
|
-
"""
|
|
998
|
-
Add a hangup verb to the current document
|
|
945
|
+
Get the base URL for this service, using proxy info if available or falling back to configured values
|
|
946
|
+
|
|
947
|
+
This is the central method for URL building that handles both startup configuration
|
|
948
|
+
and per-request proxy detection.
|
|
999
949
|
|
|
1000
950
|
Args:
|
|
1001
|
-
|
|
951
|
+
include_auth: Whether to include authentication credentials in the URL
|
|
1002
952
|
|
|
1003
953
|
Returns:
|
|
1004
|
-
|
|
954
|
+
Base URL string (protocol://[auth@]host[:port])
|
|
1005
955
|
"""
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
956
|
+
# Debug logging to understand state
|
|
957
|
+
self.log.debug("_get_base_url called",
|
|
958
|
+
has_proxy_url_base=hasattr(self, '_proxy_url_base'),
|
|
959
|
+
proxy_url_base=getattr(self, '_proxy_url_base', None),
|
|
960
|
+
proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False),
|
|
961
|
+
env_var=os.environ.get('SWML_PROXY_URL_BASE'),
|
|
962
|
+
include_auth=include_auth,
|
|
963
|
+
caller=inspect.stack()[1].function if len(inspect.stack()) > 1 else "unknown")
|
|
964
|
+
|
|
965
|
+
# Check if we have proxy information from a request
|
|
966
|
+
if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
|
|
967
|
+
base = self._proxy_url_base.rstrip('/')
|
|
968
|
+
self.log.debug("Using proxy URL base", proxy_url_base=base)
|
|
969
|
+
|
|
970
|
+
# Add auth credentials if requested
|
|
971
|
+
if include_auth:
|
|
972
|
+
username, password = self._basic_auth
|
|
973
|
+
url = urlparse(base)
|
|
974
|
+
base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
|
|
1009
975
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
def add_ai_verb(self,
|
|
1013
|
-
prompt_text: Optional[str] = None,
|
|
1014
|
-
prompt_pom: Optional[List[Dict[str, Any]]] = None,
|
|
1015
|
-
post_prompt: Optional[str] = None,
|
|
1016
|
-
post_prompt_url: Optional[str] = None,
|
|
1017
|
-
swaig: Optional[Dict[str, Any]] = None,
|
|
1018
|
-
**kwargs) -> bool:
|
|
1019
|
-
"""
|
|
1020
|
-
Add an AI verb to the current document
|
|
976
|
+
return base
|
|
1021
977
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
post_prompt: Post-prompt text
|
|
1026
|
-
post_prompt_url: Post-prompt URL
|
|
1027
|
-
swaig: SWAIG configuration
|
|
1028
|
-
**kwargs: Additional parameters
|
|
1029
|
-
|
|
1030
|
-
Returns:
|
|
1031
|
-
True if added successfully, False otherwise
|
|
1032
|
-
"""
|
|
1033
|
-
config = {}
|
|
978
|
+
# No proxy, use configured values
|
|
979
|
+
# Determine protocol based on SSL settings
|
|
980
|
+
protocol = "https" if self.ssl_enabled else "http"
|
|
1034
981
|
|
|
1035
|
-
#
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
982
|
+
# Debug logging
|
|
983
|
+
self.log.debug("_get_base_url",
|
|
984
|
+
ssl_enabled=self.ssl_enabled,
|
|
985
|
+
domain=self.domain,
|
|
986
|
+
port=self.port,
|
|
987
|
+
protocol=protocol)
|
|
988
|
+
|
|
989
|
+
# Determine host part
|
|
990
|
+
if self.ssl_enabled and self.domain:
|
|
991
|
+
# Use domain for SSL
|
|
992
|
+
if protocol == "https" and self.port == 443:
|
|
993
|
+
host_part = self.domain # Don't include port for standard HTTPS
|
|
994
|
+
elif protocol == "http" and self.port == 80:
|
|
995
|
+
host_part = self.domain # Don't include port for standard HTTP
|
|
996
|
+
else:
|
|
997
|
+
host_part = f"{self.domain}:{self.port}"
|
|
998
|
+
self.log.debug("Using domain with port", domain=self.domain, port=self.port, host_part=host_part)
|
|
999
|
+
else:
|
|
1000
|
+
# Use configured host
|
|
1001
|
+
if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
|
|
1002
|
+
host = "localhost"
|
|
1003
|
+
else:
|
|
1004
|
+
host = self.host
|
|
1040
1005
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1006
|
+
# Include port unless it's the standard port for the protocol
|
|
1007
|
+
if (protocol == "https" and self.port == 443) or (protocol == "http" and self.port == 80):
|
|
1008
|
+
host_part = host
|
|
1009
|
+
else:
|
|
1010
|
+
host_part = f"{host}:{self.port}"
|
|
1044
1011
|
|
|
1045
|
-
#
|
|
1046
|
-
if
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
config["SWAIG"] = swaig
|
|
1052
|
-
|
|
1053
|
-
# Handle additional parameters
|
|
1054
|
-
for key, value in kwargs.items():
|
|
1055
|
-
if value is not None:
|
|
1056
|
-
config[key] = value
|
|
1057
|
-
|
|
1058
|
-
return self.add_verb("ai", config)
|
|
1012
|
+
# Build base URL
|
|
1013
|
+
if include_auth:
|
|
1014
|
+
username, password = self._basic_auth
|
|
1015
|
+
base = f"{protocol}://{username}:{password}@{host_part}"
|
|
1016
|
+
else:
|
|
1017
|
+
base = f"{protocol}://{host_part}"
|
|
1059
1018
|
|
|
1060
|
-
|
|
1019
|
+
return base
|
|
1020
|
+
|
|
1021
|
+
def _build_full_url(self, endpoint: str = "", include_auth: bool = True, query_params: Optional[Dict[str, str]] = None) -> str:
|
|
1061
1022
|
"""
|
|
1062
|
-
|
|
1023
|
+
Build the full URL for this service or a specific endpoint
|
|
1024
|
+
|
|
1025
|
+
This is the internal implementation used by both get_full_url (for AgentBase compatibility)
|
|
1026
|
+
and _build_webhook_url.
|
|
1063
1027
|
|
|
1064
1028
|
Args:
|
|
1065
|
-
endpoint:
|
|
1029
|
+
endpoint: Optional endpoint path (e.g., "swaig", "post_prompt")
|
|
1030
|
+
include_auth: Whether to include authentication credentials in the URL
|
|
1066
1031
|
query_params: Optional query parameters to append
|
|
1067
1032
|
|
|
1068
1033
|
Returns:
|
|
1069
|
-
|
|
1034
|
+
Full URL string
|
|
1070
1035
|
"""
|
|
1071
|
-
#
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
#
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1036
|
+
# Get base URL using central method
|
|
1037
|
+
base = self._get_base_url(include_auth=include_auth)
|
|
1038
|
+
|
|
1039
|
+
# Build path
|
|
1040
|
+
if endpoint:
|
|
1041
|
+
# Ensure endpoint doesn't start with slash
|
|
1042
|
+
endpoint = endpoint.lstrip('/')
|
|
1043
|
+
# Add trailing slash to endpoint to prevent redirects
|
|
1044
|
+
if not endpoint.endswith('/'):
|
|
1045
|
+
endpoint = f"{endpoint}/"
|
|
1046
|
+
path = f"{self.route}/{endpoint}"
|
|
1080
1047
|
else:
|
|
1081
|
-
#
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
# Use domain if available and SSL is enabled
|
|
1085
|
-
if getattr(self, 'ssl_enabled', False) and getattr(self, 'domain', None):
|
|
1086
|
-
host_part = self.domain
|
|
1087
|
-
else:
|
|
1088
|
-
# For local URLs
|
|
1089
|
-
if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
|
|
1090
|
-
host = "localhost"
|
|
1091
|
-
else:
|
|
1092
|
-
host = self.host
|
|
1093
|
-
|
|
1094
|
-
host_part = f"{host}:{self.port}"
|
|
1095
|
-
|
|
1096
|
-
# Always include auth credentials
|
|
1097
|
-
username, password = self._basic_auth
|
|
1098
|
-
base = f"{protocol}://{username}:{password}@{host_part}"
|
|
1048
|
+
# Just the route itself
|
|
1049
|
+
path = self.route if self.route != "/" else ""
|
|
1099
1050
|
|
|
1100
|
-
# Ensure the endpoint has a trailing slash to prevent redirects
|
|
1101
|
-
if endpoint and not endpoint.endswith('/'):
|
|
1102
|
-
endpoint = f"{endpoint}/"
|
|
1103
|
-
|
|
1104
|
-
# Simple path - use the route directly with the endpoint
|
|
1105
|
-
path = f"{self.route}/{endpoint}"
|
|
1106
|
-
|
|
1107
1051
|
# Construct full URL
|
|
1108
1052
|
url = f"{base}{path}"
|
|
1109
1053
|
|
|
1110
|
-
# Add query parameters if any
|
|
1054
|
+
# Add query parameters if any
|
|
1111
1055
|
if query_params:
|
|
1112
1056
|
filtered_params = {k: v for k, v in query_params.items() if v}
|
|
1113
1057
|
if filtered_params:
|
|
1114
1058
|
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
|
1115
1059
|
url = f"{url}?{params}"
|
|
1060
|
+
|
|
1061
|
+
return url
|
|
1062
|
+
|
|
1063
|
+
def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
|
|
1064
|
+
"""
|
|
1065
|
+
Helper method to build webhook URLs consistently
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
endpoint: The endpoint path (e.g., "swaig", "post_prompt")
|
|
1069
|
+
query_params: Optional query parameters to append
|
|
1116
1070
|
|
|
1117
|
-
|
|
1071
|
+
Returns:
|
|
1072
|
+
Fully constructed webhook URL
|
|
1073
|
+
"""
|
|
1074
|
+
self.log.debug("_build_webhook_url called",
|
|
1075
|
+
endpoint=endpoint,
|
|
1076
|
+
query_params=query_params,
|
|
1077
|
+
proxy_url_base=getattr(self, '_proxy_url_base', None),
|
|
1078
|
+
proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False))
|
|
1079
|
+
|
|
1080
|
+
# Use the central URL building method
|
|
1081
|
+
return self._build_full_url(endpoint=endpoint, include_auth=True, query_params=query_params)
|
|
1118
1082
|
|
|
1119
1083
|
def _detect_proxy_from_request(self, request: Request) -> None:
|
|
1120
1084
|
"""
|
|
@@ -1124,6 +1088,10 @@ class SWMLService:
|
|
|
1124
1088
|
Args:
|
|
1125
1089
|
request: FastAPI Request object
|
|
1126
1090
|
"""
|
|
1091
|
+
# If SWML_PROXY_URL_BASE was already set (e.g., from environment), don't override it
|
|
1092
|
+
if self._proxy_url_base:
|
|
1093
|
+
return
|
|
1094
|
+
|
|
1127
1095
|
# First check for standard X-Forwarded headers (used by most proxies including ngrok)
|
|
1128
1096
|
forwarded_host = request.headers.get("X-Forwarded-Host")
|
|
1129
1097
|
forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
|
|
@@ -1208,4 +1176,4 @@ class SWMLService:
|
|
|
1208
1176
|
if proxy_url:
|
|
1209
1177
|
self._proxy_url_base = proxy_url.rstrip('/')
|
|
1210
1178
|
self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
|
|
1211
|
-
self._proxy_detection_done = True
|
|
1179
|
+
self._proxy_detection_done = True
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Copyright (c) 2025 SignalWire
|
|
4
|
+
|
|
5
|
+
This file is part of the SignalWire AI Agents SDK.
|
|
6
|
+
|
|
7
|
+
Licensed under the MIT License.
|
|
8
|
+
See LICENSE file in the project root for full license information.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
MCP-SWAIG Gateway Package
|
|
13
|
+
|
|
14
|
+
HTTP/HTTPS server that bridges MCP servers with SignalWire SWAIG functions.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .gateway_service import MCPGateway, main
|
|
18
|
+
from .session_manager import Session, SessionManager
|
|
19
|
+
from .mcp_manager import MCPService, MCPClient, MCPManager
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
'MCPGateway',
|
|
23
|
+
'main',
|
|
24
|
+
'Session',
|
|
25
|
+
'SessionManager',
|
|
26
|
+
'MCPService',
|
|
27
|
+
'MCPClient',
|
|
28
|
+
'MCPManager',
|
|
29
|
+
]
|