signalwire-agents 0.1.6__py3-none-any.whl → 0.1.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 +1 -1
- signalwire_agents/core/agent_base.py +1265 -1252
- signalwire_agents/core/security/session_manager.py +174 -86
- signalwire_agents/core/swml_service.py +90 -49
- signalwire_agents/prefabs/concierge.py +9 -2
- signalwire_agents/prefabs/faq_bot.py +3 -0
- signalwire_agents/prefabs/info_gatherer.py +3 -0
- signalwire_agents/prefabs/receptionist.py +3 -0
- signalwire_agents/prefabs/survey.py +9 -2
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-0.1.7.dist-info}/METADATA +2 -1
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-0.1.7.dist-info}/RECORD +15 -15
- {signalwire_agents-0.1.6.data → signalwire_agents-0.1.7.data}/data/schema.json +0 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-0.1.7.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.6.dist-info → signalwire_agents-0.1.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,24 +8,21 @@ 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
|
-
|
13
|
+
Base class for all SignalWire AI Agents
|
12
14
|
"""
|
13
15
|
|
14
|
-
import functools
|
15
|
-
import inspect
|
16
16
|
import os
|
17
|
-
import
|
17
|
+
import json
|
18
|
+
import time
|
18
19
|
import uuid
|
19
|
-
import tempfile
|
20
|
-
import traceback
|
21
|
-
from typing import Dict, List, Any, Optional, Union, Callable, Tuple, Type, TypeVar
|
22
20
|
import base64
|
23
|
-
import
|
24
|
-
|
25
|
-
import
|
26
|
-
from
|
27
|
-
import
|
21
|
+
import logging
|
22
|
+
import inspect
|
23
|
+
import functools
|
24
|
+
from typing import Optional, Union, List, Dict, Any, Tuple, Callable, Type
|
25
|
+
from urllib.parse import urlparse, urlencode
|
28
26
|
|
29
27
|
try:
|
30
28
|
import fastapi
|
@@ -110,7 +108,7 @@ class AgentBase(SWMLService):
|
|
110
108
|
basic_auth: Optional[Tuple[str, str]] = None,
|
111
109
|
use_pom: bool = True,
|
112
110
|
enable_state_tracking: bool = False,
|
113
|
-
token_expiry_secs: int =
|
111
|
+
token_expiry_secs: int = 3600,
|
114
112
|
auto_answer: bool = True,
|
115
113
|
record_call: bool = False,
|
116
114
|
record_format: str = "mp4",
|
@@ -355,6 +353,19 @@ class AgentBase(SWMLService):
|
|
355
353
|
self._raw_prompt = text
|
356
354
|
return self
|
357
355
|
|
356
|
+
def set_post_prompt(self, text: str) -> 'AgentBase':
|
357
|
+
"""
|
358
|
+
Set the post-prompt text for summary generation
|
359
|
+
|
360
|
+
Args:
|
361
|
+
text: The post-prompt text
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
Self for method chaining
|
365
|
+
"""
|
366
|
+
self._post_prompt = text
|
367
|
+
return self
|
368
|
+
|
358
369
|
def set_prompt_pom(self, pom: List[Dict[str, Any]]) -> 'AgentBase':
|
359
370
|
"""
|
360
371
|
Set the prompt as a POM dictionary
|
@@ -569,6 +580,44 @@ class AgentBase(SWMLService):
|
|
569
580
|
return func
|
570
581
|
return decorator
|
571
582
|
|
583
|
+
def _register_class_decorated_tools(self):
|
584
|
+
"""
|
585
|
+
Register tools defined with @AgentBase.tool class decorator
|
586
|
+
|
587
|
+
This method scans the class for methods decorated with @AgentBase.tool
|
588
|
+
and registers them automatically.
|
589
|
+
"""
|
590
|
+
# Get the class of this instance
|
591
|
+
cls = self.__class__
|
592
|
+
|
593
|
+
# Loop through all attributes in the class
|
594
|
+
for name in dir(cls):
|
595
|
+
# Get the attribute
|
596
|
+
attr = getattr(cls, name)
|
597
|
+
|
598
|
+
# Check if it's a method decorated with @AgentBase.tool
|
599
|
+
if inspect.ismethod(attr) or inspect.isfunction(attr):
|
600
|
+
if hasattr(attr, "_is_tool") and getattr(attr, "_is_tool", False):
|
601
|
+
# Extract tool information
|
602
|
+
tool_name = getattr(attr, "_tool_name", name)
|
603
|
+
tool_params = getattr(attr, "_tool_params", {})
|
604
|
+
|
605
|
+
# Get description and parameters
|
606
|
+
description = tool_params.get("description", attr.__doc__ or f"Function {tool_name}")
|
607
|
+
parameters = tool_params.get("parameters", {})
|
608
|
+
secure = tool_params.get("secure", True)
|
609
|
+
fillers = tool_params.get("fillers", None)
|
610
|
+
|
611
|
+
# Register the tool
|
612
|
+
self.define_tool(
|
613
|
+
name=tool_name,
|
614
|
+
description=description,
|
615
|
+
parameters=parameters,
|
616
|
+
handler=attr.__get__(self, cls), # Bind the method to this instance
|
617
|
+
secure=secure,
|
618
|
+
fillers=fillers
|
619
|
+
)
|
620
|
+
|
572
621
|
@classmethod
|
573
622
|
def tool(cls, name=None, **kwargs):
|
574
623
|
"""
|
@@ -730,7 +779,17 @@ class AgentBase(SWMLService):
|
|
730
779
|
Returns:
|
731
780
|
Secure token string
|
732
781
|
"""
|
733
|
-
|
782
|
+
try:
|
783
|
+
# Ensure we have a session manager
|
784
|
+
if not hasattr(self, '_session_manager'):
|
785
|
+
self.log.error("no_session_manager")
|
786
|
+
return ""
|
787
|
+
|
788
|
+
# Create the token using the session manager
|
789
|
+
return self._session_manager.create_tool_token(tool_name, call_id)
|
790
|
+
except Exception as e:
|
791
|
+
self.log.error("token_creation_error", error=str(e), tool=tool_name, call_id=call_id)
|
792
|
+
return ""
|
734
793
|
|
735
794
|
def validate_tool_token(self, function_name: str, token: str, call_id: str) -> bool:
|
736
795
|
"""
|
@@ -744,14 +803,92 @@ class AgentBase(SWMLService):
|
|
744
803
|
Returns:
|
745
804
|
True if token is valid, False otherwise
|
746
805
|
"""
|
747
|
-
|
748
|
-
|
749
|
-
|
806
|
+
try:
|
807
|
+
# Skip validation for non-secure tools
|
808
|
+
if function_name not in self._swaig_functions:
|
809
|
+
self.log.warning("unknown_function", function=function_name)
|
810
|
+
return False
|
811
|
+
|
812
|
+
# Always allow non-secure functions
|
813
|
+
if not self._swaig_functions[function_name].secure:
|
814
|
+
self.log.debug("non_secure_function_allowed", function=function_name)
|
815
|
+
return True
|
816
|
+
|
817
|
+
# Check if we have a session manager
|
818
|
+
if not hasattr(self, '_session_manager'):
|
819
|
+
self.log.error("no_session_manager")
|
820
|
+
return False
|
821
|
+
|
822
|
+
# Handle missing token
|
823
|
+
if not token:
|
824
|
+
self.log.warning("missing_token", function=function_name)
|
825
|
+
return False
|
826
|
+
|
827
|
+
# For debugging: Log token details
|
828
|
+
try:
|
829
|
+
# Capture original parameters
|
830
|
+
self.log.debug("token_validate_input",
|
831
|
+
function=function_name,
|
832
|
+
call_id=call_id,
|
833
|
+
token_length=len(token))
|
834
|
+
|
835
|
+
# Try to decode token for debugging
|
836
|
+
if hasattr(self._session_manager, 'debug_token'):
|
837
|
+
debug_info = self._session_manager.debug_token(token)
|
838
|
+
self.log.debug("token_debug", debug_info=debug_info)
|
839
|
+
|
840
|
+
# Extract token components
|
841
|
+
if debug_info.get("valid_format") and "components" in debug_info:
|
842
|
+
components = debug_info["components"]
|
843
|
+
token_call_id = components.get("call_id")
|
844
|
+
token_function = components.get("function")
|
845
|
+
token_expiry = components.get("expiry")
|
846
|
+
|
847
|
+
# Log parameter mismatches
|
848
|
+
if token_function != function_name:
|
849
|
+
self.log.warning("token_function_mismatch",
|
850
|
+
expected=function_name,
|
851
|
+
actual=token_function)
|
852
|
+
|
853
|
+
if token_call_id != call_id:
|
854
|
+
self.log.warning("token_call_id_mismatch",
|
855
|
+
expected=call_id,
|
856
|
+
actual=token_call_id)
|
857
|
+
|
858
|
+
# Check expiration
|
859
|
+
if debug_info.get("status", {}).get("is_expired"):
|
860
|
+
self.log.warning("token_expired",
|
861
|
+
expires_in=debug_info["status"].get("expires_in_seconds"))
|
862
|
+
except Exception as e:
|
863
|
+
self.log.error("token_debug_error", error=str(e))
|
864
|
+
|
865
|
+
# Use call_id from token if the provided one is empty
|
866
|
+
if not call_id and hasattr(self._session_manager, 'debug_token'):
|
867
|
+
try:
|
868
|
+
debug_info = self._session_manager.debug_token(token)
|
869
|
+
if debug_info.get("valid_format") and "components" in debug_info:
|
870
|
+
token_call_id = debug_info["components"].get("call_id")
|
871
|
+
if token_call_id:
|
872
|
+
self.log.debug("using_call_id_from_token", call_id=token_call_id)
|
873
|
+
is_valid = self._session_manager.validate_tool_token(function_name, token, token_call_id)
|
874
|
+
if is_valid:
|
875
|
+
self.log.debug("token_valid_with_extracted_call_id")
|
876
|
+
return True
|
877
|
+
except Exception as e:
|
878
|
+
self.log.error("error_using_call_id_from_token", error=str(e))
|
750
879
|
|
751
|
-
|
752
|
-
|
880
|
+
# Normal validation with provided call_id
|
881
|
+
is_valid = self._session_manager.validate_tool_token(function_name, token, call_id)
|
753
882
|
|
754
|
-
|
883
|
+
if is_valid:
|
884
|
+
self.log.debug("token_valid", function=function_name)
|
885
|
+
else:
|
886
|
+
self.log.warning("token_invalid", function=function_name)
|
887
|
+
|
888
|
+
return is_valid
|
889
|
+
except Exception as e:
|
890
|
+
self.log.error("token_validation_error", error=str(e), function=function_name)
|
891
|
+
return False
|
755
892
|
|
756
893
|
# ----------------------------------------------------------------------
|
757
894
|
# Web Server and Routing
|
@@ -836,6 +973,16 @@ class AgentBase(SWMLService):
|
|
836
973
|
Returns:
|
837
974
|
Fully constructed webhook URL
|
838
975
|
"""
|
976
|
+
# Use the parent class's implementation if available and has the same method
|
977
|
+
if hasattr(super(), '_build_webhook_url'):
|
978
|
+
# Ensure _proxy_url_base is synchronized
|
979
|
+
if getattr(self, '_proxy_url_base', None) and hasattr(super(), '_proxy_url_base'):
|
980
|
+
super()._proxy_url_base = self._proxy_url_base
|
981
|
+
|
982
|
+
# Call parent's implementation
|
983
|
+
return super()._build_webhook_url(endpoint, query_params)
|
984
|
+
|
985
|
+
# Otherwise, fall back to our own implementation
|
839
986
|
# Base URL construction
|
840
987
|
if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
|
841
988
|
# For proxy URLs
|
@@ -965,15 +1112,26 @@ class AgentBase(SWMLService):
|
|
965
1112
|
if functions:
|
966
1113
|
swaig_obj["functions"] = functions
|
967
1114
|
|
968
|
-
# Add post-prompt URL if we have a post-prompt
|
1115
|
+
# Add post-prompt URL with token if we have a post-prompt
|
969
1116
|
post_prompt_url = None
|
970
1117
|
if post_prompt:
|
971
|
-
|
1118
|
+
# Create a token for post_prompt if we have a call_id
|
1119
|
+
query_params = {}
|
1120
|
+
if call_id and hasattr(self, '_session_manager'):
|
1121
|
+
try:
|
1122
|
+
token = self._session_manager.create_tool_token("post_prompt", call_id)
|
1123
|
+
if token:
|
1124
|
+
query_params["token"] = token
|
1125
|
+
except Exception as e:
|
1126
|
+
self.log.error("post_prompt_token_creation_error", error=str(e))
|
1127
|
+
|
1128
|
+
# Build the URL with the token (if any)
|
1129
|
+
post_prompt_url = self._build_webhook_url("post_prompt", query_params)
|
972
1130
|
|
973
1131
|
# Use override if set
|
974
1132
|
if hasattr(self, '_post_prompt_url_override') and self._post_prompt_url_override:
|
975
1133
|
post_prompt_url = self._post_prompt_url_override
|
976
|
-
|
1134
|
+
|
977
1135
|
# Add answer verb with auto-answer enabled
|
978
1136
|
self.add_answer_verb()
|
979
1137
|
|
@@ -1116,1442 +1274,1297 @@ class AgentBase(SWMLService):
|
|
1116
1274
|
Returns:
|
1117
1275
|
FastAPI router
|
1118
1276
|
"""
|
1119
|
-
#
|
1120
|
-
router =
|
1277
|
+
# Create a router with explicit redirect_slashes=False
|
1278
|
+
router = APIRouter(redirect_slashes=False)
|
1121
1279
|
|
1122
|
-
#
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1280
|
+
# Register routes explicitly
|
1281
|
+
self._register_routes(router)
|
1282
|
+
|
1283
|
+
# Log all registered routes for debugging
|
1284
|
+
print(f"Registered routes for {self.name}:")
|
1285
|
+
for route in router.routes:
|
1286
|
+
print(f" {route.path}")
|
1287
|
+
|
1288
|
+
return router
|
1289
|
+
|
1290
|
+
def serve(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
|
1291
|
+
"""
|
1292
|
+
Start a web server for this agent
|
1293
|
+
|
1294
|
+
Args:
|
1295
|
+
host: Optional host to override the default
|
1296
|
+
port: Optional port to override the default
|
1297
|
+
"""
|
1298
|
+
import uvicorn
|
1299
|
+
|
1300
|
+
if self._app is None:
|
1301
|
+
# Create a FastAPI app with explicit redirect_slashes=False
|
1302
|
+
app = FastAPI(redirect_slashes=False)
|
1303
|
+
|
1304
|
+
# Get router for this agent
|
1305
|
+
router = self.as_router()
|
1306
|
+
|
1307
|
+
# Register a catch-all route for debugging and troubleshooting
|
1308
|
+
@app.get("/{full_path:path}")
|
1309
|
+
@app.post("/{full_path:path}")
|
1310
|
+
async def handle_all_routes(request: Request, full_path: str):
|
1311
|
+
print(f"Received request for path: {full_path}")
|
1312
|
+
|
1313
|
+
# Check if the path is meant for this agent
|
1314
|
+
if not full_path.startswith(self.route.lstrip("/")):
|
1315
|
+
return {"error": "Invalid route"}
|
1316
|
+
|
1317
|
+
# Extract the path relative to this agent's route
|
1318
|
+
relative_path = full_path[len(self.route.lstrip("/")):]
|
1319
|
+
relative_path = relative_path.lstrip("/")
|
1320
|
+
print(f"Relative path: {relative_path}")
|
1321
|
+
|
1322
|
+
# Perform routing based on the relative path
|
1323
|
+
if not relative_path or relative_path == "/":
|
1324
|
+
# Root endpoint
|
1325
|
+
return await self._handle_root_request(request)
|
1326
|
+
|
1327
|
+
# Strip trailing slash for processing
|
1328
|
+
clean_path = relative_path.rstrip("/")
|
1329
|
+
|
1330
|
+
# Check for standard endpoints
|
1331
|
+
if clean_path == "debug":
|
1332
|
+
return await self._handle_debug_request(request)
|
1333
|
+
elif clean_path == "swaig":
|
1334
|
+
return await self._handle_swaig_request(request, Response())
|
1335
|
+
elif clean_path == "post_prompt":
|
1336
|
+
return await self._handle_post_prompt_request(request)
|
1337
|
+
elif clean_path == "check_for_input":
|
1338
|
+
return await self._handle_check_for_input_request(request)
|
1339
|
+
|
1340
|
+
# Check for custom routing callbacks
|
1341
|
+
if hasattr(self, '_routing_callbacks'):
|
1342
|
+
for callback_path, callback_fn in self._routing_callbacks.items():
|
1343
|
+
cb_path_clean = callback_path.strip("/")
|
1344
|
+
if clean_path == cb_path_clean:
|
1345
|
+
# Found a matching callback
|
1346
|
+
request.state.callback_path = callback_path
|
1347
|
+
return await self._handle_root_request(request)
|
1348
|
+
|
1349
|
+
# Default: 404
|
1350
|
+
return {"error": "Path not found"}
|
1351
|
+
|
1352
|
+
# Include router with prefix
|
1353
|
+
app.include_router(router, prefix=self.route)
|
1354
|
+
|
1355
|
+
# Print all app routes for debugging
|
1356
|
+
print(f"All app routes:")
|
1357
|
+
for route in app.routes:
|
1358
|
+
if hasattr(route, "path"):
|
1359
|
+
print(f" {route.path}")
|
1360
|
+
|
1361
|
+
self._app = app
|
1362
|
+
|
1363
|
+
host = host or self.host
|
1364
|
+
port = port or self.port
|
1365
|
+
|
1366
|
+
# Print the auth credentials with source
|
1367
|
+
username, password, source = self.get_basic_auth_credentials(include_source=True)
|
1368
|
+
print(f"Agent '{self.name}' is available at:")
|
1369
|
+
print(f"URL: http://{host}:{port}{self.route}")
|
1370
|
+
print(f"Basic Auth: {username}:{password} (source: {source})")
|
1371
|
+
|
1372
|
+
uvicorn.run(self._app, host=host, port=port)
|
1373
|
+
|
1374
|
+
def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
|
1375
|
+
"""
|
1376
|
+
Customization point for subclasses to modify SWML based on request data
|
1377
|
+
|
1378
|
+
Args:
|
1379
|
+
request_data: Optional dictionary containing the parsed POST body
|
1380
|
+
callback_path: Optional callback path
|
1127
1381
|
|
1128
|
-
|
1382
|
+
Returns:
|
1383
|
+
Optional dict with modifications to apply to the SWML document
|
1384
|
+
"""
|
1385
|
+
# Default implementation does nothing
|
1386
|
+
return None
|
1387
|
+
|
1388
|
+
def _register_routes(self, router):
|
1389
|
+
"""
|
1390
|
+
Register routes for this agent
|
1391
|
+
|
1392
|
+
This method ensures proper route registration by handling the routes
|
1393
|
+
directly in AgentBase rather than inheriting from SWMLService.
|
1394
|
+
|
1395
|
+
Args:
|
1396
|
+
router: FastAPI router to register routes with
|
1397
|
+
"""
|
1398
|
+
# Root endpoint (handles both with and without trailing slash)
|
1129
1399
|
@router.get("/")
|
1130
1400
|
@router.post("/")
|
1131
|
-
async def
|
1401
|
+
async def handle_root(request: Request, response: Response):
|
1402
|
+
"""Handle GET/POST requests to the root endpoint"""
|
1132
1403
|
return await self._handle_root_request(request)
|
1133
1404
|
|
1134
|
-
# Debug endpoint -
|
1405
|
+
# Debug endpoint - Both versions
|
1135
1406
|
@router.get("/debug")
|
1136
|
-
@router.post("/debug")
|
1137
|
-
async def handle_debug_no_slash(request: Request):
|
1138
|
-
return await self._handle_debug_request(request)
|
1139
|
-
|
1140
|
-
# Debug endpoint - with trailing slash
|
1141
1407
|
@router.get("/debug/")
|
1408
|
+
@router.post("/debug")
|
1142
1409
|
@router.post("/debug/")
|
1143
|
-
async def
|
1410
|
+
async def handle_debug(request: Request):
|
1411
|
+
"""Handle GET/POST requests to the debug endpoint"""
|
1144
1412
|
return await self._handle_debug_request(request)
|
1145
1413
|
|
1146
|
-
# SWAIG endpoint -
|
1414
|
+
# SWAIG endpoint - Both versions
|
1147
1415
|
@router.get("/swaig")
|
1148
|
-
@router.post("/swaig")
|
1149
|
-
async def handle_swaig_no_slash(request: Request):
|
1150
|
-
return await self._handle_swaig_request(request)
|
1151
|
-
|
1152
|
-
# SWAIG endpoint - with trailing slash
|
1153
1416
|
@router.get("/swaig/")
|
1417
|
+
@router.post("/swaig")
|
1154
1418
|
@router.post("/swaig/")
|
1155
|
-
async def
|
1156
|
-
|
1419
|
+
async def handle_swaig(request: Request, response: Response):
|
1420
|
+
"""Handle GET/POST requests to the SWAIG endpoint"""
|
1421
|
+
return await self._handle_swaig_request(request, response)
|
1157
1422
|
|
1158
|
-
# Post
|
1423
|
+
# Post prompt endpoint - Both versions
|
1159
1424
|
@router.get("/post_prompt")
|
1160
|
-
@router.post("/post_prompt")
|
1161
|
-
async def handle_post_prompt_no_slash(request: Request):
|
1162
|
-
return await self._handle_post_prompt_request(request)
|
1163
|
-
|
1164
|
-
# Post-prompt endpoint - with trailing slash
|
1165
1425
|
@router.get("/post_prompt/")
|
1426
|
+
@router.post("/post_prompt")
|
1166
1427
|
@router.post("/post_prompt/")
|
1167
|
-
async def
|
1428
|
+
async def handle_post_prompt(request: Request):
|
1429
|
+
"""Handle GET/POST requests to the post_prompt endpoint"""
|
1168
1430
|
return await self._handle_post_prompt_request(request)
|
1431
|
+
|
1432
|
+
# Check for input endpoint - Both versions
|
1433
|
+
@router.get("/check_for_input")
|
1434
|
+
@router.get("/check_for_input/")
|
1435
|
+
@router.post("/check_for_input")
|
1436
|
+
@router.post("/check_for_input/")
|
1437
|
+
async def handle_check_for_input(request: Request):
|
1438
|
+
"""Handle GET/POST requests to the check_for_input endpoint"""
|
1439
|
+
return await self._handle_check_for_input_request(request)
|
1169
1440
|
|
1170
|
-
|
1171
|
-
|
1441
|
+
# Register callback routes for routing callbacks if available
|
1442
|
+
if hasattr(self, '_routing_callbacks') and self._routing_callbacks:
|
1443
|
+
for callback_path, callback_fn in self._routing_callbacks.items():
|
1444
|
+
# Skip the root path as it's already handled
|
1445
|
+
if callback_path == "/":
|
1446
|
+
continue
|
1447
|
+
|
1448
|
+
# Register both with and without trailing slash
|
1449
|
+
path = callback_path.rstrip("/")
|
1450
|
+
path_with_slash = f"{path}/"
|
1451
|
+
|
1452
|
+
@router.get(path)
|
1453
|
+
@router.get(path_with_slash)
|
1454
|
+
@router.post(path)
|
1455
|
+
@router.post(path_with_slash)
|
1456
|
+
async def handle_callback(request: Request, response: Response, cb_path=callback_path):
|
1457
|
+
"""Handle GET/POST requests to a registered callback path"""
|
1458
|
+
# Store the callback path in request state for _handle_request to use
|
1459
|
+
request.state.callback_path = cb_path
|
1460
|
+
return await self._handle_root_request(request)
|
1461
|
+
|
1462
|
+
self.log.info("callback_endpoint_registered", path=callback_path)
|
1172
1463
|
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
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
|
1464
|
+
@classmethod
|
1465
|
+
|
1466
|
+
# ----------------------------------------------------------------------
|
1467
|
+
# AI Verb Configuration Methods
|
1468
|
+
# ----------------------------------------------------------------------
|
1469
|
+
|
1470
|
+
def add_hint(self, hint: str) -> 'AgentBase':
|
1471
|
+
"""
|
1472
|
+
Add a simple string hint to help the AI agent understand certain words better
|
1190
1473
|
|
1191
|
-
|
1192
|
-
|
1474
|
+
Args:
|
1475
|
+
hint: The hint string to add
|
1476
|
+
|
1477
|
+
Returns:
|
1478
|
+
Self for method chaining
|
1479
|
+
"""
|
1480
|
+
if isinstance(hint, str) and hint:
|
1481
|
+
self._hints.append(hint)
|
1482
|
+
return self
|
1483
|
+
|
1484
|
+
def add_hints(self, hints: List[str]) -> 'AgentBase':
|
1485
|
+
"""
|
1486
|
+
Add multiple string hints
|
1193
1487
|
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1488
|
+
Args:
|
1489
|
+
hints: List of hint strings
|
1490
|
+
|
1491
|
+
Returns:
|
1492
|
+
Self for method chaining
|
1493
|
+
"""
|
1494
|
+
if hints and isinstance(hints, list):
|
1495
|
+
for hint in hints:
|
1496
|
+
if isinstance(hint, str) and hint:
|
1497
|
+
self._hints.append(hint)
|
1498
|
+
return self
|
1499
|
+
|
1500
|
+
def add_pattern_hint(self,
|
1501
|
+
hint: str,
|
1502
|
+
pattern: str,
|
1503
|
+
replace: str,
|
1504
|
+
ignore_case: bool = False) -> 'AgentBase':
|
1505
|
+
"""
|
1506
|
+
Add a complex hint with pattern matching
|
1199
1507
|
|
1200
|
-
|
1508
|
+
Args:
|
1509
|
+
hint: The hint to match
|
1510
|
+
pattern: Regular expression pattern
|
1511
|
+
replace: Text to replace the hint with
|
1512
|
+
ignore_case: Whether to ignore case when matching
|
1513
|
+
|
1514
|
+
Returns:
|
1515
|
+
Self for method chaining
|
1516
|
+
"""
|
1517
|
+
if hint and pattern and replace:
|
1518
|
+
self._hints.append({
|
1519
|
+
"hint": hint,
|
1520
|
+
"pattern": pattern,
|
1521
|
+
"replace": replace,
|
1522
|
+
"ignore_case": ignore_case
|
1523
|
+
})
|
1524
|
+
return self
|
1525
|
+
|
1526
|
+
def add_language(self,
|
1527
|
+
name: str,
|
1528
|
+
code: str,
|
1529
|
+
voice: str,
|
1530
|
+
speech_fillers: Optional[List[str]] = None,
|
1531
|
+
function_fillers: Optional[List[str]] = None,
|
1532
|
+
engine: Optional[str] = None,
|
1533
|
+
model: Optional[str] = None) -> 'AgentBase':
|
1534
|
+
"""
|
1535
|
+
Add a language configuration to support multilingual conversations
|
1201
1536
|
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
)
|
1537
|
+
Args:
|
1538
|
+
name: Name of the language (e.g., "English", "French")
|
1539
|
+
code: Language code (e.g., "en-US", "fr-FR")
|
1540
|
+
voice: TTS voice to use. Can be a simple name (e.g., "en-US-Neural2-F")
|
1541
|
+
or a combined format "engine.voice:model" (e.g., "elevenlabs.josh:eleven_turbo_v2_5")
|
1542
|
+
speech_fillers: Optional list of filler phrases for natural speech
|
1543
|
+
function_fillers: Optional list of filler phrases during function calls
|
1544
|
+
engine: Optional explicit engine name (e.g., "elevenlabs", "rime")
|
1545
|
+
model: Optional explicit model name (e.g., "eleven_turbo_v2_5", "arcana")
|
1212
1546
|
|
1213
|
-
|
1214
|
-
|
1215
|
-
call_id = None
|
1547
|
+
Returns:
|
1548
|
+
Self for method chaining
|
1216
1549
|
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
if raw_body:
|
1221
|
-
try:
|
1222
|
-
body = await request.json()
|
1223
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
1224
|
-
if body:
|
1225
|
-
req_log.debug("request_body", body=json.dumps(body, indent=2))
|
1226
|
-
except Exception as e:
|
1227
|
-
req_log.warning("error_parsing_request_body", error=str(e), traceback=traceback.format_exc())
|
1228
|
-
req_log.debug("raw_request_body", body=raw_body.decode('utf-8', errors='replace'))
|
1229
|
-
# Continue processing with empty body
|
1230
|
-
body = {}
|
1231
|
-
else:
|
1232
|
-
req_log.debug("empty_request_body")
|
1233
|
-
|
1234
|
-
# Get call_id from body if present
|
1235
|
-
call_id = body.get("call_id")
|
1236
|
-
else:
|
1237
|
-
# Get call_id from query params for GET
|
1238
|
-
call_id = request.query_params.get("call_id")
|
1239
|
-
|
1240
|
-
# Add call_id to logger if any
|
1241
|
-
if call_id:
|
1242
|
-
req_log = req_log.bind(call_id=call_id)
|
1243
|
-
req_log.debug("call_id_identified")
|
1244
|
-
|
1245
|
-
# Check if this is a callback path and we need to apply routing
|
1246
|
-
if callback_path and hasattr(self, '_routing_callbacks') and callback_path in self._routing_callbacks:
|
1247
|
-
callback_fn = self._routing_callbacks[callback_path]
|
1248
|
-
|
1249
|
-
if request.method == "POST" and body:
|
1250
|
-
req_log.debug("processing_routing_callback", path=callback_path)
|
1251
|
-
# Call the routing callback
|
1252
|
-
try:
|
1253
|
-
route = callback_fn(request, body)
|
1254
|
-
if route is not None:
|
1255
|
-
req_log.info("routing_request", route=route)
|
1256
|
-
# Return a redirect to the new route
|
1257
|
-
return Response(
|
1258
|
-
status_code=307, # 307 Temporary Redirect preserves the method and body
|
1259
|
-
headers={"Location": route}
|
1260
|
-
)
|
1261
|
-
except Exception as e:
|
1262
|
-
req_log.error("error_in_routing_callback", error=str(e), traceback=traceback.format_exc())
|
1263
|
-
|
1264
|
-
# Allow subclasses to inspect/modify the request
|
1265
|
-
modifications = None
|
1266
|
-
if body:
|
1267
|
-
try:
|
1268
|
-
modifications = self.on_swml_request(body)
|
1269
|
-
if modifications:
|
1270
|
-
req_log.debug("request_modifications_applied")
|
1271
|
-
except Exception as e:
|
1272
|
-
req_log.error("error_in_request_modifier", error=str(e), traceback=traceback.format_exc())
|
1273
|
-
|
1274
|
-
# Render SWML
|
1275
|
-
swml = self._render_swml(call_id, modifications)
|
1276
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
1277
|
-
|
1278
|
-
# Return as JSON
|
1279
|
-
req_log.info("request_successful")
|
1280
|
-
return Response(
|
1281
|
-
content=swml,
|
1282
|
-
media_type="application/json"
|
1283
|
-
)
|
1284
|
-
except Exception as e:
|
1285
|
-
req_log.error("request_failed", error=str(e), traceback=traceback.format_exc())
|
1286
|
-
return Response(
|
1287
|
-
content=json.dumps({"error": str(e), "traceback": traceback.format_exc()}),
|
1288
|
-
status_code=500,
|
1289
|
-
media_type="application/json"
|
1290
|
-
)
|
1291
|
-
|
1292
|
-
async def _handle_debug_request(self, request: Request):
|
1293
|
-
"""Handle GET/POST requests to the debug endpoint"""
|
1294
|
-
req_log = self.log.bind(
|
1295
|
-
endpoint="debug",
|
1296
|
-
method=request.method,
|
1297
|
-
path=request.url.path
|
1298
|
-
)
|
1299
|
-
|
1300
|
-
req_log.debug("endpoint_called")
|
1301
|
-
|
1302
|
-
try:
|
1303
|
-
# Check auth
|
1304
|
-
if not self._check_basic_auth(request):
|
1305
|
-
req_log.warning("unauthorized_access_attempt")
|
1306
|
-
return Response(
|
1307
|
-
content=json.dumps({"error": "Unauthorized"}),
|
1308
|
-
status_code=401,
|
1309
|
-
headers={"WWW-Authenticate": "Basic"},
|
1310
|
-
media_type="application/json"
|
1311
|
-
)
|
1312
|
-
|
1313
|
-
# Get call_id from either query params (GET) or body (POST)
|
1314
|
-
call_id = None
|
1315
|
-
body = {}
|
1316
|
-
|
1317
|
-
if request.method == "POST":
|
1318
|
-
try:
|
1319
|
-
body = await request.json()
|
1320
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
1321
|
-
if body:
|
1322
|
-
req_log.debug("request_body", body=json.dumps(body, indent=2))
|
1323
|
-
call_id = body.get("call_id")
|
1324
|
-
except Exception as e:
|
1325
|
-
req_log.warning("error_parsing_request_body", error=str(e), traceback=traceback.format_exc())
|
1326
|
-
try:
|
1327
|
-
body_text = await request.body()
|
1328
|
-
req_log.debug("raw_request_body", body=body_text.decode('utf-8', errors='replace'))
|
1329
|
-
except:
|
1330
|
-
pass
|
1331
|
-
else:
|
1332
|
-
call_id = request.query_params.get("call_id")
|
1333
|
-
|
1334
|
-
# Add call_id to logger if any
|
1335
|
-
if call_id:
|
1336
|
-
req_log = req_log.bind(call_id=call_id)
|
1337
|
-
req_log.debug("call_id_identified")
|
1338
|
-
|
1339
|
-
# Allow subclasses to inspect/modify the request
|
1340
|
-
modifications = None
|
1341
|
-
if body:
|
1342
|
-
modifications = self.on_swml_request(body)
|
1343
|
-
if modifications:
|
1344
|
-
req_log.debug("request_modifications_applied")
|
1345
|
-
|
1346
|
-
# Render SWML
|
1347
|
-
swml = self._render_swml(call_id, modifications)
|
1348
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
1349
|
-
|
1350
|
-
# Return as JSON
|
1351
|
-
req_log.info("request_successful")
|
1352
|
-
return Response(
|
1353
|
-
content=swml,
|
1354
|
-
media_type="application/json",
|
1355
|
-
headers={"X-Debug": "true"}
|
1356
|
-
)
|
1357
|
-
except Exception as e:
|
1358
|
-
req_log.error("request_failed", error=str(e), traceback=traceback.format_exc())
|
1359
|
-
return Response(
|
1360
|
-
content=json.dumps({"error": str(e), "traceback": traceback.format_exc()}),
|
1361
|
-
status_code=500,
|
1362
|
-
media_type="application/json"
|
1363
|
-
)
|
1364
|
-
|
1365
|
-
async def _handle_swaig_request(self, request: Request):
|
1366
|
-
"""Handle GET/POST requests to the SWAIG endpoint"""
|
1367
|
-
req_log = self.log.bind(
|
1368
|
-
endpoint="swaig",
|
1369
|
-
method=request.method,
|
1370
|
-
path=request.url.path
|
1371
|
-
)
|
1372
|
-
|
1373
|
-
req_log.debug("endpoint_called")
|
1374
|
-
|
1375
|
-
try:
|
1376
|
-
# Check auth
|
1377
|
-
if not self._check_basic_auth(request):
|
1378
|
-
req_log.warning("unauthorized_access_attempt")
|
1379
|
-
return Response(
|
1380
|
-
content=json.dumps({"error": "Unauthorized"}),
|
1381
|
-
status_code=401,
|
1382
|
-
headers={"WWW-Authenticate": "Basic"},
|
1383
|
-
media_type="application/json"
|
1384
|
-
)
|
1385
|
-
|
1386
|
-
# Handle differently based on method
|
1387
|
-
if request.method == "GET":
|
1388
|
-
# For GET requests, return the SWML document (same as root endpoint)
|
1389
|
-
call_id = request.query_params.get("call_id")
|
1390
|
-
swml = self._render_swml(call_id)
|
1391
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
1392
|
-
return Response(
|
1393
|
-
content=swml,
|
1394
|
-
media_type="application/json"
|
1395
|
-
)
|
1396
|
-
|
1397
|
-
# For POST requests, process SWAIG function calls
|
1398
|
-
try:
|
1399
|
-
body = await request.json()
|
1400
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
1401
|
-
if body:
|
1402
|
-
req_log.debug("request_body", body=json.dumps(body, indent=2))
|
1403
|
-
except Exception as e:
|
1404
|
-
req_log.error("error_parsing_request_body", error=str(e), traceback=traceback.format_exc())
|
1405
|
-
body = {}
|
1406
|
-
|
1407
|
-
# Extract function name
|
1408
|
-
function_name = body.get("function")
|
1409
|
-
if not function_name:
|
1410
|
-
req_log.warning("missing_function_name")
|
1411
|
-
return Response(
|
1412
|
-
content=json.dumps({"error": "Missing function name"}),
|
1413
|
-
status_code=400,
|
1414
|
-
media_type="application/json"
|
1415
|
-
)
|
1416
|
-
|
1417
|
-
# Add function info to logger
|
1418
|
-
req_log = req_log.bind(function=function_name)
|
1419
|
-
req_log.debug("function_call_received")
|
1420
|
-
|
1421
|
-
# Extract arguments
|
1422
|
-
args = {}
|
1423
|
-
if "argument" in body and isinstance(body["argument"], dict):
|
1424
|
-
if "parsed" in body["argument"] and isinstance(body["argument"]["parsed"], list) and body["argument"]["parsed"]:
|
1425
|
-
args = body["argument"]["parsed"][0]
|
1426
|
-
req_log.debug("parsed_arguments", args=json.dumps(args, indent=2))
|
1427
|
-
elif "raw" in body["argument"]:
|
1428
|
-
try:
|
1429
|
-
args = json.loads(body["argument"]["raw"])
|
1430
|
-
req_log.debug("raw_arguments_parsed", args=json.dumps(args, indent=2))
|
1431
|
-
except Exception as e:
|
1432
|
-
req_log.error("error_parsing_raw_arguments", error=str(e), raw=body["argument"]["raw"])
|
1433
|
-
|
1434
|
-
# Get call_id from body
|
1435
|
-
call_id = body.get("call_id")
|
1436
|
-
if call_id:
|
1437
|
-
req_log = req_log.bind(call_id=call_id)
|
1438
|
-
req_log.debug("call_id_identified")
|
1550
|
+
Examples:
|
1551
|
+
# Simple voice name
|
1552
|
+
agent.add_language("English", "en-US", "en-US-Neural2-F")
|
1439
1553
|
|
1440
|
-
#
|
1441
|
-
|
1442
|
-
result = self.on_function_call(function_name, args, body)
|
1443
|
-
|
1444
|
-
# Convert result to dict if needed
|
1445
|
-
if isinstance(result, SwaigFunctionResult):
|
1446
|
-
result_dict = result.to_dict()
|
1447
|
-
elif isinstance(result, dict):
|
1448
|
-
result_dict = result
|
1449
|
-
else:
|
1450
|
-
result_dict = {"response": str(result)}
|
1451
|
-
|
1452
|
-
req_log.info("function_executed_successfully")
|
1453
|
-
req_log.debug("function_result", result=json.dumps(result_dict, indent=2))
|
1454
|
-
return result_dict
|
1455
|
-
except Exception as e:
|
1456
|
-
req_log.error("function_execution_error", error=str(e), traceback=traceback.format_exc())
|
1457
|
-
return {"error": str(e), "function": function_name}
|
1458
|
-
|
1459
|
-
except Exception as e:
|
1460
|
-
req_log.error("request_failed", error=str(e), traceback=traceback.format_exc())
|
1461
|
-
return Response(
|
1462
|
-
content=json.dumps({"error": str(e)}),
|
1463
|
-
status_code=500,
|
1464
|
-
media_type="application/json"
|
1465
|
-
)
|
1554
|
+
# Explicit parameters
|
1555
|
+
agent.add_language("English", "en-US", "josh", engine="elevenlabs", model="eleven_turbo_v2_5")
|
1466
1556
|
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1475
|
-
# Only log if not suppressed
|
1476
|
-
if not self._suppress_logs:
|
1477
|
-
req_log.debug("endpoint_called")
|
1557
|
+
# Combined format
|
1558
|
+
agent.add_language("English", "en-US", "elevenlabs.josh:eleven_turbo_v2_5")
|
1559
|
+
"""
|
1560
|
+
language = {
|
1561
|
+
"name": name,
|
1562
|
+
"code": code
|
1563
|
+
}
|
1478
1564
|
|
1479
|
-
|
1480
|
-
|
1481
|
-
if
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
# For GET requests, return the SWML document (same as root endpoint)
|
1491
|
-
if request.method == "GET":
|
1492
|
-
call_id = request.query_params.get("call_id")
|
1493
|
-
swml = self._render_swml(call_id)
|
1494
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
1495
|
-
return Response(
|
1496
|
-
content=swml,
|
1497
|
-
media_type="application/json"
|
1498
|
-
)
|
1499
|
-
|
1500
|
-
# For POST requests, process the post-prompt data
|
1565
|
+
# Handle voice formatting (either explicit params or combined string)
|
1566
|
+
if engine or model:
|
1567
|
+
# Use explicit parameters if provided
|
1568
|
+
language["voice"] = voice
|
1569
|
+
if engine:
|
1570
|
+
language["engine"] = engine
|
1571
|
+
if model:
|
1572
|
+
language["model"] = model
|
1573
|
+
elif "." in voice and ":" in voice:
|
1574
|
+
# Parse combined string format: "engine.voice:model"
|
1501
1575
|
try:
|
1502
|
-
|
1503
|
-
|
1504
|
-
# Only log if not suppressed
|
1505
|
-
if not self._suppress_logs:
|
1506
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
1507
|
-
# Log the raw body as properly formatted JSON (not Python dict representation)
|
1508
|
-
print("POST_PROMPT_BODY: " + json.dumps(body))
|
1509
|
-
except Exception as e:
|
1510
|
-
req_log.error("error_parsing_request_body", error=str(e), traceback=traceback.format_exc())
|
1511
|
-
body = {}
|
1512
|
-
|
1513
|
-
# Extract summary from the correct location in the request
|
1514
|
-
summary = self._find_summary_in_post_data(body, req_log)
|
1515
|
-
|
1516
|
-
# Save state if call_id is provided
|
1517
|
-
call_id = body.get("call_id")
|
1518
|
-
if call_id and summary:
|
1519
|
-
req_log = req_log.bind(call_id=call_id)
|
1576
|
+
engine_voice, model_part = voice.split(":", 1)
|
1577
|
+
engine_part, voice_part = engine_voice.split(".", 1)
|
1520
1578
|
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1530
|
-
req_log.warning("state_update_failed", error=str(e))
|
1531
|
-
|
1532
|
-
# Call the summary handler with the summary and the full body
|
1533
|
-
try:
|
1534
|
-
if summary:
|
1535
|
-
self.on_summary(summary, body)
|
1536
|
-
req_log.debug("summary_handler_called_successfully")
|
1537
|
-
else:
|
1538
|
-
# If no summary found but still want to process the data
|
1539
|
-
self.on_summary(None, body)
|
1540
|
-
req_log.debug("summary_handler_called_with_null_summary")
|
1541
|
-
except Exception as e:
|
1542
|
-
req_log.error("error_in_summary_handler", error=str(e), traceback=traceback.format_exc())
|
1543
|
-
|
1544
|
-
# Return success
|
1545
|
-
req_log.info("request_successful")
|
1546
|
-
return {"success": True}
|
1547
|
-
except Exception as e:
|
1548
|
-
req_log.error("request_failed", error=str(e), traceback=traceback.format_exc())
|
1549
|
-
return Response(
|
1550
|
-
content=json.dumps({"error": str(e)}),
|
1551
|
-
status_code=500,
|
1552
|
-
media_type="application/json"
|
1553
|
-
)
|
1554
|
-
|
1555
|
-
def _find_summary_in_post_data(self, body, logger):
|
1556
|
-
"""
|
1557
|
-
Extensive search for the summary in the post data
|
1558
|
-
|
1559
|
-
Args:
|
1560
|
-
body: The POST request body
|
1561
|
-
logger: The logger instance to use
|
1562
|
-
|
1563
|
-
Returns:
|
1564
|
-
The summary if found, None otherwise
|
1565
|
-
"""
|
1566
|
-
summary = None
|
1567
|
-
|
1568
|
-
# Check all the locations where the summary might be found
|
1569
|
-
|
1570
|
-
# 1. First check post_prompt_data.parsed array (new standard location)
|
1571
|
-
post_prompt_data = body.get("post_prompt_data", {})
|
1572
|
-
if post_prompt_data:
|
1573
|
-
if not self._suppress_logs:
|
1574
|
-
logger.debug("checking_post_prompt_data", data_type=type(post_prompt_data).__name__)
|
1575
|
-
|
1576
|
-
# Check for parsed array first (this is the most common location)
|
1577
|
-
if isinstance(post_prompt_data, dict) and "parsed" in post_prompt_data:
|
1578
|
-
parsed = post_prompt_data.get("parsed")
|
1579
|
-
if isinstance(parsed, list) and len(parsed) > 0:
|
1580
|
-
# The summary is the first item in the parsed array
|
1581
|
-
summary = parsed[0]
|
1582
|
-
print("SUMMARY_FOUND: " + json.dumps(summary))
|
1583
|
-
return summary
|
1584
|
-
|
1585
|
-
# Check raw field - it might contain a JSON string
|
1586
|
-
if isinstance(post_prompt_data, dict) and "raw" in post_prompt_data:
|
1587
|
-
raw = post_prompt_data.get("raw")
|
1588
|
-
if isinstance(raw, str):
|
1589
|
-
try:
|
1590
|
-
# Try to parse the raw field as JSON
|
1591
|
-
parsed_raw = json.loads(raw)
|
1592
|
-
if not self._suppress_logs:
|
1593
|
-
print("SUMMARY_FOUND_RAW: " + json.dumps(parsed_raw))
|
1594
|
-
return parsed_raw
|
1595
|
-
except:
|
1596
|
-
pass
|
1597
|
-
|
1598
|
-
# Direct access to substituted field
|
1599
|
-
if isinstance(post_prompt_data, dict) and "substituted" in post_prompt_data:
|
1600
|
-
summary = post_prompt_data.get("substituted")
|
1601
|
-
if not self._suppress_logs:
|
1602
|
-
print("SUMMARY_FOUND_SUBSTITUTED: " + json.dumps(summary) if isinstance(summary, (dict, list)) else f"SUMMARY_FOUND_SUBSTITUTED: {summary}")
|
1603
|
-
return summary
|
1604
|
-
|
1605
|
-
# Check for nested data structure
|
1606
|
-
if isinstance(post_prompt_data, dict) and "data" in post_prompt_data:
|
1607
|
-
data = post_prompt_data.get("data")
|
1608
|
-
if isinstance(data, dict):
|
1609
|
-
if "substituted" in data:
|
1610
|
-
summary = data.get("substituted")
|
1611
|
-
if not self._suppress_logs:
|
1612
|
-
print("SUMMARY_FOUND_DATA_SUBSTITUTED: " + json.dumps(summary) if isinstance(summary, (dict, list)) else f"SUMMARY_FOUND_DATA_SUBSTITUTED: {summary}")
|
1613
|
-
return summary
|
1614
|
-
|
1615
|
-
# Try text field
|
1616
|
-
if "text" in data:
|
1617
|
-
summary = data.get("text")
|
1618
|
-
if not self._suppress_logs:
|
1619
|
-
print("SUMMARY_FOUND_DATA_TEXT: " + json.dumps(summary) if isinstance(summary, (dict, list)) else f"SUMMARY_FOUND_DATA_TEXT: {summary}")
|
1620
|
-
return summary
|
1621
|
-
|
1622
|
-
# 2. Check ai_response (legacy location)
|
1623
|
-
ai_response = body.get("ai_response", {})
|
1624
|
-
if ai_response and isinstance(ai_response, dict):
|
1625
|
-
if "summary" in ai_response:
|
1626
|
-
summary = ai_response.get("summary")
|
1627
|
-
if not self._suppress_logs:
|
1628
|
-
print("SUMMARY_FOUND_AI_RESPONSE: " + json.dumps(summary) if isinstance(summary, (dict, list)) else f"SUMMARY_FOUND_AI_RESPONSE: {summary}")
|
1629
|
-
return summary
|
1630
|
-
|
1631
|
-
# 3. Look for direct fields at the top level
|
1632
|
-
for field in ["substituted", "summary", "content", "text", "result", "output"]:
|
1633
|
-
if field in body:
|
1634
|
-
summary = body.get(field)
|
1635
|
-
if not self._suppress_logs:
|
1636
|
-
print(f"SUMMARY_FOUND_TOP_LEVEL_{field}: " + json.dumps(summary) if isinstance(summary, (dict, list)) else f"SUMMARY_FOUND_TOP_LEVEL_{field}: {summary}")
|
1637
|
-
return summary
|
1638
|
-
|
1639
|
-
# 4. Recursively search for summary-like fields up to 3 levels deep
|
1640
|
-
def recursive_search(data, path="", depth=0):
|
1641
|
-
if depth > 3 or not isinstance(data, dict): # Limit recursion depth
|
1642
|
-
return None
|
1643
|
-
|
1644
|
-
# Check if any key looks like it might contain a summary
|
1645
|
-
for key in data.keys():
|
1646
|
-
if key.lower() in ["summary", "substituted", "output", "result", "content", "text"]:
|
1647
|
-
value = data.get(key)
|
1648
|
-
curr_path = f"{path}.{key}" if path else key
|
1649
|
-
if not self._suppress_logs:
|
1650
|
-
logger.info(f"potential_summary_found_at_{curr_path}",
|
1651
|
-
value_type=type(value).__name__)
|
1652
|
-
if isinstance(value, (str, dict, list)):
|
1653
|
-
return value
|
1654
|
-
|
1655
|
-
# Recursively check nested dictionaries
|
1656
|
-
for key, value in data.items():
|
1657
|
-
if isinstance(value, dict):
|
1658
|
-
curr_path = f"{path}.{key}" if path else key
|
1659
|
-
result = recursive_search(value, curr_path, depth + 1)
|
1660
|
-
if result:
|
1661
|
-
return result
|
1662
|
-
|
1663
|
-
return None
|
1664
|
-
|
1665
|
-
# Perform recursive search
|
1666
|
-
recursive_result = recursive_search(body)
|
1667
|
-
if recursive_result:
|
1668
|
-
summary = recursive_result
|
1669
|
-
if not self._suppress_logs:
|
1670
|
-
print("SUMMARY_FOUND_RECURSIVE: " + json.dumps(summary) if isinstance(summary, (dict, list)) else f"SUMMARY_FOUND_RECURSIVE: {summary}")
|
1671
|
-
return summary
|
1672
|
-
|
1673
|
-
# No summary found
|
1674
|
-
if not self._suppress_logs:
|
1675
|
-
print("NO_SUMMARY_FOUND")
|
1676
|
-
return None
|
1677
|
-
|
1678
|
-
def _register_routes(self, app):
|
1679
|
-
"""Register all routes for the agent, with both slash variants and both HTTP methods"""
|
1680
|
-
|
1681
|
-
self.log.info("registering_routes", path=self.route)
|
1682
|
-
|
1683
|
-
# Root endpoint - without trailing slash
|
1684
|
-
@app.get(f"{self.route}")
|
1685
|
-
@app.post(f"{self.route}")
|
1686
|
-
async def handle_root_no_slash(request: Request):
|
1687
|
-
return await self._handle_root_request(request)
|
1688
|
-
|
1689
|
-
# Root endpoint - with trailing slash
|
1690
|
-
@app.get(f"{self.route}/")
|
1691
|
-
@app.post(f"{self.route}/")
|
1692
|
-
async def handle_root_with_slash(request: Request):
|
1693
|
-
return await self._handle_root_request(request)
|
1694
|
-
|
1695
|
-
# Debug endpoint - without trailing slash
|
1696
|
-
@app.get(f"{self.route}/debug")
|
1697
|
-
@app.post(f"{self.route}/debug")
|
1698
|
-
async def handle_debug_no_slash(request: Request):
|
1699
|
-
return await self._handle_debug_request(request)
|
1700
|
-
|
1701
|
-
# Debug endpoint - with trailing slash
|
1702
|
-
@app.get(f"{self.route}/debug/")
|
1703
|
-
@app.post(f"{self.route}/debug/")
|
1704
|
-
async def handle_debug_with_slash(request: Request):
|
1705
|
-
return await self._handle_debug_request(request)
|
1706
|
-
|
1707
|
-
# SWAIG endpoint - without trailing slash
|
1708
|
-
@app.get(f"{self.route}/swaig")
|
1709
|
-
@app.post(f"{self.route}/swaig")
|
1710
|
-
async def handle_swaig_no_slash(request: Request):
|
1711
|
-
return await self._handle_swaig_request(request)
|
1712
|
-
|
1713
|
-
# SWAIG endpoint - with trailing slash
|
1714
|
-
@app.get(f"{self.route}/swaig/")
|
1715
|
-
@app.post(f"{self.route}/swaig/")
|
1716
|
-
async def handle_swaig_with_slash(request: Request):
|
1717
|
-
return await self._handle_swaig_request(request)
|
1718
|
-
|
1719
|
-
# Post-prompt endpoint - without trailing slash
|
1720
|
-
@app.get(f"{self.route}/post_prompt")
|
1721
|
-
@app.post(f"{self.route}/post_prompt")
|
1722
|
-
async def handle_post_prompt_no_slash(request: Request):
|
1723
|
-
return await self._handle_post_prompt_request(request)
|
1724
|
-
|
1725
|
-
# Post-prompt endpoint - with trailing slash
|
1726
|
-
@app.get(f"{self.route}/post_prompt/")
|
1727
|
-
@app.post(f"{self.route}/post_prompt/")
|
1728
|
-
async def handle_post_prompt_with_slash(request: Request):
|
1729
|
-
return await self._handle_post_prompt_request(request)
|
1579
|
+
language["voice"] = voice_part
|
1580
|
+
language["engine"] = engine_part
|
1581
|
+
language["model"] = model_part
|
1582
|
+
except ValueError:
|
1583
|
+
# If parsing fails, use the voice string as-is
|
1584
|
+
language["voice"] = voice
|
1585
|
+
else:
|
1586
|
+
# Simple voice string
|
1587
|
+
language["voice"] = voice
|
1730
1588
|
|
1731
|
-
#
|
1732
|
-
if
|
1733
|
-
|
1734
|
-
|
1735
|
-
|
1736
|
-
|
1737
|
-
|
1738
|
-
|
1739
|
-
|
1740
|
-
|
1741
|
-
|
1742
|
-
@app.get(callback_route)
|
1743
|
-
@app.post(callback_route)
|
1744
|
-
async def handle_callback_no_slash(request: Request, path_param=callback_route):
|
1745
|
-
# Store the callback path in request state for _handle_root_request to use
|
1746
|
-
request.state.callback_path = path_param
|
1747
|
-
return await self._handle_root_request(request)
|
1748
|
-
|
1749
|
-
# Register the endpoint with trailing slash if it doesn't already have one
|
1750
|
-
if not callback_route.endswith('/'):
|
1751
|
-
slash_route = f"{callback_route}/"
|
1752
|
-
|
1753
|
-
@app.get(slash_route)
|
1754
|
-
@app.post(slash_route)
|
1755
|
-
async def handle_callback_with_slash(request: Request, path_param=callback_route):
|
1756
|
-
# Store the callback path in request state for _handle_root_request to use
|
1757
|
-
request.state.callback_path = path_param
|
1758
|
-
return await self._handle_root_request(request)
|
1759
|
-
|
1760
|
-
# Log all registered routes
|
1761
|
-
routes = [f"{route.methods} {route.path}" for route in app.routes]
|
1762
|
-
self.log.debug("routes_registered", routes=routes)
|
1763
|
-
|
1764
|
-
def _register_class_decorated_tools(self):
|
1765
|
-
"""
|
1766
|
-
Register all tools decorated with @AgentBase.tool
|
1767
|
-
"""
|
1768
|
-
for name in dir(self):
|
1769
|
-
attr = getattr(self, name)
|
1770
|
-
if callable(attr) and hasattr(attr, "_is_tool"):
|
1771
|
-
# Get tool parameters
|
1772
|
-
tool_name = getattr(attr, "_tool_name", name)
|
1773
|
-
tool_params = getattr(attr, "_tool_params", {})
|
1774
|
-
|
1775
|
-
# Extract parameters
|
1776
|
-
parameters = tool_params.get("parameters", {})
|
1777
|
-
description = tool_params.get("description", attr.__doc__ or f"Function {tool_name}")
|
1778
|
-
secure = tool_params.get("secure", True)
|
1779
|
-
fillers = tool_params.get("fillers", None)
|
1780
|
-
|
1781
|
-
# Create a wrapper that binds the method to this instance
|
1782
|
-
def make_wrapper(method):
|
1783
|
-
@functools.wraps(method)
|
1784
|
-
def wrapper(args, raw_data=None):
|
1785
|
-
return method(args, raw_data)
|
1786
|
-
return wrapper
|
1787
|
-
|
1788
|
-
# Register the tool
|
1789
|
-
self.define_tool(
|
1790
|
-
name=tool_name,
|
1791
|
-
description=description,
|
1792
|
-
parameters=parameters,
|
1793
|
-
handler=make_wrapper(attr),
|
1794
|
-
secure=secure,
|
1795
|
-
fillers=fillers
|
1796
|
-
)
|
1589
|
+
# Add fillers if provided
|
1590
|
+
if speech_fillers and function_fillers:
|
1591
|
+
language["speech_fillers"] = speech_fillers
|
1592
|
+
language["function_fillers"] = function_fillers
|
1593
|
+
elif speech_fillers or function_fillers:
|
1594
|
+
# If only one type of fillers is provided, use the deprecated "fillers" field
|
1595
|
+
fillers = speech_fillers or function_fillers
|
1596
|
+
language["fillers"] = fillers
|
1597
|
+
|
1598
|
+
self._languages.append(language)
|
1599
|
+
return self
|
1797
1600
|
|
1798
|
-
|
1799
|
-
def get_state(self, call_id: str) -> Optional[Dict[str, Any]]:
|
1601
|
+
def set_languages(self, languages: List[Dict[str, Any]]) -> 'AgentBase':
|
1800
1602
|
"""
|
1801
|
-
|
1603
|
+
Set all language configurations at once
|
1802
1604
|
|
1803
1605
|
Args:
|
1804
|
-
|
1606
|
+
languages: List of language configuration dictionaries
|
1805
1607
|
|
1806
1608
|
Returns:
|
1807
|
-
|
1609
|
+
Self for method chaining
|
1808
1610
|
"""
|
1809
|
-
|
1810
|
-
|
1811
|
-
|
1812
|
-
|
1813
|
-
|
1814
|
-
|
1815
|
-
|
1816
|
-
|
1817
|
-
def set_state(self, call_id: str, data: Dict[str, Any]) -> bool:
|
1611
|
+
if languages and isinstance(languages, list):
|
1612
|
+
self._languages = languages
|
1613
|
+
return self
|
1614
|
+
|
1615
|
+
def add_pronunciation(self,
|
1616
|
+
replace: str,
|
1617
|
+
with_text: str,
|
1618
|
+
ignore_case: bool = False) -> 'AgentBase':
|
1818
1619
|
"""
|
1819
|
-
|
1620
|
+
Add a pronunciation rule to help the AI speak certain words correctly
|
1820
1621
|
|
1821
1622
|
Args:
|
1822
|
-
|
1823
|
-
|
1623
|
+
replace: The expression to replace
|
1624
|
+
with_text: The phonetic spelling to use instead
|
1625
|
+
ignore_case: Whether to ignore case when matching
|
1824
1626
|
|
1825
1627
|
Returns:
|
1826
|
-
|
1827
|
-
"""
|
1828
|
-
try:
|
1829
|
-
if hasattr(self._state_manager, 'set_state'):
|
1830
|
-
return self._state_manager.set_state(call_id, data)
|
1831
|
-
return False
|
1832
|
-
except Exception as e:
|
1833
|
-
logger.warning("set_state_failed", error=str(e))
|
1834
|
-
return False
|
1835
|
-
|
1836
|
-
def update_state(self, call_id: str, data: Dict[str, Any]) -> bool:
|
1628
|
+
Self for method chaining
|
1837
1629
|
"""
|
1838
|
-
|
1839
|
-
|
1840
|
-
|
1841
|
-
|
1842
|
-
|
1630
|
+
if replace and with_text:
|
1631
|
+
rule = {
|
1632
|
+
"replace": replace,
|
1633
|
+
"with": with_text
|
1634
|
+
}
|
1635
|
+
if ignore_case:
|
1636
|
+
rule["ignore_case"] = True
|
1843
1637
|
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
if hasattr(self._state_manager, 'update_state'):
|
1849
|
-
return self._state_manager.update_state(call_id, data)
|
1850
|
-
return self.set_state(call_id, data)
|
1851
|
-
except Exception as e:
|
1852
|
-
logger.warning("update_state_failed", error=str(e))
|
1853
|
-
return False
|
1854
|
-
|
1855
|
-
def clear_state(self, call_id: str) -> bool:
|
1638
|
+
self._pronounce.append(rule)
|
1639
|
+
return self
|
1640
|
+
|
1641
|
+
def set_pronunciations(self, pronunciations: List[Dict[str, Any]]) -> 'AgentBase':
|
1856
1642
|
"""
|
1857
|
-
|
1643
|
+
Set all pronunciation rules at once
|
1858
1644
|
|
1859
1645
|
Args:
|
1860
|
-
|
1646
|
+
pronunciations: List of pronunciation rule dictionaries
|
1861
1647
|
|
1862
1648
|
Returns:
|
1863
|
-
|
1864
|
-
"""
|
1865
|
-
try:
|
1866
|
-
if hasattr(self._state_manager, 'clear_state'):
|
1867
|
-
return self._state_manager.clear_state(call_id)
|
1868
|
-
return False
|
1869
|
-
except Exception as e:
|
1870
|
-
logger.warning("clear_state_failed", error=str(e))
|
1871
|
-
return False
|
1872
|
-
|
1873
|
-
def cleanup_expired_state(self) -> int:
|
1874
|
-
"""
|
1875
|
-
Clean up expired state
|
1876
|
-
|
1877
|
-
Returns:
|
1878
|
-
Number of expired state entries removed
|
1649
|
+
Self for method chaining
|
1879
1650
|
"""
|
1880
|
-
|
1881
|
-
|
1882
|
-
|
1883
|
-
return 0
|
1884
|
-
except Exception as e:
|
1885
|
-
logger.warning("cleanup_expired_state_failed", error=str(e))
|
1886
|
-
return 0
|
1651
|
+
if pronunciations and isinstance(pronunciations, list):
|
1652
|
+
self._pronounce = pronunciations
|
1653
|
+
return self
|
1887
1654
|
|
1888
|
-
def
|
1889
|
-
"""
|
1890
|
-
Register tools for tracking conversation state
|
1891
|
-
"""
|
1892
|
-
# Register startup hook
|
1893
|
-
self.define_tool(
|
1894
|
-
name="startup_hook",
|
1895
|
-
description="Called when the conversation starts",
|
1896
|
-
parameters={},
|
1897
|
-
handler=self._startup_hook_handler,
|
1898
|
-
secure=False
|
1899
|
-
)
|
1900
|
-
|
1901
|
-
# Register hangup hook
|
1902
|
-
self.define_tool(
|
1903
|
-
name="hangup_hook",
|
1904
|
-
description="Called when the conversation ends",
|
1905
|
-
parameters={},
|
1906
|
-
handler=self._hangup_hook_handler,
|
1907
|
-
secure=False
|
1908
|
-
)
|
1909
|
-
|
1910
|
-
def _startup_hook_handler(self, args, raw_data):
|
1655
|
+
def set_param(self, key: str, value: Any) -> 'AgentBase':
|
1911
1656
|
"""
|
1912
|
-
|
1657
|
+
Set a single AI parameter
|
1913
1658
|
|
1914
1659
|
Args:
|
1915
|
-
|
1916
|
-
|
1660
|
+
key: Parameter name
|
1661
|
+
value: Parameter value
|
1917
1662
|
|
1918
1663
|
Returns:
|
1919
|
-
|
1664
|
+
Self for method chaining
|
1920
1665
|
"""
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
1925
|
-
|
1926
|
-
# Activate the session
|
1927
|
-
self._session_manager.activate_session(call_id)
|
1928
|
-
|
1929
|
-
# Initialize state
|
1930
|
-
self.set_state(call_id, {
|
1931
|
-
"start_time": datetime.now().isoformat(),
|
1932
|
-
"events": []
|
1933
|
-
})
|
1934
|
-
|
1935
|
-
return SwaigFunctionResult("Call started and session activated")
|
1936
|
-
|
1937
|
-
def _hangup_hook_handler(self, args, raw_data):
|
1666
|
+
if key:
|
1667
|
+
self._params[key] = value
|
1668
|
+
return self
|
1669
|
+
|
1670
|
+
def set_params(self, params: Dict[str, Any]) -> 'AgentBase':
|
1938
1671
|
"""
|
1939
|
-
|
1672
|
+
Set multiple AI parameters at once
|
1940
1673
|
|
1941
1674
|
Args:
|
1942
|
-
|
1943
|
-
raw_data: Raw request data
|
1675
|
+
params: Dictionary of parameter name/value pairs
|
1944
1676
|
|
1945
1677
|
Returns:
|
1946
|
-
|
1678
|
+
Self for method chaining
|
1947
1679
|
"""
|
1948
|
-
|
1949
|
-
|
1950
|
-
|
1951
|
-
return SwaigFunctionResult("Error: Missing call_id")
|
1952
|
-
|
1953
|
-
# End the session
|
1954
|
-
self._session_manager.end_session(call_id)
|
1955
|
-
|
1956
|
-
# Update state
|
1957
|
-
state = self.get_state(call_id) or {}
|
1958
|
-
state["end_time"] = datetime.now().isoformat()
|
1959
|
-
self.update_state(call_id, state)
|
1960
|
-
|
1961
|
-
return SwaigFunctionResult("Call ended and session deactivated")
|
1680
|
+
if params and isinstance(params, dict):
|
1681
|
+
self._params.update(params)
|
1682
|
+
return self
|
1962
1683
|
|
1963
|
-
def
|
1684
|
+
def set_global_data(self, data: Dict[str, Any]) -> 'AgentBase':
|
1964
1685
|
"""
|
1965
|
-
Set the
|
1686
|
+
Set the global data available to the AI throughout the conversation
|
1966
1687
|
|
1967
1688
|
Args:
|
1968
|
-
|
1689
|
+
data: Dictionary of global data
|
1969
1690
|
|
1970
1691
|
Returns:
|
1971
1692
|
Self for method chaining
|
1972
1693
|
"""
|
1973
|
-
|
1694
|
+
if data and isinstance(data, dict):
|
1695
|
+
self._global_data = data
|
1974
1696
|
return self
|
1975
|
-
|
1976
|
-
def
|
1697
|
+
|
1698
|
+
def update_global_data(self, data: Dict[str, Any]) -> 'AgentBase':
|
1977
1699
|
"""
|
1978
|
-
|
1700
|
+
Update the global data with new values
|
1979
1701
|
|
1980
1702
|
Args:
|
1981
|
-
|
1703
|
+
data: Dictionary of global data to update
|
1982
1704
|
|
1983
1705
|
Returns:
|
1984
1706
|
Self for method chaining
|
1985
1707
|
"""
|
1986
|
-
|
1708
|
+
if data and isinstance(data, dict):
|
1709
|
+
self._global_data.update(data)
|
1987
1710
|
return self
|
1988
|
-
|
1989
|
-
def
|
1990
|
-
enabled: bool,
|
1991
|
-
format: str = "mp4",
|
1992
|
-
stereo: bool = True) -> 'AgentBase':
|
1711
|
+
|
1712
|
+
def set_native_functions(self, function_names: List[str]) -> 'AgentBase':
|
1993
1713
|
"""
|
1994
|
-
Set
|
1714
|
+
Set the list of native functions to enable
|
1995
1715
|
|
1996
1716
|
Args:
|
1997
|
-
|
1998
|
-
format: Recording format
|
1999
|
-
stereo: Whether to record in stereo
|
1717
|
+
function_names: List of native function names
|
2000
1718
|
|
2001
1719
|
Returns:
|
2002
1720
|
Self for method chaining
|
2003
1721
|
"""
|
2004
|
-
|
2005
|
-
|
2006
|
-
self._record_stereo = stereo
|
1722
|
+
if function_names and isinstance(function_names, list):
|
1723
|
+
self.native_functions = [name for name in function_names if isinstance(name, str)]
|
2007
1724
|
return self
|
2008
|
-
|
2009
|
-
def
|
1725
|
+
|
1726
|
+
def add_function_include(self, url: str, functions: List[str], meta_data: Optional[Dict[str, Any]] = None) -> 'AgentBase':
|
2010
1727
|
"""
|
2011
|
-
Add a
|
1728
|
+
Add a remote function include to the SWAIG configuration
|
2012
1729
|
|
2013
1730
|
Args:
|
2014
|
-
|
1731
|
+
url: URL to fetch remote functions from
|
1732
|
+
functions: List of function names to include
|
1733
|
+
meta_data: Optional metadata to include with the function include
|
2015
1734
|
|
2016
1735
|
Returns:
|
2017
1736
|
Self for method chaining
|
2018
1737
|
"""
|
2019
|
-
if
|
2020
|
-
|
2021
|
-
|
2022
|
-
|
2023
|
-
|
1738
|
+
if url and functions and isinstance(functions, list):
|
1739
|
+
include = {
|
1740
|
+
"url": url,
|
1741
|
+
"functions": functions
|
1742
|
+
}
|
1743
|
+
if meta_data and isinstance(meta_data, dict):
|
1744
|
+
include["meta_data"] = meta_data
|
1745
|
+
|
1746
|
+
self._function_includes.append(include)
|
2024
1747
|
return self
|
2025
1748
|
|
2026
|
-
def
|
1749
|
+
def set_function_includes(self, includes: List[Dict[str, Any]]) -> 'AgentBase':
|
2027
1750
|
"""
|
2028
|
-
|
1751
|
+
Set the complete list of function includes
|
2029
1752
|
|
2030
1753
|
Args:
|
2031
|
-
|
1754
|
+
includes: List of include objects, each with url and functions properties
|
2032
1755
|
|
2033
1756
|
Returns:
|
2034
1757
|
Self for method chaining
|
2035
1758
|
"""
|
2036
|
-
if
|
2037
|
-
|
1759
|
+
if includes and isinstance(includes, list):
|
1760
|
+
# Validate each include has required properties
|
1761
|
+
valid_includes = []
|
1762
|
+
for include in includes:
|
1763
|
+
if isinstance(include, dict) and "url" in include and "functions" in include:
|
1764
|
+
if isinstance(include["functions"], list):
|
1765
|
+
valid_includes.append(include)
|
1766
|
+
|
1767
|
+
self._function_includes = valid_includes
|
2038
1768
|
return self
|
2039
|
-
|
2040
|
-
def get_native_functions(self) -> List[str]:
|
2041
|
-
"""
|
2042
|
-
Get the list of native functions
|
2043
|
-
|
2044
|
-
Returns:
|
2045
|
-
List of native function names
|
2046
|
-
"""
|
2047
|
-
return self.native_functions.copy()
|
2048
1769
|
|
2049
|
-
def
|
1770
|
+
def enable_sip_routing(self, auto_map: bool = True, path: str = "/sip") -> 'AgentBase':
|
2050
1771
|
"""
|
2051
|
-
|
1772
|
+
Enable SIP-based routing for this agent
|
1773
|
+
|
1774
|
+
This allows the agent to automatically route SIP requests based on SIP usernames.
|
1775
|
+
When enabled, an endpoint at the specified path is automatically created
|
1776
|
+
that will handle SIP requests and deliver them to this agent.
|
2052
1777
|
|
2053
1778
|
Args:
|
2054
|
-
|
2055
|
-
|
1779
|
+
auto_map: Whether to automatically map common SIP usernames to this agent
|
1780
|
+
(based on the agent name and route path)
|
1781
|
+
path: The path to register the SIP routing endpoint (default: "/sip")
|
1782
|
+
|
2056
1783
|
Returns:
|
2057
|
-
|
1784
|
+
Self for method chaining
|
2058
1785
|
"""
|
2059
|
-
|
2060
|
-
|
1786
|
+
# Create a routing callback that handles SIP usernames
|
1787
|
+
def sip_routing_callback(request: Request, body: Dict[str, Any]) -> Optional[str]:
|
1788
|
+
# Extract SIP username from the request body
|
1789
|
+
sip_username = self.extract_sip_username(body)
|
1790
|
+
|
1791
|
+
if sip_username:
|
1792
|
+
self.log.info("sip_username_extracted", username=sip_username)
|
1793
|
+
|
1794
|
+
# Check if this username is registered with this agent
|
1795
|
+
if hasattr(self, '_sip_usernames') and sip_username.lower() in self._sip_usernames:
|
1796
|
+
self.log.info("sip_username_matched", username=sip_username)
|
1797
|
+
# This route is already being handled by the agent, no need to redirect
|
1798
|
+
return None
|
1799
|
+
else:
|
1800
|
+
self.log.info("sip_username_not_matched", username=sip_username)
|
1801
|
+
# Not registered with this agent, let routing continue
|
1802
|
+
|
1803
|
+
return None
|
2061
1804
|
|
2062
|
-
|
1805
|
+
# Register the callback with the SWMLService, specifying the path
|
1806
|
+
self.register_routing_callback(sip_routing_callback, path=path)
|
2063
1807
|
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
1808
|
+
# Auto-map common usernames if requested
|
1809
|
+
if auto_map:
|
1810
|
+
self.auto_map_sip_usernames()
|
1811
|
+
|
1812
|
+
return self
|
2067
1813
|
|
2068
|
-
|
1814
|
+
def register_sip_username(self, sip_username: str) -> 'AgentBase':
|
1815
|
+
"""
|
1816
|
+
Register a SIP username that should be routed to this agent
|
2069
1817
|
|
2070
1818
|
Args:
|
2071
|
-
|
1819
|
+
sip_username: SIP username to register
|
2072
1820
|
|
2073
1821
|
Returns:
|
2074
|
-
|
2075
|
-
"""
|
2076
|
-
# Default implementation does nothing
|
2077
|
-
return None
|
2078
|
-
|
2079
|
-
def serve(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
|
2080
|
-
"""
|
2081
|
-
Start a web server for this agent
|
2082
|
-
|
2083
|
-
Args:
|
2084
|
-
host: Optional host to override the default
|
2085
|
-
port: Optional port to override the default
|
1822
|
+
Self for method chaining
|
2086
1823
|
"""
|
2087
|
-
|
2088
|
-
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2092
|
-
# Register all routes
|
2093
|
-
self._register_routes(app)
|
2094
|
-
|
2095
|
-
host = host or self.host
|
2096
|
-
port = port or self.port
|
2097
|
-
|
2098
|
-
# Print the auth credentials with source
|
2099
|
-
username, password, source = self.get_basic_auth_credentials(include_source=True)
|
2100
|
-
self.log.info("starting_server",
|
2101
|
-
url=f"http://{host}:{port}{self.route}",
|
2102
|
-
username=username,
|
2103
|
-
password="*" * len(password),
|
2104
|
-
auth_source=source)
|
2105
|
-
|
2106
|
-
print(f"Agent '{self.name}' is available at:")
|
2107
|
-
print(f"URL: http://{host}:{port}{self.route}")
|
2108
|
-
print(f"Basic Auth: {username}:{password} (source: {source})")
|
2109
|
-
|
2110
|
-
# Check if SIP usernames are registered and print that info
|
2111
|
-
if hasattr(self, '_sip_usernames') and self._sip_usernames:
|
2112
|
-
print(f"Registered SIP usernames: {', '.join(sorted(self._sip_usernames))}")
|
2113
|
-
|
2114
|
-
# Check if callback endpoints are registered and print them
|
2115
|
-
if hasattr(self, '_routing_callbacks') and self._routing_callbacks:
|
2116
|
-
for path in sorted(self._routing_callbacks.keys()):
|
2117
|
-
if hasattr(self, '_sip_usernames') and path == "/sip":
|
2118
|
-
print(f"SIP endpoint: http://{host}:{port}{path}")
|
2119
|
-
else:
|
2120
|
-
print(f"Callback endpoint: http://{host}:{port}{path}")
|
1824
|
+
if not hasattr(self, '_sip_usernames'):
|
1825
|
+
self._sip_usernames = set()
|
1826
|
+
|
1827
|
+
self._sip_usernames.add(sip_username.lower())
|
1828
|
+
self.log.info("sip_username_registered", username=sip_username)
|
2121
1829
|
|
2122
|
-
|
2123
|
-
uvicorn_log_config = uvicorn.config.LOGGING_CONFIG
|
2124
|
-
uvicorn_log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
2125
|
-
uvicorn_log_config["formatters"]["default"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
1830
|
+
return self
|
2126
1831
|
|
2127
|
-
|
2128
|
-
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
|
2135
|
-
|
2136
|
-
|
2137
|
-
|
2138
|
-
|
2139
|
-
|
2140
|
-
|
2141
|
-
|
2142
|
-
|
1832
|
+
def auto_map_sip_usernames(self) -> 'AgentBase':
|
1833
|
+
"""
|
1834
|
+
Automatically register common SIP usernames based on this agent's
|
1835
|
+
name and route
|
1836
|
+
|
1837
|
+
Returns:
|
1838
|
+
Self for method chaining
|
1839
|
+
"""
|
1840
|
+
# Register username based on agent name
|
1841
|
+
clean_name = re.sub(r'[^a-z0-9_]', '', self.name.lower())
|
1842
|
+
if clean_name:
|
1843
|
+
self.register_sip_username(clean_name)
|
1844
|
+
|
1845
|
+
# Register username based on route (without slashes)
|
1846
|
+
clean_route = re.sub(r'[^a-z0-9_]', '', self.route.lower())
|
1847
|
+
if clean_route and clean_route != clean_name:
|
1848
|
+
self.register_sip_username(clean_route)
|
1849
|
+
|
1850
|
+
# Register common variations if they make sense
|
1851
|
+
if len(clean_name) > 3:
|
1852
|
+
# Register without vowels
|
1853
|
+
no_vowels = re.sub(r'[aeiou]', '', clean_name)
|
1854
|
+
if no_vowels != clean_name and len(no_vowels) > 2:
|
1855
|
+
self.register_sip_username(no_vowels)
|
1856
|
+
|
1857
|
+
return self
|
2143
1858
|
|
2144
|
-
def
|
1859
|
+
def set_web_hook_url(self, url: str) -> 'AgentBase':
|
2145
1860
|
"""
|
2146
|
-
|
1861
|
+
Override the default web_hook_url with a supplied URL string
|
2147
1862
|
|
2148
1863
|
Args:
|
2149
|
-
|
1864
|
+
url: The URL to use for SWAIG function webhooks
|
2150
1865
|
|
2151
1866
|
Returns:
|
2152
1867
|
Self for method chaining
|
2153
1868
|
"""
|
2154
|
-
|
2155
|
-
self._hints.append(hint)
|
1869
|
+
self._web_hook_url_override = url
|
2156
1870
|
return self
|
2157
|
-
|
2158
|
-
def
|
1871
|
+
|
1872
|
+
def set_post_prompt_url(self, url: str) -> 'AgentBase':
|
2159
1873
|
"""
|
2160
|
-
|
1874
|
+
Override the default post_prompt_url with a supplied URL string
|
2161
1875
|
|
2162
1876
|
Args:
|
2163
|
-
|
1877
|
+
url: The URL to use for post-prompt summary delivery
|
2164
1878
|
|
2165
1879
|
Returns:
|
2166
1880
|
Self for method chaining
|
2167
1881
|
"""
|
2168
|
-
|
2169
|
-
for hint in hints:
|
2170
|
-
if isinstance(hint, str) and hint:
|
2171
|
-
self._hints.append(hint)
|
1882
|
+
self._post_prompt_url_override = url
|
2172
1883
|
return self
|
2173
1884
|
|
2174
|
-
def
|
2175
|
-
|
2176
|
-
|
2177
|
-
|
2178
|
-
|
2179
|
-
|
2180
|
-
|
1885
|
+
async def _handle_swaig_request(self, request: Request, response: Response):
|
1886
|
+
"""Handle GET/POST requests to the SWAIG endpoint"""
|
1887
|
+
req_log = self.log.bind(
|
1888
|
+
endpoint="swaig",
|
1889
|
+
method=request.method,
|
1890
|
+
path=request.url.path
|
1891
|
+
)
|
1892
|
+
|
1893
|
+
req_log.debug("endpoint_called")
|
1894
|
+
|
1895
|
+
try:
|
1896
|
+
# Check auth
|
1897
|
+
if not self._check_basic_auth(request):
|
1898
|
+
req_log.warning("unauthorized_access_attempt")
|
1899
|
+
response.headers["WWW-Authenticate"] = "Basic"
|
1900
|
+
return Response(
|
1901
|
+
content=json.dumps({"error": "Unauthorized"}),
|
1902
|
+
status_code=401,
|
1903
|
+
headers={"WWW-Authenticate": "Basic"},
|
1904
|
+
media_type="application/json"
|
1905
|
+
)
|
1906
|
+
|
1907
|
+
# Handle differently based on method
|
1908
|
+
if request.method == "GET":
|
1909
|
+
# For GET requests, return the SWML document (same as root endpoint)
|
1910
|
+
call_id = request.query_params.get("call_id")
|
1911
|
+
swml = self._render_swml(call_id)
|
1912
|
+
req_log.debug("swml_rendered", swml_size=len(swml))
|
1913
|
+
return Response(
|
1914
|
+
content=swml,
|
1915
|
+
media_type="application/json"
|
1916
|
+
)
|
1917
|
+
|
1918
|
+
# For POST requests, process SWAIG function calls
|
1919
|
+
try:
|
1920
|
+
body = await request.json()
|
1921
|
+
req_log.debug("request_body_received", body_size=len(str(body)))
|
1922
|
+
if body:
|
1923
|
+
req_log.debug("request_body", body=json.dumps(body))
|
1924
|
+
except Exception as e:
|
1925
|
+
req_log.error("error_parsing_request_body", error=str(e))
|
1926
|
+
body = {}
|
1927
|
+
|
1928
|
+
# Extract function name
|
1929
|
+
function_name = body.get("function")
|
1930
|
+
if not function_name:
|
1931
|
+
req_log.warning("missing_function_name")
|
1932
|
+
return Response(
|
1933
|
+
content=json.dumps({"error": "Missing function name"}),
|
1934
|
+
status_code=400,
|
1935
|
+
media_type="application/json"
|
1936
|
+
)
|
1937
|
+
|
1938
|
+
# Add function info to logger
|
1939
|
+
req_log = req_log.bind(function=function_name)
|
1940
|
+
req_log.debug("function_call_received")
|
1941
|
+
|
1942
|
+
# Extract arguments
|
1943
|
+
args = {}
|
1944
|
+
if "argument" in body and isinstance(body["argument"], dict):
|
1945
|
+
if "parsed" in body["argument"] and isinstance(body["argument"]["parsed"], list) and body["argument"]["parsed"]:
|
1946
|
+
args = body["argument"]["parsed"][0]
|
1947
|
+
req_log.debug("parsed_arguments", args=json.dumps(args))
|
1948
|
+
elif "raw" in body["argument"]:
|
1949
|
+
try:
|
1950
|
+
args = json.loads(body["argument"]["raw"])
|
1951
|
+
req_log.debug("raw_arguments_parsed", args=json.dumps(args))
|
1952
|
+
except Exception as e:
|
1953
|
+
req_log.error("error_parsing_raw_arguments", error=str(e), raw=body["argument"]["raw"])
|
1954
|
+
|
1955
|
+
# Get call_id from body
|
1956
|
+
call_id = body.get("call_id")
|
1957
|
+
if call_id:
|
1958
|
+
req_log = req_log.bind(call_id=call_id)
|
1959
|
+
req_log.debug("call_id_identified")
|
1960
|
+
|
1961
|
+
# SECURITY BYPASS FOR DEBUGGING - make all functions work regardless of token
|
1962
|
+
# We'll log the attempt but allow it through
|
1963
|
+
token = request.query_params.get("token")
|
1964
|
+
if token:
|
1965
|
+
req_log.debug("token_found", token_length=len(token))
|
1966
|
+
|
1967
|
+
# Check token validity but don't reject the request
|
1968
|
+
if hasattr(self, '_session_manager') and function_name in self._swaig_functions:
|
1969
|
+
is_valid = self._session_manager.validate_tool_token(function_name, token, call_id)
|
1970
|
+
if is_valid:
|
1971
|
+
req_log.debug("token_valid")
|
1972
|
+
else:
|
1973
|
+
# Log but continue anyway for debugging
|
1974
|
+
req_log.warning("token_invalid")
|
1975
|
+
if hasattr(self._session_manager, 'debug_token'):
|
1976
|
+
debug_info = self._session_manager.debug_token(token)
|
1977
|
+
req_log.debug("token_debug", debug=json.dumps(debug_info))
|
1978
|
+
|
1979
|
+
# Call the function
|
1980
|
+
try:
|
1981
|
+
result = self.on_function_call(function_name, args, body)
|
1982
|
+
|
1983
|
+
# Convert result to dict if needed
|
1984
|
+
if isinstance(result, SwaigFunctionResult):
|
1985
|
+
result_dict = result.to_dict()
|
1986
|
+
elif isinstance(result, dict):
|
1987
|
+
result_dict = result
|
1988
|
+
else:
|
1989
|
+
result_dict = {"response": str(result)}
|
1990
|
+
|
1991
|
+
req_log.info("function_executed_successfully")
|
1992
|
+
req_log.debug("function_result", result=json.dumps(result_dict))
|
1993
|
+
return result_dict
|
1994
|
+
except Exception as e:
|
1995
|
+
req_log.error("function_execution_error", error=str(e))
|
1996
|
+
return {"error": str(e), "function": function_name}
|
1997
|
+
|
1998
|
+
except Exception as e:
|
1999
|
+
req_log.error("request_failed", error=str(e))
|
2000
|
+
return Response(
|
2001
|
+
content=json.dumps({"error": str(e)}),
|
2002
|
+
status_code=500,
|
2003
|
+
media_type="application/json"
|
2004
|
+
)
|
2005
|
+
|
2006
|
+
async def _handle_root_request(self, request: Request):
|
2007
|
+
"""Handle GET/POST requests to the root endpoint"""
|
2008
|
+
# Auto-detect proxy on first request if not explicitly configured
|
2009
|
+
if not getattr(self, '_proxy_detection_done', False) and not getattr(self, '_proxy_url_base', None):
|
2010
|
+
# Check for proxy headers
|
2011
|
+
forwarded_host = request.headers.get("X-Forwarded-Host")
|
2012
|
+
forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
|
2013
|
+
|
2014
|
+
if forwarded_host:
|
2015
|
+
# Set proxy_url_base on both self and super() to ensure it's shared
|
2016
|
+
self._proxy_url_base = f"{forwarded_proto}://{forwarded_host}"
|
2017
|
+
if hasattr(super(), '_proxy_url_base'):
|
2018
|
+
# Ensure parent class has the same proxy URL
|
2019
|
+
super()._proxy_url_base = self._proxy_url_base
|
2020
|
+
|
2021
|
+
self.log.info("proxy_auto_detected", proxy_url_base=self._proxy_url_base,
|
2022
|
+
source="X-Forwarded headers")
|
2023
|
+
self._proxy_detection_done = True
|
2024
|
+
|
2025
|
+
# Also set the detection flag on parent
|
2026
|
+
if hasattr(super(), '_proxy_detection_done'):
|
2027
|
+
super()._proxy_detection_done = True
|
2028
|
+
# If no explicit proxy headers, try the parent class detection method if it exists
|
2029
|
+
elif hasattr(super(), '_detect_proxy_from_request'):
|
2030
|
+
# Call the parent's detection method
|
2031
|
+
super()._detect_proxy_from_request(request)
|
2032
|
+
# Copy the result to our class
|
2033
|
+
if hasattr(super(), '_proxy_url_base') and getattr(super(), '_proxy_url_base', None):
|
2034
|
+
self._proxy_url_base = super()._proxy_url_base
|
2035
|
+
self._proxy_detection_done = True
|
2036
|
+
|
2037
|
+
# Check if this is a callback path request
|
2038
|
+
callback_path = getattr(request.state, "callback_path", None)
|
2039
|
+
|
2040
|
+
req_log = self.log.bind(
|
2041
|
+
endpoint="root" if not callback_path else f"callback:{callback_path}",
|
2042
|
+
method=request.method,
|
2043
|
+
path=request.url.path
|
2044
|
+
)
|
2045
|
+
|
2046
|
+
req_log.debug("endpoint_called")
|
2047
|
+
|
2048
|
+
try:
|
2049
|
+
# Check auth
|
2050
|
+
if not self._check_basic_auth(request):
|
2051
|
+
req_log.warning("unauthorized_access_attempt")
|
2052
|
+
return Response(
|
2053
|
+
content=json.dumps({"error": "Unauthorized"}),
|
2054
|
+
status_code=401,
|
2055
|
+
headers={"WWW-Authenticate": "Basic"},
|
2056
|
+
media_type="application/json"
|
2057
|
+
)
|
2058
|
+
|
2059
|
+
# Try to parse request body for POST
|
2060
|
+
body = {}
|
2061
|
+
call_id = None
|
2062
|
+
|
2063
|
+
if request.method == "POST":
|
2064
|
+
# Check if body is empty first
|
2065
|
+
raw_body = await request.body()
|
2066
|
+
if raw_body:
|
2067
|
+
try:
|
2068
|
+
body = await request.json()
|
2069
|
+
req_log.debug("request_body_received", body_size=len(str(body)))
|
2070
|
+
if body:
|
2071
|
+
req_log.debug("request_body")
|
2072
|
+
except Exception as e:
|
2073
|
+
req_log.warning("error_parsing_request_body", error=str(e))
|
2074
|
+
# Continue processing with empty body
|
2075
|
+
body = {}
|
2076
|
+
else:
|
2077
|
+
req_log.debug("empty_request_body")
|
2078
|
+
|
2079
|
+
# Get call_id from body if present
|
2080
|
+
call_id = body.get("call_id")
|
2081
|
+
else:
|
2082
|
+
# Get call_id from query params for GET
|
2083
|
+
call_id = request.query_params.get("call_id")
|
2084
|
+
|
2085
|
+
# Add call_id to logger if any
|
2086
|
+
if call_id:
|
2087
|
+
req_log = req_log.bind(call_id=call_id)
|
2088
|
+
req_log.debug("call_id_identified")
|
2089
|
+
|
2090
|
+
# Check if this is a callback path and we need to apply routing
|
2091
|
+
if callback_path and hasattr(self, '_routing_callbacks') and callback_path in self._routing_callbacks:
|
2092
|
+
callback_fn = self._routing_callbacks[callback_path]
|
2093
|
+
|
2094
|
+
if request.method == "POST" and body:
|
2095
|
+
req_log.debug("processing_routing_callback", path=callback_path)
|
2096
|
+
# Call the routing callback
|
2097
|
+
try:
|
2098
|
+
route = callback_fn(request, body)
|
2099
|
+
if route is not None:
|
2100
|
+
req_log.info("routing_request", route=route)
|
2101
|
+
# Return a redirect to the new route
|
2102
|
+
return Response(
|
2103
|
+
status_code=307, # 307 Temporary Redirect preserves the method and body
|
2104
|
+
headers={"Location": route}
|
2105
|
+
)
|
2106
|
+
except Exception as e:
|
2107
|
+
req_log.error("error_in_routing_callback", error=str(e))
|
2108
|
+
|
2109
|
+
# Allow subclasses to inspect/modify the request
|
2110
|
+
modifications = None
|
2111
|
+
if body:
|
2112
|
+
try:
|
2113
|
+
modifications = self.on_swml_request(body, callback_path)
|
2114
|
+
if modifications:
|
2115
|
+
req_log.debug("request_modifications_applied")
|
2116
|
+
except Exception as e:
|
2117
|
+
req_log.error("error_in_request_modifier", error=str(e))
|
2118
|
+
|
2119
|
+
# Render SWML
|
2120
|
+
swml = self._render_swml(call_id)
|
2121
|
+
req_log.debug("swml_rendered", swml_size=len(swml))
|
2122
|
+
|
2123
|
+
# Return as JSON
|
2124
|
+
req_log.info("request_successful")
|
2125
|
+
return Response(
|
2126
|
+
content=swml,
|
2127
|
+
media_type="application/json"
|
2128
|
+
)
|
2129
|
+
except Exception as e:
|
2130
|
+
req_log.error("request_failed", error=str(e))
|
2131
|
+
return Response(
|
2132
|
+
content=json.dumps({"error": str(e)}),
|
2133
|
+
status_code=500,
|
2134
|
+
media_type="application/json"
|
2135
|
+
)
|
2136
|
+
|
2137
|
+
async def _handle_debug_request(self, request: Request):
|
2138
|
+
"""Handle GET/POST requests to the debug endpoint"""
|
2139
|
+
req_log = self.log.bind(
|
2140
|
+
endpoint="debug",
|
2141
|
+
method=request.method,
|
2142
|
+
path=request.url.path
|
2143
|
+
)
|
2181
2144
|
|
2182
|
-
|
2183
|
-
hint: The hint to match
|
2184
|
-
pattern: Regular expression pattern
|
2185
|
-
replace: Text to replace the hint with
|
2186
|
-
ignore_case: Whether to ignore case when matching
|
2187
|
-
|
2188
|
-
Returns:
|
2189
|
-
Self for method chaining
|
2190
|
-
"""
|
2191
|
-
if hint and pattern and replace:
|
2192
|
-
self._hints.append({
|
2193
|
-
"hint": hint,
|
2194
|
-
"pattern": pattern,
|
2195
|
-
"replace": replace,
|
2196
|
-
"ignore_case": ignore_case
|
2197
|
-
})
|
2198
|
-
return self
|
2199
|
-
|
2200
|
-
def add_language(self,
|
2201
|
-
name: str,
|
2202
|
-
code: str,
|
2203
|
-
voice: str,
|
2204
|
-
speech_fillers: Optional[List[str]] = None,
|
2205
|
-
function_fillers: Optional[List[str]] = None,
|
2206
|
-
engine: Optional[str] = None,
|
2207
|
-
model: Optional[str] = None) -> 'AgentBase':
|
2208
|
-
"""
|
2209
|
-
Add a language configuration to support multilingual conversations
|
2145
|
+
req_log.debug("endpoint_called")
|
2210
2146
|
|
2211
|
-
|
2212
|
-
|
2213
|
-
|
2214
|
-
|
2215
|
-
|
2216
|
-
|
2217
|
-
|
2218
|
-
|
2219
|
-
|
2147
|
+
try:
|
2148
|
+
# Check auth
|
2149
|
+
if not self._check_basic_auth(request):
|
2150
|
+
req_log.warning("unauthorized_access_attempt")
|
2151
|
+
return Response(
|
2152
|
+
content=json.dumps({"error": "Unauthorized"}),
|
2153
|
+
status_code=401,
|
2154
|
+
headers={"WWW-Authenticate": "Basic"},
|
2155
|
+
media_type="application/json"
|
2156
|
+
)
|
2220
2157
|
|
2221
|
-
|
2222
|
-
|
2158
|
+
# Get call_id from either query params (GET) or body (POST)
|
2159
|
+
call_id = None
|
2160
|
+
body = {}
|
2223
2161
|
|
2224
|
-
|
2225
|
-
|
2226
|
-
|
2162
|
+
if request.method == "POST":
|
2163
|
+
try:
|
2164
|
+
body = await request.json()
|
2165
|
+
req_log.debug("request_body_received", body_size=len(str(body)))
|
2166
|
+
call_id = body.get("call_id")
|
2167
|
+
except Exception as e:
|
2168
|
+
req_log.warning("error_parsing_request_body", error=str(e))
|
2169
|
+
else:
|
2170
|
+
call_id = request.query_params.get("call_id")
|
2227
2171
|
|
2228
|
-
#
|
2229
|
-
|
2172
|
+
# Add call_id to logger if any
|
2173
|
+
if call_id:
|
2174
|
+
req_log = req_log.bind(call_id=call_id)
|
2175
|
+
req_log.debug("call_id_identified")
|
2176
|
+
|
2177
|
+
# Allow subclasses to inspect/modify the request
|
2178
|
+
modifications = None
|
2179
|
+
if body:
|
2180
|
+
modifications = self.on_swml_request(body)
|
2181
|
+
if modifications:
|
2182
|
+
req_log.debug("request_modifications_applied")
|
2183
|
+
|
2184
|
+
# Render SWML
|
2185
|
+
swml = self._render_swml(call_id)
|
2186
|
+
req_log.debug("swml_rendered", swml_size=len(swml))
|
2230
2187
|
|
2231
|
-
#
|
2232
|
-
|
2233
|
-
|
2234
|
-
|
2235
|
-
|
2236
|
-
|
2237
|
-
|
2188
|
+
# Return as JSON
|
2189
|
+
req_log.info("request_successful")
|
2190
|
+
return Response(
|
2191
|
+
content=swml,
|
2192
|
+
media_type="application/json",
|
2193
|
+
headers={"X-Debug": "true"}
|
2194
|
+
)
|
2195
|
+
except Exception as e:
|
2196
|
+
req_log.error("request_failed", error=str(e))
|
2197
|
+
return Response(
|
2198
|
+
content=json.dumps({"error": str(e)}),
|
2199
|
+
status_code=500,
|
2200
|
+
media_type="application/json"
|
2201
|
+
)
|
2202
|
+
|
2203
|
+
async def _handle_post_prompt_request(self, request: Request):
|
2204
|
+
"""Handle GET/POST requests to the post_prompt endpoint"""
|
2205
|
+
req_log = self.log.bind(
|
2206
|
+
endpoint="post_prompt",
|
2207
|
+
method=request.method,
|
2208
|
+
path=request.url.path
|
2209
|
+
)
|
2238
2210
|
|
2239
|
-
#
|
2240
|
-
if
|
2241
|
-
|
2242
|
-
|
2243
|
-
|
2244
|
-
|
2245
|
-
if
|
2246
|
-
|
2247
|
-
|
2248
|
-
|
2211
|
+
# Only log if not suppressed
|
2212
|
+
if not getattr(self, '_suppress_logs', False):
|
2213
|
+
req_log.debug("endpoint_called")
|
2214
|
+
|
2215
|
+
try:
|
2216
|
+
# Check auth
|
2217
|
+
if not self._check_basic_auth(request):
|
2218
|
+
req_log.warning("unauthorized_access_attempt")
|
2219
|
+
return Response(
|
2220
|
+
content=json.dumps({"error": "Unauthorized"}),
|
2221
|
+
status_code=401,
|
2222
|
+
headers={"WWW-Authenticate": "Basic"},
|
2223
|
+
media_type="application/json"
|
2224
|
+
)
|
2225
|
+
|
2226
|
+
# Extract call_id for use with token validation
|
2227
|
+
call_id = request.query_params.get("call_id")
|
2228
|
+
|
2229
|
+
# For POST requests, try to also get call_id from body
|
2230
|
+
if request.method == "POST":
|
2231
|
+
try:
|
2232
|
+
body_text = await request.body()
|
2233
|
+
if body_text:
|
2234
|
+
body_data = json.loads(body_text)
|
2235
|
+
if call_id is None:
|
2236
|
+
call_id = body_data.get("call_id")
|
2237
|
+
# Save body_data for later use
|
2238
|
+
setattr(request, "_post_prompt_body", body_data)
|
2239
|
+
except Exception as e:
|
2240
|
+
req_log.error("error_extracting_call_id", error=str(e))
|
2241
|
+
|
2242
|
+
# If we have a call_id, add it to the logger context
|
2243
|
+
if call_id:
|
2244
|
+
req_log = req_log.bind(call_id=call_id)
|
2245
|
+
|
2246
|
+
# Check token if provided
|
2247
|
+
token = request.query_params.get("token")
|
2248
|
+
token_validated = False
|
2249
|
+
|
2250
|
+
if token:
|
2251
|
+
req_log.debug("token_found", token_length=len(token))
|
2252
|
+
|
2253
|
+
# Try to validate token, but continue processing regardless
|
2254
|
+
# for backward compatibility with existing implementations
|
2255
|
+
if call_id and hasattr(self, '_session_manager'):
|
2256
|
+
try:
|
2257
|
+
is_valid = self._session_manager.validate_tool_token("post_prompt", token, call_id)
|
2258
|
+
if is_valid:
|
2259
|
+
req_log.debug("token_valid")
|
2260
|
+
token_validated = True
|
2261
|
+
else:
|
2262
|
+
req_log.warning("invalid_token")
|
2263
|
+
# Debug information for token validation issues
|
2264
|
+
if hasattr(self._session_manager, 'debug_token'):
|
2265
|
+
debug_info = self._session_manager.debug_token(token)
|
2266
|
+
req_log.debug("token_debug", debug=json.dumps(debug_info))
|
2267
|
+
except Exception as e:
|
2268
|
+
req_log.error("token_validation_error", error=str(e))
|
2269
|
+
|
2270
|
+
# For GET requests, return the SWML document
|
2271
|
+
if request.method == "GET":
|
2272
|
+
swml = self._render_swml(call_id)
|
2273
|
+
req_log.debug("swml_rendered", swml_size=len(swml))
|
2274
|
+
return Response(
|
2275
|
+
content=swml,
|
2276
|
+
media_type="application/json"
|
2277
|
+
)
|
2278
|
+
|
2279
|
+
# For POST requests, process the post-prompt data
|
2249
2280
|
try:
|
2250
|
-
|
2251
|
-
|
2281
|
+
# Try to reuse the body we already parsed for call_id extraction
|
2282
|
+
if hasattr(request, "_post_prompt_body"):
|
2283
|
+
body = getattr(request, "_post_prompt_body")
|
2284
|
+
else:
|
2285
|
+
body = await request.json()
|
2252
2286
|
|
2253
|
-
|
2254
|
-
|
2255
|
-
|
2256
|
-
|
2257
|
-
|
2258
|
-
|
2259
|
-
|
2260
|
-
|
2261
|
-
|
2262
|
-
|
2263
|
-
|
2264
|
-
if speech_fillers and function_fillers:
|
2265
|
-
language["speech_fillers"] = speech_fillers
|
2266
|
-
language["function_fillers"] = function_fillers
|
2267
|
-
elif speech_fillers or function_fillers:
|
2268
|
-
# If only one type of fillers is provided, use the deprecated "fillers" field
|
2269
|
-
fillers = speech_fillers or function_fillers
|
2270
|
-
language["fillers"] = fillers
|
2271
|
-
|
2272
|
-
self._languages.append(language)
|
2273
|
-
return self
|
2274
|
-
|
2275
|
-
def set_languages(self, languages: List[Dict[str, Any]]) -> 'AgentBase':
|
2276
|
-
"""
|
2277
|
-
Set all language configurations at once
|
2278
|
-
|
2279
|
-
Args:
|
2280
|
-
languages: List of language configuration dictionaries
|
2287
|
+
# Only log if not suppressed
|
2288
|
+
if not getattr(self, '_suppress_logs', False):
|
2289
|
+
req_log.debug("request_body_received", body_size=len(str(body)))
|
2290
|
+
# Log the raw body directly (let the logger handle the JSON encoding)
|
2291
|
+
req_log.info("post_prompt_body", body=body)
|
2292
|
+
except Exception as e:
|
2293
|
+
req_log.error("error_parsing_request_body", error=str(e))
|
2294
|
+
body = {}
|
2295
|
+
|
2296
|
+
# Extract summary from the correct location in the request
|
2297
|
+
summary = self._find_summary_in_post_data(body, req_log)
|
2281
2298
|
|
2282
|
-
|
2283
|
-
|
2284
|
-
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2288
|
-
|
2289
|
-
|
2290
|
-
|
2291
|
-
|
2292
|
-
|
2293
|
-
|
2294
|
-
|
2299
|
+
# Call the summary handler with the summary and the full body
|
2300
|
+
try:
|
2301
|
+
if summary:
|
2302
|
+
self.on_summary(summary, body)
|
2303
|
+
req_log.debug("summary_handler_called_successfully")
|
2304
|
+
else:
|
2305
|
+
# If no summary found but still want to process the data
|
2306
|
+
self.on_summary(None, body)
|
2307
|
+
req_log.debug("summary_handler_called_with_null_summary")
|
2308
|
+
except Exception as e:
|
2309
|
+
req_log.error("error_in_summary_handler", error=str(e))
|
2310
|
+
|
2311
|
+
# Return success
|
2312
|
+
req_log.info("request_successful")
|
2313
|
+
return {"success": True}
|
2314
|
+
except Exception as e:
|
2315
|
+
req_log.error("request_failed", error=str(e))
|
2316
|
+
return Response(
|
2317
|
+
content=json.dumps({"error": str(e)}),
|
2318
|
+
status_code=500,
|
2319
|
+
media_type="application/json"
|
2320
|
+
)
|
2321
|
+
|
2322
|
+
async def _handle_check_for_input_request(self, request: Request):
|
2323
|
+
"""Handle GET/POST requests to the check_for_input endpoint"""
|
2324
|
+
req_log = self.log.bind(
|
2325
|
+
endpoint="check_for_input",
|
2326
|
+
method=request.method,
|
2327
|
+
path=request.url.path
|
2328
|
+
)
|
2295
2329
|
|
2296
|
-
|
2297
|
-
|
2298
|
-
|
2299
|
-
|
2330
|
+
req_log.debug("endpoint_called")
|
2331
|
+
|
2332
|
+
try:
|
2333
|
+
# Check auth
|
2334
|
+
if not self._check_basic_auth(request):
|
2335
|
+
req_log.warning("unauthorized_access_attempt")
|
2336
|
+
return Response(
|
2337
|
+
content=json.dumps({"error": "Unauthorized"}),
|
2338
|
+
status_code=401,
|
2339
|
+
headers={"WWW-Authenticate": "Basic"},
|
2340
|
+
media_type="application/json"
|
2341
|
+
)
|
2300
2342
|
|
2301
|
-
|
2302
|
-
|
2303
|
-
"""
|
2304
|
-
if replace and with_text:
|
2305
|
-
rule = {
|
2306
|
-
"replace": replace,
|
2307
|
-
"with": with_text
|
2308
|
-
}
|
2309
|
-
if ignore_case:
|
2310
|
-
rule["ignore_case"] = True
|
2343
|
+
# For both GET and POST requests, process input check
|
2344
|
+
conversation_id = None
|
2311
2345
|
|
2312
|
-
|
2313
|
-
|
2314
|
-
|
2315
|
-
|
2316
|
-
|
2317
|
-
|
2318
|
-
|
2319
|
-
|
2320
|
-
|
2346
|
+
if request.method == "POST":
|
2347
|
+
try:
|
2348
|
+
body = await request.json()
|
2349
|
+
req_log.debug("request_body_received", body_size=len(str(body)))
|
2350
|
+
conversation_id = body.get("conversation_id")
|
2351
|
+
except Exception as e:
|
2352
|
+
req_log.error("error_parsing_request_body", error=str(e))
|
2353
|
+
else:
|
2354
|
+
conversation_id = request.query_params.get("conversation_id")
|
2321
2355
|
|
2322
|
-
|
2323
|
-
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2327
|
-
|
2328
|
-
|
2329
|
-
def set_param(self, key: str, value: Any) -> 'AgentBase':
|
2330
|
-
"""
|
2331
|
-
Set a single AI parameter
|
2332
|
-
|
2333
|
-
Args:
|
2334
|
-
key: Parameter name
|
2335
|
-
value: Parameter value
|
2356
|
+
if not conversation_id:
|
2357
|
+
req_log.warning("missing_conversation_id")
|
2358
|
+
return Response(
|
2359
|
+
content=json.dumps({"error": "Missing conversation_id parameter"}),
|
2360
|
+
status_code=400,
|
2361
|
+
media_type="application/json"
|
2362
|
+
)
|
2336
2363
|
|
2337
|
-
|
2338
|
-
|
2339
|
-
|
2340
|
-
|
2341
|
-
|
2342
|
-
|
2343
|
-
|
2344
|
-
|
2364
|
+
# Here you would typically check for new input in some external system
|
2365
|
+
# For this implementation, we'll return an empty result
|
2366
|
+
return {
|
2367
|
+
"status": "success",
|
2368
|
+
"conversation_id": conversation_id,
|
2369
|
+
"new_input": False,
|
2370
|
+
"messages": []
|
2371
|
+
}
|
2372
|
+
except Exception as e:
|
2373
|
+
req_log.error("request_failed", error=str(e))
|
2374
|
+
return Response(
|
2375
|
+
content=json.dumps({"error": str(e)}),
|
2376
|
+
status_code=500,
|
2377
|
+
media_type="application/json"
|
2378
|
+
)
|
2379
|
+
|
2380
|
+
def _find_summary_in_post_data(self, body, logger):
|
2345
2381
|
"""
|
2346
|
-
|
2382
|
+
Attempt to find a summary in the post-prompt response data
|
2347
2383
|
|
2348
2384
|
Args:
|
2349
|
-
|
2385
|
+
body: The request body
|
2386
|
+
logger: Logger instance
|
2350
2387
|
|
2351
2388
|
Returns:
|
2352
|
-
|
2389
|
+
Summary data or None if not found
|
2353
2390
|
"""
|
2354
|
-
if
|
2355
|
-
|
2356
|
-
return self
|
2391
|
+
if not body:
|
2392
|
+
return None
|
2357
2393
|
|
2358
|
-
|
2394
|
+
# Various ways to get summary data
|
2395
|
+
if "summary" in body:
|
2396
|
+
return body["summary"]
|
2397
|
+
|
2398
|
+
if "post_prompt_data" in body:
|
2399
|
+
pdata = body["post_prompt_data"]
|
2400
|
+
if isinstance(pdata, dict):
|
2401
|
+
if "parsed" in pdata and isinstance(pdata["parsed"], list) and pdata["parsed"]:
|
2402
|
+
return pdata["parsed"][0]
|
2403
|
+
elif "raw" in pdata and pdata["raw"]:
|
2404
|
+
try:
|
2405
|
+
# Try to parse JSON from raw text
|
2406
|
+
parsed = json.loads(pdata["raw"])
|
2407
|
+
return parsed
|
2408
|
+
except:
|
2409
|
+
return pdata["raw"]
|
2410
|
+
|
2411
|
+
return None
|
2412
|
+
|
2413
|
+
def _register_state_tracking_tools(self):
|
2359
2414
|
"""
|
2360
|
-
|
2415
|
+
Register special tools for state tracking
|
2361
2416
|
|
2362
|
-
|
2363
|
-
|
2364
|
-
|
2365
|
-
Returns:
|
2366
|
-
Self for method chaining
|
2417
|
+
This adds startup_hook and hangup_hook SWAIG functions that automatically
|
2418
|
+
activate and deactivate the session when called. These are useful for
|
2419
|
+
tracking call state and cleaning up resources when a call ends.
|
2367
2420
|
"""
|
2368
|
-
|
2369
|
-
|
2370
|
-
|
2371
|
-
|
2372
|
-
|
2421
|
+
# Register startup hook to activate session
|
2422
|
+
self.define_tool(
|
2423
|
+
name="startup_hook",
|
2424
|
+
description="Called when a new conversation starts to initialize state",
|
2425
|
+
parameters={},
|
2426
|
+
handler=lambda args, raw_data: self._handle_startup_hook(args, raw_data),
|
2427
|
+
secure=False # No auth needed for this system function
|
2428
|
+
)
|
2429
|
+
|
2430
|
+
# Register hangup hook to end session
|
2431
|
+
self.define_tool(
|
2432
|
+
name="hangup_hook",
|
2433
|
+
description="Called when conversation ends to clean up resources",
|
2434
|
+
parameters={},
|
2435
|
+
handler=lambda args, raw_data: self._handle_hangup_hook(args, raw_data),
|
2436
|
+
secure=False # No auth needed for this system function
|
2437
|
+
)
|
2438
|
+
|
2439
|
+
def _handle_startup_hook(self, args, raw_data):
|
2373
2440
|
"""
|
2374
|
-
|
2441
|
+
Handle the startup hook function call
|
2375
2442
|
|
2376
2443
|
Args:
|
2377
|
-
|
2444
|
+
args: Function arguments (empty for this hook)
|
2445
|
+
raw_data: Raw request data containing call_id
|
2378
2446
|
|
2379
2447
|
Returns:
|
2380
|
-
|
2448
|
+
Success response
|
2381
2449
|
"""
|
2382
|
-
if
|
2383
|
-
|
2384
|
-
|
2385
|
-
|
2386
|
-
|
2450
|
+
call_id = raw_data.get("call_id") if raw_data else None
|
2451
|
+
if call_id:
|
2452
|
+
self.log.info("session_activated", call_id=call_id)
|
2453
|
+
self._session_manager.activate_session(call_id)
|
2454
|
+
return SwaigFunctionResult("Session activated")
|
2455
|
+
else:
|
2456
|
+
self.log.warning("session_activation_failed", error="No call_id provided")
|
2457
|
+
return SwaigFunctionResult("Failed to activate session: No call_id provided")
|
2458
|
+
|
2459
|
+
def _handle_hangup_hook(self, args, raw_data):
|
2387
2460
|
"""
|
2388
|
-
|
2461
|
+
Handle the hangup hook function call
|
2389
2462
|
|
2390
2463
|
Args:
|
2391
|
-
|
2464
|
+
args: Function arguments (empty for this hook)
|
2465
|
+
raw_data: Raw request data containing call_id
|
2392
2466
|
|
2393
2467
|
Returns:
|
2394
|
-
|
2468
|
+
Success response
|
2395
2469
|
"""
|
2396
|
-
if
|
2397
|
-
|
2398
|
-
|
2470
|
+
call_id = raw_data.get("call_id") if raw_data else None
|
2471
|
+
if call_id:
|
2472
|
+
self.log.info("session_ended", call_id=call_id)
|
2473
|
+
self._session_manager.end_session(call_id)
|
2474
|
+
return SwaigFunctionResult("Session ended")
|
2475
|
+
else:
|
2476
|
+
self.log.warning("session_end_failed", error="No call_id provided")
|
2477
|
+
return SwaigFunctionResult("Failed to end session: No call_id provided")
|
2399
2478
|
|
2400
|
-
def
|
2479
|
+
def on_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
|
2401
2480
|
"""
|
2402
|
-
|
2481
|
+
Called when SWML is requested, with request data when available
|
2482
|
+
|
2483
|
+
This method overrides SWMLService's on_request to properly handle SWML generation
|
2484
|
+
for AI Agents. It forwards the call to on_swml_request for compatibility.
|
2403
2485
|
|
2404
2486
|
Args:
|
2405
|
-
|
2406
|
-
|
2407
|
-
meta_data: Optional metadata to include with the function include
|
2487
|
+
request_data: Optional dictionary containing the parsed POST body
|
2488
|
+
callback_path: Optional callback path
|
2408
2489
|
|
2409
2490
|
Returns:
|
2410
|
-
|
2491
|
+
None to use the default SWML rendering (which will call _render_swml)
|
2411
2492
|
"""
|
2412
|
-
|
2413
|
-
|
2414
|
-
|
2415
|
-
"functions": functions
|
2416
|
-
}
|
2417
|
-
if meta_data and isinstance(meta_data, dict):
|
2418
|
-
include["meta_data"] = meta_data
|
2493
|
+
# First try to call on_swml_request if it exists (backward compatibility)
|
2494
|
+
if hasattr(self, 'on_swml_request') and callable(getattr(self, 'on_swml_request')):
|
2495
|
+
return self.on_swml_request(request_data, callback_path)
|
2419
2496
|
|
2420
|
-
|
2421
|
-
|
2422
|
-
|
2423
|
-
|
2497
|
+
# If no on_swml_request or it returned None, we'll proceed with default rendering
|
2498
|
+
# We're not returning any modifications here because _render_swml will be called
|
2499
|
+
# to generate the complete SWML document
|
2500
|
+
return None
|
2501
|
+
|
2502
|
+
def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
|
2424
2503
|
"""
|
2425
|
-
|
2504
|
+
Customization point for subclasses to modify SWML based on request data
|
2426
2505
|
|
2427
2506
|
Args:
|
2428
|
-
|
2507
|
+
request_data: Optional dictionary containing the parsed POST body
|
2508
|
+
callback_path: Optional callback path
|
2429
2509
|
|
2430
2510
|
Returns:
|
2431
|
-
|
2511
|
+
Optional dict with modifications to apply to the SWML document
|
2432
2512
|
"""
|
2433
|
-
|
2434
|
-
|
2435
|
-
valid_includes = []
|
2436
|
-
for include in includes:
|
2437
|
-
if isinstance(include, dict) and "url" in include and "functions" in include:
|
2438
|
-
if isinstance(include["functions"], list):
|
2439
|
-
valid_includes.append(include)
|
2440
|
-
|
2441
|
-
self._function_includes = valid_includes
|
2442
|
-
return self
|
2513
|
+
# Default implementation does nothing
|
2514
|
+
return None
|
2443
2515
|
|
2444
|
-
def
|
2516
|
+
def register_routing_callback(self, callback_fn: Callable[[Request, Dict[str, Any]], Optional[str]],
|
2517
|
+
path: str = "/sip") -> None:
|
2445
2518
|
"""
|
2446
|
-
|
2519
|
+
Register a callback function that will be called to determine routing
|
2520
|
+
based on POST data.
|
2447
2521
|
|
2448
|
-
|
2449
|
-
|
2450
|
-
|
2522
|
+
When a routing callback is registered, an endpoint at the specified path is automatically
|
2523
|
+
created that will handle requests. This endpoint will use the callback to
|
2524
|
+
determine if the request should be processed by this service or redirected.
|
2451
2525
|
|
2452
|
-
|
2453
|
-
|
2454
|
-
|
2455
|
-
path: The path to register the SIP routing endpoint (default: "/sip")
|
2526
|
+
The callback should take a request object and request body dictionary and return:
|
2527
|
+
- A route string if it should be routed to a different endpoint
|
2528
|
+
- None if normal processing should continue
|
2456
2529
|
|
2457
|
-
|
2458
|
-
|
2530
|
+
Args:
|
2531
|
+
callback_fn: The callback function to register
|
2532
|
+
path: The path where this callback should be registered (default: "/sip")
|
2533
|
+
"""
|
2534
|
+
# Normalize the path (remove trailing slash)
|
2535
|
+
normalized_path = path.rstrip("/")
|
2536
|
+
if not normalized_path.startswith("/"):
|
2537
|
+
normalized_path = f"/{normalized_path}"
|
2538
|
+
|
2539
|
+
# Store the callback with the normalized path (without trailing slash)
|
2540
|
+
self.log.info("registering_routing_callback", path=normalized_path)
|
2541
|
+
if not hasattr(self, '_routing_callbacks'):
|
2542
|
+
self._routing_callbacks = {}
|
2543
|
+
self._routing_callbacks[normalized_path] = callback_fn
|
2544
|
+
|
2545
|
+
def manual_set_proxy_url(self, proxy_url: str) -> 'AgentBase':
|
2459
2546
|
"""
|
2460
|
-
|
2461
|
-
def sip_routing_callback(request: Request, body: Dict[str, Any]) -> Optional[str]:
|
2462
|
-
# Extract SIP username from the request body
|
2463
|
-
sip_username = self.extract_sip_username(body)
|
2464
|
-
|
2465
|
-
if sip_username:
|
2466
|
-
self.log.info("sip_username_extracted", username=sip_username)
|
2467
|
-
|
2468
|
-
# Check if this username is registered with this agent
|
2469
|
-
if hasattr(self, '_sip_usernames') and sip_username.lower() in self._sip_usernames:
|
2470
|
-
self.log.info("sip_username_matched", username=sip_username)
|
2471
|
-
# This route is already being handled by the agent, no need to redirect
|
2472
|
-
return None
|
2473
|
-
else:
|
2474
|
-
self.log.info("sip_username_not_matched", username=sip_username)
|
2475
|
-
# Not registered with this agent, let routing continue
|
2476
|
-
|
2477
|
-
return None
|
2478
|
-
|
2479
|
-
# Register the callback with the SWMLService, specifying the path
|
2480
|
-
self.register_routing_callback(sip_routing_callback, path=path)
|
2481
|
-
|
2482
|
-
# Auto-map common usernames if requested
|
2483
|
-
if auto_map:
|
2484
|
-
self.auto_map_sip_usernames()
|
2485
|
-
|
2486
|
-
return self
|
2547
|
+
Manually set the proxy URL base for webhook callbacks
|
2487
2548
|
|
2488
|
-
|
2489
|
-
"""
|
2490
|
-
Register a SIP username that should be routed to this agent
|
2549
|
+
This can be called at runtime to set or update the proxy URL
|
2491
2550
|
|
2492
2551
|
Args:
|
2493
|
-
|
2552
|
+
proxy_url: The base URL to use for webhooks (e.g., https://example.ngrok.io)
|
2494
2553
|
|
2495
2554
|
Returns:
|
2496
2555
|
Self for method chaining
|
2497
2556
|
"""
|
2498
|
-
if
|
2499
|
-
|
2500
|
-
|
2501
|
-
|
2502
|
-
self.log.info("sip_username_registered", username=sip_username)
|
2503
|
-
|
2504
|
-
return self
|
2505
|
-
|
2506
|
-
def auto_map_sip_usernames(self) -> 'AgentBase':
|
2507
|
-
"""
|
2508
|
-
Automatically register common SIP usernames based on this agent's
|
2509
|
-
name and route
|
2510
|
-
|
2511
|
-
Returns:
|
2512
|
-
Self for method chaining
|
2513
|
-
"""
|
2514
|
-
# Register username based on agent name
|
2515
|
-
clean_name = re.sub(r'[^a-z0-9_]', '', self.name.lower())
|
2516
|
-
if clean_name:
|
2517
|
-
self.register_sip_username(clean_name)
|
2518
|
-
|
2519
|
-
# Register username based on route (without slashes)
|
2520
|
-
clean_route = re.sub(r'[^a-z0-9_]', '', self.route.lower())
|
2521
|
-
if clean_route and clean_route != clean_name:
|
2522
|
-
self.register_sip_username(clean_route)
|
2557
|
+
if proxy_url:
|
2558
|
+
# Set on self
|
2559
|
+
self._proxy_url_base = proxy_url.rstrip('/')
|
2560
|
+
self._proxy_detection_done = True
|
2523
2561
|
|
2524
|
-
|
2525
|
-
|
2526
|
-
|
2527
|
-
|
2528
|
-
|
2529
|
-
self.register_sip_username(no_vowels)
|
2562
|
+
# Set on parent if it has these attributes
|
2563
|
+
if hasattr(super(), '_proxy_url_base'):
|
2564
|
+
super()._proxy_url_base = self._proxy_url_base
|
2565
|
+
if hasattr(super(), '_proxy_detection_done'):
|
2566
|
+
super()._proxy_detection_done = True
|
2530
2567
|
|
2531
|
-
|
2532
|
-
|
2533
|
-
def set_web_hook_url(self, url: str) -> 'AgentBase':
|
2534
|
-
"""
|
2535
|
-
Override the default web_hook_url with a supplied URL string
|
2536
|
-
|
2537
|
-
Args:
|
2538
|
-
url: The URL to use for SWAIG function webhooks
|
2539
|
-
|
2540
|
-
Returns:
|
2541
|
-
Self for method chaining
|
2542
|
-
"""
|
2543
|
-
self._web_hook_url_override = url
|
2544
|
-
return self
|
2545
|
-
|
2546
|
-
def set_post_prompt_url(self, url: str) -> 'AgentBase':
|
2547
|
-
"""
|
2548
|
-
Override the default post_prompt_url with a supplied URL string
|
2549
|
-
|
2550
|
-
Args:
|
2551
|
-
url: The URL to use for post-prompt summary delivery
|
2568
|
+
self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
|
2552
2569
|
|
2553
|
-
Returns:
|
2554
|
-
Self for method chaining
|
2555
|
-
"""
|
2556
|
-
self._post_prompt_url_override = url
|
2557
2570
|
return self
|