signalwire-agents 0.1.2__py3-none-any.whl → 0.1.6__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 +1 -1
- signalwire_agents/core/agent_base.py +16 -0
- signalwire_agents/core/swml_service.py +105 -1
- signalwire_agents/prefabs/receptionist.py +17 -16
- {signalwire_agents-0.1.2.dist-info → signalwire_agents-0.1.6.dist-info}/METADATA +1 -1
- {signalwire_agents-0.1.2.dist-info → signalwire_agents-0.1.6.dist-info}/RECORD +10 -10
- {signalwire_agents-0.1.2.data → signalwire_agents-0.1.6.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.2.dist-info → signalwire_agents-0.1.6.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.2.dist-info → signalwire_agents-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.2.dist-info → signalwire_agents-0.1.6.dist-info}/top_level.txt +0 -0
signalwire_agents/__init__.py
CHANGED
@@ -14,7 +14,7 @@ SignalWire AI Agents SDK
|
|
14
14
|
A package for building AI agents using SignalWire's AI and SWML capabilities.
|
15
15
|
"""
|
16
16
|
|
17
|
-
__version__ = "0.1.
|
17
|
+
__version__ = "0.1.6"
|
18
18
|
|
19
19
|
# Import core classes for easier access
|
20
20
|
from signalwire_agents.core.agent_base import AgentBase
|
@@ -1172,6 +1172,22 @@ class AgentBase(SWMLService):
|
|
1172
1172
|
|
1173
1173
|
async def _handle_root_request(self, request: Request):
|
1174
1174
|
"""Handle GET/POST requests to the root endpoint"""
|
1175
|
+
# Auto-detect proxy on first request if not explicitly configured
|
1176
|
+
if not getattr(self, '_proxy_detection_done', False) and not getattr(self, '_proxy_url_base', None):
|
1177
|
+
# Check for proxy headers
|
1178
|
+
forwarded_host = request.headers.get("X-Forwarded-Host")
|
1179
|
+
forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
|
1180
|
+
|
1181
|
+
if forwarded_host:
|
1182
|
+
self._proxy_url_base = f"{forwarded_proto}://{forwarded_host}"
|
1183
|
+
self.log.info("proxy_auto_detected", proxy_url_base=self._proxy_url_base,
|
1184
|
+
source="X-Forwarded headers")
|
1185
|
+
self._proxy_detection_done = True
|
1186
|
+
# If no explicit proxy headers, try the parent class detection method if it exists
|
1187
|
+
elif hasattr(super(), '_detect_proxy_from_request'):
|
1188
|
+
super()._detect_proxy_from_request(request)
|
1189
|
+
self._proxy_detection_done = True
|
1190
|
+
|
1175
1191
|
# Check if this is a callback path request
|
1176
1192
|
callback_path = getattr(request.state, "callback_path", None)
|
1177
1193
|
|
@@ -126,6 +126,11 @@ class SWMLService:
|
|
126
126
|
self.ssl_enabled = False
|
127
127
|
self.domain = None
|
128
128
|
|
129
|
+
# Initialize proxy detection attributes
|
130
|
+
self._proxy_url_base = os.environ.get('SWML_PROXY_URL_BASE')
|
131
|
+
self._proxy_detection_done = False
|
132
|
+
self._proxy_debug = os.environ.get('SWML_PROXY_DEBUG', '').lower() in ('true', '1', 'yes')
|
133
|
+
|
129
134
|
# Initialize logger for this instance
|
130
135
|
self.log = logger.bind(service=name)
|
131
136
|
self.log.info("service_initializing", route=self.route, host=host, port=port)
|
@@ -691,6 +696,11 @@ class SWMLService:
|
|
691
696
|
Returns:
|
692
697
|
Response with SWML document or error
|
693
698
|
"""
|
699
|
+
# Auto-detect proxy on first request if not explicitly configured
|
700
|
+
if not self._proxy_detection_done and not self._proxy_url_base:
|
701
|
+
self._detect_proxy_from_request(request)
|
702
|
+
self._proxy_detection_done = True
|
703
|
+
|
694
704
|
# Check auth
|
695
705
|
if not self._check_basic_auth(request):
|
696
706
|
response.headers["WWW-Authenticate"] = "Basic"
|
@@ -1054,4 +1064,98 @@ class SWMLService:
|
|
1054
1064
|
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
1055
1065
|
url = f"{url}?{params}"
|
1056
1066
|
|
1057
|
-
return url
|
1067
|
+
return url
|
1068
|
+
|
1069
|
+
def _detect_proxy_from_request(self, request: Request) -> None:
|
1070
|
+
"""
|
1071
|
+
Detect if we're behind a proxy by examining request headers
|
1072
|
+
and auto-configure proxy_url_base if needed
|
1073
|
+
|
1074
|
+
Args:
|
1075
|
+
request: FastAPI Request object
|
1076
|
+
"""
|
1077
|
+
# First check for standard X-Forwarded headers (used by most proxies including ngrok)
|
1078
|
+
forwarded_host = request.headers.get("X-Forwarded-Host")
|
1079
|
+
forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
|
1080
|
+
|
1081
|
+
if forwarded_host:
|
1082
|
+
# Direct X-Forwarded-* headers - most common case
|
1083
|
+
self._proxy_url_base = f"{forwarded_proto}://{forwarded_host}"
|
1084
|
+
self.log.info("proxy_auto_detected", proxy_url_base=self._proxy_url_base,
|
1085
|
+
source="X-Forwarded headers")
|
1086
|
+
return
|
1087
|
+
|
1088
|
+
# If no standard headers, check other proxy detection methods
|
1089
|
+
|
1090
|
+
# Check for Forwarded header (RFC 7239)
|
1091
|
+
forwarded = request.headers.get("Forwarded")
|
1092
|
+
if forwarded:
|
1093
|
+
# Parse RFC 7239 Forwarded header
|
1094
|
+
try:
|
1095
|
+
# Extract host and proto from Forwarded: for=X;host=Y;proto=Z
|
1096
|
+
parts = [p.strip() for p in forwarded.split(';')]
|
1097
|
+
host_part = next((p for p in parts if p.startswith("host=")), None)
|
1098
|
+
proto_part = next((p for p in parts if p.startswith("proto=")), None)
|
1099
|
+
|
1100
|
+
if host_part:
|
1101
|
+
host = host_part.split('=', 1)[1].strip('"')
|
1102
|
+
proto = proto_part.split('=', 1)[1].strip('"') if proto_part else "http"
|
1103
|
+
self._proxy_url_base = f"{proto}://{host}"
|
1104
|
+
self.log.info("proxy_auto_detected", proxy_url_base=self._proxy_url_base,
|
1105
|
+
source="Forwarded header")
|
1106
|
+
return
|
1107
|
+
except Exception as e:
|
1108
|
+
self.log.warning("forwarded_header_parse_error", error=str(e))
|
1109
|
+
|
1110
|
+
# Try to detect from the URL itself for transparent proxies
|
1111
|
+
if str(request.url).startswith(("https://", "http://")) and not any(
|
1112
|
+
str(request.url).startswith(f"http://{h}") for h in ["localhost", "127.0.0.1", self.host, "0.0.0.0"]
|
1113
|
+
):
|
1114
|
+
# This is likely a transparent proxy - extract base URL
|
1115
|
+
parsed = urlparse(str(request.url))
|
1116
|
+
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
1117
|
+
self._proxy_url_base = base_url
|
1118
|
+
self.log.info("proxy_auto_detected", proxy_url_base=base_url,
|
1119
|
+
source="request URL (transparent proxy)")
|
1120
|
+
return
|
1121
|
+
|
1122
|
+
# Check for other common proxy setups
|
1123
|
+
original_host = request.headers.get("X-Original-Host") or request.headers.get("Host")
|
1124
|
+
if original_host:
|
1125
|
+
# Only use Host if it doesn't look like our local server
|
1126
|
+
local_hosts = [self.host, "localhost", "127.0.0.1", "0.0.0.0"]
|
1127
|
+
local_port = f":{self.port}"
|
1128
|
+
|
1129
|
+
# If host doesn't look like local server or doesn't contain our port
|
1130
|
+
if not any(h in original_host for h in local_hosts) and local_port not in original_host:
|
1131
|
+
proto = "https" if request.url.scheme == "https" else "http"
|
1132
|
+
self._proxy_url_base = f"{proto}://{original_host}"
|
1133
|
+
self.log.info("proxy_auto_detected", proxy_url_base=self._proxy_url_base,
|
1134
|
+
source="Host header")
|
1135
|
+
return
|
1136
|
+
|
1137
|
+
# If forward_for header exists, we're likely behind a proxy but couldn't determine the URL
|
1138
|
+
forwarded_for = request.headers.get("X-Forwarded-For")
|
1139
|
+
if forwarded_for:
|
1140
|
+
self.log.warning("proxy_detected_but_url_unknown",
|
1141
|
+
client_ip=forwarded_for,
|
1142
|
+
message="Proxy detected via X-Forwarded-For header but could not determine public URL")
|
1143
|
+
|
1144
|
+
# No proxy detected, or unable to determine the public URL
|
1145
|
+
if self._proxy_debug:
|
1146
|
+
self.log.info("proxy_detection_failed",
|
1147
|
+
message="Could not auto-detect proxy. If you are behind a proxy, set SWML_PROXY_URL_BASE manually.")
|
1148
|
+
|
1149
|
+
def manual_set_proxy_url(self, proxy_url: str) -> None:
|
1150
|
+
"""
|
1151
|
+
Manually set the proxy URL base for webhook callbacks
|
1152
|
+
|
1153
|
+
This can be called at runtime to set or update the proxy URL
|
1154
|
+
|
1155
|
+
Args:
|
1156
|
+
proxy_url: The base URL to use for webhooks (e.g., https://example.ngrok.io)
|
1157
|
+
"""
|
1158
|
+
if proxy_url:
|
1159
|
+
self._proxy_url_base = proxy_url.rstrip('/')
|
1160
|
+
self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
|
1161
|
+
self._proxy_detection_done = True
|
@@ -166,9 +166,7 @@ class ReceptionistAgent(AgentBase):
|
|
166
166
|
self.add_language(
|
167
167
|
name="English",
|
168
168
|
code="en-US",
|
169
|
-
voice=voice
|
170
|
-
speech_fillers=["Let me get that information for you...", "One moment please..."],
|
171
|
-
function_fillers=["I'm processing that...", "Let me check which department can help you best..."]
|
169
|
+
voice=voice
|
172
170
|
)
|
173
171
|
|
174
172
|
def _register_tools(self):
|
@@ -260,23 +258,26 @@ class ReceptionistAgent(AgentBase):
|
|
260
258
|
# Get transfer number
|
261
259
|
transfer_number = department.get("number", "")
|
262
260
|
|
263
|
-
# Create message for caller
|
264
|
-
message = f"I'll transfer you to our {department_name} department now. Thank you for calling, {name}!"
|
265
|
-
|
266
261
|
# Create result with transfer SWML
|
267
|
-
result = SwaigFunctionResult(
|
262
|
+
result = SwaigFunctionResult(f"I'll transfer you to our {department_name} department now. Thank you for calling, {name}!")
|
268
263
|
|
269
264
|
# Add the SWML to execute the transfer
|
270
|
-
|
271
|
-
|
272
|
-
"play": {
|
273
|
-
"url": f"say:{message}"
|
274
|
-
}
|
275
|
-
},
|
265
|
+
# Add actions to update global data
|
266
|
+
result.add_actions([
|
276
267
|
{
|
277
|
-
"
|
278
|
-
"
|
279
|
-
|
268
|
+
"SWML": {
|
269
|
+
"sections": {
|
270
|
+
"main": [
|
271
|
+
{
|
272
|
+
"connect": {
|
273
|
+
"to": transfer_number
|
274
|
+
}
|
275
|
+
}
|
276
|
+
]
|
277
|
+
},
|
278
|
+
"version": "1.0.0"
|
279
|
+
},
|
280
|
+
"transfer": "true"
|
280
281
|
}
|
281
282
|
])
|
282
283
|
|
@@ -1,15 +1,15 @@
|
|
1
|
-
signalwire_agents/__init__.py,sha256=
|
1
|
+
signalwire_agents/__init__.py,sha256=q_3tXAaEKU5Yrua20bVq-dFYg0Q4burukwe_9KBujdw,800
|
2
2
|
signalwire_agents/agent_server.py,sha256=se_YzOQE5UUoRUKCbTnOg9qr4G3qN7iVuQLutwXEwFU,12850
|
3
3
|
signalwire_agents/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
|
4
4
|
signalwire_agents/core/__init__.py,sha256=mVDLbpq1pg_WwiqsQR28NNZwJ6-VUXFIfg-vN7pk0ew,806
|
5
|
-
signalwire_agents/core/agent_base.py,sha256=
|
5
|
+
signalwire_agents/core/agent_base.py,sha256=Qd25AARkFl7zGj01o7XZogtk6f8PpXYsz7bfRcpwUuA,99436
|
6
6
|
signalwire_agents/core/function_result.py,sha256=vD8eBDJBQNNnss1jShadfogCZw_prODB6eBkTuVgZKA,3538
|
7
7
|
signalwire_agents/core/pom_builder.py,sha256=ywuiIfP8BeLBPo_G4X1teZlG6zTCMkW71CZnmyoDTAQ,6636
|
8
8
|
signalwire_agents/core/swaig_function.py,sha256=WoHGQuCmU9L9k39pttRunmfRtIa_PnNRn9W0Xq3MfIk,6316
|
9
9
|
signalwire_agents/core/swml_builder.py,sha256=Y_eHThVVso-_Hz4f73eEuu3HJstsM9PtBl-36GvAXhI,6594
|
10
10
|
signalwire_agents/core/swml_handler.py,sha256=KvphI_YY47VWGVXaix_N3SuQSyygHEUr9We6xESQK44,7002
|
11
11
|
signalwire_agents/core/swml_renderer.py,sha256=iobMWWoBR7dkHndI3Qlwf4C0fg2p7DmAU2Rb7ZmmLhA,13891
|
12
|
-
signalwire_agents/core/swml_service.py,sha256=
|
12
|
+
signalwire_agents/core/swml_service.py,sha256=J3IHJxU172bQa0zd2xRtFwVWonui4f1v_ki1KYHl9lI,47414
|
13
13
|
signalwire_agents/core/security/__init__.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
14
14
|
signalwire_agents/core/security/session_manager.py,sha256=5y0Mr3cI63cO34ctFgq-KX26mdG0Tpr7rTEsgoTS_44,5229
|
15
15
|
signalwire_agents/core/state/__init__.py,sha256=qpYIfQBApet6VQsy6diS3yu0lMxCBC6REOUk5l1McUw,379
|
@@ -19,16 +19,16 @@ signalwire_agents/prefabs/__init__.py,sha256=MW11J63XH7KxF2MWguRsMFM9iqMWexaEO9y
|
|
19
19
|
signalwire_agents/prefabs/concierge.py,sha256=--esvAV1lozQGYXHAqvSg8_TtlIoVfQK75APJ5zqX9I,9779
|
20
20
|
signalwire_agents/prefabs/faq_bot.py,sha256=cUuHhnDB8S4aVg-DiQe4jBmCAPrYQrND_Mff9iaeEa0,10572
|
21
21
|
signalwire_agents/prefabs/info_gatherer.py,sha256=y9gxjq1vF0-TrE6m734dCcAHqxt0I_DyCqxoM45QY8U,9940
|
22
|
-
signalwire_agents/prefabs/receptionist.py,sha256=
|
22
|
+
signalwire_agents/prefabs/receptionist.py,sha256=Wb22igXTD8QK9lQVcJq88eIk7qQ2GSSIzSGuYyK-Dbk,10293
|
23
23
|
signalwire_agents/prefabs/survey.py,sha256=1pyUeZ5heDqFAYqkYs5fHN_jQ7TKqJInnOOUQEajSsY,14358
|
24
24
|
signalwire_agents/utils/__init__.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
25
25
|
signalwire_agents/utils/pom_utils.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
26
26
|
signalwire_agents/utils/schema_utils.py,sha256=LvFCFvJTQk_xYK0B-NXbkXKEF7Zmv-LqpV_vfpPnOb4,13473
|
27
27
|
signalwire_agents/utils/token_generators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
28
28
|
signalwire_agents/utils/validators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
|
29
|
-
signalwire_agents-0.1.
|
30
|
-
signalwire_agents-0.1.
|
31
|
-
signalwire_agents-0.1.
|
32
|
-
signalwire_agents-0.1.
|
33
|
-
signalwire_agents-0.1.
|
34
|
-
signalwire_agents-0.1.
|
29
|
+
signalwire_agents-0.1.6.data/data/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
|
30
|
+
signalwire_agents-0.1.6.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
|
31
|
+
signalwire_agents-0.1.6.dist-info/METADATA,sha256=A4krRD2w2jb0ukbBI1kHZ6nYctSZodaaMC6Oyja13nI,7414
|
32
|
+
signalwire_agents-0.1.6.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
33
|
+
signalwire_agents-0.1.6.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
|
34
|
+
signalwire_agents-0.1.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|