signalwire-agents 0.1.6__py3-none-any.whl → 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- signalwire_agents/__init__.py +130 -4
- signalwire_agents/agent_server.py +438 -32
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +18 -0
- signalwire_agents/cli/build_search.py +1367 -0
- 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 +809 -0
- 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 +959 -2166
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +707 -0
- signalwire_agents/core/data_map.py +487 -0
- signalwire_agents/core/function_result.py +1150 -1
- signalwire_agents/core/logging_config.py +376 -0
- 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/session_manager.py +174 -86
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +200 -0
- signalwire_agents/core/skill_manager.py +244 -0
- signalwire_agents/core/swaig_function.py +33 -9
- signalwire_agents/core/swml_builder.py +212 -12
- signalwire_agents/core/swml_handler.py +43 -13
- signalwire_agents/core/swml_renderer.py +123 -297
- signalwire_agents/core/swml_service.py +277 -260
- signalwire_agents/prefabs/concierge.py +6 -2
- signalwire_agents/prefabs/info_gatherer.py +149 -33
- signalwire_agents/prefabs/receptionist.py +14 -22
- signalwire_agents/prefabs/survey.py +6 -2
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +137 -0
- signalwire_agents/search/document_processor.py +1223 -0
- signalwire_agents/search/index_builder.py +804 -0
- 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 +502 -0
- signalwire_agents/search/search_engine.py +1264 -0
- signalwire_agents/search/search_service.py +574 -0
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +23 -0
- 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/__init__.py +12 -0
- signalwire_agents/skills/datasphere/skill.py +310 -0
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +10 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +237 -0
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +10 -0
- signalwire_agents/skills/datetime/skill.py +126 -0
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +10 -0
- signalwire_agents/skills/joke/skill.py +109 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +10 -0
- signalwire_agents/skills/math/skill.py +105 -0
- 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 +10 -0
- signalwire_agents/skills/native_vector_search/skill.py +820 -0
- 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 +459 -0
- 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 +10 -0
- signalwire_agents/skills/web_search/skill.py +739 -0
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/wikipedia_search/skill.py +210 -0
- signalwire_agents/utils/__init__.py +14 -0
- signalwire_agents/utils/schema_utils.py +111 -44
- 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-1.0.7.dist-info/METADATA +992 -0
- signalwire_agents-1.0.7.dist-info/RECORD +142 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +1 -1
- 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-0.1.6.data/data/schema.json +0 -5611
- signalwire_agents-0.1.6.dist-info/METADATA +0 -199
- signalwire_agents-0.1.6.dist-info/RECORD +0 -34
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
1
2
|
"""
|
|
2
3
|
Copyright (c) 2025 SignalWire
|
|
3
4
|
|
|
@@ -7,11 +8,9 @@ Licensed under the MIT License.
|
|
|
7
8
|
See LICENSE file in the project root for full license information.
|
|
8
9
|
"""
|
|
9
10
|
|
|
11
|
+
# -*- coding: utf-8 -*-
|
|
10
12
|
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
This class provides the foundation for creating and serving SWML documents.
|
|
14
|
-
It handles schema validation, document creation, and web service functionality.
|
|
13
|
+
Base SWML Service for SignalWire Agents
|
|
15
14
|
"""
|
|
16
15
|
|
|
17
16
|
import os
|
|
@@ -25,51 +24,11 @@ import types
|
|
|
25
24
|
from typing import Dict, List, Any, Optional, Union, Callable, Tuple, Type
|
|
26
25
|
from urllib.parse import urlparse
|
|
27
26
|
|
|
28
|
-
# Import
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if not hasattr(structlog, "_configured") or not structlog._configured:
|
|
34
|
-
structlog.configure(
|
|
35
|
-
processors=[
|
|
36
|
-
structlog.stdlib.filter_by_level,
|
|
37
|
-
structlog.stdlib.add_logger_name,
|
|
38
|
-
structlog.stdlib.add_log_level,
|
|
39
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
40
|
-
structlog.processors.TimeStamper(fmt="iso"),
|
|
41
|
-
structlog.processors.StackInfoRenderer(),
|
|
42
|
-
structlog.processors.format_exc_info,
|
|
43
|
-
structlog.processors.UnicodeDecoder(),
|
|
44
|
-
structlog.processors.JSONRenderer()
|
|
45
|
-
],
|
|
46
|
-
context_class=dict,
|
|
47
|
-
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
48
|
-
wrapper_class=structlog.stdlib.BoundLogger,
|
|
49
|
-
cache_logger_on_first_use=True,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
# Set up root logger with structlog
|
|
53
|
-
logging.basicConfig(
|
|
54
|
-
format="%(message)s",
|
|
55
|
-
stream=sys.stdout,
|
|
56
|
-
level=logging.INFO,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Mark as configured to avoid duplicate configuration
|
|
60
|
-
structlog._configured = True
|
|
61
|
-
|
|
62
|
-
# Create the module logger
|
|
63
|
-
logger = structlog.get_logger("swml_service")
|
|
64
|
-
|
|
65
|
-
except ImportError:
|
|
66
|
-
# Fallback to standard logging if structlog is not available
|
|
67
|
-
logging.basicConfig(
|
|
68
|
-
level=logging.INFO,
|
|
69
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
70
|
-
stream=sys.stdout
|
|
71
|
-
)
|
|
72
|
-
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")
|
|
73
32
|
|
|
74
33
|
try:
|
|
75
34
|
import fastapi
|
|
@@ -83,6 +42,7 @@ except ImportError:
|
|
|
83
42
|
|
|
84
43
|
from signalwire_agents.utils.schema_utils import SchemaUtils
|
|
85
44
|
from signalwire_agents.core.swml_handler import VerbHandlerRegistry, SWMLVerbHandler
|
|
45
|
+
from signalwire_agents.core.security_config import SecurityConfig
|
|
86
46
|
|
|
87
47
|
|
|
88
48
|
class SWMLService:
|
|
@@ -106,7 +66,8 @@ class SWMLService:
|
|
|
106
66
|
host: str = "0.0.0.0",
|
|
107
67
|
port: int = 3000,
|
|
108
68
|
basic_auth: Optional[Tuple[str, str]] = None,
|
|
109
|
-
schema_path: Optional[str] = None
|
|
69
|
+
schema_path: Optional[str] = None,
|
|
70
|
+
config_file: Optional[str] = None
|
|
110
71
|
):
|
|
111
72
|
"""
|
|
112
73
|
Initialize a new SWML service
|
|
@@ -118,21 +79,34 @@ class SWMLService:
|
|
|
118
79
|
port: Port to bind the web server to
|
|
119
80
|
basic_auth: Optional (username, password) tuple for basic auth
|
|
120
81
|
schema_path: Optional path to the schema file
|
|
82
|
+
config_file: Optional path to configuration file
|
|
121
83
|
"""
|
|
122
84
|
self.name = name
|
|
123
85
|
self.route = route.rstrip("/") # Ensure no trailing slash
|
|
124
86
|
self.host = host
|
|
125
87
|
self.port = port
|
|
126
|
-
|
|
127
|
-
|
|
88
|
+
|
|
89
|
+
# Initialize logger for this instance FIRST before using it
|
|
90
|
+
self.log = logger.bind(service=name)
|
|
91
|
+
|
|
92
|
+
# Load unified security configuration with optional config file
|
|
93
|
+
self.security = SecurityConfig(config_file=config_file, service_name=name)
|
|
94
|
+
self.security.log_config("SWMLService")
|
|
95
|
+
|
|
96
|
+
# For backward compatibility, expose SSL settings as instance attributes
|
|
97
|
+
self.ssl_enabled = self.security.ssl_enabled
|
|
98
|
+
self.domain = self.security.domain
|
|
99
|
+
self.ssl_cert_path = self.security.ssl_cert_path
|
|
100
|
+
self.ssl_key_path = self.security.ssl_key_path
|
|
128
101
|
|
|
129
102
|
# Initialize proxy detection attributes
|
|
130
103
|
self._proxy_url_base = os.environ.get('SWML_PROXY_URL_BASE')
|
|
104
|
+
self._proxy_url_base_from_env = bool(self._proxy_url_base) # Track if it came from environment
|
|
105
|
+
if self._proxy_url_base:
|
|
106
|
+
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.",
|
|
107
|
+
proxy_url_base=self._proxy_url_base)
|
|
131
108
|
self._proxy_detection_done = False
|
|
132
109
|
self._proxy_debug = os.environ.get('SWML_PROXY_DEBUG', '').lower() in ('true', '1', 'yes')
|
|
133
|
-
|
|
134
|
-
# Initialize logger for this instance
|
|
135
|
-
self.log = logger.bind(service=name)
|
|
136
110
|
self.log.info("service_initializing", route=self.route, host=host, port=port)
|
|
137
111
|
|
|
138
112
|
# Set basic auth credentials
|
|
@@ -140,18 +114,8 @@ class SWMLService:
|
|
|
140
114
|
# Use provided credentials
|
|
141
115
|
self._basic_auth = basic_auth
|
|
142
116
|
else:
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
env_pass = os.environ.get('SWML_BASIC_AUTH_PASSWORD')
|
|
146
|
-
|
|
147
|
-
if env_user and env_pass:
|
|
148
|
-
# Use environment variables
|
|
149
|
-
self._basic_auth = (env_user, env_pass)
|
|
150
|
-
else:
|
|
151
|
-
# Generate random credentials as fallback
|
|
152
|
-
username = f"user_{secrets.token_hex(4)}"
|
|
153
|
-
password = secrets.token_urlsafe(16)
|
|
154
|
-
self._basic_auth = (username, password)
|
|
117
|
+
# Use unified security config for auth credentials
|
|
118
|
+
self._basic_auth = self.security.get_basic_auth()
|
|
155
119
|
|
|
156
120
|
# Find the schema file if not provided
|
|
157
121
|
if schema_path is None:
|
|
@@ -188,17 +152,21 @@ class SWMLService:
|
|
|
188
152
|
"""
|
|
189
153
|
Create auto-vivified methods for all verbs at initialization time
|
|
190
154
|
"""
|
|
191
|
-
|
|
155
|
+
self.log.debug("creating_verb_methods")
|
|
192
156
|
|
|
193
157
|
# Get all verb names from the schema
|
|
158
|
+
if not self.schema_utils:
|
|
159
|
+
self.log.warning("no_schema_utils_available")
|
|
160
|
+
return
|
|
161
|
+
|
|
194
162
|
verb_names = self.schema_utils.get_all_verb_names()
|
|
195
|
-
|
|
163
|
+
self.log.debug("found_verbs_in_schema", count=len(verb_names))
|
|
196
164
|
|
|
197
165
|
# Create a method for each verb
|
|
198
166
|
for verb_name in verb_names:
|
|
199
167
|
# Skip verbs that already have specific methods
|
|
200
168
|
if hasattr(self, verb_name):
|
|
201
|
-
|
|
169
|
+
self.log.debug("skipping_verb_has_method", verb=verb_name)
|
|
202
170
|
continue
|
|
203
171
|
|
|
204
172
|
# Handle sleep verb specially since it takes an integer directly
|
|
@@ -210,7 +178,7 @@ class SWMLService:
|
|
|
210
178
|
Args:
|
|
211
179
|
duration: The amount of time to sleep in milliseconds
|
|
212
180
|
"""
|
|
213
|
-
|
|
181
|
+
self.log.debug("executing_sleep_verb", duration=duration)
|
|
214
182
|
# Sleep verb takes a direct integer parameter in SWML
|
|
215
183
|
if duration is not None:
|
|
216
184
|
return self_instance.add_verb("sleep", duration)
|
|
@@ -226,7 +194,7 @@ class SWMLService:
|
|
|
226
194
|
# Also cache it for later
|
|
227
195
|
self._verb_methods_cache[verb_name] = sleep_method
|
|
228
196
|
|
|
229
|
-
|
|
197
|
+
self.log.debug("created_special_method", verb=verb_name)
|
|
230
198
|
continue
|
|
231
199
|
|
|
232
200
|
# Generate the method implementation for normal verbs
|
|
@@ -235,7 +203,7 @@ class SWMLService:
|
|
|
235
203
|
"""
|
|
236
204
|
Dynamically generated method for SWML verb
|
|
237
205
|
"""
|
|
238
|
-
|
|
206
|
+
self.log.debug("executing_verb_method", verb=name, kwargs_count=len(kwargs))
|
|
239
207
|
config = {}
|
|
240
208
|
for key, value in kwargs.items():
|
|
241
209
|
if value is not None:
|
|
@@ -260,7 +228,7 @@ class SWMLService:
|
|
|
260
228
|
# Also cache it for later
|
|
261
229
|
self._verb_methods_cache[verb_name] = method
|
|
262
230
|
|
|
263
|
-
|
|
231
|
+
self.log.debug("created_verb_method", verb=verb_name)
|
|
264
232
|
|
|
265
233
|
def __getattr__(self, name: str) -> Any:
|
|
266
234
|
"""
|
|
@@ -280,21 +248,26 @@ class SWMLService:
|
|
|
280
248
|
Raises:
|
|
281
249
|
AttributeError: If name is not a valid SWML verb
|
|
282
250
|
"""
|
|
283
|
-
|
|
251
|
+
self.log.debug("getattr_called", attribute=name)
|
|
284
252
|
|
|
285
253
|
# Simple version to match our test script
|
|
286
254
|
# First check if this is a valid SWML verb
|
|
255
|
+
if not self.schema_utils:
|
|
256
|
+
msg = f"'{self.__class__.__name__}' object has no attribute '{name}' (no schema available)"
|
|
257
|
+
self.log.debug("getattr_no_schema", attribute=name)
|
|
258
|
+
raise AttributeError(msg)
|
|
259
|
+
|
|
287
260
|
verb_names = self.schema_utils.get_all_verb_names()
|
|
288
261
|
|
|
289
262
|
if name in verb_names:
|
|
290
|
-
|
|
263
|
+
self.log.debug("getattr_valid_verb", verb=name)
|
|
291
264
|
|
|
292
265
|
# Check if we already have this method in the cache
|
|
293
266
|
if not hasattr(self, '_verb_methods_cache'):
|
|
294
267
|
self._verb_methods_cache = {}
|
|
295
268
|
|
|
296
269
|
if name in self._verb_methods_cache:
|
|
297
|
-
|
|
270
|
+
self.log.debug("getattr_cached_method", verb=name)
|
|
298
271
|
return types.MethodType(self._verb_methods_cache[name], self)
|
|
299
272
|
|
|
300
273
|
# Handle sleep verb specially since it takes an integer directly
|
|
@@ -306,7 +279,7 @@ class SWMLService:
|
|
|
306
279
|
Args:
|
|
307
280
|
duration: The amount of time to sleep in milliseconds
|
|
308
281
|
"""
|
|
309
|
-
|
|
282
|
+
self.log.debug("executing_sleep_method", duration=duration)
|
|
310
283
|
# Sleep verb takes a direct integer parameter in SWML
|
|
311
284
|
if duration is not None:
|
|
312
285
|
return self_instance.add_verb("sleep", duration)
|
|
@@ -317,7 +290,7 @@ class SWMLService:
|
|
|
317
290
|
raise TypeError("sleep() missing required argument: 'duration'")
|
|
318
291
|
|
|
319
292
|
# Cache the method for future use
|
|
320
|
-
|
|
293
|
+
self.log.debug("caching_sleep_method", verb=name)
|
|
321
294
|
self._verb_methods_cache[name] = sleep_method
|
|
322
295
|
|
|
323
296
|
# Return the bound method
|
|
@@ -328,7 +301,7 @@ class SWMLService:
|
|
|
328
301
|
"""
|
|
329
302
|
Dynamically generated method for SWML verb
|
|
330
303
|
"""
|
|
331
|
-
|
|
304
|
+
self.log.debug("executing_dynamic_verb", verb=name, kwargs_count=len(kwargs))
|
|
332
305
|
config = {}
|
|
333
306
|
for key, value in kwargs.items():
|
|
334
307
|
if value is not None:
|
|
@@ -343,7 +316,7 @@ class SWMLService:
|
|
|
343
316
|
verb_method.__doc__ = f"Add the {name} verb to the document."
|
|
344
317
|
|
|
345
318
|
# Cache the method for future use
|
|
346
|
-
|
|
319
|
+
self.log.debug("caching_verb_method", verb=name)
|
|
347
320
|
self._verb_methods_cache[name] = verb_method
|
|
348
321
|
|
|
349
322
|
# Return the bound method
|
|
@@ -351,7 +324,7 @@ class SWMLService:
|
|
|
351
324
|
|
|
352
325
|
# Not a valid verb
|
|
353
326
|
msg = f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
354
|
-
|
|
327
|
+
self.log.debug("getattr_invalid_attribute", attribute=name, error=msg)
|
|
355
328
|
raise AttributeError(msg)
|
|
356
329
|
|
|
357
330
|
def _find_schema_path(self) -> Optional[str]:
|
|
@@ -565,58 +538,41 @@ class SWMLService:
|
|
|
565
538
|
|
|
566
539
|
def as_router(self) -> APIRouter:
|
|
567
540
|
"""
|
|
568
|
-
|
|
541
|
+
Create a FastAPI router for this service
|
|
569
542
|
|
|
570
543
|
Returns:
|
|
571
|
-
FastAPI router
|
|
544
|
+
APIRouter: FastAPI router
|
|
572
545
|
"""
|
|
573
|
-
router = APIRouter()
|
|
546
|
+
router = APIRouter(redirect_slashes=False)
|
|
574
547
|
|
|
575
|
-
# Root endpoint
|
|
576
|
-
@router.get("")
|
|
577
|
-
@router.post("")
|
|
578
|
-
async def handle_root_no_slash(request: Request, response: Response):
|
|
579
|
-
"""Handle GET/POST requests to the root endpoint"""
|
|
580
|
-
return await self._handle_request(request, response)
|
|
581
|
-
|
|
582
|
-
# Root endpoint - with trailing slash
|
|
548
|
+
# Root endpoint with and without trailing slash
|
|
583
549
|
@router.get("/")
|
|
584
550
|
@router.post("/")
|
|
585
|
-
async def
|
|
586
|
-
"""Handle
|
|
551
|
+
async def handle_root(request: Request, response: Response):
|
|
552
|
+
"""Handle requests to the root endpoint"""
|
|
587
553
|
return await self._handle_request(request, response)
|
|
588
554
|
|
|
589
|
-
#
|
|
555
|
+
# Register routing callbacks as needed
|
|
590
556
|
if hasattr(self, '_routing_callbacks') and self._routing_callbacks:
|
|
591
557
|
for callback_path, callback_fn in self._routing_callbacks.items():
|
|
592
|
-
# Skip the root path
|
|
558
|
+
# Skip the root path which is already handled
|
|
593
559
|
if callback_path == "/":
|
|
594
560
|
continue
|
|
595
561
|
|
|
596
|
-
# Register
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
562
|
+
# Register both versions: with and without trailing slash
|
|
563
|
+
path = callback_path.rstrip("/")
|
|
564
|
+
path_with_slash = f"{path}/"
|
|
565
|
+
|
|
566
|
+
@router.get(path)
|
|
567
|
+
@router.get(path_with_slash)
|
|
568
|
+
@router.post(path)
|
|
569
|
+
@router.post(path_with_slash)
|
|
570
|
+
async def handle_callback(request: Request, response: Response, cb_path=callback_path):
|
|
571
|
+
"""Handle requests to callback endpoints"""
|
|
572
|
+
# Store the callback path in the request state
|
|
602
573
|
request.state.callback_path = cb_path
|
|
603
574
|
return await self._handle_request(request, response)
|
|
604
|
-
|
|
605
|
-
# Register the endpoint with trailing slash if it doesn't already have one
|
|
606
|
-
if not callback_path.endswith('/'):
|
|
607
|
-
slash_path = f"{callback_path}/"
|
|
608
|
-
|
|
609
|
-
@router.get(slash_path)
|
|
610
|
-
@router.post(slash_path)
|
|
611
|
-
async def handle_callback_with_slash(request: Request, response: Response, cb_path=callback_path):
|
|
612
|
-
"""Handle GET/POST requests to a registered callback path with trailing slash"""
|
|
613
|
-
# Store the callback path in request state for _handle_request to use
|
|
614
|
-
request.state.callback_path = cb_path
|
|
615
|
-
return await self._handle_request(request, response)
|
|
616
|
-
|
|
617
|
-
self.log.info("callback_endpoint_registered", path=callback_path)
|
|
618
575
|
|
|
619
|
-
self._router = router
|
|
620
576
|
return router
|
|
621
577
|
|
|
622
578
|
def register_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]],
|
|
@@ -696,10 +652,8 @@ class SWMLService:
|
|
|
696
652
|
Returns:
|
|
697
653
|
Response with SWML document or error
|
|
698
654
|
"""
|
|
699
|
-
#
|
|
700
|
-
|
|
701
|
-
self._detect_proxy_from_request(request)
|
|
702
|
-
self._proxy_detection_done = True
|
|
655
|
+
# Always detect proxy from current request - allows mixing direct and proxied access
|
|
656
|
+
self._detect_proxy_from_request(request)
|
|
703
657
|
|
|
704
658
|
# Check auth
|
|
705
659
|
if not self._check_basic_auth(request):
|
|
@@ -789,64 +743,126 @@ class SWMLService:
|
|
|
789
743
|
Start a web server for this service
|
|
790
744
|
|
|
791
745
|
Args:
|
|
792
|
-
host:
|
|
793
|
-
port:
|
|
746
|
+
host: Host to bind to (defaults to self.host)
|
|
747
|
+
port: Port to bind to (defaults to self.port)
|
|
794
748
|
ssl_cert: Path to SSL certificate file
|
|
795
|
-
ssl_key: Path to SSL
|
|
796
|
-
ssl_enabled: Whether to enable SSL
|
|
797
|
-
domain: Domain name for
|
|
749
|
+
ssl_key: Path to SSL key file
|
|
750
|
+
ssl_enabled: Whether to enable SSL
|
|
751
|
+
domain: Domain name for SSL certificate
|
|
798
752
|
"""
|
|
799
753
|
import uvicorn
|
|
800
754
|
|
|
801
|
-
#
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
755
|
+
# Store SSL configuration (override environment if explicitly provided)
|
|
756
|
+
if ssl_enabled is not None:
|
|
757
|
+
self.ssl_enabled = ssl_enabled
|
|
758
|
+
if domain is not None:
|
|
759
|
+
self.domain = domain
|
|
760
|
+
|
|
761
|
+
# Set SSL paths (use provided paths or fall back to environment)
|
|
762
|
+
ssl_cert_path = ssl_cert or getattr(self, 'ssl_cert_path', None)
|
|
763
|
+
ssl_key_path = ssl_key or getattr(self, 'ssl_key_path', None)
|
|
806
764
|
|
|
807
765
|
# Validate SSL configuration if enabled
|
|
808
766
|
if self.ssl_enabled:
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
self.
|
|
812
|
-
elif not ssl_key_path or not os.path.exists(ssl_key_path):
|
|
813
|
-
self.log.warning("ssl_key_not_found", path=ssl_key_path)
|
|
767
|
+
is_valid, error = self.security.validate_ssl_config()
|
|
768
|
+
if not is_valid:
|
|
769
|
+
self.log.warning("ssl_config_invalid", error=error)
|
|
814
770
|
self.ssl_enabled = False
|
|
815
771
|
elif not self.domain:
|
|
816
772
|
self.log.warning("ssl_domain_not_specified")
|
|
817
773
|
# We'll continue, but URLs might not be correctly generated
|
|
818
774
|
|
|
819
775
|
if self._app is None:
|
|
820
|
-
|
|
776
|
+
# Use redirect_slashes=False to be consistent with AgentBase
|
|
777
|
+
app = FastAPI(redirect_slashes=False)
|
|
821
778
|
router = self.as_router()
|
|
822
|
-
|
|
779
|
+
|
|
780
|
+
# Normalize the route to ensure it starts with a slash and doesn't end with one
|
|
781
|
+
# This avoids the FastAPI error about prefixes ending with slashes
|
|
782
|
+
normalized_route = "/" + self.route.strip("/")
|
|
783
|
+
|
|
784
|
+
# Include router with the normalized prefix (handle root route special case)
|
|
785
|
+
if normalized_route == "/":
|
|
786
|
+
app.include_router(router)
|
|
787
|
+
else:
|
|
788
|
+
app.include_router(router, prefix=normalized_route)
|
|
789
|
+
|
|
790
|
+
# Add a catch-all route handler that will handle both /path and /path/ formats
|
|
791
|
+
# This provides the same behavior without using a trailing slash in the prefix
|
|
792
|
+
@app.get("/{full_path:path}")
|
|
793
|
+
@app.post("/{full_path:path}")
|
|
794
|
+
async def handle_all_routes(request: Request, response: Response, full_path: str):
|
|
795
|
+
# Get our route path without leading slash for comparison
|
|
796
|
+
route_path = normalized_route.lstrip("/")
|
|
797
|
+
route_with_slash = route_path + "/"
|
|
798
|
+
|
|
799
|
+
# Log the incoming path for debugging
|
|
800
|
+
self.log.debug("catch_all_route_hit", path=full_path, route=route_path)
|
|
801
|
+
|
|
802
|
+
# Check for exact match to our route (without trailing slash)
|
|
803
|
+
if full_path == route_path:
|
|
804
|
+
# This is our exact route - handle it directly
|
|
805
|
+
return await self._handle_request(request, response)
|
|
806
|
+
|
|
807
|
+
# Check for our route with a trailing slash or subpaths
|
|
808
|
+
elif full_path == route_with_slash or full_path.startswith(route_with_slash):
|
|
809
|
+
# This is our route with a trailing slash
|
|
810
|
+
# Extract the path after our route prefix
|
|
811
|
+
sub_path = full_path[len(route_with_slash):]
|
|
812
|
+
|
|
813
|
+
# Forward to the appropriate handler in our router
|
|
814
|
+
if not sub_path:
|
|
815
|
+
# Root endpoint
|
|
816
|
+
return await self._handle_request(request, response)
|
|
817
|
+
|
|
818
|
+
# Check for routing callbacks if there are any
|
|
819
|
+
if hasattr(self, '_routing_callbacks'):
|
|
820
|
+
for callback_path, callback_fn in self._routing_callbacks.items():
|
|
821
|
+
cb_path_clean = callback_path.strip("/")
|
|
822
|
+
if sub_path == cb_path_clean or sub_path.startswith(cb_path_clean + "/"):
|
|
823
|
+
# Store the callback path in request state for handlers to use
|
|
824
|
+
request.state.callback_path = callback_path
|
|
825
|
+
return await self._handle_request(request, response)
|
|
826
|
+
|
|
827
|
+
# Not our route or not matching our patterns
|
|
828
|
+
self.log.debug("no_route_match", path=full_path)
|
|
829
|
+
return {"error": "Path not found"}
|
|
830
|
+
|
|
831
|
+
# Log all routes for debugging
|
|
832
|
+
self.log.debug("registered_routes", service=self.name)
|
|
833
|
+
for route in app.routes:
|
|
834
|
+
if hasattr(route, "path"):
|
|
835
|
+
self.log.debug("route_registered", path=route.path)
|
|
836
|
+
|
|
823
837
|
self._app = app
|
|
824
838
|
|
|
825
839
|
host = host or self.host
|
|
826
840
|
port = port or self.port
|
|
827
841
|
|
|
828
|
-
#
|
|
842
|
+
# Get the auth credentials
|
|
829
843
|
username, password = self._basic_auth
|
|
830
844
|
|
|
831
|
-
#
|
|
832
|
-
|
|
833
|
-
display_host = self.domain if self.ssl_enabled and self.domain else f"{host}:{port}"
|
|
845
|
+
# Get the proper URL using unified URL building
|
|
846
|
+
startup_url = self._build_full_url(include_auth=False)
|
|
834
847
|
|
|
835
848
|
self.log.info("starting_server",
|
|
836
|
-
url=
|
|
849
|
+
url=startup_url,
|
|
837
850
|
ssl_enabled=self.ssl_enabled,
|
|
838
851
|
username=username,
|
|
839
852
|
password_length=len(password))
|
|
840
853
|
|
|
854
|
+
# Print user-friendly startup message (keep for UX)
|
|
841
855
|
print(f"Service '{self.name}' is available at:")
|
|
842
|
-
print(f"URL: {
|
|
856
|
+
print(f"URL: {startup_url}")
|
|
857
|
+
print(f"URL with trailing slash: {startup_url}/")
|
|
843
858
|
print(f"Basic Auth: {username}:{password}")
|
|
844
859
|
|
|
845
|
-
# Check if SIP routing is enabled and
|
|
860
|
+
# Check if SIP routing is enabled and log additional info
|
|
846
861
|
if self._routing_callbacks:
|
|
847
862
|
print(f"Callback endpoints:")
|
|
848
863
|
for path in self._routing_callbacks:
|
|
849
|
-
|
|
864
|
+
callback_url = self._build_full_url(endpoint=path.lstrip('/'), include_auth=False)
|
|
865
|
+
print(f" {callback_url}")
|
|
850
866
|
|
|
851
867
|
# Start uvicorn with or without SSL
|
|
852
868
|
if self.ssl_enabled and ssl_cert_path and ssl_key_path:
|
|
@@ -922,149 +938,146 @@ class SWMLService:
|
|
|
922
938
|
|
|
923
939
|
return username, password
|
|
924
940
|
|
|
925
|
-
# Keep the existing methods for backward compatibility
|
|
926
|
-
|
|
927
|
-
def add_answer_verb(self, max_duration: Optional[int] = None, codecs: Optional[str] = None) -> bool:
|
|
928
|
-
"""
|
|
929
|
-
Add an answer verb to the current document
|
|
930
941
|
|
|
931
|
-
|
|
932
|
-
max_duration: Maximum duration in seconds
|
|
933
|
-
codecs: Comma-separated list of codecs
|
|
934
|
-
|
|
935
|
-
Returns:
|
|
936
|
-
True if added successfully, False otherwise
|
|
937
|
-
"""
|
|
938
|
-
config = {}
|
|
939
|
-
if max_duration is not None:
|
|
940
|
-
config["max_duration"] = max_duration
|
|
941
|
-
if codecs is not None:
|
|
942
|
-
config["codecs"] = codecs
|
|
943
|
-
|
|
944
|
-
return self.add_verb("answer", config)
|
|
945
|
-
|
|
946
|
-
def add_hangup_verb(self, reason: Optional[str] = None) -> bool:
|
|
942
|
+
def _get_base_url(self, include_auth: bool = True) -> str:
|
|
947
943
|
"""
|
|
948
|
-
|
|
944
|
+
Get the base URL for this service, using proxy info if available or falling back to configured values
|
|
945
|
+
|
|
946
|
+
This is the central method for URL building that handles both startup configuration
|
|
947
|
+
and per-request proxy detection.
|
|
949
948
|
|
|
950
949
|
Args:
|
|
951
|
-
|
|
950
|
+
include_auth: Whether to include authentication credentials in the URL
|
|
952
951
|
|
|
953
952
|
Returns:
|
|
954
|
-
|
|
953
|
+
Base URL string (protocol://[auth@]host[:port])
|
|
955
954
|
"""
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
955
|
+
# Debug logging to understand state
|
|
956
|
+
self.log.debug("_get_base_url called",
|
|
957
|
+
has_proxy_url_base=hasattr(self, '_proxy_url_base'),
|
|
958
|
+
proxy_url_base=getattr(self, '_proxy_url_base', None),
|
|
959
|
+
proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False),
|
|
960
|
+
env_var=os.environ.get('SWML_PROXY_URL_BASE'),
|
|
961
|
+
include_auth=include_auth,
|
|
962
|
+
caller=inspect.stack()[1].function if len(inspect.stack()) > 1 else "unknown")
|
|
963
|
+
|
|
964
|
+
# Check if we have proxy information from a request
|
|
965
|
+
if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
|
|
966
|
+
base = self._proxy_url_base.rstrip('/')
|
|
967
|
+
self.log.debug("Using proxy URL base", proxy_url_base=base)
|
|
968
|
+
|
|
969
|
+
# Add auth credentials if requested
|
|
970
|
+
if include_auth:
|
|
971
|
+
username, password = self._basic_auth
|
|
972
|
+
url = urlparse(base)
|
|
973
|
+
base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
|
|
959
974
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
def add_ai_verb(self,
|
|
963
|
-
prompt_text: Optional[str] = None,
|
|
964
|
-
prompt_pom: Optional[List[Dict[str, Any]]] = None,
|
|
965
|
-
post_prompt: Optional[str] = None,
|
|
966
|
-
post_prompt_url: Optional[str] = None,
|
|
967
|
-
swaig: Optional[Dict[str, Any]] = None,
|
|
968
|
-
**kwargs) -> bool:
|
|
969
|
-
"""
|
|
970
|
-
Add an AI verb to the current document
|
|
975
|
+
return base
|
|
971
976
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
post_prompt: Post-prompt text
|
|
976
|
-
post_prompt_url: Post-prompt URL
|
|
977
|
-
swaig: SWAIG configuration
|
|
978
|
-
**kwargs: Additional parameters
|
|
979
|
-
|
|
980
|
-
Returns:
|
|
981
|
-
True if added successfully, False otherwise
|
|
982
|
-
"""
|
|
983
|
-
config = {}
|
|
977
|
+
# No proxy, use configured values
|
|
978
|
+
# Determine protocol based on SSL settings
|
|
979
|
+
protocol = "https" if self.ssl_enabled else "http"
|
|
984
980
|
|
|
985
|
-
#
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
981
|
+
# Debug logging
|
|
982
|
+
self.log.debug("_get_base_url",
|
|
983
|
+
ssl_enabled=self.ssl_enabled,
|
|
984
|
+
domain=self.domain,
|
|
985
|
+
port=self.port,
|
|
986
|
+
protocol=protocol)
|
|
987
|
+
|
|
988
|
+
# Determine host part
|
|
989
|
+
if self.ssl_enabled and self.domain:
|
|
990
|
+
# Use domain for SSL
|
|
991
|
+
if protocol == "https" and self.port == 443:
|
|
992
|
+
host_part = self.domain # Don't include port for standard HTTPS
|
|
993
|
+
elif protocol == "http" and self.port == 80:
|
|
994
|
+
host_part = self.domain # Don't include port for standard HTTP
|
|
995
|
+
else:
|
|
996
|
+
host_part = f"{self.domain}:{self.port}"
|
|
997
|
+
self.log.debug("Using domain with port", domain=self.domain, port=self.port, host_part=host_part)
|
|
998
|
+
else:
|
|
999
|
+
# Use configured host
|
|
1000
|
+
if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
|
|
1001
|
+
host = "localhost"
|
|
1002
|
+
else:
|
|
1003
|
+
host = self.host
|
|
990
1004
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1005
|
+
# Include port unless it's the standard port for the protocol
|
|
1006
|
+
if (protocol == "https" and self.port == 443) or (protocol == "http" and self.port == 80):
|
|
1007
|
+
host_part = host
|
|
1008
|
+
else:
|
|
1009
|
+
host_part = f"{host}:{self.port}"
|
|
994
1010
|
|
|
995
|
-
#
|
|
996
|
-
if
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
config["SWAIG"] = swaig
|
|
1002
|
-
|
|
1003
|
-
# Handle additional parameters
|
|
1004
|
-
for key, value in kwargs.items():
|
|
1005
|
-
if value is not None:
|
|
1006
|
-
config[key] = value
|
|
1007
|
-
|
|
1008
|
-
return self.add_verb("ai", config)
|
|
1011
|
+
# Build base URL
|
|
1012
|
+
if include_auth:
|
|
1013
|
+
username, password = self._basic_auth
|
|
1014
|
+
base = f"{protocol}://{username}:{password}@{host_part}"
|
|
1015
|
+
else:
|
|
1016
|
+
base = f"{protocol}://{host_part}"
|
|
1009
1017
|
|
|
1010
|
-
|
|
1018
|
+
return base
|
|
1019
|
+
|
|
1020
|
+
def _build_full_url(self, endpoint: str = "", include_auth: bool = True, query_params: Optional[Dict[str, str]] = None) -> str:
|
|
1011
1021
|
"""
|
|
1012
|
-
|
|
1022
|
+
Build the full URL for this service or a specific endpoint
|
|
1023
|
+
|
|
1024
|
+
This is the internal implementation used by both get_full_url (for AgentBase compatibility)
|
|
1025
|
+
and _build_webhook_url.
|
|
1013
1026
|
|
|
1014
1027
|
Args:
|
|
1015
|
-
endpoint:
|
|
1028
|
+
endpoint: Optional endpoint path (e.g., "swaig", "post_prompt")
|
|
1029
|
+
include_auth: Whether to include authentication credentials in the URL
|
|
1016
1030
|
query_params: Optional query parameters to append
|
|
1017
1031
|
|
|
1018
1032
|
Returns:
|
|
1019
|
-
|
|
1033
|
+
Full URL string
|
|
1020
1034
|
"""
|
|
1021
|
-
#
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
#
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1035
|
+
# Get base URL using central method
|
|
1036
|
+
base = self._get_base_url(include_auth=include_auth)
|
|
1037
|
+
|
|
1038
|
+
# Build path
|
|
1039
|
+
if endpoint:
|
|
1040
|
+
# Ensure endpoint doesn't start with slash
|
|
1041
|
+
endpoint = endpoint.lstrip('/')
|
|
1042
|
+
# Add trailing slash to endpoint to prevent redirects
|
|
1043
|
+
if not endpoint.endswith('/'):
|
|
1044
|
+
endpoint = f"{endpoint}/"
|
|
1045
|
+
path = f"{self.route}/{endpoint}"
|
|
1030
1046
|
else:
|
|
1031
|
-
#
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
# Use domain if available and SSL is enabled
|
|
1035
|
-
if getattr(self, 'ssl_enabled', False) and getattr(self, 'domain', None):
|
|
1036
|
-
host_part = self.domain
|
|
1037
|
-
else:
|
|
1038
|
-
# For local URLs
|
|
1039
|
-
if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
|
|
1040
|
-
host = "localhost"
|
|
1041
|
-
else:
|
|
1042
|
-
host = self.host
|
|
1043
|
-
|
|
1044
|
-
host_part = f"{host}:{self.port}"
|
|
1045
|
-
|
|
1046
|
-
# Always include auth credentials
|
|
1047
|
-
username, password = self._basic_auth
|
|
1048
|
-
base = f"{protocol}://{username}:{password}@{host_part}"
|
|
1047
|
+
# Just the route itself
|
|
1048
|
+
path = self.route if self.route != "/" else ""
|
|
1049
1049
|
|
|
1050
|
-
# Ensure the endpoint has a trailing slash to prevent redirects
|
|
1051
|
-
if endpoint and not endpoint.endswith('/'):
|
|
1052
|
-
endpoint = f"{endpoint}/"
|
|
1053
|
-
|
|
1054
|
-
# Simple path - use the route directly with the endpoint
|
|
1055
|
-
path = f"{self.route}/{endpoint}"
|
|
1056
|
-
|
|
1057
1050
|
# Construct full URL
|
|
1058
1051
|
url = f"{base}{path}"
|
|
1059
1052
|
|
|
1060
|
-
# Add query parameters if any
|
|
1053
|
+
# Add query parameters if any
|
|
1061
1054
|
if query_params:
|
|
1062
1055
|
filtered_params = {k: v for k, v in query_params.items() if v}
|
|
1063
1056
|
if filtered_params:
|
|
1064
1057
|
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
|
1065
1058
|
url = f"{url}?{params}"
|
|
1059
|
+
|
|
1060
|
+
return url
|
|
1061
|
+
|
|
1062
|
+
def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
|
|
1063
|
+
"""
|
|
1064
|
+
Helper method to build webhook URLs consistently
|
|
1065
|
+
|
|
1066
|
+
Args:
|
|
1067
|
+
endpoint: The endpoint path (e.g., "swaig", "post_prompt")
|
|
1068
|
+
query_params: Optional query parameters to append
|
|
1066
1069
|
|
|
1067
|
-
|
|
1070
|
+
Returns:
|
|
1071
|
+
Fully constructed webhook URL
|
|
1072
|
+
"""
|
|
1073
|
+
self.log.debug("_build_webhook_url called",
|
|
1074
|
+
endpoint=endpoint,
|
|
1075
|
+
query_params=query_params,
|
|
1076
|
+
proxy_url_base=getattr(self, '_proxy_url_base', None),
|
|
1077
|
+
proxy_url_base_from_env=getattr(self, '_proxy_url_base_from_env', False))
|
|
1078
|
+
|
|
1079
|
+
# Use the central URL building method
|
|
1080
|
+
return self._build_full_url(endpoint=endpoint, include_auth=True, query_params=query_params)
|
|
1068
1081
|
|
|
1069
1082
|
def _detect_proxy_from_request(self, request: Request) -> None:
|
|
1070
1083
|
"""
|
|
@@ -1074,6 +1087,10 @@ class SWMLService:
|
|
|
1074
1087
|
Args:
|
|
1075
1088
|
request: FastAPI Request object
|
|
1076
1089
|
"""
|
|
1090
|
+
# If SWML_PROXY_URL_BASE was already set (e.g., from environment), don't override it
|
|
1091
|
+
if self._proxy_url_base:
|
|
1092
|
+
return
|
|
1093
|
+
|
|
1077
1094
|
# First check for standard X-Forwarded headers (used by most proxies including ngrok)
|
|
1078
1095
|
forwarded_host = request.headers.get("X-Forwarded-Host")
|
|
1079
1096
|
forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
|
|
@@ -1158,4 +1175,4 @@ class SWMLService:
|
|
|
1158
1175
|
if proxy_url:
|
|
1159
1176
|
self._proxy_url_base = proxy_url.rstrip('/')
|
|
1160
1177
|
self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
|
|
1161
|
-
self._proxy_detection_done = True
|
|
1178
|
+
self._proxy_detection_done = True
|