signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
|
@@ -25,7 +25,7 @@ import re
|
|
|
25
25
|
import signal
|
|
26
26
|
import sys
|
|
27
27
|
from typing import Optional, Union, List, Dict, Any, Tuple, Callable, Type
|
|
28
|
-
from urllib.parse import urlparse, urlencode
|
|
28
|
+
from urllib.parse import urlparse, urlencode, urlunparse
|
|
29
29
|
|
|
30
30
|
try:
|
|
31
31
|
import fastapi
|
|
@@ -44,211 +44,47 @@ except ImportError:
|
|
|
44
44
|
"uvicorn is required. Install it with: pip install uvicorn"
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
try:
|
|
48
|
-
import structlog
|
|
49
|
-
# Configure structlog only if not already configured
|
|
50
|
-
if not structlog.is_configured():
|
|
51
|
-
structlog.configure(
|
|
52
|
-
processors=[
|
|
53
|
-
structlog.stdlib.filter_by_level,
|
|
54
|
-
structlog.stdlib.add_logger_name,
|
|
55
|
-
structlog.stdlib.add_log_level,
|
|
56
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
57
|
-
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
|
58
|
-
structlog.processors.StackInfoRenderer(),
|
|
59
|
-
structlog.processors.format_exc_info,
|
|
60
|
-
structlog.processors.UnicodeDecoder(),
|
|
61
|
-
structlog.dev.ConsoleRenderer()
|
|
62
|
-
],
|
|
63
|
-
context_class=dict,
|
|
64
|
-
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
65
|
-
wrapper_class=structlog.stdlib.BoundLogger,
|
|
66
|
-
cache_logger_on_first_use=True,
|
|
67
|
-
)
|
|
68
|
-
except ImportError:
|
|
69
|
-
raise ImportError(
|
|
70
|
-
"structlog is required. Install it with: pip install structlog"
|
|
71
|
-
)
|
|
72
|
-
|
|
73
47
|
from signalwire_agents.core.pom_builder import PomBuilder
|
|
74
48
|
from signalwire_agents.core.swaig_function import SWAIGFunction
|
|
75
49
|
from signalwire_agents.core.function_result import SwaigFunctionResult
|
|
76
50
|
from signalwire_agents.core.swml_renderer import SwmlRenderer
|
|
77
51
|
from signalwire_agents.core.security.session_manager import SessionManager
|
|
78
|
-
from signalwire_agents.core.state import StateManager, FileStateManager
|
|
79
52
|
from signalwire_agents.core.swml_service import SWMLService
|
|
80
53
|
from signalwire_agents.core.swml_handler import AIVerbHandler
|
|
81
54
|
from signalwire_agents.core.skill_manager import SkillManager
|
|
82
55
|
from signalwire_agents.utils.schema_utils import SchemaUtils
|
|
83
56
|
from signalwire_agents.core.logging_config import get_logger, get_execution_mode
|
|
84
57
|
|
|
85
|
-
#
|
|
86
|
-
|
|
58
|
+
# Import refactored components
|
|
59
|
+
from signalwire_agents.core.agent.prompt.manager import PromptManager
|
|
60
|
+
from signalwire_agents.core.agent.tools.registry import ToolRegistry
|
|
61
|
+
from signalwire_agents.core.agent.tools.decorator import ToolDecorator
|
|
87
62
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if isinstance(hint, str) and hint:
|
|
114
|
-
self._hints.append(hint)
|
|
115
|
-
return self
|
|
116
|
-
|
|
117
|
-
def add_hints(self, hints: List[str]) -> 'EphemeralAgentConfig':
|
|
118
|
-
"""Add multiple string hints"""
|
|
119
|
-
if hints and isinstance(hints, list):
|
|
120
|
-
for hint in hints:
|
|
121
|
-
if isinstance(hint, str) and hint:
|
|
122
|
-
self._hints.append(hint)
|
|
123
|
-
return self
|
|
124
|
-
|
|
125
|
-
def add_language(self, name: str, code: str, voice: str, **kwargs) -> 'EphemeralAgentConfig':
|
|
126
|
-
"""Add a language configuration"""
|
|
127
|
-
language = {
|
|
128
|
-
"name": name,
|
|
129
|
-
"code": code,
|
|
130
|
-
"voice": voice
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
# Handle additional parameters
|
|
134
|
-
for key, value in kwargs.items():
|
|
135
|
-
if key in ["engine", "model", "speech_fillers", "function_fillers", "fillers"]:
|
|
136
|
-
language[key] = value
|
|
137
|
-
|
|
138
|
-
self._languages.append(language)
|
|
139
|
-
return self
|
|
140
|
-
|
|
141
|
-
def add_pronunciation(self, replace: str, with_text: str, ignore_case: bool = False) -> 'EphemeralAgentConfig':
|
|
142
|
-
"""Add a pronunciation rule"""
|
|
143
|
-
if replace and with_text:
|
|
144
|
-
rule = {"replace": replace, "with": with_text}
|
|
145
|
-
if ignore_case:
|
|
146
|
-
rule["ignore_case"] = True
|
|
147
|
-
self._pronounce.append(rule)
|
|
148
|
-
return self
|
|
149
|
-
|
|
150
|
-
def set_param(self, key: str, value: Any) -> 'EphemeralAgentConfig':
|
|
151
|
-
"""Set a single AI parameter"""
|
|
152
|
-
if key:
|
|
153
|
-
self._params[key] = value
|
|
154
|
-
return self
|
|
155
|
-
|
|
156
|
-
def set_params(self, params: Dict[str, Any]) -> 'EphemeralAgentConfig':
|
|
157
|
-
"""Set multiple AI parameters"""
|
|
158
|
-
if params and isinstance(params, dict):
|
|
159
|
-
self._params.update(params)
|
|
160
|
-
return self
|
|
161
|
-
|
|
162
|
-
def set_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
|
|
163
|
-
"""Set global data"""
|
|
164
|
-
if data and isinstance(data, dict):
|
|
165
|
-
self._global_data = data
|
|
166
|
-
return self
|
|
167
|
-
|
|
168
|
-
def update_global_data(self, data: Dict[str, Any]) -> 'EphemeralAgentConfig':
|
|
169
|
-
"""Update global data"""
|
|
170
|
-
if data and isinstance(data, dict):
|
|
171
|
-
self._global_data.update(data)
|
|
172
|
-
return self
|
|
173
|
-
|
|
174
|
-
def set_prompt_text(self, text: str) -> 'EphemeralAgentConfig':
|
|
175
|
-
"""Set raw prompt text"""
|
|
176
|
-
self._raw_prompt = text
|
|
177
|
-
return self
|
|
178
|
-
|
|
179
|
-
def set_post_prompt(self, text: str) -> 'EphemeralAgentConfig':
|
|
180
|
-
"""Set post-prompt text"""
|
|
181
|
-
self._post_prompt = text
|
|
182
|
-
return self
|
|
183
|
-
|
|
184
|
-
def prompt_add_section(self, title: str, body: str = "", bullets: Optional[List[str]] = None, **kwargs) -> 'EphemeralAgentConfig':
|
|
185
|
-
"""Add a prompt section"""
|
|
186
|
-
section = {
|
|
187
|
-
"title": title,
|
|
188
|
-
"body": body
|
|
189
|
-
}
|
|
190
|
-
if bullets:
|
|
191
|
-
section["bullets"] = bullets
|
|
192
|
-
|
|
193
|
-
# Handle additional parameters
|
|
194
|
-
for key, value in kwargs.items():
|
|
195
|
-
if key in ["numbered", "numbered_bullets", "subsections"]:
|
|
196
|
-
section[key] = value
|
|
197
|
-
|
|
198
|
-
self._prompt_sections.append(section)
|
|
199
|
-
return self
|
|
200
|
-
|
|
201
|
-
def set_native_functions(self, function_names: List[str]) -> 'EphemeralAgentConfig':
|
|
202
|
-
"""Set native functions"""
|
|
203
|
-
if function_names and isinstance(function_names, list):
|
|
204
|
-
self._native_functions = [name for name in function_names if isinstance(name, str)]
|
|
205
|
-
return self
|
|
206
|
-
|
|
207
|
-
def add_function_include(self, url: str, functions: List[str], meta_data: Optional[Dict[str, Any]] = None) -> 'EphemeralAgentConfig':
|
|
208
|
-
"""Add a function include"""
|
|
209
|
-
if url and functions and isinstance(functions, list):
|
|
210
|
-
include = {"url": url, "functions": functions}
|
|
211
|
-
if meta_data and isinstance(meta_data, dict):
|
|
212
|
-
include["meta_data"] = meta_data
|
|
213
|
-
self._function_includes.append(include)
|
|
214
|
-
return self
|
|
215
|
-
|
|
216
|
-
def extract_config(self) -> Dict[str, Any]:
|
|
217
|
-
"""
|
|
218
|
-
Extract the configuration as a dictionary for applying to the real agent.
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
Dictionary containing all the configuration changes
|
|
222
|
-
"""
|
|
223
|
-
config = {}
|
|
224
|
-
|
|
225
|
-
if self._hints:
|
|
226
|
-
config["hints"] = self._hints
|
|
227
|
-
if self._languages:
|
|
228
|
-
config["languages"] = self._languages
|
|
229
|
-
if self._pronounce:
|
|
230
|
-
config["pronounce"] = self._pronounce
|
|
231
|
-
if self._params:
|
|
232
|
-
config["params"] = self._params
|
|
233
|
-
if self._global_data:
|
|
234
|
-
config["global_data"] = self._global_data
|
|
235
|
-
if self._function_includes:
|
|
236
|
-
config["function_includes"] = self._function_includes
|
|
237
|
-
if self._native_functions:
|
|
238
|
-
config["native_functions"] = self._native_functions
|
|
239
|
-
|
|
240
|
-
# Handle prompt sections - these should be applied to the agent's POM, not as raw config
|
|
241
|
-
# The calling code should use these to build the prompt properly
|
|
242
|
-
if self._prompt_sections:
|
|
243
|
-
config["_ephemeral_prompt_sections"] = self._prompt_sections
|
|
244
|
-
if self._raw_prompt:
|
|
245
|
-
config["_ephemeral_raw_prompt"] = self._raw_prompt
|
|
246
|
-
if self._post_prompt:
|
|
247
|
-
config["_ephemeral_post_prompt"] = self._post_prompt
|
|
248
|
-
|
|
249
|
-
return config
|
|
250
|
-
|
|
251
|
-
class AgentBase(SWMLService):
|
|
63
|
+
# Import all mixins
|
|
64
|
+
from signalwire_agents.core.mixins.prompt_mixin import PromptMixin
|
|
65
|
+
from signalwire_agents.core.mixins.tool_mixin import ToolMixin
|
|
66
|
+
from signalwire_agents.core.mixins.web_mixin import WebMixin
|
|
67
|
+
from signalwire_agents.core.mixins.auth_mixin import AuthMixin
|
|
68
|
+
from signalwire_agents.core.mixins.skill_mixin import SkillMixin
|
|
69
|
+
from signalwire_agents.core.mixins.ai_config_mixin import AIConfigMixin
|
|
70
|
+
from signalwire_agents.core.mixins.serverless_mixin import ServerlessMixin
|
|
71
|
+
from signalwire_agents.core.mixins.state_mixin import StateMixin
|
|
72
|
+
|
|
73
|
+
# Create a logger using centralized system
|
|
74
|
+
logger = get_logger("agent_base")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AgentBase(
|
|
78
|
+
AuthMixin,
|
|
79
|
+
WebMixin,
|
|
80
|
+
SWMLService,
|
|
81
|
+
PromptMixin,
|
|
82
|
+
ToolMixin,
|
|
83
|
+
SkillMixin,
|
|
84
|
+
AIConfigMixin,
|
|
85
|
+
ServerlessMixin,
|
|
86
|
+
StateMixin
|
|
87
|
+
):
|
|
252
88
|
"""
|
|
253
89
|
Base class for all SignalWire AI Agents.
|
|
254
90
|
|
|
@@ -273,23 +109,22 @@ class AgentBase(SWMLService):
|
|
|
273
109
|
name: str,
|
|
274
110
|
route: str = "/",
|
|
275
111
|
host: str = "0.0.0.0",
|
|
276
|
-
port: int =
|
|
112
|
+
port: Optional[int] = None,
|
|
277
113
|
basic_auth: Optional[Tuple[str, str]] = None,
|
|
278
114
|
use_pom: bool = True,
|
|
279
|
-
enable_state_tracking: bool = False,
|
|
280
115
|
token_expiry_secs: int = 3600,
|
|
281
116
|
auto_answer: bool = True,
|
|
282
117
|
record_call: bool = False,
|
|
283
118
|
record_format: str = "mp4",
|
|
284
119
|
record_stereo: bool = True,
|
|
285
|
-
state_manager: Optional[StateManager] = None,
|
|
286
120
|
default_webhook_url: Optional[str] = None,
|
|
287
121
|
agent_id: Optional[str] = None,
|
|
288
122
|
native_functions: Optional[List[str]] = None,
|
|
289
123
|
schema_path: Optional[str] = None,
|
|
290
|
-
|
|
124
|
+
suppress_logs: bool = False,
|
|
291
125
|
enable_post_prompt_override: bool = False,
|
|
292
|
-
check_for_input_override: bool = False
|
|
126
|
+
check_for_input_override: bool = False,
|
|
127
|
+
config_file: Optional[str] = None
|
|
293
128
|
):
|
|
294
129
|
"""
|
|
295
130
|
Initialize a new agent
|
|
@@ -301,13 +136,11 @@ class AgentBase(SWMLService):
|
|
|
301
136
|
port: Port to bind the web server to
|
|
302
137
|
basic_auth: Optional (username, password) tuple for basic auth
|
|
303
138
|
use_pom: Whether to use POM for prompt building
|
|
304
|
-
enable_state_tracking: Whether to register startup_hook and hangup_hook SWAIG functions to track conversation state
|
|
305
139
|
token_expiry_secs: Seconds until tokens expire
|
|
306
140
|
auto_answer: Whether to automatically answer calls
|
|
307
141
|
record_call: Whether to record calls
|
|
308
142
|
record_format: Recording format
|
|
309
143
|
record_stereo: Whether to record in stereo
|
|
310
|
-
state_manager: Optional state manager for this agent
|
|
311
144
|
default_webhook_url: Optional default webhook URL for all SWAIG functions
|
|
312
145
|
agent_id: Optional unique ID for this agent, generated if not provided
|
|
313
146
|
native_functions: Optional list of native functions to include in the SWAIG object
|
|
@@ -315,6 +148,7 @@ class AgentBase(SWMLService):
|
|
|
315
148
|
suppress_logs: Whether to suppress structured logs
|
|
316
149
|
enable_post_prompt_override: Whether to enable post-prompt override
|
|
317
150
|
check_for_input_override: Whether to enable check-for-input override
|
|
151
|
+
config_file: Optional path to configuration file
|
|
318
152
|
"""
|
|
319
153
|
# Import SWMLService here to avoid circular imports
|
|
320
154
|
from signalwire_agents.core.swml_service import SWMLService
|
|
@@ -322,14 +156,25 @@ class AgentBase(SWMLService):
|
|
|
322
156
|
# If schema_path is not provided, we'll let SWMLService find it through its _find_schema_path method
|
|
323
157
|
# which will be called in its __init__
|
|
324
158
|
|
|
159
|
+
# Load service configuration from config file before initializing SWMLService
|
|
160
|
+
service_config = self._load_service_config(config_file, name)
|
|
161
|
+
|
|
162
|
+
# Apply service config values, with constructor parameters taking precedence
|
|
163
|
+
final_route = route if route != "/" else service_config.get('route', route)
|
|
164
|
+
final_host = host if host != "0.0.0.0" else service_config.get('host', host)
|
|
165
|
+
# For port: use explicit param if provided, else config file, else let SWMLService use PORT env var
|
|
166
|
+
final_port = port if port is not None else service_config.get('port', None)
|
|
167
|
+
final_name = service_config.get('name', name)
|
|
168
|
+
|
|
325
169
|
# Initialize the SWMLService base class
|
|
326
170
|
super().__init__(
|
|
327
|
-
name=
|
|
328
|
-
route=
|
|
329
|
-
host=
|
|
330
|
-
port=
|
|
171
|
+
name=final_name,
|
|
172
|
+
route=final_route,
|
|
173
|
+
host=final_host,
|
|
174
|
+
port=final_port,
|
|
331
175
|
basic_auth=basic_auth,
|
|
332
|
-
schema_path=schema_path
|
|
176
|
+
schema_path=schema_path,
|
|
177
|
+
config_file=config_file
|
|
333
178
|
)
|
|
334
179
|
|
|
335
180
|
# Log the schema path if found and not suppressing logs
|
|
@@ -338,7 +183,7 @@ class AgentBase(SWMLService):
|
|
|
338
183
|
|
|
339
184
|
# Setup logger for this instance
|
|
340
185
|
self.log = logger.bind(agent=name)
|
|
341
|
-
self.log.info("agent_initializing", route=route, host=host, port=port)
|
|
186
|
+
self.log.info("agent_initializing", agent=name, route=route, host=self.host, port=self.port)
|
|
342
187
|
|
|
343
188
|
# Store agent-specific parameters
|
|
344
189
|
self._default_webhook_url = default_webhook_url
|
|
@@ -352,8 +197,6 @@ class AgentBase(SWMLService):
|
|
|
352
197
|
|
|
353
198
|
# Initialize prompt handling
|
|
354
199
|
self._use_pom = use_pom
|
|
355
|
-
self._raw_prompt = None
|
|
356
|
-
self._post_prompt = None
|
|
357
200
|
|
|
358
201
|
# Initialize POM if needed
|
|
359
202
|
if self._use_pom:
|
|
@@ -369,11 +212,9 @@ class AgentBase(SWMLService):
|
|
|
369
212
|
self.pom = None
|
|
370
213
|
|
|
371
214
|
# Initialize tool registry (separate from SWMLService verb registry)
|
|
372
|
-
self._swaig_functions = {}
|
|
373
215
|
|
|
374
216
|
# Initialize session manager
|
|
375
217
|
self._session_manager = SessionManager(token_expiry_secs=token_expiry_secs)
|
|
376
|
-
self._enable_state_tracking = enable_state_tracking
|
|
377
218
|
|
|
378
219
|
# URL override variables
|
|
379
220
|
self._web_hook_url_override = None
|
|
@@ -388,21 +229,20 @@ class AgentBase(SWMLService):
|
|
|
388
229
|
self._record_format = record_format
|
|
389
230
|
self._record_stereo = record_stereo
|
|
390
231
|
|
|
232
|
+
# Initialize refactored managers early
|
|
233
|
+
self._prompt_manager = PromptManager(self)
|
|
234
|
+
self._tool_registry = ToolRegistry(self)
|
|
235
|
+
|
|
391
236
|
# Process declarative PROMPT_SECTIONS if defined in subclass
|
|
392
237
|
self._process_prompt_sections()
|
|
393
238
|
|
|
394
|
-
# Initialize state manager
|
|
395
|
-
self._state_manager = state_manager or FileStateManager()
|
|
396
239
|
|
|
397
240
|
# Process class-decorated tools (using @AgentBase.tool)
|
|
398
|
-
self.
|
|
241
|
+
self._tool_registry.register_class_decorated_tools()
|
|
399
242
|
|
|
400
243
|
# Add native_functions parameter
|
|
401
244
|
self.native_functions = native_functions or []
|
|
402
245
|
|
|
403
|
-
# Register state tracking tools if enabled
|
|
404
|
-
if enable_state_tracking:
|
|
405
|
-
self._register_state_tracking_tools()
|
|
406
246
|
|
|
407
247
|
# Initialize new configuration containers
|
|
408
248
|
self._hints = []
|
|
@@ -411,6 +251,9 @@ class AgentBase(SWMLService):
|
|
|
411
251
|
self._params = {}
|
|
412
252
|
self._global_data = {}
|
|
413
253
|
self._function_includes = []
|
|
254
|
+
# Initialize LLM params as empty - only send if explicitly set
|
|
255
|
+
self._prompt_llm_params = {}
|
|
256
|
+
self._post_prompt_llm_params = {}
|
|
414
257
|
|
|
415
258
|
# Dynamic configuration callback
|
|
416
259
|
self._dynamic_config_callback = None
|
|
@@ -422,1045 +265,466 @@ class AgentBase(SWMLService):
|
|
|
422
265
|
self._contexts_builder = None
|
|
423
266
|
self._contexts_defined = False
|
|
424
267
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
268
|
+
# Initialize SWAIG query params for dynamic config
|
|
269
|
+
self._swaig_query_params = {}
|
|
270
|
+
|
|
271
|
+
# Initialize verb insertion points for call flow customization
|
|
272
|
+
self._pre_answer_verbs = [] # Verbs to run before answer (e.g., ringback, screening)
|
|
273
|
+
self._answer_config = {} # Configuration for the answer verb
|
|
274
|
+
self._post_answer_verbs = [] # Verbs to run after answer, before AI (e.g., announcements)
|
|
275
|
+
self._post_ai_verbs = [] # Verbs to run after AI ends (e.g., cleanup, transfers)
|
|
276
|
+
|
|
277
|
+
# Verb categories for pre-answer validation
|
|
278
|
+
_PRE_ANSWER_SAFE_VERBS = {
|
|
279
|
+
"transfer", "execute", "return", "label", "goto", "request",
|
|
280
|
+
"switch", "cond", "if", "eval", "set", "unset", "hangup",
|
|
281
|
+
"send_sms", "sleep", "stop_record_call", "stop_denoise", "stop_tap"
|
|
282
|
+
}
|
|
283
|
+
_AUTO_ANSWER_VERBS = {"play", "connect"}
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _load_service_config(config_file: Optional[str], service_name: str) -> dict:
|
|
287
|
+
"""Load service configuration from config file if available"""
|
|
288
|
+
from signalwire_agents.core.config_loader import ConfigLoader
|
|
289
|
+
|
|
290
|
+
# Find config file
|
|
291
|
+
if not config_file:
|
|
292
|
+
config_file = ConfigLoader.find_config_file(service_name)
|
|
293
|
+
|
|
294
|
+
if not config_file:
|
|
295
|
+
return {}
|
|
296
|
+
|
|
297
|
+
# Load config
|
|
298
|
+
config_loader = ConfigLoader([config_file])
|
|
299
|
+
if not config_loader.has_config():
|
|
300
|
+
return {}
|
|
428
301
|
|
|
302
|
+
# Get service section
|
|
303
|
+
service_config = config_loader.get_section('service')
|
|
304
|
+
if service_config:
|
|
305
|
+
return service_config
|
|
306
|
+
|
|
307
|
+
return {}
|
|
429
308
|
|
|
430
|
-
def
|
|
309
|
+
def get_name(self) -> str:
|
|
431
310
|
"""
|
|
432
|
-
|
|
311
|
+
Get agent name
|
|
433
312
|
|
|
434
|
-
|
|
435
|
-
|
|
313
|
+
Returns:
|
|
314
|
+
Agent name
|
|
436
315
|
"""
|
|
437
|
-
|
|
438
|
-
cls = self.__class__
|
|
439
|
-
if not hasattr(cls, 'PROMPT_SECTIONS') or cls.PROMPT_SECTIONS is None or not self._use_pom:
|
|
440
|
-
return
|
|
441
|
-
|
|
442
|
-
sections = cls.PROMPT_SECTIONS
|
|
443
|
-
|
|
444
|
-
# If sections is a dictionary mapping section names to content
|
|
445
|
-
if isinstance(sections, dict):
|
|
446
|
-
for title, content in sections.items():
|
|
447
|
-
# Handle different content types
|
|
448
|
-
if isinstance(content, str):
|
|
449
|
-
# Plain text - add as body
|
|
450
|
-
self.prompt_add_section(title, body=content)
|
|
451
|
-
elif isinstance(content, list) and content: # Only add if non-empty
|
|
452
|
-
# List of strings - add as bullets
|
|
453
|
-
self.prompt_add_section(title, bullets=content)
|
|
454
|
-
elif isinstance(content, dict):
|
|
455
|
-
# Dictionary with body/bullets/subsections
|
|
456
|
-
body = content.get('body', '')
|
|
457
|
-
bullets = content.get('bullets', [])
|
|
458
|
-
numbered = content.get('numbered', False)
|
|
459
|
-
numbered_bullets = content.get('numberedBullets', False)
|
|
460
|
-
|
|
461
|
-
# Only create section if it has content
|
|
462
|
-
if body or bullets or 'subsections' in content:
|
|
463
|
-
# Create the section
|
|
464
|
-
self.prompt_add_section(
|
|
465
|
-
title,
|
|
466
|
-
body=body,
|
|
467
|
-
bullets=bullets if bullets else None,
|
|
468
|
-
numbered=numbered,
|
|
469
|
-
numbered_bullets=numbered_bullets
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
# Process subsections if any
|
|
473
|
-
subsections = content.get('subsections', [])
|
|
474
|
-
for subsection in subsections:
|
|
475
|
-
if 'title' in subsection:
|
|
476
|
-
sub_title = subsection['title']
|
|
477
|
-
sub_body = subsection.get('body', '')
|
|
478
|
-
sub_bullets = subsection.get('bullets', [])
|
|
479
|
-
|
|
480
|
-
# Only add subsection if it has content
|
|
481
|
-
if sub_body or sub_bullets:
|
|
482
|
-
self.prompt_add_subsection(
|
|
483
|
-
title,
|
|
484
|
-
sub_title,
|
|
485
|
-
body=sub_body,
|
|
486
|
-
bullets=sub_bullets if sub_bullets else None
|
|
487
|
-
)
|
|
488
|
-
# If sections is a list of section objects, use the POM format directly
|
|
489
|
-
elif isinstance(sections, list):
|
|
490
|
-
if self.pom:
|
|
491
|
-
# Process each section using auto-vivifying methods
|
|
492
|
-
for section in sections:
|
|
493
|
-
if 'title' in section:
|
|
494
|
-
title = section['title']
|
|
495
|
-
body = section.get('body', '')
|
|
496
|
-
bullets = section.get('bullets', [])
|
|
497
|
-
numbered = section.get('numbered', False)
|
|
498
|
-
numbered_bullets = section.get('numberedBullets', False)
|
|
499
|
-
|
|
500
|
-
# Only create section if it has content
|
|
501
|
-
if body or bullets or 'subsections' in section:
|
|
502
|
-
self.prompt_add_section(
|
|
503
|
-
title,
|
|
504
|
-
body=body,
|
|
505
|
-
bullets=bullets if bullets else None,
|
|
506
|
-
numbered=numbered,
|
|
507
|
-
numbered_bullets=numbered_bullets
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
# Process subsections if any
|
|
511
|
-
subsections = section.get('subsections', [])
|
|
512
|
-
for subsection in subsections:
|
|
513
|
-
if 'title' in subsection:
|
|
514
|
-
sub_title = subsection['title']
|
|
515
|
-
sub_body = subsection.get('body', '')
|
|
516
|
-
sub_bullets = subsection.get('bullets', [])
|
|
517
|
-
|
|
518
|
-
# Only add subsection if it has content
|
|
519
|
-
if sub_body or sub_bullets:
|
|
520
|
-
self.prompt_add_subsection(
|
|
521
|
-
title,
|
|
522
|
-
sub_title,
|
|
523
|
-
body=sub_body,
|
|
524
|
-
bullets=sub_bullets if sub_bullets else None
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
# ----------------------------------------------------------------------
|
|
528
|
-
# Prompt Building Methods
|
|
529
|
-
# ----------------------------------------------------------------------
|
|
316
|
+
return self.name
|
|
530
317
|
|
|
531
|
-
def
|
|
318
|
+
def get_full_url(self, include_auth: bool = False) -> str:
|
|
532
319
|
"""
|
|
533
|
-
|
|
534
|
-
|
|
320
|
+
Get the full URL for this agent's endpoint
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
include_auth: Whether to include authentication credentials in the URL
|
|
324
|
+
|
|
535
325
|
Returns:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
Note:
|
|
539
|
-
Contexts can coexist with traditional prompts. The restriction is only
|
|
540
|
-
that you can't mix POM sections with raw text in the main prompt.
|
|
326
|
+
Full URL including host, port, and route (with auth if requested)
|
|
541
327
|
"""
|
|
542
|
-
#
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
328
|
+
# If _proxy_url_base is set (e.g., from request URL detection), use it
|
|
329
|
+
if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
|
|
330
|
+
base_url = self._proxy_url_base.rstrip('/')
|
|
331
|
+
# Add authentication if requested
|
|
332
|
+
if include_auth:
|
|
333
|
+
username, password = self.get_basic_auth_credentials()
|
|
334
|
+
if username and password:
|
|
335
|
+
from urllib.parse import urlparse, urlunparse
|
|
336
|
+
parsed = urlparse(base_url)
|
|
337
|
+
base_url = urlunparse((
|
|
338
|
+
parsed.scheme,
|
|
339
|
+
f"{username}:{password}@{parsed.netloc}",
|
|
340
|
+
parsed.path,
|
|
341
|
+
parsed.params,
|
|
342
|
+
parsed.query,
|
|
343
|
+
parsed.fragment
|
|
344
|
+
))
|
|
345
|
+
return base_url
|
|
346
|
+
|
|
347
|
+
mode = get_execution_mode()
|
|
348
|
+
|
|
349
|
+
if mode == 'cgi':
|
|
350
|
+
protocol = 'https' if os.getenv('HTTPS') == 'on' else 'http'
|
|
351
|
+
host = os.getenv('HTTP_HOST') or os.getenv('SERVER_NAME') or 'localhost'
|
|
352
|
+
script_name = os.getenv('SCRIPT_NAME', '')
|
|
353
|
+
base_url = f"{protocol}://{host}{script_name}"
|
|
354
|
+
elif mode == 'lambda':
|
|
355
|
+
# AWS Lambda Function URL format
|
|
356
|
+
lambda_url = os.getenv('AWS_LAMBDA_FUNCTION_URL')
|
|
357
|
+
if lambda_url:
|
|
358
|
+
base_url = lambda_url.rstrip('/')
|
|
359
|
+
else:
|
|
360
|
+
# Fallback construction for Lambda
|
|
361
|
+
region = os.getenv('AWS_REGION', 'us-east-1')
|
|
362
|
+
function_name = os.getenv('AWS_LAMBDA_FUNCTION_NAME', 'unknown')
|
|
363
|
+
base_url = f"https://{function_name}.lambda-url.{region}.on.aws"
|
|
364
|
+
elif mode == 'google_cloud_function':
|
|
365
|
+
# Google Cloud Functions URL format
|
|
366
|
+
project_id = os.getenv('GOOGLE_CLOUD_PROJECT') or os.getenv('GCP_PROJECT')
|
|
367
|
+
region = os.getenv('FUNCTION_REGION') or os.getenv('GOOGLE_CLOUD_REGION', 'us-central1')
|
|
368
|
+
service_name = os.getenv('K_SERVICE') or os.getenv('FUNCTION_TARGET', 'unknown')
|
|
369
|
+
|
|
370
|
+
if project_id:
|
|
371
|
+
base_url = f"https://{region}-{project_id}.cloudfunctions.net/{service_name}"
|
|
372
|
+
else:
|
|
373
|
+
# Fallback for local testing or incomplete environment
|
|
374
|
+
base_url = f"https://localhost:8080"
|
|
375
|
+
elif mode == 'azure_function':
|
|
376
|
+
# Azure Functions URL format
|
|
377
|
+
function_app_name = os.getenv('WEBSITE_SITE_NAME') or os.getenv('AZURE_FUNCTIONS_APP_NAME')
|
|
378
|
+
function_name = os.getenv('AZURE_FUNCTION_NAME', 'unknown')
|
|
379
|
+
|
|
380
|
+
if function_app_name:
|
|
381
|
+
base_url = f"https://{function_app_name}.azurewebsites.net/api/{function_name}"
|
|
382
|
+
else:
|
|
383
|
+
# Fallback for local testing
|
|
384
|
+
base_url = f"https://localhost:7071/api/{function_name}"
|
|
385
|
+
else:
|
|
386
|
+
# Server mode - use the SWMLService's unified URL building
|
|
387
|
+
# Build the full URL using the parent's method
|
|
388
|
+
base_url = self._build_full_url(endpoint="", include_auth=include_auth)
|
|
389
|
+
return base_url
|
|
390
|
+
|
|
391
|
+
# For serverless modes, add authentication if requested
|
|
392
|
+
if include_auth:
|
|
393
|
+
username, password = self.get_basic_auth_credentials()
|
|
394
|
+
if username and password:
|
|
395
|
+
# Parse URL to insert auth
|
|
396
|
+
from urllib.parse import urlparse, urlunparse
|
|
397
|
+
parsed = urlparse(base_url)
|
|
398
|
+
# Reconstruct with auth
|
|
399
|
+
base_url = urlunparse((
|
|
400
|
+
parsed.scheme,
|
|
401
|
+
f"{username}:{password}@{parsed.netloc}",
|
|
402
|
+
parsed.path,
|
|
403
|
+
parsed.params,
|
|
404
|
+
parsed.query,
|
|
405
|
+
parsed.fragment
|
|
406
|
+
))
|
|
407
|
+
|
|
408
|
+
# Add route for serverless modes
|
|
409
|
+
if self.route and self.route != "/" and not base_url.endswith(self.route):
|
|
410
|
+
base_url = f"{base_url}/{self.route.lstrip('/')}"
|
|
548
411
|
|
|
549
|
-
return
|
|
412
|
+
return base_url
|
|
550
413
|
|
|
551
|
-
def
|
|
414
|
+
def on_summary(self, summary: Optional[Dict[str, Any]], raw_data: Optional[Dict[str, Any]] = None) -> None:
|
|
552
415
|
"""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
416
|
+
Called when a post-prompt summary is received
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
summary: The summary object or None if no summary was found
|
|
420
|
+
raw_data: The complete raw POST data from the request
|
|
421
|
+
"""
|
|
422
|
+
# Default implementation does nothing
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
# ==================== Call Flow Verb Insertion Methods ====================
|
|
426
|
+
|
|
427
|
+
def add_pre_answer_verb(self, verb_name: str, config: Dict[str, Any]) -> 'AgentBase':
|
|
428
|
+
"""
|
|
429
|
+
Add a verb to run before the call is answered.
|
|
430
|
+
|
|
431
|
+
Pre-answer verbs execute while the call is still ringing. Only certain
|
|
432
|
+
verbs are safe to use before answering:
|
|
433
|
+
|
|
434
|
+
Safe verbs: transfer, execute, return, label, goto, request, switch,
|
|
435
|
+
cond, if, eval, set, unset, hangup, send_sms, sleep,
|
|
436
|
+
stop_record_call, stop_denoise, stop_tap
|
|
437
|
+
|
|
438
|
+
Verbs with auto_answer option (play, connect): Must include
|
|
439
|
+
"auto_answer": False in config to prevent automatic answering.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
verb_name: The SWML verb name (e.g., "play", "sleep", "request")
|
|
443
|
+
config: Verb configuration dictionary
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Self for method chaining
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
ValueError: If verb is not safe for pre-answer use
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
# Play ringback tone before answering
|
|
453
|
+
agent.add_pre_answer_verb("play", {
|
|
454
|
+
"urls": ["ring:us"],
|
|
455
|
+
"auto_answer": False
|
|
456
|
+
})
|
|
556
457
|
"""
|
|
557
|
-
#
|
|
558
|
-
if
|
|
458
|
+
# Validate verb is safe for pre-answer use
|
|
459
|
+
if verb_name in self._AUTO_ANSWER_VERBS:
|
|
460
|
+
if not config.get("auto_answer") is False:
|
|
461
|
+
self.log.warning(
|
|
462
|
+
"pre_answer_verb_will_answer",
|
|
463
|
+
verb=verb_name,
|
|
464
|
+
hint=f"Add 'auto_answer': False to prevent {verb_name} from answering the call"
|
|
465
|
+
)
|
|
466
|
+
elif verb_name not in self._PRE_ANSWER_SAFE_VERBS:
|
|
559
467
|
raise ValueError(
|
|
560
|
-
"
|
|
561
|
-
"
|
|
468
|
+
f"Verb '{verb_name}' is not safe for pre-answer use. "
|
|
469
|
+
f"Safe verbs: {', '.join(sorted(self._PRE_ANSWER_SAFE_VERBS))}"
|
|
562
470
|
)
|
|
563
|
-
|
|
564
|
-
|
|
471
|
+
|
|
472
|
+
self._pre_answer_verbs.append((verb_name, config))
|
|
473
|
+
return self
|
|
474
|
+
|
|
475
|
+
def add_answer_verb(self, config: Optional[Dict[str, Any]] = None) -> 'AgentBase':
|
|
565
476
|
"""
|
|
566
|
-
|
|
567
|
-
|
|
477
|
+
Configure the answer verb.
|
|
478
|
+
|
|
479
|
+
The answer verb connects the call. Use this method to customize
|
|
480
|
+
answer behavior, such as setting max_duration.
|
|
481
|
+
|
|
568
482
|
Args:
|
|
569
|
-
|
|
570
|
-
|
|
483
|
+
config: Optional answer verb configuration (e.g., {"max_duration": 3600})
|
|
484
|
+
|
|
571
485
|
Returns:
|
|
572
486
|
Self for method chaining
|
|
487
|
+
|
|
488
|
+
Example:
|
|
489
|
+
# Set maximum call duration to 1 hour
|
|
490
|
+
agent.add_answer_verb({"max_duration": 3600})
|
|
573
491
|
"""
|
|
574
|
-
self.
|
|
575
|
-
self._raw_prompt = text
|
|
492
|
+
self._answer_config = config or {}
|
|
576
493
|
return self
|
|
577
|
-
|
|
578
|
-
def
|
|
494
|
+
|
|
495
|
+
def add_post_answer_verb(self, verb_name: str, config: Dict[str, Any]) -> 'AgentBase':
|
|
579
496
|
"""
|
|
580
|
-
|
|
581
|
-
|
|
497
|
+
Add a verb to run after the call is answered but before the AI starts.
|
|
498
|
+
|
|
499
|
+
Post-answer verbs run after the call is connected. Common uses include
|
|
500
|
+
welcome messages, legal disclaimers, and hold music.
|
|
501
|
+
|
|
582
502
|
Args:
|
|
583
|
-
|
|
584
|
-
|
|
503
|
+
verb_name: The SWML verb name (e.g., "play", "sleep")
|
|
504
|
+
config: Verb configuration dictionary
|
|
505
|
+
|
|
585
506
|
Returns:
|
|
586
507
|
Self for method chaining
|
|
508
|
+
|
|
509
|
+
Example:
|
|
510
|
+
# Play welcome message
|
|
511
|
+
agent.add_post_answer_verb("play", {
|
|
512
|
+
"url": "say:Welcome to Acme Corporation."
|
|
513
|
+
})
|
|
514
|
+
# Brief pause
|
|
515
|
+
agent.add_post_answer_verb("sleep", {"time": 500})
|
|
587
516
|
"""
|
|
588
|
-
self.
|
|
517
|
+
self._post_answer_verbs.append((verb_name, config))
|
|
589
518
|
return self
|
|
590
|
-
|
|
591
|
-
def
|
|
519
|
+
|
|
520
|
+
def add_post_ai_verb(self, verb_name: str, config: Dict[str, Any]) -> 'AgentBase':
|
|
592
521
|
"""
|
|
593
|
-
|
|
594
|
-
|
|
522
|
+
Add a verb to run after the AI conversation ends.
|
|
523
|
+
|
|
524
|
+
Post-AI verbs run when the AI completes its conversation. Common uses
|
|
525
|
+
include clean disconnects, transfers, and logging.
|
|
526
|
+
|
|
595
527
|
Args:
|
|
596
|
-
|
|
597
|
-
|
|
528
|
+
verb_name: The SWML verb name (e.g., "hangup", "transfer", "request")
|
|
529
|
+
config: Verb configuration dictionary
|
|
530
|
+
|
|
598
531
|
Returns:
|
|
599
532
|
Self for method chaining
|
|
533
|
+
|
|
534
|
+
Example:
|
|
535
|
+
# Log call completion and hang up
|
|
536
|
+
agent.add_post_ai_verb("request", {
|
|
537
|
+
"url": "https://api.example.com/call-complete",
|
|
538
|
+
"method": "POST"
|
|
539
|
+
})
|
|
540
|
+
agent.add_post_ai_verb("hangup", {})
|
|
600
541
|
"""
|
|
601
|
-
|
|
602
|
-
self.pom = pom
|
|
603
|
-
else:
|
|
604
|
-
raise ValueError("use_pom must be True to use set_prompt_pom")
|
|
542
|
+
self._post_ai_verbs.append((verb_name, config))
|
|
605
543
|
return self
|
|
606
|
-
|
|
607
|
-
def
|
|
608
|
-
self,
|
|
609
|
-
title: str,
|
|
610
|
-
body: str = "",
|
|
611
|
-
bullets: Optional[List[str]] = None,
|
|
612
|
-
numbered: bool = False,
|
|
613
|
-
numbered_bullets: bool = False,
|
|
614
|
-
subsections: Optional[List[Dict[str, Any]]] = None
|
|
615
|
-
) -> 'AgentBase':
|
|
544
|
+
|
|
545
|
+
def clear_pre_answer_verbs(self) -> 'AgentBase':
|
|
616
546
|
"""
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
Args:
|
|
620
|
-
title: Section title
|
|
621
|
-
body: Optional section body text
|
|
622
|
-
bullets: Optional list of bullet points
|
|
623
|
-
numbered: Whether this section should be numbered
|
|
624
|
-
numbered_bullets: Whether bullets should be numbered
|
|
625
|
-
subsections: Optional list of subsection objects
|
|
626
|
-
|
|
547
|
+
Remove all pre-answer verbs.
|
|
548
|
+
|
|
627
549
|
Returns:
|
|
628
550
|
Self for method chaining
|
|
629
551
|
"""
|
|
630
|
-
self.
|
|
631
|
-
if self._use_pom and self.pom:
|
|
632
|
-
# Create parameters for add_section based on what's supported
|
|
633
|
-
kwargs = {}
|
|
634
|
-
|
|
635
|
-
# Start with basic parameters
|
|
636
|
-
kwargs['title'] = title
|
|
637
|
-
kwargs['body'] = body
|
|
638
|
-
if bullets:
|
|
639
|
-
kwargs['bullets'] = bullets
|
|
640
|
-
|
|
641
|
-
# Add optional parameters if they look supported
|
|
642
|
-
if hasattr(self.pom, 'add_section'):
|
|
643
|
-
sig = inspect.signature(self.pom.add_section)
|
|
644
|
-
if 'numbered' in sig.parameters:
|
|
645
|
-
kwargs['numbered'] = numbered
|
|
646
|
-
if 'numberedBullets' in sig.parameters:
|
|
647
|
-
kwargs['numberedBullets'] = numbered_bullets
|
|
648
|
-
|
|
649
|
-
# Create the section
|
|
650
|
-
section = self.pom.add_section(**kwargs)
|
|
651
|
-
|
|
652
|
-
# Now add subsections if provided, by calling add_subsection on the section
|
|
653
|
-
if subsections:
|
|
654
|
-
for subsection in subsections:
|
|
655
|
-
if 'title' in subsection:
|
|
656
|
-
section.add_subsection(
|
|
657
|
-
title=subsection.get('title'),
|
|
658
|
-
body=subsection.get('body', ''),
|
|
659
|
-
bullets=subsection.get('bullets', [])
|
|
660
|
-
)
|
|
661
|
-
|
|
552
|
+
self._pre_answer_verbs = []
|
|
662
553
|
return self
|
|
663
|
-
|
|
664
|
-
def
|
|
665
|
-
self,
|
|
666
|
-
title: str,
|
|
667
|
-
body: Optional[str] = None,
|
|
668
|
-
bullet: Optional[str] = None,
|
|
669
|
-
bullets: Optional[List[str]] = None
|
|
670
|
-
) -> 'AgentBase':
|
|
554
|
+
|
|
555
|
+
def clear_post_answer_verbs(self) -> 'AgentBase':
|
|
671
556
|
"""
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
Args:
|
|
675
|
-
title: Section title
|
|
676
|
-
body: Optional text to append to section body
|
|
677
|
-
bullet: Optional single bullet point to add
|
|
678
|
-
bullets: Optional list of bullet points to add
|
|
679
|
-
|
|
557
|
+
Remove all post-answer verbs.
|
|
558
|
+
|
|
680
559
|
Returns:
|
|
681
560
|
Self for method chaining
|
|
682
561
|
"""
|
|
683
|
-
|
|
684
|
-
self.pom.add_to_section(
|
|
685
|
-
title=title,
|
|
686
|
-
body=body,
|
|
687
|
-
bullet=bullet,
|
|
688
|
-
bullets=bullets
|
|
689
|
-
)
|
|
562
|
+
self._post_answer_verbs = []
|
|
690
563
|
return self
|
|
691
|
-
|
|
692
|
-
def
|
|
693
|
-
self,
|
|
694
|
-
parent_title: str,
|
|
695
|
-
title: str,
|
|
696
|
-
body: str = "",
|
|
697
|
-
bullets: Optional[List[str]] = None
|
|
698
|
-
) -> 'AgentBase':
|
|
564
|
+
|
|
565
|
+
def clear_post_ai_verbs(self) -> 'AgentBase':
|
|
699
566
|
"""
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
Args:
|
|
703
|
-
parent_title: Parent section title
|
|
704
|
-
title: Subsection title
|
|
705
|
-
body: Optional subsection body text
|
|
706
|
-
bullets: Optional list of bullet points
|
|
707
|
-
|
|
567
|
+
Remove all post-AI verbs.
|
|
568
|
+
|
|
708
569
|
Returns:
|
|
709
570
|
Self for method chaining
|
|
710
571
|
"""
|
|
711
|
-
|
|
712
|
-
# First find or create the parent section
|
|
713
|
-
parent_section = None
|
|
714
|
-
|
|
715
|
-
# Try to find the parent section by title
|
|
716
|
-
if hasattr(self.pom, 'sections'):
|
|
717
|
-
for section in self.pom.sections:
|
|
718
|
-
if hasattr(section, 'title') and section.title == parent_title:
|
|
719
|
-
parent_section = section
|
|
720
|
-
break
|
|
721
|
-
|
|
722
|
-
# If parent section not found, create it
|
|
723
|
-
if not parent_section:
|
|
724
|
-
parent_section = self.pom.add_section(title=parent_title)
|
|
725
|
-
|
|
726
|
-
# Now call add_subsection on the parent section object, not on POM
|
|
727
|
-
parent_section.add_subsection(
|
|
728
|
-
title=title,
|
|
729
|
-
body=body,
|
|
730
|
-
bullets=bullets or []
|
|
731
|
-
)
|
|
732
|
-
|
|
572
|
+
self._post_ai_verbs = []
|
|
733
573
|
return self
|
|
734
|
-
|
|
735
|
-
#
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
def define_tool(
|
|
740
|
-
self,
|
|
741
|
-
name: str,
|
|
742
|
-
description: str,
|
|
743
|
-
parameters: Dict[str, Any],
|
|
744
|
-
handler: Callable,
|
|
745
|
-
secure: bool = True,
|
|
746
|
-
fillers: Optional[Dict[str, List[str]]] = None,
|
|
747
|
-
webhook_url: Optional[str] = None,
|
|
748
|
-
**swaig_fields
|
|
749
|
-
) -> 'AgentBase':
|
|
574
|
+
|
|
575
|
+
# ==================== End Call Flow Verb Insertion Methods ====================
|
|
576
|
+
|
|
577
|
+
def enable_sip_routing(self, auto_map: bool = True, path: str = "/sip") -> 'AgentBase':
|
|
750
578
|
"""
|
|
751
|
-
|
|
579
|
+
Enable SIP-based routing for this agent
|
|
580
|
+
|
|
581
|
+
This allows the agent to automatically route SIP requests based on SIP usernames.
|
|
582
|
+
When enabled, an endpoint at the specified path is automatically created
|
|
583
|
+
that will handle SIP requests and deliver them to this agent.
|
|
752
584
|
|
|
753
585
|
Args:
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
secure: Whether to require token validation
|
|
759
|
-
fillers: Optional dict mapping language codes to arrays of filler phrases
|
|
760
|
-
webhook_url: Optional external webhook URL to use instead of local handling
|
|
761
|
-
**swaig_fields: Additional SWAIG fields to include in function definition
|
|
762
|
-
|
|
586
|
+
auto_map: Whether to automatically map common SIP usernames to this agent
|
|
587
|
+
(based on the agent name and route path)
|
|
588
|
+
path: The path to register the SIP routing endpoint (default: "/sip")
|
|
589
|
+
|
|
763
590
|
Returns:
|
|
764
591
|
Self for method chaining
|
|
765
592
|
"""
|
|
766
|
-
|
|
767
|
-
|
|
593
|
+
# Create a routing callback that handles SIP usernames
|
|
594
|
+
def sip_routing_callback(request: Request, body: Dict[str, Any]) -> Optional[str]:
|
|
595
|
+
# Extract SIP username from the request body
|
|
596
|
+
sip_username = self.extract_sip_username(body)
|
|
597
|
+
|
|
598
|
+
if sip_username:
|
|
599
|
+
self.log.info("sip_username_extracted", username=sip_username)
|
|
600
|
+
|
|
601
|
+
# Check if this username is registered with this agent
|
|
602
|
+
if hasattr(self, '_sip_usernames') and sip_username.lower() in self._sip_usernames:
|
|
603
|
+
self.log.info("sip_username_matched", username=sip_username)
|
|
604
|
+
# This route is already being handled by the agent, no need to redirect
|
|
605
|
+
return None
|
|
606
|
+
else:
|
|
607
|
+
self.log.info("sip_username_not_matched", username=sip_username)
|
|
608
|
+
# Not registered with this agent, let routing continue
|
|
609
|
+
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
# Register the callback with the SWMLService, specifying the path
|
|
613
|
+
self.register_routing_callback(sip_routing_callback, path=path)
|
|
614
|
+
|
|
615
|
+
# Auto-map common usernames if requested
|
|
616
|
+
if auto_map:
|
|
617
|
+
self.auto_map_sip_usernames()
|
|
768
618
|
|
|
769
|
-
self._swaig_functions[name] = SWAIGFunction(
|
|
770
|
-
name=name,
|
|
771
|
-
description=description,
|
|
772
|
-
parameters=parameters,
|
|
773
|
-
handler=handler,
|
|
774
|
-
secure=secure,
|
|
775
|
-
fillers=fillers,
|
|
776
|
-
webhook_url=webhook_url,
|
|
777
|
-
**swaig_fields
|
|
778
|
-
)
|
|
779
619
|
return self
|
|
780
620
|
|
|
781
|
-
def
|
|
621
|
+
def register_sip_username(self, sip_username: str) -> 'AgentBase':
|
|
782
622
|
"""
|
|
783
|
-
Register a
|
|
623
|
+
Register a SIP username that should be routed to this agent
|
|
784
624
|
|
|
785
625
|
Args:
|
|
786
|
-
|
|
626
|
+
sip_username: SIP username to register
|
|
787
627
|
|
|
788
628
|
Returns:
|
|
789
629
|
Self for method chaining
|
|
790
630
|
"""
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
raise ValueError("Function dictionary must contain 'function' field with the function name")
|
|
631
|
+
if not hasattr(self, '_sip_usernames'):
|
|
632
|
+
self._sip_usernames = set()
|
|
794
633
|
|
|
795
|
-
|
|
796
|
-
|
|
634
|
+
self._sip_usernames.add(sip_username.lower())
|
|
635
|
+
self.log.info("sip_username_registered", username=sip_username)
|
|
797
636
|
|
|
798
|
-
# Store the raw function dictionary for data_map tools
|
|
799
|
-
# These don't have handlers since they execute on SignalWire's server
|
|
800
|
-
self._swaig_functions[function_name] = function_dict
|
|
801
637
|
return self
|
|
802
638
|
|
|
803
|
-
def
|
|
804
|
-
"""
|
|
805
|
-
Decorator for defining SWAIG tools in a class
|
|
806
|
-
|
|
807
|
-
Used as:
|
|
808
|
-
|
|
809
|
-
@agent.tool(name="example_function", parameters={...})
|
|
810
|
-
def example_function(self, param1):
|
|
811
|
-
# ...
|
|
812
|
-
"""
|
|
813
|
-
def decorator(func):
|
|
814
|
-
nonlocal name
|
|
815
|
-
if name is None:
|
|
816
|
-
name = func.__name__
|
|
817
|
-
|
|
818
|
-
parameters = kwargs.pop("parameters", {})
|
|
819
|
-
description = kwargs.pop("description", func.__doc__ or f"Function {name}")
|
|
820
|
-
secure = kwargs.pop("secure", True)
|
|
821
|
-
fillers = kwargs.pop("fillers", None)
|
|
822
|
-
webhook_url = kwargs.pop("webhook_url", None)
|
|
823
|
-
|
|
824
|
-
self.define_tool(
|
|
825
|
-
name=name,
|
|
826
|
-
description=description,
|
|
827
|
-
parameters=parameters,
|
|
828
|
-
handler=func,
|
|
829
|
-
secure=secure,
|
|
830
|
-
fillers=fillers,
|
|
831
|
-
webhook_url=webhook_url,
|
|
832
|
-
**kwargs # Pass through any additional swaig_fields
|
|
833
|
-
)
|
|
834
|
-
return func
|
|
835
|
-
return decorator
|
|
836
|
-
|
|
837
|
-
def _register_class_decorated_tools(self):
|
|
639
|
+
def auto_map_sip_usernames(self) -> 'AgentBase':
|
|
838
640
|
"""
|
|
839
|
-
|
|
641
|
+
Automatically register common SIP usernames based on this agent's
|
|
642
|
+
name and route
|
|
840
643
|
|
|
841
|
-
|
|
842
|
-
|
|
644
|
+
Returns:
|
|
645
|
+
Self for method chaining
|
|
843
646
|
"""
|
|
844
|
-
#
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
for name in dir(cls):
|
|
849
|
-
# Get the attribute
|
|
850
|
-
attr = getattr(cls, name)
|
|
647
|
+
# Register username based on agent name
|
|
648
|
+
clean_name = re.sub(r'[^a-z0-9_]', '', self.name.lower())
|
|
649
|
+
if clean_name:
|
|
650
|
+
self.register_sip_username(clean_name)
|
|
851
651
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
tool_name = getattr(attr, "_tool_name", name)
|
|
857
|
-
tool_params = getattr(attr, "_tool_params", {})
|
|
858
|
-
|
|
859
|
-
# Extract known parameters and pass through the rest as swaig_fields
|
|
860
|
-
tool_params_copy = tool_params.copy()
|
|
861
|
-
description = tool_params_copy.pop("description", attr.__doc__ or f"Function {tool_name}")
|
|
862
|
-
parameters = tool_params_copy.pop("parameters", {})
|
|
863
|
-
secure = tool_params_copy.pop("secure", True)
|
|
864
|
-
fillers = tool_params_copy.pop("fillers", None)
|
|
865
|
-
webhook_url = tool_params_copy.pop("webhook_url", None)
|
|
866
|
-
|
|
867
|
-
# Register the tool with any remaining params as swaig_fields
|
|
868
|
-
self.define_tool(
|
|
869
|
-
name=tool_name,
|
|
870
|
-
description=description,
|
|
871
|
-
parameters=parameters,
|
|
872
|
-
handler=attr.__get__(self, cls), # Bind the method to this instance
|
|
873
|
-
secure=secure,
|
|
874
|
-
fillers=fillers,
|
|
875
|
-
webhook_url=webhook_url,
|
|
876
|
-
**tool_params_copy # Pass through any additional swaig_fields
|
|
877
|
-
)
|
|
878
|
-
|
|
879
|
-
@classmethod
|
|
880
|
-
def tool(cls, name=None, **kwargs):
|
|
881
|
-
"""
|
|
882
|
-
Class method decorator for defining SWAIG tools
|
|
883
|
-
|
|
884
|
-
Used as:
|
|
885
|
-
|
|
886
|
-
@AgentBase.tool(name="example_function", parameters={...})
|
|
887
|
-
def example_function(self, param1):
|
|
888
|
-
# ...
|
|
889
|
-
"""
|
|
890
|
-
def decorator(func):
|
|
891
|
-
setattr(func, "_is_tool", True)
|
|
892
|
-
setattr(func, "_tool_name", name or func.__name__)
|
|
893
|
-
setattr(func, "_tool_params", kwargs)
|
|
894
|
-
return func
|
|
895
|
-
return decorator
|
|
896
|
-
|
|
897
|
-
# ----------------------------------------------------------------------
|
|
898
|
-
# Override Points for Subclasses
|
|
899
|
-
# ----------------------------------------------------------------------
|
|
900
|
-
|
|
901
|
-
def get_name(self) -> str:
|
|
902
|
-
"""
|
|
903
|
-
Get agent name
|
|
904
|
-
|
|
905
|
-
Returns:
|
|
906
|
-
Agent name
|
|
907
|
-
"""
|
|
908
|
-
return self.name
|
|
909
|
-
|
|
910
|
-
def get_app(self):
|
|
911
|
-
"""
|
|
912
|
-
Get the FastAPI application instance for deployment adapters like Lambda/Mangum
|
|
913
|
-
|
|
914
|
-
This method ensures the FastAPI app is properly initialized and configured,
|
|
915
|
-
then returns it for use with deployment adapters like Mangum for AWS Lambda.
|
|
916
|
-
|
|
917
|
-
Returns:
|
|
918
|
-
FastAPI: The configured FastAPI application instance
|
|
919
|
-
"""
|
|
920
|
-
if self._app is None:
|
|
921
|
-
# Initialize the app if it hasn't been created yet
|
|
922
|
-
# This follows the same initialization logic as serve() but without running uvicorn
|
|
923
|
-
from fastapi import FastAPI
|
|
924
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
925
|
-
|
|
926
|
-
# Create a FastAPI app with explicit redirect_slashes=False
|
|
927
|
-
app = FastAPI(redirect_slashes=False)
|
|
928
|
-
|
|
929
|
-
# Add health and ready endpoints directly to the main app to avoid conflicts with catch-all
|
|
930
|
-
@app.get("/health")
|
|
931
|
-
@app.post("/health")
|
|
932
|
-
async def health_check():
|
|
933
|
-
"""Health check endpoint for Kubernetes liveness probe"""
|
|
934
|
-
return {
|
|
935
|
-
"status": "healthy",
|
|
936
|
-
"agent": self.get_name(),
|
|
937
|
-
"route": self.route,
|
|
938
|
-
"functions": len(self._swaig_functions)
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
@app.get("/ready")
|
|
942
|
-
@app.post("/ready")
|
|
943
|
-
async def readiness_check():
|
|
944
|
-
"""Readiness check endpoint for Kubernetes readiness probe"""
|
|
945
|
-
return {
|
|
946
|
-
"status": "ready",
|
|
947
|
-
"agent": self.get_name(),
|
|
948
|
-
"route": self.route,
|
|
949
|
-
"functions": len(self._swaig_functions)
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
# Add CORS middleware if needed
|
|
953
|
-
app.add_middleware(
|
|
954
|
-
CORSMiddleware,
|
|
955
|
-
allow_origins=["*"],
|
|
956
|
-
allow_credentials=True,
|
|
957
|
-
allow_methods=["*"],
|
|
958
|
-
allow_headers=["*"],
|
|
959
|
-
)
|
|
960
|
-
|
|
961
|
-
# Create router and register routes
|
|
962
|
-
router = self.as_router()
|
|
963
|
-
|
|
964
|
-
# Log registered routes for debugging
|
|
965
|
-
self.log.debug("router_routes_registered")
|
|
966
|
-
for route in router.routes:
|
|
967
|
-
if hasattr(route, "path"):
|
|
968
|
-
self.log.debug("router_route", path=route.path)
|
|
969
|
-
|
|
970
|
-
# Include the router
|
|
971
|
-
app.include_router(router, prefix=self.route)
|
|
972
|
-
|
|
973
|
-
# Register a catch-all route for debugging and troubleshooting
|
|
974
|
-
@app.get("/{full_path:path}")
|
|
975
|
-
@app.post("/{full_path:path}")
|
|
976
|
-
async def handle_all_routes(request: Request, full_path: str):
|
|
977
|
-
self.log.debug("request_received", path=full_path)
|
|
978
|
-
|
|
979
|
-
# Check if the path is meant for this agent
|
|
980
|
-
if not full_path.startswith(self.route.lstrip("/")):
|
|
981
|
-
return {"error": "Invalid route"}
|
|
982
|
-
|
|
983
|
-
# Extract the path relative to this agent's route
|
|
984
|
-
relative_path = full_path[len(self.route.lstrip("/")):]
|
|
985
|
-
relative_path = relative_path.lstrip("/")
|
|
986
|
-
self.log.debug("relative_path_extracted", path=relative_path)
|
|
987
|
-
|
|
988
|
-
return {"error": "Path not found"}
|
|
989
|
-
|
|
990
|
-
# Log all app routes for debugging
|
|
991
|
-
self.log.debug("app_routes_registered")
|
|
992
|
-
for route in app.routes:
|
|
993
|
-
if hasattr(route, "path"):
|
|
994
|
-
self.log.debug("app_route", path=route.path)
|
|
652
|
+
# Register username based on route (without slashes)
|
|
653
|
+
clean_route = re.sub(r'[^a-z0-9_]', '', self.route.lower())
|
|
654
|
+
if clean_route and clean_route != clean_name:
|
|
655
|
+
self.register_sip_username(clean_route)
|
|
995
656
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
Get the prompt for the agent
|
|
1003
|
-
|
|
1004
|
-
Returns:
|
|
1005
|
-
Either a string prompt or a POM object as list of dicts
|
|
1006
|
-
"""
|
|
1007
|
-
# If using POM, return the POM structure
|
|
1008
|
-
if self._use_pom and self.pom:
|
|
1009
|
-
try:
|
|
1010
|
-
# Try different methods that might be available on the POM implementation
|
|
1011
|
-
if hasattr(self.pom, 'render_dict'):
|
|
1012
|
-
return self.pom.render_dict()
|
|
1013
|
-
elif hasattr(self.pom, 'to_dict'):
|
|
1014
|
-
return self.pom.to_dict()
|
|
1015
|
-
elif hasattr(self.pom, 'to_list'):
|
|
1016
|
-
return self.pom.to_list()
|
|
1017
|
-
elif hasattr(self.pom, 'render'):
|
|
1018
|
-
render_result = self.pom.render()
|
|
1019
|
-
# If render returns a string, we need to convert it to JSON
|
|
1020
|
-
if isinstance(render_result, str):
|
|
1021
|
-
try:
|
|
1022
|
-
import json
|
|
1023
|
-
return json.loads(render_result)
|
|
1024
|
-
except:
|
|
1025
|
-
# If we can't parse as JSON, fall back to raw text
|
|
1026
|
-
pass
|
|
1027
|
-
return render_result
|
|
1028
|
-
else:
|
|
1029
|
-
# Last resort: attempt to convert the POM object directly to a list/dict
|
|
1030
|
-
# This assumes the POM object has a reasonable __str__ or __repr__ method
|
|
1031
|
-
pom_data = self.pom.__dict__
|
|
1032
|
-
if '_sections' in pom_data and isinstance(pom_data['_sections'], list):
|
|
1033
|
-
return pom_data['_sections']
|
|
1034
|
-
# Fall through to default if nothing worked
|
|
1035
|
-
except Exception as e:
|
|
1036
|
-
self.log.error("pom_rendering_failed", error=str(e))
|
|
1037
|
-
# Fall back to raw text if POM fails
|
|
657
|
+
# Register common variations if they make sense
|
|
658
|
+
if len(clean_name) > 3:
|
|
659
|
+
# Register without vowels
|
|
660
|
+
no_vowels = re.sub(r'[aeiou]', '', clean_name)
|
|
661
|
+
if no_vowels != clean_name and len(no_vowels) > 2:
|
|
662
|
+
self.register_sip_username(no_vowels)
|
|
1038
663
|
|
|
1039
|
-
|
|
1040
|
-
return self._raw_prompt or f"You are {self.name}, a helpful AI assistant."
|
|
1041
|
-
|
|
1042
|
-
def get_post_prompt(self) -> Optional[str]:
|
|
1043
|
-
"""
|
|
1044
|
-
Get the post-prompt for the agent
|
|
1045
|
-
|
|
1046
|
-
Returns:
|
|
1047
|
-
Post-prompt text or None if not set
|
|
1048
|
-
"""
|
|
1049
|
-
return self._post_prompt
|
|
1050
|
-
|
|
1051
|
-
def define_tools(self) -> List[SWAIGFunction]:
|
|
1052
|
-
"""
|
|
1053
|
-
Define the tools this agent can use
|
|
1054
|
-
|
|
1055
|
-
Returns:
|
|
1056
|
-
List of SWAIGFunction objects or raw dictionaries (for data_map tools)
|
|
1057
|
-
|
|
1058
|
-
This method can be overridden by subclasses.
|
|
1059
|
-
"""
|
|
1060
|
-
tools = []
|
|
1061
|
-
for func in self._swaig_functions.values():
|
|
1062
|
-
if isinstance(func, dict):
|
|
1063
|
-
# Raw dictionary from register_swaig_function (e.g., DataMap)
|
|
1064
|
-
tools.append(func)
|
|
1065
|
-
else:
|
|
1066
|
-
# SWAIGFunction object from define_tool
|
|
1067
|
-
tools.append(func)
|
|
1068
|
-
return tools
|
|
1069
|
-
|
|
1070
|
-
def on_summary(self, summary: Optional[Dict[str, Any]], raw_data: Optional[Dict[str, Any]] = None) -> None:
|
|
1071
|
-
"""
|
|
1072
|
-
Called when a post-prompt summary is received
|
|
1073
|
-
|
|
1074
|
-
Args:
|
|
1075
|
-
summary: The summary object or None if no summary was found
|
|
1076
|
-
raw_data: The complete raw POST data from the request
|
|
1077
|
-
"""
|
|
1078
|
-
# Default implementation does nothing
|
|
1079
|
-
pass
|
|
664
|
+
return self
|
|
1080
665
|
|
|
1081
|
-
def
|
|
666
|
+
def set_web_hook_url(self, url: str) -> 'AgentBase':
|
|
1082
667
|
"""
|
|
1083
|
-
|
|
668
|
+
Override the default web_hook_url with a supplied URL string
|
|
1084
669
|
|
|
1085
670
|
Args:
|
|
1086
|
-
|
|
1087
|
-
args: Function arguments
|
|
1088
|
-
raw_data: Raw request data
|
|
671
|
+
url: The URL to use for SWAIG function webhooks
|
|
1089
672
|
|
|
1090
673
|
Returns:
|
|
1091
|
-
|
|
674
|
+
Self for method chaining
|
|
1092
675
|
"""
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
# If the function is not found, return an error
|
|
1096
|
-
return {"response": f"Function '{name}' not found"}
|
|
1097
|
-
|
|
1098
|
-
# Get the function
|
|
1099
|
-
func = self._swaig_functions[name]
|
|
1100
|
-
|
|
1101
|
-
# Check if this is a data_map function (raw dictionary)
|
|
1102
|
-
if isinstance(func, dict):
|
|
1103
|
-
# Data_map functions execute on SignalWire's server, not here
|
|
1104
|
-
# This should never be called, but if it is, return an error
|
|
1105
|
-
return {"response": f"Data map function '{name}' should be executed by SignalWire server, not locally"}
|
|
1106
|
-
|
|
1107
|
-
# Check if this is an external webhook function
|
|
1108
|
-
if hasattr(func, 'webhook_url') and func.webhook_url:
|
|
1109
|
-
# External webhook functions should be called directly by SignalWire, not locally
|
|
1110
|
-
return {"response": f"External webhook function '{name}' should be executed by SignalWire at {func.webhook_url}, not locally"}
|
|
1111
|
-
|
|
1112
|
-
# Call the handler for regular SWAIG functions
|
|
1113
|
-
try:
|
|
1114
|
-
result = func.handler(args, raw_data)
|
|
1115
|
-
if result is None:
|
|
1116
|
-
# If the handler returns None, create a default response
|
|
1117
|
-
result = SwaigFunctionResult("Function executed successfully")
|
|
1118
|
-
return result
|
|
1119
|
-
except Exception as e:
|
|
1120
|
-
# If the handler raises an exception, return an error response
|
|
1121
|
-
return {"response": f"Error executing function '{name}': {str(e)}"}
|
|
676
|
+
self._web_hook_url_override = url
|
|
677
|
+
return self
|
|
1122
678
|
|
|
1123
|
-
def
|
|
679
|
+
def set_post_prompt_url(self, url: str) -> 'AgentBase':
|
|
1124
680
|
"""
|
|
1125
|
-
|
|
681
|
+
Override the default post_prompt_url with a supplied URL string
|
|
1126
682
|
|
|
1127
683
|
Args:
|
|
1128
|
-
|
|
1129
|
-
password: Password from request
|
|
684
|
+
url: The URL to use for post-prompt summary delivery
|
|
1130
685
|
|
|
1131
686
|
Returns:
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
This method can be overridden by subclasses.
|
|
687
|
+
Self for method chaining
|
|
1135
688
|
"""
|
|
1136
|
-
|
|
689
|
+
self._post_prompt_url_override = url
|
|
690
|
+
return self
|
|
1137
691
|
|
|
1138
|
-
def
|
|
692
|
+
def add_swaig_query_params(self, params: Dict[str, str]) -> 'AgentBase':
|
|
1139
693
|
"""
|
|
1140
|
-
|
|
694
|
+
Add query parameters that will be included in all SWAIG webhook URLs
|
|
1141
695
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
Returns:
|
|
1147
|
-
Secure token string
|
|
1148
|
-
"""
|
|
1149
|
-
try:
|
|
1150
|
-
# Ensure we have a session manager
|
|
1151
|
-
if not hasattr(self, '_session_manager'):
|
|
1152
|
-
self.log.error("no_session_manager")
|
|
1153
|
-
return ""
|
|
1154
|
-
|
|
1155
|
-
# Create the token using the session manager
|
|
1156
|
-
return self._session_manager.create_tool_token(tool_name, call_id)
|
|
1157
|
-
except Exception as e:
|
|
1158
|
-
self.log.error("token_creation_error", error=str(e), tool=tool_name, call_id=call_id)
|
|
1159
|
-
return ""
|
|
1160
|
-
|
|
1161
|
-
def validate_tool_token(self, function_name: str, token: str, call_id: str) -> bool:
|
|
1162
|
-
"""
|
|
1163
|
-
Validate a tool token
|
|
696
|
+
This is particularly useful for preserving dynamic configuration state
|
|
697
|
+
across SWAIG callbacks. For example, if your dynamic config adds skills
|
|
698
|
+
based on query parameters, you can pass those same parameters through
|
|
699
|
+
to the SWAIG webhook so the same configuration is applied.
|
|
1164
700
|
|
|
1165
701
|
Args:
|
|
1166
|
-
|
|
1167
|
-
token: Token to validate
|
|
1168
|
-
call_id: Call ID for the session
|
|
702
|
+
params: Dictionary of query parameters to add to SWAIG URLs
|
|
1169
703
|
|
|
1170
704
|
Returns:
|
|
1171
|
-
|
|
1172
|
-
"""
|
|
1173
|
-
try:
|
|
1174
|
-
# Skip validation for non-secure tools
|
|
1175
|
-
if function_name not in self._swaig_functions:
|
|
1176
|
-
self.log.warning("unknown_function", function=function_name)
|
|
1177
|
-
return False
|
|
1178
|
-
|
|
1179
|
-
# Get the function and check if it's secure
|
|
1180
|
-
func = self._swaig_functions[function_name]
|
|
1181
|
-
is_secure = True # Default to secure
|
|
1182
|
-
|
|
1183
|
-
if isinstance(func, dict):
|
|
1184
|
-
# For raw dictionaries (DataMap functions), they're always secure
|
|
1185
|
-
is_secure = True
|
|
1186
|
-
else:
|
|
1187
|
-
# For SWAIGFunction objects, check the secure attribute
|
|
1188
|
-
is_secure = func.secure
|
|
1189
|
-
|
|
1190
|
-
# Always allow non-secure functions
|
|
1191
|
-
if not is_secure:
|
|
1192
|
-
self.log.debug("non_secure_function_allowed", function=function_name)
|
|
1193
|
-
return True
|
|
1194
|
-
|
|
1195
|
-
# Check if we have a session manager
|
|
1196
|
-
if not hasattr(self, '_session_manager'):
|
|
1197
|
-
self.log.error("no_session_manager")
|
|
1198
|
-
return False
|
|
1199
|
-
|
|
1200
|
-
# Handle missing token
|
|
1201
|
-
if not token:
|
|
1202
|
-
self.log.warning("missing_token", function=function_name)
|
|
1203
|
-
return False
|
|
1204
|
-
|
|
1205
|
-
# For debugging: Log token details
|
|
1206
|
-
try:
|
|
1207
|
-
# Capture original parameters
|
|
1208
|
-
self.log.debug("token_validate_input",
|
|
1209
|
-
function=function_name,
|
|
1210
|
-
call_id=call_id,
|
|
1211
|
-
token_length=len(token))
|
|
1212
|
-
|
|
1213
|
-
# Try to decode token for debugging
|
|
1214
|
-
if hasattr(self._session_manager, 'debug_token'):
|
|
1215
|
-
debug_info = self._session_manager.debug_token(token)
|
|
1216
|
-
self.log.debug("token_debug", debug_info=debug_info)
|
|
1217
|
-
|
|
1218
|
-
# Extract token components
|
|
1219
|
-
if debug_info.get("valid_format") and "components" in debug_info:
|
|
1220
|
-
components = debug_info["components"]
|
|
1221
|
-
token_call_id = components.get("call_id")
|
|
1222
|
-
token_function = components.get("function")
|
|
1223
|
-
token_expiry = components.get("expiry")
|
|
1224
|
-
|
|
1225
|
-
# Log parameter mismatches
|
|
1226
|
-
if token_function != function_name:
|
|
1227
|
-
self.log.warning("token_function_mismatch",
|
|
1228
|
-
expected=function_name,
|
|
1229
|
-
actual=token_function)
|
|
1230
|
-
|
|
1231
|
-
if token_call_id != call_id:
|
|
1232
|
-
self.log.warning("token_call_id_mismatch",
|
|
1233
|
-
expected=call_id,
|
|
1234
|
-
actual=token_call_id)
|
|
1235
|
-
|
|
1236
|
-
# Check expiration
|
|
1237
|
-
if debug_info.get("status", {}).get("is_expired"):
|
|
1238
|
-
self.log.warning("token_expired",
|
|
1239
|
-
expires_in=debug_info["status"].get("expires_in_seconds"))
|
|
1240
|
-
except Exception as e:
|
|
1241
|
-
self.log.error("token_debug_error", error=str(e))
|
|
1242
|
-
|
|
1243
|
-
# Use call_id from token if the provided one is empty
|
|
1244
|
-
if not call_id and hasattr(self._session_manager, 'debug_token'):
|
|
1245
|
-
try:
|
|
1246
|
-
debug_info = self._session_manager.debug_token(token)
|
|
1247
|
-
if debug_info.get("valid_format") and "components" in debug_info:
|
|
1248
|
-
token_call_id = debug_info["components"].get("call_id")
|
|
1249
|
-
if token_call_id:
|
|
1250
|
-
self.log.debug("using_call_id_from_token", call_id=token_call_id)
|
|
1251
|
-
is_valid = self._session_manager.validate_tool_token(function_name, token, token_call_id)
|
|
1252
|
-
if is_valid:
|
|
1253
|
-
self.log.debug("token_valid_with_extracted_call_id")
|
|
1254
|
-
return True
|
|
1255
|
-
except Exception as e:
|
|
1256
|
-
self.log.error("error_using_call_id_from_token", error=str(e))
|
|
1257
|
-
|
|
1258
|
-
# Normal validation with provided call_id
|
|
1259
|
-
is_valid = self._session_manager.validate_tool_token(function_name, token, call_id)
|
|
1260
|
-
|
|
1261
|
-
if is_valid:
|
|
1262
|
-
self.log.debug("token_valid", function=function_name)
|
|
1263
|
-
else:
|
|
1264
|
-
self.log.warning("token_invalid", function=function_name)
|
|
1265
|
-
|
|
1266
|
-
return is_valid
|
|
1267
|
-
except Exception as e:
|
|
1268
|
-
self.log.error("token_validation_error", error=str(e), function=function_name)
|
|
1269
|
-
return False
|
|
1270
|
-
|
|
1271
|
-
# ----------------------------------------------------------------------
|
|
1272
|
-
# Web Server and Routing
|
|
1273
|
-
# ----------------------------------------------------------------------
|
|
1274
|
-
|
|
1275
|
-
def get_basic_auth_credentials(self, include_source: bool = False) -> Union[Tuple[str, str], Tuple[str, str, str]]:
|
|
1276
|
-
"""
|
|
1277
|
-
Get the basic auth credentials
|
|
1278
|
-
|
|
1279
|
-
Args:
|
|
1280
|
-
include_source: Whether to include the source of the credentials
|
|
705
|
+
Self for method chaining
|
|
1281
706
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
(
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
707
|
+
Example:
|
|
708
|
+
def dynamic_config(query_params, body_params, headers, agent):
|
|
709
|
+
if query_params.get('tier') == 'premium':
|
|
710
|
+
agent.add_skill('advanced_search')
|
|
711
|
+
# Preserve the tier param so SWAIG callbacks work
|
|
712
|
+
agent.add_swaig_query_params({'tier': 'premium'})
|
|
1288
713
|
"""
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
return (username, password)
|
|
1293
|
-
|
|
1294
|
-
# Determine source of credentials
|
|
1295
|
-
env_user = os.environ.get('SWML_BASIC_AUTH_USER')
|
|
1296
|
-
env_pass = os.environ.get('SWML_BASIC_AUTH_PASSWORD')
|
|
1297
|
-
|
|
1298
|
-
# More robust source detection
|
|
1299
|
-
if env_user and env_pass and username == env_user and password == env_pass:
|
|
1300
|
-
source = "environment"
|
|
1301
|
-
elif username.startswith("user_") and len(password) > 20: # Format of generated credentials
|
|
1302
|
-
source = "generated"
|
|
1303
|
-
else:
|
|
1304
|
-
source = "provided"
|
|
1305
|
-
|
|
1306
|
-
return (username, password, source)
|
|
714
|
+
if params and isinstance(params, dict):
|
|
715
|
+
self._swaig_query_params.update(params)
|
|
716
|
+
return self
|
|
1307
717
|
|
|
1308
|
-
def
|
|
1309
|
-
"""
|
|
1310
|
-
Get the full URL for this agent's endpoint
|
|
1311
|
-
|
|
1312
|
-
Args:
|
|
1313
|
-
include_auth: Whether to include authentication credentials in the URL
|
|
1314
|
-
|
|
1315
|
-
Returns:
|
|
1316
|
-
Full URL including host, port, and route (with auth if requested)
|
|
1317
|
-
"""
|
|
1318
|
-
mode = get_execution_mode()
|
|
1319
|
-
|
|
1320
|
-
if mode == 'cgi':
|
|
1321
|
-
protocol = 'https' if os.getenv('HTTPS') == 'on' else 'http'
|
|
1322
|
-
host = os.getenv('HTTP_HOST') or os.getenv('SERVER_NAME') or 'localhost'
|
|
1323
|
-
script_name = os.getenv('SCRIPT_NAME', '')
|
|
1324
|
-
base_url = f"{protocol}://{host}{script_name}"
|
|
1325
|
-
elif mode == 'lambda':
|
|
1326
|
-
function_url = os.getenv('AWS_LAMBDA_FUNCTION_URL')
|
|
1327
|
-
if function_url and ('amazonaws.com' in function_url or 'on.aws' in function_url):
|
|
1328
|
-
base_url = function_url.rstrip('/')
|
|
1329
|
-
else:
|
|
1330
|
-
api_id = os.getenv('AWS_API_GATEWAY_ID')
|
|
1331
|
-
if api_id:
|
|
1332
|
-
region = os.getenv('AWS_REGION', 'us-east-1')
|
|
1333
|
-
stage = os.getenv('AWS_API_GATEWAY_STAGE', 'prod')
|
|
1334
|
-
base_url = f"https://{api_id}.execute-api.{region}.amazonaws.com/{stage}"
|
|
1335
|
-
else:
|
|
1336
|
-
import logging
|
|
1337
|
-
logging.warning("Lambda mode detected but no URL configuration found")
|
|
1338
|
-
base_url = "https://lambda-url-not-configured"
|
|
1339
|
-
elif mode == 'cloud_function':
|
|
1340
|
-
function_url = os.getenv('FUNCTION_URL')
|
|
1341
|
-
if function_url:
|
|
1342
|
-
base_url = function_url
|
|
1343
|
-
else:
|
|
1344
|
-
project = os.getenv('GOOGLE_CLOUD_PROJECT')
|
|
1345
|
-
if project:
|
|
1346
|
-
region = os.getenv('GOOGLE_CLOUD_REGION', 'us-central1')
|
|
1347
|
-
service = os.getenv('K_SERVICE', 'function')
|
|
1348
|
-
base_url = f"https://{region}-{project}.cloudfunctions.net/{service}"
|
|
1349
|
-
else:
|
|
1350
|
-
import logging
|
|
1351
|
-
logging.warning("Cloud Function mode detected but no URL configuration found")
|
|
1352
|
-
base_url = "https://cloud-function-url-not-configured"
|
|
1353
|
-
else:
|
|
1354
|
-
# Server mode - preserve existing logic
|
|
1355
|
-
if self._proxy_url_base:
|
|
1356
|
-
proxy_base = self._proxy_url_base.rstrip('/')
|
|
1357
|
-
route = self.route if self.route.startswith('/') else f"/{self.route}"
|
|
1358
|
-
base_url = f"{proxy_base}{route}"
|
|
1359
|
-
else:
|
|
1360
|
-
if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
|
|
1361
|
-
host = "localhost"
|
|
1362
|
-
else:
|
|
1363
|
-
host = self.host
|
|
1364
|
-
base_url = f"http://{host}:{self.port}{self.route}"
|
|
1365
|
-
|
|
1366
|
-
# Add auth if requested (only for server mode)
|
|
1367
|
-
if include_auth and mode == 'server':
|
|
1368
|
-
username, password = self._basic_auth
|
|
1369
|
-
url = urlparse(base_url)
|
|
1370
|
-
return url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
|
|
1371
|
-
|
|
1372
|
-
return base_url
|
|
1373
|
-
|
|
1374
|
-
def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
|
|
718
|
+
def clear_swaig_query_params(self) -> 'AgentBase':
|
|
1375
719
|
"""
|
|
1376
|
-
|
|
720
|
+
Clear all SWAIG query parameters
|
|
1377
721
|
|
|
1378
|
-
Args:
|
|
1379
|
-
endpoint: The endpoint path (e.g., "swaig", "post_prompt")
|
|
1380
|
-
query_params: Optional query parameters to append
|
|
1381
|
-
|
|
1382
722
|
Returns:
|
|
1383
|
-
|
|
723
|
+
Self for method chaining
|
|
1384
724
|
"""
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
if mode != 'server':
|
|
1389
|
-
# In serverless mode, use the serverless-appropriate URL
|
|
1390
|
-
base_url = self.get_full_url()
|
|
1391
|
-
|
|
1392
|
-
# For serverless, we don't need auth in webhook URLs since auth is handled differently
|
|
1393
|
-
# and we want to return the actual platform URL
|
|
1394
|
-
|
|
1395
|
-
# Ensure the endpoint has a trailing slash to prevent redirects
|
|
1396
|
-
if endpoint in ["swaig", "post_prompt"]:
|
|
1397
|
-
endpoint = f"{endpoint}/"
|
|
1398
|
-
|
|
1399
|
-
# Build the full webhook URL
|
|
1400
|
-
url = f"{base_url}/{endpoint}"
|
|
1401
|
-
|
|
1402
|
-
# Add query parameters if any (only if they have values)
|
|
1403
|
-
if query_params:
|
|
1404
|
-
# Remove any call_id from query params
|
|
1405
|
-
filtered_params = {k: v for k, v in query_params.items() if k != "call_id" and v}
|
|
1406
|
-
if filtered_params:
|
|
1407
|
-
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
|
1408
|
-
url = f"{url}?{params}"
|
|
1409
|
-
|
|
1410
|
-
return url
|
|
1411
|
-
|
|
1412
|
-
# Server mode - use existing logic with proxy/auth support
|
|
1413
|
-
# Use the parent class's implementation if available and has the same method
|
|
1414
|
-
if hasattr(super(), '_build_webhook_url'):
|
|
1415
|
-
# Ensure _proxy_url_base is synchronized
|
|
1416
|
-
if getattr(self, '_proxy_url_base', None) and hasattr(super(), '_proxy_url_base'):
|
|
1417
|
-
super()._proxy_url_base = self._proxy_url_base
|
|
1418
|
-
|
|
1419
|
-
# Call parent's implementation
|
|
1420
|
-
return super()._build_webhook_url(endpoint, query_params)
|
|
1421
|
-
|
|
1422
|
-
# Otherwise, fall back to our own implementation for server mode
|
|
1423
|
-
# Base URL construction
|
|
1424
|
-
if hasattr(self, '_proxy_url_base') and self._proxy_url_base:
|
|
1425
|
-
# For proxy URLs
|
|
1426
|
-
base = self._proxy_url_base.rstrip('/')
|
|
1427
|
-
|
|
1428
|
-
# Always add auth credentials
|
|
1429
|
-
username, password = self._basic_auth
|
|
1430
|
-
url = urlparse(base)
|
|
1431
|
-
base = url._replace(netloc=f"{username}:{password}@{url.netloc}").geturl()
|
|
1432
|
-
else:
|
|
1433
|
-
# For local URLs
|
|
1434
|
-
if self.host in ("0.0.0.0", "127.0.0.1", "localhost"):
|
|
1435
|
-
host = "localhost"
|
|
1436
|
-
else:
|
|
1437
|
-
host = self.host
|
|
1438
|
-
|
|
1439
|
-
# Always include auth credentials
|
|
1440
|
-
username, password = self._basic_auth
|
|
1441
|
-
base = f"http://{username}:{password}@{host}:{self.port}"
|
|
1442
|
-
|
|
1443
|
-
# Ensure the endpoint has a trailing slash to prevent redirects
|
|
1444
|
-
if endpoint in ["swaig", "post_prompt"]:
|
|
1445
|
-
endpoint = f"{endpoint}/"
|
|
1446
|
-
|
|
1447
|
-
# Simple path - use the route directly with the endpoint
|
|
1448
|
-
path = f"{self.route}/{endpoint}"
|
|
1449
|
-
|
|
1450
|
-
# Construct full URL
|
|
1451
|
-
url = f"{base}{path}"
|
|
1452
|
-
|
|
1453
|
-
# Add query parameters if any (only if they have values)
|
|
1454
|
-
# But NEVER add call_id parameter - it should be in the body, not the URL
|
|
1455
|
-
if query_params:
|
|
1456
|
-
# Remove any call_id from query params
|
|
1457
|
-
filtered_params = {k: v for k, v in query_params.items() if k != "call_id" and v}
|
|
1458
|
-
if filtered_params:
|
|
1459
|
-
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
|
1460
|
-
url = f"{url}?{params}"
|
|
1461
|
-
|
|
1462
|
-
return url
|
|
1463
|
-
|
|
725
|
+
self._swaig_query_params = {}
|
|
726
|
+
return self
|
|
727
|
+
|
|
1464
728
|
def _render_swml(self, call_id: str = None, modifications: Optional[dict] = None) -> str:
|
|
1465
729
|
"""
|
|
1466
730
|
Render the complete SWML document using SWMLService methods
|
|
@@ -1472,52 +736,114 @@ class AgentBase(SWMLService):
|
|
|
1472
736
|
Returns:
|
|
1473
737
|
SWML document as a string
|
|
1474
738
|
"""
|
|
739
|
+
self.log.debug("_render_swml_called",
|
|
740
|
+
call_id=call_id,
|
|
741
|
+
has_modifications=bool(modifications),
|
|
742
|
+
use_ephemeral=bool(modifications and modifications.get("__use_ephemeral_agent")),
|
|
743
|
+
has_dynamic_callback=bool(self._dynamic_config_callback))
|
|
744
|
+
|
|
745
|
+
# Check if we need to use an ephemeral agent for dynamic configuration
|
|
746
|
+
agent_to_use = self
|
|
747
|
+
if modifications and modifications.get("__use_ephemeral_agent"):
|
|
748
|
+
# Create an ephemeral copy for this request
|
|
749
|
+
self.log.debug("creating_ephemeral_agent",
|
|
750
|
+
original_sections=len(self._prompt_manager._sections) if hasattr(self._prompt_manager, '_sections') else 0)
|
|
751
|
+
agent_to_use = self._create_ephemeral_copy()
|
|
752
|
+
self.log.debug("ephemeral_agent_created",
|
|
753
|
+
ephemeral_sections=len(agent_to_use._prompt_manager._sections) if hasattr(agent_to_use._prompt_manager, '_sections') else 0)
|
|
754
|
+
|
|
755
|
+
# Extract the request data
|
|
756
|
+
request = modifications.get("__request")
|
|
757
|
+
request_data = modifications.get("__request_data", {})
|
|
758
|
+
|
|
759
|
+
if self._dynamic_config_callback:
|
|
760
|
+
try:
|
|
761
|
+
# Extract request data
|
|
762
|
+
if request:
|
|
763
|
+
query_params = dict(request.query_params)
|
|
764
|
+
headers = dict(request.headers)
|
|
765
|
+
else:
|
|
766
|
+
# No request object - use empty defaults
|
|
767
|
+
query_params = {}
|
|
768
|
+
headers = {}
|
|
769
|
+
body_params = request_data
|
|
770
|
+
|
|
771
|
+
# Call the dynamic config callback with the ephemeral agent
|
|
772
|
+
# This allows FULL dynamic configuration including adding skills
|
|
773
|
+
self.log.debug("calling_dynamic_config_on_ephemeral", has_request=bool(request))
|
|
774
|
+
self._dynamic_config_callback(query_params, body_params, headers, agent_to_use)
|
|
775
|
+
self.log.debug("dynamic_config_complete",
|
|
776
|
+
ephemeral_sections_after=len(agent_to_use._prompt_manager._sections) if hasattr(agent_to_use._prompt_manager, '_sections') else 0)
|
|
777
|
+
|
|
778
|
+
except Exception as e:
|
|
779
|
+
self.log.error("dynamic_config_error", error=str(e))
|
|
780
|
+
|
|
781
|
+
# Clear the special markers so they don't affect rendering
|
|
782
|
+
modifications = None
|
|
783
|
+
|
|
1475
784
|
# Reset the document to a clean state
|
|
1476
|
-
|
|
785
|
+
agent_to_use.reset_document()
|
|
1477
786
|
|
|
1478
787
|
# Get prompt
|
|
1479
|
-
prompt =
|
|
788
|
+
prompt = agent_to_use.get_prompt()
|
|
1480
789
|
prompt_is_pom = isinstance(prompt, list)
|
|
1481
790
|
|
|
1482
791
|
# Get post-prompt
|
|
1483
|
-
post_prompt =
|
|
792
|
+
post_prompt = agent_to_use.get_post_prompt()
|
|
1484
793
|
|
|
1485
794
|
# Generate a call ID if needed
|
|
1486
|
-
if
|
|
1487
|
-
call_id =
|
|
795
|
+
if call_id is None:
|
|
796
|
+
call_id = agent_to_use._session_manager.create_session()
|
|
797
|
+
self.log.debug("generated_call_id", call_id=call_id)
|
|
798
|
+
else:
|
|
799
|
+
self.log.debug("using_provided_call_id", call_id=call_id)
|
|
1488
800
|
|
|
1489
|
-
#
|
|
1490
|
-
query_params = {}
|
|
801
|
+
# Start with any SWAIG query params that were set
|
|
802
|
+
query_params = agent_to_use._swaig_query_params.copy() if agent_to_use._swaig_query_params else {}
|
|
1491
803
|
|
|
1492
804
|
# Get the default webhook URL with auth
|
|
1493
|
-
default_webhook_url =
|
|
805
|
+
default_webhook_url = agent_to_use._build_webhook_url("swaig", query_params)
|
|
1494
806
|
|
|
1495
807
|
# Use override if set
|
|
1496
|
-
if hasattr(
|
|
1497
|
-
default_webhook_url =
|
|
808
|
+
if hasattr(agent_to_use, '_web_hook_url_override') and agent_to_use._web_hook_url_override:
|
|
809
|
+
default_webhook_url = agent_to_use._web_hook_url_override
|
|
1498
810
|
|
|
1499
811
|
# Prepare SWAIG object (correct format)
|
|
1500
812
|
swaig_obj = {}
|
|
1501
813
|
|
|
1502
|
-
# Add defaults if we have functions
|
|
1503
|
-
if self._swaig_functions:
|
|
1504
|
-
swaig_obj["defaults"] = {
|
|
1505
|
-
"web_hook_url": default_webhook_url
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
814
|
# Add native_functions if any are defined
|
|
1509
|
-
if
|
|
1510
|
-
swaig_obj["native_functions"] =
|
|
815
|
+
if agent_to_use.native_functions:
|
|
816
|
+
swaig_obj["native_functions"] = agent_to_use.native_functions
|
|
1511
817
|
|
|
1512
818
|
# Add includes if any are defined
|
|
1513
|
-
if
|
|
1514
|
-
swaig_obj["includes"] =
|
|
819
|
+
if agent_to_use._function_includes:
|
|
820
|
+
swaig_obj["includes"] = agent_to_use._function_includes
|
|
821
|
+
|
|
822
|
+
# Add internal_fillers if any are defined
|
|
823
|
+
if hasattr(agent_to_use, '_internal_fillers') and agent_to_use._internal_fillers:
|
|
824
|
+
swaig_obj["internal_fillers"] = agent_to_use._internal_fillers
|
|
1515
825
|
|
|
1516
826
|
# Create functions array
|
|
1517
827
|
functions = []
|
|
1518
828
|
|
|
829
|
+
# Debug logging to see what functions we have
|
|
830
|
+
self.log.debug("checking_swaig_functions",
|
|
831
|
+
agent_name=agent_to_use.name,
|
|
832
|
+
is_ephemeral=getattr(agent_to_use, '_is_ephemeral', False),
|
|
833
|
+
registry_id=id(agent_to_use._tool_registry),
|
|
834
|
+
agent_id=id(agent_to_use),
|
|
835
|
+
function_count=len(agent_to_use._tool_registry._swaig_functions) if hasattr(agent_to_use._tool_registry, '_swaig_functions') else 0,
|
|
836
|
+
functions=list(agent_to_use._tool_registry._swaig_functions.keys()) if hasattr(agent_to_use._tool_registry, '_swaig_functions') else [])
|
|
837
|
+
|
|
1519
838
|
# Add each function to the functions array
|
|
1520
|
-
|
|
839
|
+
# Check if the registry has the _swaig_functions attribute
|
|
840
|
+
if not hasattr(agent_to_use._tool_registry, '_swaig_functions'):
|
|
841
|
+
self.log.warning("tool_registry_missing_swaig_functions",
|
|
842
|
+
registry_id=id(agent_to_use._tool_registry),
|
|
843
|
+
agent_id=id(agent_to_use))
|
|
844
|
+
agent_to_use._tool_registry._swaig_functions = {}
|
|
845
|
+
|
|
846
|
+
for name, func in agent_to_use._tool_registry._swaig_functions.items():
|
|
1521
847
|
if isinstance(func, dict):
|
|
1522
848
|
# For raw dictionaries (DataMap functions), use the entire dictionary as-is
|
|
1523
849
|
# This preserves data_map and any other special fields
|
|
@@ -1531,76 +857,133 @@ class AgentBase(SWMLService):
|
|
|
1531
857
|
# Check if it's secure and get token for secure functions when we have a call_id
|
|
1532
858
|
token = None
|
|
1533
859
|
if func.secure and call_id:
|
|
1534
|
-
token =
|
|
860
|
+
token = agent_to_use._create_tool_token(tool_name=name, call_id=call_id)
|
|
861
|
+
self.log.debug("created_token_for_function", function=name, call_id=call_id, token_prefix=token[:20] if token else None)
|
|
1535
862
|
|
|
1536
863
|
# Prepare function entry
|
|
1537
864
|
function_entry = {
|
|
1538
865
|
"function": name,
|
|
1539
866
|
"description": func.description,
|
|
1540
|
-
"parameters":
|
|
1541
|
-
"type": "object",
|
|
1542
|
-
"properties": func.parameters
|
|
1543
|
-
}
|
|
867
|
+
"parameters": func._ensure_parameter_structure()
|
|
1544
868
|
}
|
|
1545
869
|
|
|
1546
|
-
# Add
|
|
1547
|
-
if func.
|
|
870
|
+
# Add wait_file if present (audio/video file URL)
|
|
871
|
+
if hasattr(func, 'wait_file') and func.wait_file:
|
|
872
|
+
wait_file_url = func.wait_file
|
|
873
|
+
# If wait_file is a relative URL, convert it to absolute using agent's base URL
|
|
874
|
+
if wait_file_url and not wait_file_url.startswith(('http://', 'https://', '//')):
|
|
875
|
+
# Build full URL using the agent's base URL
|
|
876
|
+
base_url = agent_to_use._get_base_url(include_auth=False)
|
|
877
|
+
# Handle relative paths appropriately
|
|
878
|
+
if not wait_file_url.startswith('/'):
|
|
879
|
+
wait_file_url = '/' + wait_file_url
|
|
880
|
+
wait_file_url = f"{base_url}{wait_file_url}"
|
|
881
|
+
function_entry["wait_file"] = wait_file_url
|
|
882
|
+
|
|
883
|
+
# Add fillers if present (text phrases to say while processing)
|
|
884
|
+
if hasattr(func, 'fillers') and func.fillers:
|
|
1548
885
|
function_entry["fillers"] = func.fillers
|
|
1549
886
|
|
|
887
|
+
# Add wait_file_loops if present
|
|
888
|
+
if hasattr(func, 'wait_file_loops') and func.wait_file_loops is not None:
|
|
889
|
+
function_entry["wait_file_loops"] = func.wait_file_loops
|
|
890
|
+
|
|
1550
891
|
# Handle webhook URL
|
|
1551
892
|
if hasattr(func, 'webhook_url') and func.webhook_url:
|
|
1552
893
|
# External webhook function - use the provided URL directly
|
|
1553
894
|
function_entry["web_hook_url"] = func.webhook_url
|
|
1554
|
-
elif token:
|
|
1555
|
-
# Local function with token - build local webhook URL
|
|
1556
|
-
|
|
1557
|
-
|
|
895
|
+
elif token or agent_to_use._swaig_query_params:
|
|
896
|
+
# Local function with token OR SWAIG query params - build local webhook URL
|
|
897
|
+
# Start with SWAIG query params
|
|
898
|
+
url_params = agent_to_use._swaig_query_params.copy() if agent_to_use._swaig_query_params else {}
|
|
899
|
+
if token:
|
|
900
|
+
url_params["__token"] = token # Use __token to avoid collision
|
|
901
|
+
function_entry["web_hook_url"] = agent_to_use._build_webhook_url("swaig", url_params)
|
|
1558
902
|
|
|
1559
903
|
functions.append(function_entry)
|
|
1560
904
|
|
|
1561
905
|
# Add functions array to SWAIG object if we have any
|
|
1562
906
|
if functions:
|
|
1563
907
|
swaig_obj["functions"] = functions
|
|
908
|
+
# Add defaults section now that we know we have functions
|
|
909
|
+
if "defaults" not in swaig_obj:
|
|
910
|
+
swaig_obj["defaults"] = {
|
|
911
|
+
"web_hook_url": default_webhook_url
|
|
912
|
+
}
|
|
1564
913
|
|
|
1565
914
|
# Add post-prompt URL with token if we have a post-prompt
|
|
1566
915
|
post_prompt_url = None
|
|
1567
916
|
if post_prompt:
|
|
1568
917
|
# Create a token for post_prompt if we have a call_id
|
|
1569
|
-
|
|
1570
|
-
if
|
|
918
|
+
# Start with SWAIG query params
|
|
919
|
+
query_params = agent_to_use._swaig_query_params.copy() if agent_to_use._swaig_query_params else {}
|
|
920
|
+
if call_id and hasattr(agent_to_use, '_session_manager'):
|
|
1571
921
|
try:
|
|
1572
|
-
token =
|
|
922
|
+
token = agent_to_use._session_manager.create_tool_token("post_prompt", call_id)
|
|
1573
923
|
if token:
|
|
1574
|
-
query_params["
|
|
924
|
+
query_params["__token"] = token # Use __token to avoid collision
|
|
1575
925
|
except Exception as e:
|
|
1576
|
-
|
|
926
|
+
agent_to_use.log.error("post_prompt_token_creation_error", error=str(e))
|
|
1577
927
|
|
|
1578
928
|
# Build the URL with the token (if any)
|
|
1579
|
-
post_prompt_url =
|
|
929
|
+
post_prompt_url = agent_to_use._build_webhook_url("post_prompt", query_params)
|
|
1580
930
|
|
|
1581
931
|
# Use override if set
|
|
1582
|
-
if hasattr(
|
|
1583
|
-
post_prompt_url =
|
|
932
|
+
if hasattr(agent_to_use, '_post_prompt_url_override') and agent_to_use._post_prompt_url_override:
|
|
933
|
+
post_prompt_url = agent_to_use._post_prompt_url_override
|
|
1584
934
|
|
|
1585
|
-
#
|
|
1586
|
-
|
|
935
|
+
# ========== PHASE 1: PRE-ANSWER VERBS ==========
|
|
936
|
+
# These run while the call is still ringing
|
|
937
|
+
for verb_name, verb_config in agent_to_use._pre_answer_verbs:
|
|
938
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
939
|
+
|
|
940
|
+
# ========== PHASE 2: ANSWER VERB ==========
|
|
941
|
+
# Only add answer verb if auto_answer is enabled
|
|
942
|
+
if agent_to_use._auto_answer:
|
|
943
|
+
agent_to_use.add_verb("answer", agent_to_use._answer_config)
|
|
944
|
+
|
|
945
|
+
# ========== PHASE 3: POST-ANSWER VERBS ==========
|
|
946
|
+
# These run after answer but before AI
|
|
947
|
+
|
|
948
|
+
# Add recording if enabled (this is a post-answer verb)
|
|
949
|
+
if agent_to_use._record_call:
|
|
950
|
+
agent_to_use.add_verb("record_call", {
|
|
951
|
+
"format": agent_to_use._record_format,
|
|
952
|
+
"stereo": agent_to_use._record_stereo
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
# Add user-defined post-answer verbs
|
|
956
|
+
for verb_name, verb_config in agent_to_use._post_answer_verbs:
|
|
957
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1587
958
|
|
|
1588
959
|
# Use the AI verb handler to build and validate the AI verb config
|
|
1589
960
|
ai_config = {}
|
|
1590
961
|
|
|
1591
962
|
# Get the AI verb handler
|
|
1592
|
-
ai_handler =
|
|
963
|
+
ai_handler = agent_to_use.verb_registry.get_handler("ai")
|
|
1593
964
|
if ai_handler:
|
|
1594
965
|
try:
|
|
1595
966
|
# Check if we're in contexts mode
|
|
1596
|
-
if
|
|
1597
|
-
# Generate contexts
|
|
1598
|
-
contexts_dict =
|
|
967
|
+
if agent_to_use._contexts_defined and agent_to_use._contexts_builder:
|
|
968
|
+
# Generate contexts and combine with base prompt
|
|
969
|
+
contexts_dict = agent_to_use._contexts_builder.to_dict()
|
|
970
|
+
|
|
971
|
+
# Determine base prompt (required when using contexts)
|
|
972
|
+
base_prompt_text = None
|
|
973
|
+
base_prompt_pom = None
|
|
1599
974
|
|
|
1600
|
-
|
|
975
|
+
if prompt_is_pom:
|
|
976
|
+
base_prompt_pom = prompt
|
|
977
|
+
elif prompt:
|
|
978
|
+
base_prompt_text = prompt
|
|
979
|
+
else:
|
|
980
|
+
# Provide default base prompt if none exists
|
|
981
|
+
base_prompt_text = f"You are {agent_to_use.name}, a helpful AI assistant that follows structured workflows."
|
|
982
|
+
|
|
983
|
+
# Build AI config with base prompt + contexts
|
|
1601
984
|
ai_config = ai_handler.build_config(
|
|
1602
|
-
prompt_text=
|
|
1603
|
-
prompt_pom=
|
|
985
|
+
prompt_text=base_prompt_text,
|
|
986
|
+
prompt_pom=base_prompt_pom,
|
|
1604
987
|
contexts=contexts_dict,
|
|
1605
988
|
post_prompt=post_prompt,
|
|
1606
989
|
post_prompt_url=post_prompt_url,
|
|
@@ -1619,28 +1002,54 @@ class AgentBase(SWMLService):
|
|
|
1619
1002
|
# Add new configuration parameters to the AI config
|
|
1620
1003
|
|
|
1621
1004
|
# Add hints if any
|
|
1622
|
-
if
|
|
1623
|
-
ai_config["hints"] =
|
|
1005
|
+
if agent_to_use._hints:
|
|
1006
|
+
ai_config["hints"] = agent_to_use._hints
|
|
1624
1007
|
|
|
1625
1008
|
# Add languages if any
|
|
1626
|
-
if
|
|
1627
|
-
ai_config["languages"] =
|
|
1009
|
+
if agent_to_use._languages:
|
|
1010
|
+
ai_config["languages"] = agent_to_use._languages
|
|
1628
1011
|
|
|
1629
1012
|
# Add pronunciation rules if any
|
|
1630
|
-
if
|
|
1631
|
-
ai_config["pronounce"] =
|
|
1013
|
+
if agent_to_use._pronounce:
|
|
1014
|
+
ai_config["pronounce"] = agent_to_use._pronounce
|
|
1632
1015
|
|
|
1633
1016
|
# Add params if any
|
|
1634
|
-
if
|
|
1635
|
-
ai_config["params"] =
|
|
1017
|
+
if agent_to_use._params:
|
|
1018
|
+
ai_config["params"] = agent_to_use._params
|
|
1636
1019
|
|
|
1637
1020
|
# Add global_data if any
|
|
1638
|
-
if
|
|
1639
|
-
ai_config["global_data"] =
|
|
1021
|
+
if agent_to_use._global_data:
|
|
1022
|
+
ai_config["global_data"] = agent_to_use._global_data
|
|
1023
|
+
|
|
1024
|
+
# Always add LLM parameters to prompt
|
|
1025
|
+
if "prompt" in ai_config:
|
|
1026
|
+
# Only add LLM params if explicitly set
|
|
1027
|
+
if agent_to_use._prompt_llm_params:
|
|
1028
|
+
if isinstance(ai_config["prompt"], dict):
|
|
1029
|
+
ai_config["prompt"].update(agent_to_use._prompt_llm_params)
|
|
1030
|
+
elif isinstance(ai_config["prompt"], str):
|
|
1031
|
+
# Convert string prompt to dict format
|
|
1032
|
+
ai_config["prompt"] = {
|
|
1033
|
+
"text": ai_config["prompt"],
|
|
1034
|
+
**agent_to_use._prompt_llm_params
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
# Only add LLM parameters to post_prompt if explicitly set
|
|
1038
|
+
if post_prompt and "post_prompt" in ai_config:
|
|
1039
|
+
# Only add LLM params if explicitly set
|
|
1040
|
+
if agent_to_use._post_prompt_llm_params:
|
|
1041
|
+
if isinstance(ai_config["post_prompt"], dict):
|
|
1042
|
+
ai_config["post_prompt"].update(agent_to_use._post_prompt_llm_params)
|
|
1043
|
+
elif isinstance(ai_config["post_prompt"], str):
|
|
1044
|
+
# Convert string post_prompt to dict format
|
|
1045
|
+
ai_config["post_prompt"] = {
|
|
1046
|
+
"text": ai_config["post_prompt"],
|
|
1047
|
+
**agent_to_use._post_prompt_llm_params
|
|
1048
|
+
}
|
|
1640
1049
|
|
|
1641
1050
|
except ValueError as e:
|
|
1642
|
-
if not
|
|
1643
|
-
|
|
1051
|
+
if not agent_to_use._suppress_logs:
|
|
1052
|
+
agent_to_use.log.error("ai_verb_config_error", error=str(e))
|
|
1644
1053
|
else:
|
|
1645
1054
|
# Fallback if no handler (shouldn't happen but just in case)
|
|
1646
1055
|
ai_config = {
|
|
@@ -1658,24 +1067,30 @@ class AgentBase(SWMLService):
|
|
|
1658
1067
|
ai_config["SWAIG"] = swaig_obj
|
|
1659
1068
|
|
|
1660
1069
|
# Add the new configurations if not already added by the handler
|
|
1661
|
-
if
|
|
1662
|
-
ai_config["hints"] =
|
|
1663
|
-
|
|
1664
|
-
if self._languages and "languages" not in ai_config:
|
|
1665
|
-
ai_config["languages"] = self._languages
|
|
1070
|
+
if agent_to_use._hints and "hints" not in ai_config:
|
|
1071
|
+
ai_config["hints"] = agent_to_use._hints
|
|
1666
1072
|
|
|
1667
|
-
if
|
|
1668
|
-
ai_config["
|
|
1073
|
+
if agent_to_use._languages and "languages" not in ai_config:
|
|
1074
|
+
ai_config["languages"] = agent_to_use._languages
|
|
1669
1075
|
|
|
1670
|
-
if
|
|
1671
|
-
ai_config["
|
|
1076
|
+
if agent_to_use._pronounce and "pronounce" not in ai_config:
|
|
1077
|
+
ai_config["pronounce"] = agent_to_use._pronounce
|
|
1672
1078
|
|
|
1673
|
-
if
|
|
1674
|
-
ai_config["
|
|
1079
|
+
if agent_to_use._params and "params" not in ai_config:
|
|
1080
|
+
ai_config["params"] = agent_to_use._params
|
|
1675
1081
|
|
|
1082
|
+
if agent_to_use._global_data and "global_data" not in ai_config:
|
|
1083
|
+
ai_config["global_data"] = agent_to_use._global_data
|
|
1084
|
+
|
|
1085
|
+
# ========== PHASE 4: AI VERB ==========
|
|
1676
1086
|
# Add the AI verb to the document
|
|
1677
|
-
|
|
1678
|
-
|
|
1087
|
+
agent_to_use.add_verb("ai", ai_config)
|
|
1088
|
+
|
|
1089
|
+
# ========== PHASE 5: POST-AI VERBS ==========
|
|
1090
|
+
# These run after the AI conversation ends
|
|
1091
|
+
for verb_name, verb_config in agent_to_use._post_ai_verbs:
|
|
1092
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1093
|
+
|
|
1679
1094
|
# Apply any modifications from the callback to agent state
|
|
1680
1095
|
if modifications and isinstance(modifications, dict):
|
|
1681
1096
|
# Handle global_data modifications by updating the AI config directly
|
|
@@ -1690,1457 +1105,74 @@ class AgentBase(SWMLService):
|
|
|
1690
1105
|
ai_config[key] = value
|
|
1691
1106
|
|
|
1692
1107
|
# Clear and rebuild the document with the modified AI config
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
except Exception:
|
|
1720
|
-
return False
|
|
1721
|
-
|
|
1722
|
-
def as_router(self) -> APIRouter:
|
|
1723
|
-
"""
|
|
1724
|
-
Get a FastAPI router for this agent
|
|
1725
|
-
|
|
1726
|
-
Returns:
|
|
1727
|
-
FastAPI router
|
|
1728
|
-
"""
|
|
1729
|
-
# Create a router with explicit redirect_slashes=False
|
|
1730
|
-
router = APIRouter(redirect_slashes=False)
|
|
1731
|
-
|
|
1732
|
-
# Register routes explicitly
|
|
1733
|
-
self._register_routes(router)
|
|
1734
|
-
|
|
1735
|
-
# Log all registered routes for debugging
|
|
1736
|
-
self.log.debug("routes_registered", agent=self.name)
|
|
1737
|
-
for route in router.routes:
|
|
1738
|
-
self.log.debug("route_registered", path=route.path)
|
|
1739
|
-
|
|
1740
|
-
return router
|
|
1741
|
-
|
|
1742
|
-
def serve(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
|
|
1743
|
-
"""
|
|
1744
|
-
Start a web server for this agent
|
|
1745
|
-
|
|
1746
|
-
Args:
|
|
1747
|
-
host: Optional host to override the default
|
|
1748
|
-
port: Optional port to override the default
|
|
1749
|
-
"""
|
|
1750
|
-
import uvicorn
|
|
1751
|
-
|
|
1752
|
-
if self._app is None:
|
|
1753
|
-
# Create a FastAPI app with explicit redirect_slashes=False
|
|
1754
|
-
app = FastAPI(redirect_slashes=False)
|
|
1755
|
-
|
|
1756
|
-
# Add health and ready endpoints directly to the main app to avoid conflicts with catch-all
|
|
1757
|
-
@app.get("/health")
|
|
1758
|
-
@app.post("/health")
|
|
1759
|
-
async def health_check():
|
|
1760
|
-
"""Health check endpoint for Kubernetes liveness probe"""
|
|
1761
|
-
return {
|
|
1762
|
-
"status": "healthy",
|
|
1763
|
-
"agent": self.get_name(),
|
|
1764
|
-
"route": self.route,
|
|
1765
|
-
"functions": len(self._swaig_functions)
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
@app.get("/ready")
|
|
1769
|
-
@app.post("/ready")
|
|
1770
|
-
async def readiness_check():
|
|
1771
|
-
"""Readiness check endpoint for Kubernetes readiness probe"""
|
|
1772
|
-
# Check if agent is properly initialized
|
|
1773
|
-
ready = (
|
|
1774
|
-
hasattr(self, '_swaig_functions') and
|
|
1775
|
-
hasattr(self, 'schema_utils') and
|
|
1776
|
-
self.schema_utils is not None
|
|
1777
|
-
)
|
|
1778
|
-
|
|
1779
|
-
status_code = 200 if ready else 503
|
|
1780
|
-
return Response(
|
|
1781
|
-
content=json.dumps({
|
|
1782
|
-
"status": "ready" if ready else "not_ready",
|
|
1783
|
-
"agent": self.get_name(),
|
|
1784
|
-
"initialized": ready
|
|
1785
|
-
}),
|
|
1786
|
-
status_code=status_code,
|
|
1787
|
-
media_type="application/json"
|
|
1788
|
-
)
|
|
1789
|
-
|
|
1790
|
-
# Get router for this agent
|
|
1791
|
-
router = self.as_router()
|
|
1792
|
-
|
|
1793
|
-
# Register a catch-all route for debugging and troubleshooting
|
|
1794
|
-
@app.get("/{full_path:path}")
|
|
1795
|
-
@app.post("/{full_path:path}")
|
|
1796
|
-
async def handle_all_routes(request: Request, full_path: str):
|
|
1797
|
-
self.log.debug("request_received", path=full_path)
|
|
1798
|
-
|
|
1799
|
-
# Check if the path is meant for this agent
|
|
1800
|
-
if not full_path.startswith(self.route.lstrip("/")):
|
|
1801
|
-
return {"error": "Invalid route"}
|
|
1802
|
-
|
|
1803
|
-
# Extract the path relative to this agent's route
|
|
1804
|
-
relative_path = full_path[len(self.route.lstrip("/")):]
|
|
1805
|
-
relative_path = relative_path.lstrip("/")
|
|
1806
|
-
self.log.debug("path_extracted", relative_path=relative_path)
|
|
1807
|
-
|
|
1808
|
-
# Perform routing based on the relative path
|
|
1809
|
-
if not relative_path or relative_path == "/":
|
|
1810
|
-
# Root endpoint
|
|
1811
|
-
return await self._handle_root_request(request)
|
|
1812
|
-
|
|
1813
|
-
# Strip trailing slash for processing
|
|
1814
|
-
clean_path = relative_path.rstrip("/")
|
|
1815
|
-
|
|
1816
|
-
# Check for standard endpoints
|
|
1817
|
-
if clean_path == "debug":
|
|
1818
|
-
return await self._handle_debug_request(request)
|
|
1819
|
-
elif clean_path == "swaig":
|
|
1820
|
-
return await self._handle_swaig_request(request, Response())
|
|
1821
|
-
elif clean_path == "post_prompt":
|
|
1822
|
-
return await self._handle_post_prompt_request(request)
|
|
1823
|
-
elif clean_path == "check_for_input":
|
|
1824
|
-
return await self._handle_check_for_input_request(request)
|
|
1825
|
-
|
|
1826
|
-
# Check for custom routing callbacks
|
|
1827
|
-
if hasattr(self, '_routing_callbacks'):
|
|
1828
|
-
for callback_path, callback_fn in self._routing_callbacks.items():
|
|
1829
|
-
cb_path_clean = callback_path.strip("/")
|
|
1830
|
-
if clean_path == cb_path_clean:
|
|
1831
|
-
# Found a matching callback
|
|
1832
|
-
request.state.callback_path = callback_path
|
|
1833
|
-
return await self._handle_root_request(request)
|
|
1834
|
-
|
|
1835
|
-
# Default: 404
|
|
1836
|
-
return {"error": "Path not found"}
|
|
1837
|
-
|
|
1838
|
-
# Include router with prefix
|
|
1839
|
-
app.include_router(router, prefix=self.route)
|
|
1840
|
-
|
|
1841
|
-
# Log all app routes for debugging
|
|
1842
|
-
self.log.debug("app_routes_registered")
|
|
1843
|
-
for route in app.routes:
|
|
1844
|
-
if hasattr(route, "path"):
|
|
1845
|
-
self.log.debug("app_route", path=route.path)
|
|
1846
|
-
|
|
1847
|
-
self._app = app
|
|
1848
|
-
|
|
1849
|
-
host = host or self.host
|
|
1850
|
-
port = port or self.port
|
|
1851
|
-
|
|
1852
|
-
# Print the auth credentials with source
|
|
1853
|
-
username, password, source = self.get_basic_auth_credentials(include_source=True)
|
|
1854
|
-
|
|
1855
|
-
# Log startup information using structured logging
|
|
1856
|
-
self.log.info("agent_starting",
|
|
1857
|
-
agent=self.name,
|
|
1858
|
-
url=f"http://{host}:{port}{self.route}",
|
|
1859
|
-
username=username,
|
|
1860
|
-
password_length=len(password),
|
|
1861
|
-
auth_source=source)
|
|
1862
|
-
|
|
1863
|
-
# Print user-friendly startup message (keep this for development UX)
|
|
1864
|
-
print(f"Agent '{self.name}' is available at:")
|
|
1865
|
-
print(f"URL: http://{host}:{port}{self.route}")
|
|
1866
|
-
print(f"Basic Auth: {username}:{password} (source: {source})")
|
|
1867
|
-
|
|
1868
|
-
uvicorn.run(self._app, host=host, port=port)
|
|
1869
|
-
|
|
1870
|
-
def run(self, event=None, context=None, force_mode=None, host: Optional[str] = None, port: Optional[int] = None):
|
|
1871
|
-
"""
|
|
1872
|
-
Smart run method that automatically detects environment and handles accordingly
|
|
1873
|
-
|
|
1874
|
-
Args:
|
|
1875
|
-
event: Serverless event object (Lambda, Cloud Functions)
|
|
1876
|
-
context: Serverless context object (Lambda, Cloud Functions)
|
|
1877
|
-
force_mode: Override automatic mode detection for testing
|
|
1878
|
-
host: Host override for server mode
|
|
1879
|
-
port: Port override for server mode
|
|
1880
|
-
|
|
1881
|
-
Returns:
|
|
1882
|
-
Response for serverless modes, None for server mode
|
|
1883
|
-
"""
|
|
1884
|
-
mode = force_mode or get_execution_mode()
|
|
1885
|
-
|
|
1886
|
-
try:
|
|
1887
|
-
if mode in ['cgi', 'cloud_function', 'azure_function']:
|
|
1888
|
-
response = self.handle_serverless_request(event, context, mode)
|
|
1889
|
-
print(response)
|
|
1890
|
-
return response
|
|
1891
|
-
elif mode == 'lambda':
|
|
1892
|
-
return self.handle_serverless_request(event, context, mode)
|
|
1893
|
-
else:
|
|
1894
|
-
# Server mode - use existing serve method
|
|
1895
|
-
self.serve(host, port)
|
|
1896
|
-
except Exception as e:
|
|
1897
|
-
import logging
|
|
1898
|
-
logging.error(f"Error in run method: {e}")
|
|
1899
|
-
if mode == 'lambda':
|
|
1900
|
-
return {
|
|
1901
|
-
"statusCode": 500,
|
|
1902
|
-
"headers": {"Content-Type": "application/json"},
|
|
1903
|
-
"body": json.dumps({"error": str(e)})
|
|
1904
|
-
}
|
|
1905
|
-
else:
|
|
1906
|
-
raise
|
|
1907
|
-
|
|
1908
|
-
def handle_serverless_request(self, event=None, context=None, mode=None):
|
|
1909
|
-
"""
|
|
1910
|
-
Handle serverless environment requests (CGI, Lambda, Cloud Functions)
|
|
1911
|
-
|
|
1912
|
-
Args:
|
|
1913
|
-
event: Serverless event object (Lambda, Cloud Functions)
|
|
1914
|
-
context: Serverless context object (Lambda, Cloud Functions)
|
|
1915
|
-
mode: Override execution mode (from force_mode in run())
|
|
1916
|
-
|
|
1917
|
-
Returns:
|
|
1918
|
-
Response appropriate for the serverless platform
|
|
1919
|
-
"""
|
|
1920
|
-
if mode is None:
|
|
1921
|
-
mode = get_execution_mode()
|
|
1922
|
-
|
|
1923
|
-
try:
|
|
1924
|
-
if mode == 'cgi':
|
|
1925
|
-
path_info = os.getenv('PATH_INFO', '').strip('/')
|
|
1926
|
-
if not path_info:
|
|
1927
|
-
return self._render_swml()
|
|
1928
|
-
else:
|
|
1929
|
-
# Parse CGI request for SWAIG function call
|
|
1930
|
-
args = {}
|
|
1931
|
-
call_id = None
|
|
1932
|
-
raw_data = None
|
|
1933
|
-
|
|
1934
|
-
# Try to parse POST data from stdin for CGI
|
|
1935
|
-
import sys
|
|
1936
|
-
content_length = os.getenv('CONTENT_LENGTH')
|
|
1937
|
-
if content_length and content_length.isdigit():
|
|
1938
|
-
try:
|
|
1939
|
-
post_data = sys.stdin.read(int(content_length))
|
|
1940
|
-
if post_data:
|
|
1941
|
-
raw_data = json.loads(post_data)
|
|
1942
|
-
call_id = raw_data.get("call_id")
|
|
1943
|
-
|
|
1944
|
-
# Extract arguments like the FastAPI handler does
|
|
1945
|
-
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
|
1946
|
-
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
|
1947
|
-
args = raw_data["argument"]["parsed"][0]
|
|
1948
|
-
elif "raw" in raw_data["argument"]:
|
|
1949
|
-
try:
|
|
1950
|
-
args = json.loads(raw_data["argument"]["raw"])
|
|
1951
|
-
except Exception:
|
|
1952
|
-
pass
|
|
1953
|
-
except Exception:
|
|
1954
|
-
# If parsing fails, continue with empty args
|
|
1955
|
-
pass
|
|
1956
|
-
|
|
1957
|
-
return self._execute_swaig_function(path_info, args, call_id, raw_data)
|
|
1958
|
-
|
|
1959
|
-
elif mode == 'lambda':
|
|
1960
|
-
if event:
|
|
1961
|
-
path = event.get('pathParameters', {}).get('proxy', '') if event.get('pathParameters') else ''
|
|
1962
|
-
if not path:
|
|
1963
|
-
swml_response = self._render_swml()
|
|
1964
|
-
return {
|
|
1965
|
-
"statusCode": 200,
|
|
1966
|
-
"headers": {"Content-Type": "application/json"},
|
|
1967
|
-
"body": swml_response
|
|
1968
|
-
}
|
|
1969
|
-
else:
|
|
1970
|
-
# Parse Lambda event for SWAIG function call
|
|
1971
|
-
args = {}
|
|
1972
|
-
call_id = None
|
|
1973
|
-
raw_data = None
|
|
1974
|
-
|
|
1975
|
-
# Parse request body if present
|
|
1976
|
-
body_content = event.get('body')
|
|
1977
|
-
if body_content:
|
|
1978
|
-
try:
|
|
1979
|
-
if isinstance(body_content, str):
|
|
1980
|
-
raw_data = json.loads(body_content)
|
|
1981
|
-
else:
|
|
1982
|
-
raw_data = body_content
|
|
1983
|
-
|
|
1984
|
-
call_id = raw_data.get("call_id")
|
|
1985
|
-
|
|
1986
|
-
# Extract arguments like the FastAPI handler does
|
|
1987
|
-
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
|
1988
|
-
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
|
1989
|
-
args = raw_data["argument"]["parsed"][0]
|
|
1990
|
-
elif "raw" in raw_data["argument"]:
|
|
1991
|
-
try:
|
|
1992
|
-
args = json.loads(raw_data["argument"]["raw"])
|
|
1993
|
-
except Exception:
|
|
1994
|
-
pass
|
|
1995
|
-
except Exception:
|
|
1996
|
-
# If parsing fails, continue with empty args
|
|
1997
|
-
pass
|
|
1998
|
-
|
|
1999
|
-
result = self._execute_swaig_function(path, args, call_id, raw_data)
|
|
2000
|
-
return {
|
|
2001
|
-
"statusCode": 200,
|
|
2002
|
-
"headers": {"Content-Type": "application/json"},
|
|
2003
|
-
"body": json.dumps(result) if isinstance(result, dict) else str(result)
|
|
2004
|
-
}
|
|
2005
|
-
else:
|
|
2006
|
-
# Handle case when event is None (direct Lambda call with no event)
|
|
2007
|
-
swml_response = self._render_swml()
|
|
2008
|
-
return {
|
|
2009
|
-
"statusCode": 200,
|
|
2010
|
-
"headers": {"Content-Type": "application/json"},
|
|
2011
|
-
"body": swml_response
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
elif mode in ['cloud_function', 'azure_function']:
|
|
2015
|
-
return self._handle_cloud_function_request(event)
|
|
2016
|
-
|
|
2017
|
-
except Exception as e:
|
|
2018
|
-
import logging
|
|
2019
|
-
logging.error(f"Error in serverless request handler: {e}")
|
|
2020
|
-
if mode == 'lambda':
|
|
2021
|
-
return {
|
|
2022
|
-
"statusCode": 500,
|
|
2023
|
-
"headers": {"Content-Type": "application/json"},
|
|
2024
|
-
"body": json.dumps({"error": str(e)})
|
|
2025
|
-
}
|
|
2026
|
-
else:
|
|
2027
|
-
raise
|
|
2028
|
-
|
|
2029
|
-
def _handle_cloud_function_request(self, request):
|
|
2030
|
-
"""
|
|
2031
|
-
Handle Cloud Function specific requests
|
|
2032
|
-
|
|
2033
|
-
Args:
|
|
2034
|
-
request: Cloud Function request object
|
|
2035
|
-
|
|
2036
|
-
Returns:
|
|
2037
|
-
Cloud Function response
|
|
2038
|
-
"""
|
|
2039
|
-
# Platform-specific implementation would go here
|
|
2040
|
-
# For now, return basic SWML response
|
|
2041
|
-
return self._render_swml()
|
|
2042
|
-
|
|
2043
|
-
def _execute_swaig_function(self, function_name: str, args: Optional[Dict[str, Any]] = None, call_id: Optional[str] = None, raw_data: Optional[Dict[str, Any]] = None):
|
|
2044
|
-
"""
|
|
2045
|
-
Execute a SWAIG function in serverless context
|
|
2046
|
-
|
|
2047
|
-
Args:
|
|
2048
|
-
function_name: Name of the function to execute
|
|
2049
|
-
args: Function arguments dictionary
|
|
2050
|
-
call_id: Optional call ID
|
|
2051
|
-
raw_data: Optional raw request data
|
|
2052
|
-
|
|
2053
|
-
Returns:
|
|
2054
|
-
Function execution result
|
|
2055
|
-
"""
|
|
2056
|
-
import structlog
|
|
2057
|
-
|
|
2058
|
-
# Use the existing logger
|
|
2059
|
-
req_log = self.log.bind(
|
|
2060
|
-
endpoint="serverless_swaig",
|
|
2061
|
-
function=function_name
|
|
2062
|
-
)
|
|
2063
|
-
|
|
2064
|
-
if call_id:
|
|
2065
|
-
req_log = req_log.bind(call_id=call_id)
|
|
2066
|
-
|
|
2067
|
-
req_log.debug("serverless_function_call_received")
|
|
2068
|
-
|
|
2069
|
-
try:
|
|
2070
|
-
# Validate function exists
|
|
2071
|
-
if function_name not in self._swaig_functions:
|
|
2072
|
-
req_log.warning("function_not_found", available_functions=list(self._swaig_functions.keys()))
|
|
2073
|
-
return {"error": f"Function '{function_name}' not found"}
|
|
2074
|
-
|
|
2075
|
-
# Use empty args if not provided
|
|
2076
|
-
if args is None:
|
|
2077
|
-
args = {}
|
|
2078
|
-
|
|
2079
|
-
# Use empty raw_data if not provided, but include function call structure
|
|
2080
|
-
if raw_data is None:
|
|
2081
|
-
raw_data = {
|
|
2082
|
-
"function": function_name,
|
|
2083
|
-
"argument": {
|
|
2084
|
-
"parsed": [args] if args else [],
|
|
2085
|
-
"raw": json.dumps(args) if args else "{}"
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
if call_id:
|
|
2089
|
-
raw_data["call_id"] = call_id
|
|
2090
|
-
|
|
2091
|
-
req_log.debug("executing_function", args=json.dumps(args))
|
|
2092
|
-
|
|
2093
|
-
# Call the function using the existing on_function_call method
|
|
2094
|
-
result = self.on_function_call(function_name, args, raw_data)
|
|
2095
|
-
|
|
2096
|
-
# Convert result to dict if needed (same logic as in _handle_swaig_request)
|
|
2097
|
-
if isinstance(result, SwaigFunctionResult):
|
|
2098
|
-
result_dict = result.to_dict()
|
|
2099
|
-
elif isinstance(result, dict):
|
|
2100
|
-
result_dict = result
|
|
2101
|
-
else:
|
|
2102
|
-
result_dict = {"response": str(result)}
|
|
2103
|
-
|
|
2104
|
-
req_log.info("serverless_function_executed_successfully")
|
|
2105
|
-
req_log.debug("function_result", result=json.dumps(result_dict))
|
|
2106
|
-
return result_dict
|
|
2107
|
-
|
|
2108
|
-
except Exception as e:
|
|
2109
|
-
req_log.error("serverless_function_execution_error", error=str(e))
|
|
2110
|
-
return {"error": str(e), "function": function_name}
|
|
2111
|
-
|
|
2112
|
-
def setup_graceful_shutdown(self) -> None:
|
|
2113
|
-
"""
|
|
2114
|
-
Setup signal handlers for graceful shutdown (useful for Kubernetes)
|
|
2115
|
-
"""
|
|
2116
|
-
def signal_handler(signum, frame):
|
|
2117
|
-
self.log.info("shutdown_signal_received", signal=signum)
|
|
2118
|
-
|
|
2119
|
-
# Perform cleanup
|
|
2120
|
-
try:
|
|
2121
|
-
# Close any open resources
|
|
2122
|
-
if hasattr(self, '_session_manager'):
|
|
2123
|
-
# Could add cleanup logic here
|
|
2124
|
-
pass
|
|
2125
|
-
|
|
2126
|
-
self.log.info("cleanup_completed")
|
|
2127
|
-
except Exception as e:
|
|
2128
|
-
self.log.error("cleanup_error", error=str(e))
|
|
2129
|
-
finally:
|
|
2130
|
-
sys.exit(0)
|
|
2131
|
-
|
|
2132
|
-
# Register handlers for common termination signals
|
|
2133
|
-
signal.signal(signal.SIGTERM, signal_handler) # Kubernetes sends this
|
|
2134
|
-
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
|
|
2135
|
-
|
|
2136
|
-
self.log.debug("graceful_shutdown_handlers_registered")
|
|
2137
|
-
|
|
2138
|
-
def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
|
|
2139
|
-
"""
|
|
2140
|
-
Customization point for subclasses to modify SWML based on request data
|
|
2141
|
-
|
|
2142
|
-
Args:
|
|
2143
|
-
request_data: Optional dictionary containing the parsed POST body
|
|
2144
|
-
callback_path: Optional callback path
|
|
2145
|
-
|
|
2146
|
-
Returns:
|
|
2147
|
-
Optional dict with modifications to apply to the SWML document
|
|
2148
|
-
"""
|
|
2149
|
-
# Default implementation does nothing
|
|
2150
|
-
return None
|
|
2151
|
-
|
|
2152
|
-
def _register_routes(self, router):
|
|
2153
|
-
"""
|
|
2154
|
-
Register routes for this agent
|
|
2155
|
-
|
|
2156
|
-
This method ensures proper route registration by handling the routes
|
|
2157
|
-
directly in AgentBase rather than inheriting from SWMLService.
|
|
2158
|
-
|
|
2159
|
-
Args:
|
|
2160
|
-
router: FastAPI router to register routes with
|
|
2161
|
-
"""
|
|
2162
|
-
# Health check endpoints are now registered directly on the main app
|
|
2163
|
-
|
|
2164
|
-
# Root endpoint (handles both with and without trailing slash)
|
|
2165
|
-
@router.get("/")
|
|
2166
|
-
@router.post("/")
|
|
2167
|
-
async def handle_root(request: Request, response: Response):
|
|
2168
|
-
"""Handle GET/POST requests to the root endpoint"""
|
|
2169
|
-
return await self._handle_root_request(request)
|
|
2170
|
-
|
|
2171
|
-
# Debug endpoint - Both versions
|
|
2172
|
-
@router.get("/debug")
|
|
2173
|
-
@router.get("/debug/")
|
|
2174
|
-
@router.post("/debug")
|
|
2175
|
-
@router.post("/debug/")
|
|
2176
|
-
async def handle_debug(request: Request):
|
|
2177
|
-
"""Handle GET/POST requests to the debug endpoint"""
|
|
2178
|
-
return await self._handle_debug_request(request)
|
|
2179
|
-
|
|
2180
|
-
# SWAIG endpoint - Both versions
|
|
2181
|
-
@router.get("/swaig")
|
|
2182
|
-
@router.get("/swaig/")
|
|
2183
|
-
@router.post("/swaig")
|
|
2184
|
-
@router.post("/swaig/")
|
|
2185
|
-
async def handle_swaig(request: Request, response: Response):
|
|
2186
|
-
"""Handle GET/POST requests to the SWAIG endpoint"""
|
|
2187
|
-
return await self._handle_swaig_request(request, response)
|
|
2188
|
-
|
|
2189
|
-
# Post prompt endpoint - Both versions
|
|
2190
|
-
@router.get("/post_prompt")
|
|
2191
|
-
@router.get("/post_prompt/")
|
|
2192
|
-
@router.post("/post_prompt")
|
|
2193
|
-
@router.post("/post_prompt/")
|
|
2194
|
-
async def handle_post_prompt(request: Request):
|
|
2195
|
-
"""Handle GET/POST requests to the post_prompt endpoint"""
|
|
2196
|
-
return await self._handle_post_prompt_request(request)
|
|
2197
|
-
|
|
2198
|
-
# Check for input endpoint - Both versions
|
|
2199
|
-
@router.get("/check_for_input")
|
|
2200
|
-
@router.get("/check_for_input/")
|
|
2201
|
-
@router.post("/check_for_input")
|
|
2202
|
-
@router.post("/check_for_input/")
|
|
2203
|
-
async def handle_check_for_input(request: Request):
|
|
2204
|
-
"""Handle GET/POST requests to the check_for_input endpoint"""
|
|
2205
|
-
return await self._handle_check_for_input_request(request)
|
|
2206
|
-
|
|
2207
|
-
# Register callback routes for routing callbacks if available
|
|
2208
|
-
if hasattr(self, '_routing_callbacks') and self._routing_callbacks:
|
|
2209
|
-
for callback_path, callback_fn in self._routing_callbacks.items():
|
|
2210
|
-
# Skip the root path as it's already handled
|
|
2211
|
-
if callback_path == "/":
|
|
2212
|
-
continue
|
|
2213
|
-
|
|
2214
|
-
# Register both with and without trailing slash
|
|
2215
|
-
path = callback_path.rstrip("/")
|
|
2216
|
-
path_with_slash = f"{path}/"
|
|
2217
|
-
|
|
2218
|
-
@router.get(path)
|
|
2219
|
-
@router.get(path_with_slash)
|
|
2220
|
-
@router.post(path)
|
|
2221
|
-
@router.post(path_with_slash)
|
|
2222
|
-
async def handle_callback(request: Request, response: Response, cb_path=callback_path):
|
|
2223
|
-
"""Handle GET/POST requests to a registered callback path"""
|
|
2224
|
-
# Store the callback path in request state for _handle_request to use
|
|
2225
|
-
request.state.callback_path = cb_path
|
|
2226
|
-
return await self._handle_root_request(request)
|
|
2227
|
-
|
|
2228
|
-
self.log.info("callback_endpoint_registered", path=callback_path)
|
|
2229
|
-
|
|
2230
|
-
# ----------------------------------------------------------------------
|
|
2231
|
-
# AI Verb Configuration Methods
|
|
2232
|
-
# ----------------------------------------------------------------------
|
|
2233
|
-
|
|
2234
|
-
def add_hint(self, hint: str) -> 'AgentBase':
|
|
2235
|
-
"""
|
|
2236
|
-
Add a simple string hint to help the AI agent understand certain words better
|
|
2237
|
-
|
|
2238
|
-
Args:
|
|
2239
|
-
hint: The hint string to add
|
|
2240
|
-
|
|
2241
|
-
Returns:
|
|
2242
|
-
Self for method chaining
|
|
2243
|
-
"""
|
|
2244
|
-
if isinstance(hint, str) and hint:
|
|
2245
|
-
self._hints.append(hint)
|
|
2246
|
-
return self
|
|
2247
|
-
|
|
2248
|
-
def add_hints(self, hints: List[str]) -> 'AgentBase':
|
|
2249
|
-
"""
|
|
2250
|
-
Add multiple string hints
|
|
2251
|
-
|
|
2252
|
-
Args:
|
|
2253
|
-
hints: List of hint strings
|
|
2254
|
-
|
|
2255
|
-
Returns:
|
|
2256
|
-
Self for method chaining
|
|
2257
|
-
"""
|
|
2258
|
-
if hints and isinstance(hints, list):
|
|
2259
|
-
for hint in hints:
|
|
2260
|
-
if isinstance(hint, str) and hint:
|
|
2261
|
-
self._hints.append(hint)
|
|
2262
|
-
return self
|
|
2263
|
-
|
|
2264
|
-
def add_pattern_hint(self,
|
|
2265
|
-
hint: str,
|
|
2266
|
-
pattern: str,
|
|
2267
|
-
replace: str,
|
|
2268
|
-
ignore_case: bool = False) -> 'AgentBase':
|
|
2269
|
-
"""
|
|
2270
|
-
Add a complex hint with pattern matching
|
|
2271
|
-
|
|
2272
|
-
Args:
|
|
2273
|
-
hint: The hint to match
|
|
2274
|
-
pattern: Regular expression pattern
|
|
2275
|
-
replace: Text to replace the hint with
|
|
2276
|
-
ignore_case: Whether to ignore case when matching
|
|
2277
|
-
|
|
2278
|
-
Returns:
|
|
2279
|
-
Self for method chaining
|
|
2280
|
-
"""
|
|
2281
|
-
if hint and pattern and replace:
|
|
2282
|
-
self._hints.append({
|
|
2283
|
-
"hint": hint,
|
|
2284
|
-
"pattern": pattern,
|
|
2285
|
-
"replace": replace,
|
|
2286
|
-
"ignore_case": ignore_case
|
|
2287
|
-
})
|
|
2288
|
-
return self
|
|
2289
|
-
|
|
2290
|
-
def add_language(self,
|
|
2291
|
-
name: str,
|
|
2292
|
-
code: str,
|
|
2293
|
-
voice: str,
|
|
2294
|
-
speech_fillers: Optional[List[str]] = None,
|
|
2295
|
-
function_fillers: Optional[List[str]] = None,
|
|
2296
|
-
engine: Optional[str] = None,
|
|
2297
|
-
model: Optional[str] = None) -> 'AgentBase':
|
|
2298
|
-
"""
|
|
2299
|
-
Add a language configuration to support multilingual conversations
|
|
2300
|
-
|
|
2301
|
-
Args:
|
|
2302
|
-
name: Name of the language (e.g., "English", "French")
|
|
2303
|
-
code: Language code (e.g., "en-US", "fr-FR")
|
|
2304
|
-
voice: TTS voice to use. Can be a simple name (e.g., "en-US-Neural2-F")
|
|
2305
|
-
or a combined format "engine.voice:model" (e.g., "elevenlabs.josh:eleven_turbo_v2_5")
|
|
2306
|
-
speech_fillers: Optional list of filler phrases for natural speech
|
|
2307
|
-
function_fillers: Optional list of filler phrases during function calls
|
|
2308
|
-
engine: Optional explicit engine name (e.g., "elevenlabs", "rime")
|
|
2309
|
-
model: Optional explicit model name (e.g., "eleven_turbo_v2_5", "arcana")
|
|
2310
|
-
|
|
2311
|
-
Returns:
|
|
2312
|
-
Self for method chaining
|
|
2313
|
-
|
|
2314
|
-
Examples:
|
|
2315
|
-
# Simple voice name
|
|
2316
|
-
agent.add_language("English", "en-US", "en-US-Neural2-F")
|
|
2317
|
-
|
|
2318
|
-
# Explicit parameters
|
|
2319
|
-
agent.add_language("English", "en-US", "josh", engine="elevenlabs", model="eleven_turbo_v2_5")
|
|
2320
|
-
|
|
2321
|
-
# Combined format
|
|
2322
|
-
agent.add_language("English", "en-US", "elevenlabs.josh:eleven_turbo_v2_5")
|
|
2323
|
-
"""
|
|
2324
|
-
language = {
|
|
2325
|
-
"name": name,
|
|
2326
|
-
"code": code
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
# Handle voice formatting (either explicit params or combined string)
|
|
2330
|
-
if engine or model:
|
|
2331
|
-
# Use explicit parameters if provided
|
|
2332
|
-
language["voice"] = voice
|
|
2333
|
-
if engine:
|
|
2334
|
-
language["engine"] = engine
|
|
2335
|
-
if model:
|
|
2336
|
-
language["model"] = model
|
|
2337
|
-
elif "." in voice and ":" in voice:
|
|
2338
|
-
# Parse combined string format: "engine.voice:model"
|
|
2339
|
-
try:
|
|
2340
|
-
engine_voice, model_part = voice.split(":", 1)
|
|
2341
|
-
engine_part, voice_part = engine_voice.split(".", 1)
|
|
2342
|
-
|
|
2343
|
-
language["voice"] = voice_part
|
|
2344
|
-
language["engine"] = engine_part
|
|
2345
|
-
language["model"] = model_part
|
|
2346
|
-
except ValueError:
|
|
2347
|
-
# If parsing fails, use the voice string as-is
|
|
2348
|
-
language["voice"] = voice
|
|
2349
|
-
else:
|
|
2350
|
-
# Simple voice string
|
|
2351
|
-
language["voice"] = voice
|
|
2352
|
-
|
|
2353
|
-
# Add fillers if provided
|
|
2354
|
-
if speech_fillers and function_fillers:
|
|
2355
|
-
language["speech_fillers"] = speech_fillers
|
|
2356
|
-
language["function_fillers"] = function_fillers
|
|
2357
|
-
elif speech_fillers or function_fillers:
|
|
2358
|
-
# If only one type of fillers is provided, use the deprecated "fillers" field
|
|
2359
|
-
fillers = speech_fillers or function_fillers
|
|
2360
|
-
language["fillers"] = fillers
|
|
2361
|
-
|
|
2362
|
-
self._languages.append(language)
|
|
2363
|
-
return self
|
|
2364
|
-
|
|
2365
|
-
def set_languages(self, languages: List[Dict[str, Any]]) -> 'AgentBase':
|
|
2366
|
-
"""
|
|
2367
|
-
Set all language configurations at once
|
|
2368
|
-
|
|
2369
|
-
Args:
|
|
2370
|
-
languages: List of language configuration dictionaries
|
|
2371
|
-
|
|
2372
|
-
Returns:
|
|
2373
|
-
Self for method chaining
|
|
2374
|
-
"""
|
|
2375
|
-
if languages and isinstance(languages, list):
|
|
2376
|
-
self._languages = languages
|
|
2377
|
-
return self
|
|
2378
|
-
|
|
2379
|
-
def add_pronunciation(self,
|
|
2380
|
-
replace: str,
|
|
2381
|
-
with_text: str,
|
|
2382
|
-
ignore_case: bool = False) -> 'AgentBase':
|
|
2383
|
-
"""
|
|
2384
|
-
Add a pronunciation rule to help the AI speak certain words correctly
|
|
2385
|
-
|
|
2386
|
-
Args:
|
|
2387
|
-
replace: The expression to replace
|
|
2388
|
-
with_text: The phonetic spelling to use instead
|
|
2389
|
-
ignore_case: Whether to ignore case when matching
|
|
2390
|
-
|
|
2391
|
-
Returns:
|
|
2392
|
-
Self for method chaining
|
|
2393
|
-
"""
|
|
2394
|
-
if replace and with_text:
|
|
2395
|
-
rule = {
|
|
2396
|
-
"replace": replace,
|
|
2397
|
-
"with": with_text
|
|
2398
|
-
}
|
|
2399
|
-
if ignore_case:
|
|
2400
|
-
rule["ignore_case"] = True
|
|
2401
|
-
|
|
2402
|
-
self._pronounce.append(rule)
|
|
2403
|
-
return self
|
|
2404
|
-
|
|
2405
|
-
def set_pronunciations(self, pronunciations: List[Dict[str, Any]]) -> 'AgentBase':
|
|
2406
|
-
"""
|
|
2407
|
-
Set all pronunciation rules at once
|
|
2408
|
-
|
|
2409
|
-
Args:
|
|
2410
|
-
pronunciations: List of pronunciation rule dictionaries
|
|
2411
|
-
|
|
2412
|
-
Returns:
|
|
2413
|
-
Self for method chaining
|
|
2414
|
-
"""
|
|
2415
|
-
if pronunciations and isinstance(pronunciations, list):
|
|
2416
|
-
self._pronounce = pronunciations
|
|
2417
|
-
return self
|
|
2418
|
-
|
|
2419
|
-
def set_param(self, key: str, value: Any) -> 'AgentBase':
|
|
2420
|
-
"""
|
|
2421
|
-
Set a single AI parameter
|
|
2422
|
-
|
|
2423
|
-
Args:
|
|
2424
|
-
key: Parameter name
|
|
2425
|
-
value: Parameter value
|
|
2426
|
-
|
|
2427
|
-
Returns:
|
|
2428
|
-
Self for method chaining
|
|
2429
|
-
"""
|
|
2430
|
-
if key:
|
|
2431
|
-
self._params[key] = value
|
|
2432
|
-
return self
|
|
2433
|
-
|
|
2434
|
-
def set_params(self, params: Dict[str, Any]) -> 'AgentBase':
|
|
2435
|
-
"""
|
|
2436
|
-
Set multiple AI parameters at once
|
|
2437
|
-
|
|
2438
|
-
Args:
|
|
2439
|
-
params: Dictionary of parameter name/value pairs
|
|
2440
|
-
|
|
2441
|
-
Returns:
|
|
2442
|
-
Self for method chaining
|
|
2443
|
-
"""
|
|
2444
|
-
if params and isinstance(params, dict):
|
|
2445
|
-
self._params.update(params)
|
|
2446
|
-
return self
|
|
2447
|
-
|
|
2448
|
-
def set_global_data(self, data: Dict[str, Any]) -> 'AgentBase':
|
|
2449
|
-
"""
|
|
2450
|
-
Set the global data available to the AI throughout the conversation
|
|
2451
|
-
|
|
2452
|
-
Args:
|
|
2453
|
-
data: Dictionary of global data
|
|
2454
|
-
|
|
2455
|
-
Returns:
|
|
2456
|
-
Self for method chaining
|
|
2457
|
-
"""
|
|
2458
|
-
if data and isinstance(data, dict):
|
|
2459
|
-
self._global_data = data
|
|
2460
|
-
return self
|
|
2461
|
-
|
|
2462
|
-
def update_global_data(self, data: Dict[str, Any]) -> 'AgentBase':
|
|
2463
|
-
"""
|
|
2464
|
-
Update the global data with new values
|
|
2465
|
-
|
|
2466
|
-
Args:
|
|
2467
|
-
data: Dictionary of global data to update
|
|
2468
|
-
|
|
2469
|
-
Returns:
|
|
2470
|
-
Self for method chaining
|
|
2471
|
-
"""
|
|
2472
|
-
if data and isinstance(data, dict):
|
|
2473
|
-
self._global_data.update(data)
|
|
2474
|
-
return self
|
|
2475
|
-
|
|
2476
|
-
def set_native_functions(self, function_names: List[str]) -> 'AgentBase':
|
|
2477
|
-
"""
|
|
2478
|
-
Set the list of native functions to enable
|
|
2479
|
-
|
|
2480
|
-
Args:
|
|
2481
|
-
function_names: List of native function names
|
|
2482
|
-
|
|
2483
|
-
Returns:
|
|
2484
|
-
Self for method chaining
|
|
2485
|
-
"""
|
|
2486
|
-
if function_names and isinstance(function_names, list):
|
|
2487
|
-
self.native_functions = [name for name in function_names if isinstance(name, str)]
|
|
2488
|
-
return self
|
|
2489
|
-
|
|
2490
|
-
def add_function_include(self, url: str, functions: List[str], meta_data: Optional[Dict[str, Any]] = None) -> 'AgentBase':
|
|
2491
|
-
"""
|
|
2492
|
-
Add a remote function include to the SWAIG configuration
|
|
2493
|
-
|
|
2494
|
-
Args:
|
|
2495
|
-
url: URL to fetch remote functions from
|
|
2496
|
-
functions: List of function names to include
|
|
2497
|
-
meta_data: Optional metadata to include with the function include
|
|
2498
|
-
|
|
2499
|
-
Returns:
|
|
2500
|
-
Self for method chaining
|
|
2501
|
-
"""
|
|
2502
|
-
if url and functions and isinstance(functions, list):
|
|
2503
|
-
include = {
|
|
2504
|
-
"url": url,
|
|
2505
|
-
"functions": functions
|
|
2506
|
-
}
|
|
2507
|
-
if meta_data and isinstance(meta_data, dict):
|
|
2508
|
-
include["meta_data"] = meta_data
|
|
2509
|
-
|
|
2510
|
-
self._function_includes.append(include)
|
|
2511
|
-
return self
|
|
2512
|
-
|
|
2513
|
-
def set_function_includes(self, includes: List[Dict[str, Any]]) -> 'AgentBase':
|
|
2514
|
-
"""
|
|
2515
|
-
Set the complete list of function includes
|
|
2516
|
-
|
|
2517
|
-
Args:
|
|
2518
|
-
includes: List of include objects, each with url and functions properties
|
|
2519
|
-
|
|
2520
|
-
Returns:
|
|
2521
|
-
Self for method chaining
|
|
2522
|
-
"""
|
|
2523
|
-
if includes and isinstance(includes, list):
|
|
2524
|
-
# Validate each include has required properties
|
|
2525
|
-
valid_includes = []
|
|
2526
|
-
for include in includes:
|
|
2527
|
-
if isinstance(include, dict) and "url" in include and "functions" in include:
|
|
2528
|
-
if isinstance(include["functions"], list):
|
|
2529
|
-
valid_includes.append(include)
|
|
2530
|
-
|
|
2531
|
-
self._function_includes = valid_includes
|
|
2532
|
-
return self
|
|
2533
|
-
|
|
2534
|
-
def enable_sip_routing(self, auto_map: bool = True, path: str = "/sip") -> 'AgentBase':
|
|
2535
|
-
"""
|
|
2536
|
-
Enable SIP-based routing for this agent
|
|
2537
|
-
|
|
2538
|
-
This allows the agent to automatically route SIP requests based on SIP usernames.
|
|
2539
|
-
When enabled, an endpoint at the specified path is automatically created
|
|
2540
|
-
that will handle SIP requests and deliver them to this agent.
|
|
2541
|
-
|
|
2542
|
-
Args:
|
|
2543
|
-
auto_map: Whether to automatically map common SIP usernames to this agent
|
|
2544
|
-
(based on the agent name and route path)
|
|
2545
|
-
path: The path to register the SIP routing endpoint (default: "/sip")
|
|
2546
|
-
|
|
2547
|
-
Returns:
|
|
2548
|
-
Self for method chaining
|
|
2549
|
-
"""
|
|
2550
|
-
# Create a routing callback that handles SIP usernames
|
|
2551
|
-
def sip_routing_callback(request: Request, body: Dict[str, Any]) -> Optional[str]:
|
|
2552
|
-
# Extract SIP username from the request body
|
|
2553
|
-
sip_username = self.extract_sip_username(body)
|
|
2554
|
-
|
|
2555
|
-
if sip_username:
|
|
2556
|
-
self.log.info("sip_username_extracted", username=sip_username)
|
|
2557
|
-
|
|
2558
|
-
# Check if this username is registered with this agent
|
|
2559
|
-
if hasattr(self, '_sip_usernames') and sip_username.lower() in self._sip_usernames:
|
|
2560
|
-
self.log.info("sip_username_matched", username=sip_username)
|
|
2561
|
-
# This route is already being handled by the agent, no need to redirect
|
|
2562
|
-
return None
|
|
2563
|
-
else:
|
|
2564
|
-
self.log.info("sip_username_not_matched", username=sip_username)
|
|
2565
|
-
# Not registered with this agent, let routing continue
|
|
2566
|
-
|
|
2567
|
-
return None
|
|
2568
|
-
|
|
2569
|
-
# Register the callback with the SWMLService, specifying the path
|
|
2570
|
-
self.register_routing_callback(sip_routing_callback, path=path)
|
|
2571
|
-
|
|
2572
|
-
# Auto-map common usernames if requested
|
|
2573
|
-
if auto_map:
|
|
2574
|
-
self.auto_map_sip_usernames()
|
|
2575
|
-
|
|
2576
|
-
return self
|
|
2577
|
-
|
|
2578
|
-
def register_sip_username(self, sip_username: str) -> 'AgentBase':
|
|
2579
|
-
"""
|
|
2580
|
-
Register a SIP username that should be routed to this agent
|
|
2581
|
-
|
|
2582
|
-
Args:
|
|
2583
|
-
sip_username: SIP username to register
|
|
2584
|
-
|
|
2585
|
-
Returns:
|
|
2586
|
-
Self for method chaining
|
|
2587
|
-
"""
|
|
2588
|
-
if not hasattr(self, '_sip_usernames'):
|
|
2589
|
-
self._sip_usernames = set()
|
|
2590
|
-
|
|
2591
|
-
self._sip_usernames.add(sip_username.lower())
|
|
2592
|
-
self.log.info("sip_username_registered", username=sip_username)
|
|
2593
|
-
|
|
2594
|
-
return self
|
|
2595
|
-
|
|
2596
|
-
def auto_map_sip_usernames(self) -> 'AgentBase':
|
|
2597
|
-
"""
|
|
2598
|
-
Automatically register common SIP usernames based on this agent's
|
|
2599
|
-
name and route
|
|
2600
|
-
|
|
2601
|
-
Returns:
|
|
2602
|
-
Self for method chaining
|
|
2603
|
-
"""
|
|
2604
|
-
# Register username based on agent name
|
|
2605
|
-
clean_name = re.sub(r'[^a-z0-9_]', '', self.name.lower())
|
|
2606
|
-
if clean_name:
|
|
2607
|
-
self.register_sip_username(clean_name)
|
|
2608
|
-
|
|
2609
|
-
# Register username based on route (without slashes)
|
|
2610
|
-
clean_route = re.sub(r'[^a-z0-9_]', '', self.route.lower())
|
|
2611
|
-
if clean_route and clean_route != clean_name:
|
|
2612
|
-
self.register_sip_username(clean_route)
|
|
2613
|
-
|
|
2614
|
-
# Register common variations if they make sense
|
|
2615
|
-
if len(clean_name) > 3:
|
|
2616
|
-
# Register without vowels
|
|
2617
|
-
no_vowels = re.sub(r'[aeiou]', '', clean_name)
|
|
2618
|
-
if no_vowels != clean_name and len(no_vowels) > 2:
|
|
2619
|
-
self.register_sip_username(no_vowels)
|
|
2620
|
-
|
|
2621
|
-
return self
|
|
2622
|
-
|
|
2623
|
-
def set_web_hook_url(self, url: str) -> 'AgentBase':
|
|
2624
|
-
"""
|
|
2625
|
-
Override the default web_hook_url with a supplied URL string
|
|
2626
|
-
|
|
2627
|
-
Args:
|
|
2628
|
-
url: The URL to use for SWAIG function webhooks
|
|
2629
|
-
|
|
2630
|
-
Returns:
|
|
2631
|
-
Self for method chaining
|
|
2632
|
-
"""
|
|
2633
|
-
self._web_hook_url_override = url
|
|
2634
|
-
return self
|
|
2635
|
-
|
|
2636
|
-
def set_post_prompt_url(self, url: str) -> 'AgentBase':
|
|
2637
|
-
"""
|
|
2638
|
-
Override the default post_prompt_url with a supplied URL string
|
|
2639
|
-
|
|
2640
|
-
Args:
|
|
2641
|
-
url: The URL to use for post-prompt summary delivery
|
|
2642
|
-
|
|
2643
|
-
Returns:
|
|
2644
|
-
Self for method chaining
|
|
2645
|
-
"""
|
|
2646
|
-
self._post_prompt_url_override = url
|
|
2647
|
-
return self
|
|
2648
|
-
|
|
2649
|
-
async def _handle_swaig_request(self, request: Request, response: Response):
|
|
2650
|
-
"""Handle GET/POST requests to the SWAIG endpoint"""
|
|
2651
|
-
req_log = self.log.bind(
|
|
2652
|
-
endpoint="swaig",
|
|
2653
|
-
method=request.method,
|
|
2654
|
-
path=request.url.path
|
|
2655
|
-
)
|
|
2656
|
-
|
|
2657
|
-
req_log.debug("endpoint_called")
|
|
2658
|
-
|
|
2659
|
-
try:
|
|
2660
|
-
# Check auth
|
|
2661
|
-
if not self._check_basic_auth(request):
|
|
2662
|
-
req_log.warning("unauthorized_access_attempt")
|
|
2663
|
-
response.headers["WWW-Authenticate"] = "Basic"
|
|
2664
|
-
return Response(
|
|
2665
|
-
content=json.dumps({"error": "Unauthorized"}),
|
|
2666
|
-
status_code=401,
|
|
2667
|
-
headers={"WWW-Authenticate": "Basic"},
|
|
2668
|
-
media_type="application/json"
|
|
2669
|
-
)
|
|
2670
|
-
|
|
2671
|
-
# Handle differently based on method
|
|
2672
|
-
if request.method == "GET":
|
|
2673
|
-
# For GET requests, return the SWML document (same as root endpoint)
|
|
2674
|
-
call_id = request.query_params.get("call_id")
|
|
2675
|
-
swml = self._render_swml(call_id)
|
|
2676
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
|
2677
|
-
return Response(
|
|
2678
|
-
content=swml,
|
|
2679
|
-
media_type="application/json"
|
|
2680
|
-
)
|
|
2681
|
-
|
|
2682
|
-
# For POST requests, process SWAIG function calls
|
|
2683
|
-
try:
|
|
2684
|
-
body = await request.json()
|
|
2685
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
|
2686
|
-
if body:
|
|
2687
|
-
req_log.debug("request_body", body=json.dumps(body))
|
|
2688
|
-
except Exception as e:
|
|
2689
|
-
req_log.error("error_parsing_request_body", error=str(e))
|
|
2690
|
-
body = {}
|
|
2691
|
-
|
|
2692
|
-
# Extract function name
|
|
2693
|
-
function_name = body.get("function")
|
|
2694
|
-
if not function_name:
|
|
2695
|
-
req_log.warning("missing_function_name")
|
|
2696
|
-
return Response(
|
|
2697
|
-
content=json.dumps({"error": "Missing function name"}),
|
|
2698
|
-
status_code=400,
|
|
2699
|
-
media_type="application/json"
|
|
2700
|
-
)
|
|
2701
|
-
|
|
2702
|
-
# Add function info to logger
|
|
2703
|
-
req_log = req_log.bind(function=function_name)
|
|
2704
|
-
req_log.debug("function_call_received")
|
|
2705
|
-
|
|
2706
|
-
# Extract arguments
|
|
2707
|
-
args = {}
|
|
2708
|
-
if "argument" in body and isinstance(body["argument"], dict):
|
|
2709
|
-
if "parsed" in body["argument"] and isinstance(body["argument"]["parsed"], list) and body["argument"]["parsed"]:
|
|
2710
|
-
args = body["argument"]["parsed"][0]
|
|
2711
|
-
req_log.debug("parsed_arguments", args=json.dumps(args))
|
|
2712
|
-
elif "raw" in body["argument"]:
|
|
2713
|
-
try:
|
|
2714
|
-
args = json.loads(body["argument"]["raw"])
|
|
2715
|
-
req_log.debug("raw_arguments_parsed", args=json.dumps(args))
|
|
2716
|
-
except Exception as e:
|
|
2717
|
-
req_log.error("error_parsing_raw_arguments", error=str(e), raw=body["argument"]["raw"])
|
|
2718
|
-
|
|
2719
|
-
# Get call_id from body
|
|
2720
|
-
call_id = body.get("call_id")
|
|
2721
|
-
if call_id:
|
|
2722
|
-
req_log = req_log.bind(call_id=call_id)
|
|
2723
|
-
req_log.debug("call_id_identified")
|
|
2724
|
-
|
|
2725
|
-
# SECURITY BYPASS FOR DEBUGGING - make all functions work regardless of token
|
|
2726
|
-
# We'll log the attempt but allow it through
|
|
2727
|
-
token = request.query_params.get("token")
|
|
2728
|
-
if token:
|
|
2729
|
-
req_log.debug("token_found", token_length=len(token))
|
|
2730
|
-
|
|
2731
|
-
# Check token validity but don't reject the request
|
|
2732
|
-
if hasattr(self, '_session_manager') and function_name in self._swaig_functions:
|
|
2733
|
-
is_valid = self._session_manager.validate_tool_token(function_name, token, call_id)
|
|
2734
|
-
if is_valid:
|
|
2735
|
-
req_log.debug("token_valid")
|
|
2736
|
-
else:
|
|
2737
|
-
# Log but continue anyway for debugging
|
|
2738
|
-
req_log.warning("token_invalid")
|
|
2739
|
-
if hasattr(self._session_manager, 'debug_token'):
|
|
2740
|
-
debug_info = self._session_manager.debug_token(token)
|
|
2741
|
-
req_log.debug("token_debug", debug=json.dumps(debug_info))
|
|
2742
|
-
|
|
2743
|
-
# Call the function
|
|
2744
|
-
try:
|
|
2745
|
-
result = self.on_function_call(function_name, args, body)
|
|
2746
|
-
|
|
2747
|
-
# Convert result to dict if needed
|
|
2748
|
-
if isinstance(result, SwaigFunctionResult):
|
|
2749
|
-
result_dict = result.to_dict()
|
|
2750
|
-
elif isinstance(result, dict):
|
|
2751
|
-
result_dict = result
|
|
2752
|
-
else:
|
|
2753
|
-
result_dict = {"response": str(result)}
|
|
2754
|
-
|
|
2755
|
-
req_log.info("function_executed_successfully")
|
|
2756
|
-
req_log.debug("function_result", result=json.dumps(result_dict))
|
|
2757
|
-
return result_dict
|
|
2758
|
-
except Exception as e:
|
|
2759
|
-
req_log.error("function_execution_error", error=str(e))
|
|
2760
|
-
return {"error": str(e), "function": function_name}
|
|
2761
|
-
|
|
2762
|
-
except Exception as e:
|
|
2763
|
-
req_log.error("request_failed", error=str(e))
|
|
2764
|
-
return Response(
|
|
2765
|
-
content=json.dumps({"error": str(e)}),
|
|
2766
|
-
status_code=500,
|
|
2767
|
-
media_type="application/json"
|
|
2768
|
-
)
|
|
2769
|
-
|
|
2770
|
-
async def _handle_root_request(self, request: Request):
|
|
2771
|
-
"""Handle GET/POST requests to the root endpoint"""
|
|
2772
|
-
# Auto-detect proxy on first request if not explicitly configured
|
|
2773
|
-
if not getattr(self, '_proxy_detection_done', False) and not getattr(self, '_proxy_url_base', None):
|
|
2774
|
-
# Check for proxy headers
|
|
2775
|
-
forwarded_host = request.headers.get("X-Forwarded-Host")
|
|
2776
|
-
forwarded_proto = request.headers.get("X-Forwarded-Proto", "http")
|
|
2777
|
-
|
|
2778
|
-
if forwarded_host:
|
|
2779
|
-
# Set proxy_url_base on both self and super() to ensure it's shared
|
|
2780
|
-
self._proxy_url_base = f"{forwarded_proto}://{forwarded_host}"
|
|
2781
|
-
if hasattr(super(), '_proxy_url_base'):
|
|
2782
|
-
# Ensure parent class has the same proxy URL
|
|
2783
|
-
super()._proxy_url_base = self._proxy_url_base
|
|
2784
|
-
|
|
2785
|
-
self.log.info("proxy_auto_detected", proxy_url_base=self._proxy_url_base,
|
|
2786
|
-
source="X-Forwarded headers")
|
|
2787
|
-
self._proxy_detection_done = True
|
|
2788
|
-
|
|
2789
|
-
# Also set the detection flag on parent
|
|
2790
|
-
if hasattr(super(), '_proxy_detection_done'):
|
|
2791
|
-
super()._proxy_detection_done = True
|
|
2792
|
-
# If no explicit proxy headers, try the parent class detection method if it exists
|
|
2793
|
-
elif hasattr(super(), '_detect_proxy_from_request'):
|
|
2794
|
-
# Call the parent's detection method
|
|
2795
|
-
super()._detect_proxy_from_request(request)
|
|
2796
|
-
# Copy the result to our class
|
|
2797
|
-
if hasattr(super(), '_proxy_url_base') and getattr(super(), '_proxy_url_base', None):
|
|
2798
|
-
self._proxy_url_base = super()._proxy_url_base
|
|
2799
|
-
self._proxy_detection_done = True
|
|
2800
|
-
|
|
2801
|
-
# Check if this is a callback path request
|
|
2802
|
-
callback_path = getattr(request.state, "callback_path", None)
|
|
2803
|
-
|
|
2804
|
-
req_log = self.log.bind(
|
|
2805
|
-
endpoint="root" if not callback_path else f"callback:{callback_path}",
|
|
2806
|
-
method=request.method,
|
|
2807
|
-
path=request.url.path
|
|
2808
|
-
)
|
|
2809
|
-
|
|
2810
|
-
req_log.debug("endpoint_called")
|
|
2811
|
-
|
|
2812
|
-
try:
|
|
2813
|
-
# Check auth
|
|
2814
|
-
if not self._check_basic_auth(request):
|
|
2815
|
-
req_log.warning("unauthorized_access_attempt")
|
|
2816
|
-
return Response(
|
|
2817
|
-
content=json.dumps({"error": "Unauthorized"}),
|
|
2818
|
-
status_code=401,
|
|
2819
|
-
headers={"WWW-Authenticate": "Basic"},
|
|
2820
|
-
media_type="application/json"
|
|
2821
|
-
)
|
|
2822
|
-
|
|
2823
|
-
# Try to parse request body for POST
|
|
2824
|
-
body = {}
|
|
2825
|
-
call_id = None
|
|
2826
|
-
|
|
2827
|
-
if request.method == "POST":
|
|
2828
|
-
# Check if body is empty first
|
|
2829
|
-
raw_body = await request.body()
|
|
2830
|
-
if raw_body:
|
|
2831
|
-
try:
|
|
2832
|
-
body = await request.json()
|
|
2833
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
|
2834
|
-
if body:
|
|
2835
|
-
req_log.debug("request_body")
|
|
2836
|
-
except Exception as e:
|
|
2837
|
-
req_log.warning("error_parsing_request_body", error=str(e))
|
|
2838
|
-
# Continue processing with empty body
|
|
2839
|
-
body = {}
|
|
2840
|
-
else:
|
|
2841
|
-
req_log.debug("empty_request_body")
|
|
2842
|
-
|
|
2843
|
-
# Get call_id from body if present
|
|
2844
|
-
call_id = body.get("call_id")
|
|
2845
|
-
else:
|
|
2846
|
-
# Get call_id from query params for GET
|
|
2847
|
-
call_id = request.query_params.get("call_id")
|
|
2848
|
-
|
|
2849
|
-
# Add call_id to logger if any
|
|
2850
|
-
if call_id:
|
|
2851
|
-
req_log = req_log.bind(call_id=call_id)
|
|
2852
|
-
req_log.debug("call_id_identified")
|
|
2853
|
-
|
|
2854
|
-
# Check if this is a callback path and we need to apply routing
|
|
2855
|
-
if callback_path and hasattr(self, '_routing_callbacks') and callback_path in self._routing_callbacks:
|
|
2856
|
-
callback_fn = self._routing_callbacks[callback_path]
|
|
2857
|
-
|
|
2858
|
-
if request.method == "POST" and body:
|
|
2859
|
-
req_log.debug("processing_routing_callback", path=callback_path)
|
|
2860
|
-
# Call the routing callback
|
|
2861
|
-
try:
|
|
2862
|
-
route = callback_fn(request, body)
|
|
2863
|
-
if route is not None:
|
|
2864
|
-
req_log.info("routing_request", route=route)
|
|
2865
|
-
# Return a redirect to the new route
|
|
2866
|
-
return Response(
|
|
2867
|
-
status_code=307, # 307 Temporary Redirect preserves the method and body
|
|
2868
|
-
headers={"Location": route}
|
|
2869
|
-
)
|
|
2870
|
-
except Exception as e:
|
|
2871
|
-
req_log.error("error_in_routing_callback", error=str(e))
|
|
2872
|
-
|
|
2873
|
-
# Allow subclasses to inspect/modify the request
|
|
2874
|
-
modifications = None
|
|
2875
|
-
try:
|
|
2876
|
-
modifications = self.on_swml_request(body, callback_path, request)
|
|
2877
|
-
if modifications:
|
|
2878
|
-
req_log.debug("request_modifications_applied")
|
|
2879
|
-
except Exception as e:
|
|
2880
|
-
req_log.error("error_in_request_modifier", error=str(e))
|
|
2881
|
-
|
|
2882
|
-
# Render SWML
|
|
2883
|
-
swml = self._render_swml(call_id, modifications)
|
|
2884
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
|
2885
|
-
|
|
2886
|
-
# Return as JSON
|
|
2887
|
-
req_log.info("request_successful")
|
|
2888
|
-
return Response(
|
|
2889
|
-
content=swml,
|
|
2890
|
-
media_type="application/json"
|
|
2891
|
-
)
|
|
2892
|
-
except Exception as e:
|
|
2893
|
-
req_log.error("request_failed", error=str(e))
|
|
2894
|
-
return Response(
|
|
2895
|
-
content=json.dumps({"error": str(e)}),
|
|
2896
|
-
status_code=500,
|
|
2897
|
-
media_type="application/json"
|
|
2898
|
-
)
|
|
2899
|
-
|
|
2900
|
-
async def _handle_debug_request(self, request: Request):
|
|
2901
|
-
"""Handle GET/POST requests to the debug endpoint"""
|
|
2902
|
-
req_log = self.log.bind(
|
|
2903
|
-
endpoint="debug",
|
|
2904
|
-
method=request.method,
|
|
2905
|
-
path=request.url.path
|
|
2906
|
-
)
|
|
2907
|
-
|
|
2908
|
-
req_log.debug("endpoint_called")
|
|
1108
|
+
agent_to_use.reset_document()
|
|
1109
|
+
|
|
1110
|
+
# Rebuild with 5-phase approach
|
|
1111
|
+
# PHASE 1: Pre-answer verbs
|
|
1112
|
+
for verb_name, verb_config in agent_to_use._pre_answer_verbs:
|
|
1113
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1114
|
+
|
|
1115
|
+
# PHASE 2: Answer verb (if auto_answer enabled)
|
|
1116
|
+
if agent_to_use._auto_answer:
|
|
1117
|
+
agent_to_use.add_verb("answer", agent_to_use._answer_config)
|
|
1118
|
+
|
|
1119
|
+
# PHASE 3: Post-answer verbs
|
|
1120
|
+
if agent_to_use._record_call:
|
|
1121
|
+
agent_to_use.add_verb("record_call", {
|
|
1122
|
+
"format": agent_to_use._record_format,
|
|
1123
|
+
"stereo": agent_to_use._record_stereo
|
|
1124
|
+
})
|
|
1125
|
+
for verb_name, verb_config in agent_to_use._post_answer_verbs:
|
|
1126
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1127
|
+
|
|
1128
|
+
# PHASE 4: AI verb
|
|
1129
|
+
agent_to_use.add_verb("ai", ai_config)
|
|
1130
|
+
|
|
1131
|
+
# PHASE 5: Post-AI verbs
|
|
1132
|
+
for verb_name, verb_config in agent_to_use._post_ai_verbs:
|
|
1133
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
2909
1134
|
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
if not self._check_basic_auth(request):
|
|
2913
|
-
req_log.warning("unauthorized_access_attempt")
|
|
2914
|
-
return Response(
|
|
2915
|
-
content=json.dumps({"error": "Unauthorized"}),
|
|
2916
|
-
status_code=401,
|
|
2917
|
-
headers={"WWW-Authenticate": "Basic"},
|
|
2918
|
-
media_type="application/json"
|
|
2919
|
-
)
|
|
2920
|
-
|
|
2921
|
-
# Get call_id from either query params (GET) or body (POST)
|
|
2922
|
-
call_id = None
|
|
2923
|
-
body = {}
|
|
2924
|
-
|
|
2925
|
-
if request.method == "POST":
|
|
2926
|
-
try:
|
|
2927
|
-
body = await request.json()
|
|
2928
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
|
2929
|
-
call_id = body.get("call_id")
|
|
2930
|
-
except Exception as e:
|
|
2931
|
-
req_log.warning("error_parsing_request_body", error=str(e))
|
|
2932
|
-
else:
|
|
2933
|
-
call_id = request.query_params.get("call_id")
|
|
2934
|
-
|
|
2935
|
-
# Add call_id to logger if any
|
|
2936
|
-
if call_id:
|
|
2937
|
-
req_log = req_log.bind(call_id=call_id)
|
|
2938
|
-
req_log.debug("call_id_identified")
|
|
2939
|
-
|
|
2940
|
-
# Allow subclasses to inspect/modify the request
|
|
2941
|
-
modifications = None
|
|
2942
|
-
try:
|
|
2943
|
-
modifications = self.on_swml_request(body, None, request)
|
|
2944
|
-
if modifications:
|
|
2945
|
-
req_log.debug("request_modifications_applied")
|
|
2946
|
-
except Exception as e:
|
|
2947
|
-
req_log.error("error_in_request_modifier", error=str(e))
|
|
2948
|
-
|
|
2949
|
-
# Render SWML
|
|
2950
|
-
swml = self._render_swml(call_id, modifications)
|
|
2951
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
|
2952
|
-
|
|
2953
|
-
# Return as JSON
|
|
2954
|
-
req_log.info("request_successful")
|
|
2955
|
-
return Response(
|
|
2956
|
-
content=swml,
|
|
2957
|
-
media_type="application/json",
|
|
2958
|
-
headers={"X-Debug": "true"}
|
|
2959
|
-
)
|
|
2960
|
-
except Exception as e:
|
|
2961
|
-
req_log.error("request_failed", error=str(e))
|
|
2962
|
-
return Response(
|
|
2963
|
-
content=json.dumps({"error": str(e)}),
|
|
2964
|
-
status_code=500,
|
|
2965
|
-
media_type="application/json"
|
|
2966
|
-
)
|
|
1135
|
+
# Return the rendered document as a string
|
|
1136
|
+
return agent_to_use.render_document()
|
|
2967
1137
|
|
|
2968
|
-
|
|
2969
|
-
"""
|
|
2970
|
-
|
|
2971
|
-
endpoint="post_prompt",
|
|
2972
|
-
method=request.method,
|
|
2973
|
-
path=request.url.path
|
|
2974
|
-
)
|
|
2975
|
-
|
|
2976
|
-
# Only log if not suppressed
|
|
2977
|
-
if not getattr(self, '_suppress_logs', False):
|
|
2978
|
-
req_log.debug("endpoint_called")
|
|
1138
|
+
def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
|
|
1139
|
+
"""
|
|
1140
|
+
Helper method to build webhook URLs consistently
|
|
2979
1141
|
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
req_log.warning("unauthorized_access_attempt")
|
|
2984
|
-
return Response(
|
|
2985
|
-
content=json.dumps({"error": "Unauthorized"}),
|
|
2986
|
-
status_code=401,
|
|
2987
|
-
headers={"WWW-Authenticate": "Basic"},
|
|
2988
|
-
media_type="application/json"
|
|
2989
|
-
)
|
|
2990
|
-
|
|
2991
|
-
# Extract call_id for use with token validation
|
|
2992
|
-
call_id = request.query_params.get("call_id")
|
|
2993
|
-
|
|
2994
|
-
# For POST requests, try to also get call_id from body
|
|
2995
|
-
if request.method == "POST":
|
|
2996
|
-
try:
|
|
2997
|
-
body_text = await request.body()
|
|
2998
|
-
if body_text:
|
|
2999
|
-
body_data = json.loads(body_text)
|
|
3000
|
-
if call_id is None:
|
|
3001
|
-
call_id = body_data.get("call_id")
|
|
3002
|
-
# Save body_data for later use
|
|
3003
|
-
setattr(request, "_post_prompt_body", body_data)
|
|
3004
|
-
except Exception as e:
|
|
3005
|
-
req_log.error("error_extracting_call_id", error=str(e))
|
|
3006
|
-
|
|
3007
|
-
# If we have a call_id, add it to the logger context
|
|
3008
|
-
if call_id:
|
|
3009
|
-
req_log = req_log.bind(call_id=call_id)
|
|
3010
|
-
|
|
3011
|
-
# Check token if provided
|
|
3012
|
-
token = request.query_params.get("token")
|
|
3013
|
-
token_validated = False
|
|
1142
|
+
Args:
|
|
1143
|
+
endpoint: The endpoint path (e.g., "swaig", "post_prompt")
|
|
1144
|
+
query_params: Optional query parameters to append
|
|
3014
1145
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
req_log.debug("token_valid")
|
|
3025
|
-
token_validated = True
|
|
3026
|
-
else:
|
|
3027
|
-
req_log.warning("invalid_token")
|
|
3028
|
-
# Debug information for token validation issues
|
|
3029
|
-
if hasattr(self._session_manager, 'debug_token'):
|
|
3030
|
-
debug_info = self._session_manager.debug_token(token)
|
|
3031
|
-
req_log.debug("token_debug", debug=json.dumps(debug_info))
|
|
3032
|
-
except Exception as e:
|
|
3033
|
-
req_log.error("token_validation_error", error=str(e))
|
|
3034
|
-
|
|
3035
|
-
# For GET requests, return the SWML document
|
|
3036
|
-
if request.method == "GET":
|
|
3037
|
-
swml = self._render_swml(call_id)
|
|
3038
|
-
req_log.debug("swml_rendered", swml_size=len(swml))
|
|
3039
|
-
return Response(
|
|
3040
|
-
content=swml,
|
|
3041
|
-
media_type="application/json"
|
|
3042
|
-
)
|
|
1146
|
+
Returns:
|
|
1147
|
+
Fully constructed webhook URL
|
|
1148
|
+
"""
|
|
1149
|
+
# Check for serverless environment and use appropriate URL generation
|
|
1150
|
+
mode = get_execution_mode()
|
|
1151
|
+
|
|
1152
|
+
if mode != 'server':
|
|
1153
|
+
# In serverless mode, use the serverless-appropriate URL with auth
|
|
1154
|
+
base_url = self.get_full_url(include_auth=True)
|
|
3043
1155
|
|
|
3044
|
-
#
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
if hasattr(request, "_post_prompt_body"):
|
|
3048
|
-
body = getattr(request, "_post_prompt_body")
|
|
3049
|
-
else:
|
|
3050
|
-
body = await request.json()
|
|
3051
|
-
|
|
3052
|
-
# Only log if not suppressed
|
|
3053
|
-
if not getattr(self, '_suppress_logs', False):
|
|
3054
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
|
3055
|
-
# Log the raw body directly (let the logger handle the JSON encoding)
|
|
3056
|
-
req_log.info("post_prompt_body", body=body)
|
|
3057
|
-
except Exception as e:
|
|
3058
|
-
req_log.error("error_parsing_request_body", error=str(e))
|
|
3059
|
-
body = {}
|
|
1156
|
+
# Ensure the endpoint has a trailing slash to prevent redirects
|
|
1157
|
+
if endpoint in ["swaig", "post_prompt"]:
|
|
1158
|
+
endpoint = f"{endpoint}/"
|
|
3060
1159
|
|
|
3061
|
-
#
|
|
3062
|
-
|
|
1160
|
+
# Build the full webhook URL
|
|
1161
|
+
url = f"{base_url}/{endpoint}"
|
|
3063
1162
|
|
|
3064
|
-
#
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
self.on_summary(None, body)
|
|
3072
|
-
req_log.debug("summary_handler_called_with_null_summary")
|
|
3073
|
-
except Exception as e:
|
|
3074
|
-
req_log.error("error_in_summary_handler", error=str(e))
|
|
1163
|
+
# Add query parameters if any (only if they have values)
|
|
1164
|
+
if query_params:
|
|
1165
|
+
# Remove any call_id from query params
|
|
1166
|
+
filtered_params = {k: v for k, v in query_params.items() if k != "call_id" and v}
|
|
1167
|
+
if filtered_params:
|
|
1168
|
+
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
|
1169
|
+
url = f"{url}?{params}"
|
|
3075
1170
|
|
|
3076
|
-
|
|
3077
|
-
req_log.info("request_successful")
|
|
3078
|
-
return {"success": True}
|
|
3079
|
-
except Exception as e:
|
|
3080
|
-
req_log.error("request_failed", error=str(e))
|
|
3081
|
-
return Response(
|
|
3082
|
-
content=json.dumps({"error": str(e)}),
|
|
3083
|
-
status_code=500,
|
|
3084
|
-
media_type="application/json"
|
|
3085
|
-
)
|
|
3086
|
-
|
|
3087
|
-
async def _handle_check_for_input_request(self, request: Request):
|
|
3088
|
-
"""Handle GET/POST requests to the check_for_input endpoint"""
|
|
3089
|
-
req_log = self.log.bind(
|
|
3090
|
-
endpoint="check_for_input",
|
|
3091
|
-
method=request.method,
|
|
3092
|
-
path=request.url.path
|
|
3093
|
-
)
|
|
3094
|
-
|
|
3095
|
-
req_log.debug("endpoint_called")
|
|
1171
|
+
return url
|
|
3096
1172
|
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
req_log.warning("unauthorized_access_attempt")
|
|
3101
|
-
return Response(
|
|
3102
|
-
content=json.dumps({"error": "Unauthorized"}),
|
|
3103
|
-
status_code=401,
|
|
3104
|
-
headers={"WWW-Authenticate": "Basic"},
|
|
3105
|
-
media_type="application/json"
|
|
3106
|
-
)
|
|
3107
|
-
|
|
3108
|
-
# For both GET and POST requests, process input check
|
|
3109
|
-
conversation_id = None
|
|
3110
|
-
|
|
3111
|
-
if request.method == "POST":
|
|
3112
|
-
try:
|
|
3113
|
-
body = await request.json()
|
|
3114
|
-
req_log.debug("request_body_received", body_size=len(str(body)))
|
|
3115
|
-
conversation_id = body.get("conversation_id")
|
|
3116
|
-
except Exception as e:
|
|
3117
|
-
req_log.error("error_parsing_request_body", error=str(e))
|
|
3118
|
-
else:
|
|
3119
|
-
conversation_id = request.query_params.get("conversation_id")
|
|
3120
|
-
|
|
3121
|
-
if not conversation_id:
|
|
3122
|
-
req_log.warning("missing_conversation_id")
|
|
3123
|
-
return Response(
|
|
3124
|
-
content=json.dumps({"error": "Missing conversation_id parameter"}),
|
|
3125
|
-
status_code=400,
|
|
3126
|
-
media_type="application/json"
|
|
3127
|
-
)
|
|
3128
|
-
|
|
3129
|
-
# Here you would typically check for new input in some external system
|
|
3130
|
-
# For this implementation, we'll return an empty result
|
|
3131
|
-
return {
|
|
3132
|
-
"status": "success",
|
|
3133
|
-
"conversation_id": conversation_id,
|
|
3134
|
-
"new_input": False,
|
|
3135
|
-
"messages": []
|
|
3136
|
-
}
|
|
3137
|
-
except Exception as e:
|
|
3138
|
-
req_log.error("request_failed", error=str(e))
|
|
3139
|
-
return Response(
|
|
3140
|
-
content=json.dumps({"error": str(e)}),
|
|
3141
|
-
status_code=500,
|
|
3142
|
-
media_type="application/json"
|
|
3143
|
-
)
|
|
1173
|
+
# Server mode - use the parent class's implementation from SWMLService
|
|
1174
|
+
# which properly handles SWML_PROXY_URL_BASE environment variable
|
|
1175
|
+
return super()._build_webhook_url(endpoint, query_params)
|
|
3144
1176
|
|
|
3145
1177
|
def _find_summary_in_post_data(self, body, logger):
|
|
3146
1178
|
"""
|
|
@@ -3174,268 +1206,165 @@ class AgentBase(SWMLService):
|
|
|
3174
1206
|
return pdata["raw"]
|
|
3175
1207
|
|
|
3176
1208
|
return None
|
|
3177
|
-
|
|
3178
|
-
def _register_state_tracking_tools(self):
|
|
3179
|
-
"""
|
|
3180
|
-
Register special tools for state tracking
|
|
3181
|
-
|
|
3182
|
-
This adds startup_hook and hangup_hook SWAIG functions that automatically
|
|
3183
|
-
activate and deactivate the session when called. These are useful for
|
|
3184
|
-
tracking call state and cleaning up resources when a call ends.
|
|
3185
|
-
"""
|
|
3186
|
-
# Register startup hook to activate session
|
|
3187
|
-
self.define_tool(
|
|
3188
|
-
name="startup_hook",
|
|
3189
|
-
description="Called when a new conversation starts to initialize state",
|
|
3190
|
-
parameters={},
|
|
3191
|
-
handler=lambda args, raw_data: self._handle_startup_hook(args, raw_data),
|
|
3192
|
-
secure=False # No auth needed for this system function
|
|
3193
|
-
)
|
|
3194
|
-
|
|
3195
|
-
# Register hangup hook to end session
|
|
3196
|
-
self.define_tool(
|
|
3197
|
-
name="hangup_hook",
|
|
3198
|
-
description="Called when conversation ends to clean up resources",
|
|
3199
|
-
parameters={},
|
|
3200
|
-
handler=lambda args, raw_data: self._handle_hangup_hook(args, raw_data),
|
|
3201
|
-
secure=False # No auth needed for this system function
|
|
3202
|
-
)
|
|
3203
|
-
|
|
3204
|
-
def _handle_startup_hook(self, args, raw_data):
|
|
3205
|
-
"""
|
|
3206
|
-
Handle the startup hook function call
|
|
3207
|
-
|
|
3208
|
-
Args:
|
|
3209
|
-
args: Function arguments (empty for this hook)
|
|
3210
|
-
raw_data: Raw request data containing call_id
|
|
3211
|
-
|
|
3212
|
-
Returns:
|
|
3213
|
-
Success response
|
|
3214
|
-
"""
|
|
3215
|
-
call_id = raw_data.get("call_id") if raw_data else None
|
|
3216
|
-
if call_id:
|
|
3217
|
-
self.log.info("session_activated", call_id=call_id)
|
|
3218
|
-
self._session_manager.activate_session(call_id)
|
|
3219
|
-
return SwaigFunctionResult("Session activated")
|
|
3220
|
-
else:
|
|
3221
|
-
self.log.warning("session_activation_failed", error="No call_id provided")
|
|
3222
|
-
return SwaigFunctionResult("Failed to activate session: No call_id provided")
|
|
3223
1209
|
|
|
3224
|
-
def
|
|
1210
|
+
def _create_ephemeral_copy(self):
|
|
3225
1211
|
"""
|
|
3226
|
-
|
|
1212
|
+
Create a lightweight copy of this agent for ephemeral configuration.
|
|
3227
1213
|
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
Returns:
|
|
3233
|
-
Success response
|
|
3234
|
-
"""
|
|
3235
|
-
call_id = raw_data.get("call_id") if raw_data else None
|
|
3236
|
-
if call_id:
|
|
3237
|
-
self.log.info("session_ended", call_id=call_id)
|
|
3238
|
-
self._session_manager.end_session(call_id)
|
|
3239
|
-
return SwaigFunctionResult("Session ended")
|
|
3240
|
-
else:
|
|
3241
|
-
self.log.warning("session_end_failed", error="No call_id provided")
|
|
3242
|
-
return SwaigFunctionResult("Failed to end session: No call_id provided")
|
|
3243
|
-
|
|
3244
|
-
def on_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None) -> Optional[dict]:
|
|
3245
|
-
"""
|
|
3246
|
-
Called when SWML is requested, with request data when available
|
|
3247
|
-
|
|
3248
|
-
This method overrides SWMLService's on_request to properly handle SWML generation
|
|
3249
|
-
for AI Agents. It forwards the call to on_swml_request for compatibility.
|
|
3250
|
-
|
|
3251
|
-
Args:
|
|
3252
|
-
request_data: Optional dictionary containing the parsed POST body
|
|
3253
|
-
callback_path: Optional callback path
|
|
3254
|
-
|
|
3255
|
-
Returns:
|
|
3256
|
-
None to use the default SWML rendering (which will call _render_swml)
|
|
3257
|
-
"""
|
|
3258
|
-
# First try to call on_swml_request if it exists (backward compatibility)
|
|
3259
|
-
if hasattr(self, 'on_swml_request') and callable(getattr(self, 'on_swml_request')):
|
|
3260
|
-
return self.on_swml_request(request_data, callback_path, None)
|
|
3261
|
-
|
|
3262
|
-
# If no on_swml_request or it returned None, we'll proceed with default rendering
|
|
3263
|
-
# We're not returning any modifications here because _render_swml will be called
|
|
3264
|
-
# to generate the complete SWML document
|
|
3265
|
-
return None
|
|
3266
|
-
|
|
3267
|
-
def on_swml_request(self, request_data: Optional[dict] = None, callback_path: Optional[str] = None, request: Optional[Request] = None) -> Optional[dict]:
|
|
3268
|
-
"""
|
|
3269
|
-
Customization point for subclasses to modify SWML based on request data
|
|
1214
|
+
This creates a partial copy that shares most resources but has independent
|
|
1215
|
+
configuration for SWML generation. Used when dynamic configuration callbacks
|
|
1216
|
+
need to modify the agent without affecting the persistent state.
|
|
3270
1217
|
|
|
3271
|
-
Args:
|
|
3272
|
-
request_data: Optional dictionary containing the parsed POST body
|
|
3273
|
-
callback_path: Optional callback path
|
|
3274
|
-
request: Optional FastAPI Request object for accessing query params, headers, etc.
|
|
3275
|
-
|
|
3276
1218
|
Returns:
|
|
3277
|
-
|
|
1219
|
+
A lightweight copy of the agent suitable for ephemeral modifications
|
|
3278
1220
|
"""
|
|
3279
|
-
|
|
3280
|
-
if self._dynamic_config_callback and request:
|
|
3281
|
-
try:
|
|
3282
|
-
# Extract request data
|
|
3283
|
-
query_params = dict(request.query_params)
|
|
3284
|
-
body_params = request_data or {}
|
|
3285
|
-
headers = dict(request.headers)
|
|
3286
|
-
|
|
3287
|
-
# Create ephemeral configurator
|
|
3288
|
-
agent_config = EphemeralAgentConfig()
|
|
3289
|
-
|
|
3290
|
-
# Call the user's configuration callback
|
|
3291
|
-
self._dynamic_config_callback(query_params, body_params, headers, agent_config)
|
|
3292
|
-
|
|
3293
|
-
# Extract the configuration
|
|
3294
|
-
config = agent_config.extract_config()
|
|
3295
|
-
if config:
|
|
3296
|
-
# Handle ephemeral prompt sections by applying them to this agent instance
|
|
3297
|
-
if "_ephemeral_prompt_sections" in config:
|
|
3298
|
-
for section in config["_ephemeral_prompt_sections"]:
|
|
3299
|
-
self.prompt_add_section(
|
|
3300
|
-
section["title"],
|
|
3301
|
-
section.get("body", ""),
|
|
3302
|
-
section.get("bullets"),
|
|
3303
|
-
**{k: v for k, v in section.items() if k not in ["title", "body", "bullets"]}
|
|
3304
|
-
)
|
|
3305
|
-
del config["_ephemeral_prompt_sections"]
|
|
3306
|
-
|
|
3307
|
-
if "_ephemeral_raw_prompt" in config:
|
|
3308
|
-
self._raw_prompt = config["_ephemeral_raw_prompt"]
|
|
3309
|
-
del config["_ephemeral_raw_prompt"]
|
|
3310
|
-
|
|
3311
|
-
if "_ephemeral_post_prompt" in config:
|
|
3312
|
-
self._post_prompt = config["_ephemeral_post_prompt"]
|
|
3313
|
-
del config["_ephemeral_post_prompt"]
|
|
3314
|
-
|
|
3315
|
-
return config
|
|
3316
|
-
|
|
3317
|
-
except Exception as e:
|
|
3318
|
-
self.log.error("dynamic_config_error", error=str(e))
|
|
1221
|
+
import copy
|
|
3319
1222
|
|
|
3320
|
-
#
|
|
3321
|
-
|
|
1223
|
+
# Create a new instance of the same class
|
|
1224
|
+
cls = self.__class__
|
|
1225
|
+
ephemeral_agent = cls.__new__(cls)
|
|
1226
|
+
|
|
1227
|
+
# Copy all attributes as shallow references first
|
|
1228
|
+
for key, value in self.__dict__.items():
|
|
1229
|
+
setattr(ephemeral_agent, key, value)
|
|
1230
|
+
|
|
1231
|
+
# Deep copy only the configuration that affects SWML generation
|
|
1232
|
+
# These are the parts that dynamic config might modify
|
|
1233
|
+
ephemeral_agent._params = copy.deepcopy(self._params)
|
|
1234
|
+
ephemeral_agent._hints = copy.deepcopy(self._hints)
|
|
1235
|
+
ephemeral_agent._languages = copy.deepcopy(self._languages)
|
|
1236
|
+
ephemeral_agent._pronounce = copy.deepcopy(self._pronounce)
|
|
1237
|
+
ephemeral_agent._global_data = copy.deepcopy(self._global_data)
|
|
1238
|
+
ephemeral_agent._function_includes = copy.deepcopy(self._function_includes)
|
|
3322
1239
|
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
1240
|
+
# Deep copy verb insertion points for call flow customization
|
|
1241
|
+
ephemeral_agent._pre_answer_verbs = copy.deepcopy(self._pre_answer_verbs)
|
|
1242
|
+
ephemeral_agent._answer_config = copy.deepcopy(self._answer_config)
|
|
1243
|
+
ephemeral_agent._post_answer_verbs = copy.deepcopy(self._post_answer_verbs)
|
|
1244
|
+
ephemeral_agent._post_ai_verbs = copy.deepcopy(self._post_ai_verbs)
|
|
1245
|
+
|
|
1246
|
+
# Deep copy LLM parameters
|
|
1247
|
+
ephemeral_agent._prompt_llm_params = copy.deepcopy(self._prompt_llm_params)
|
|
1248
|
+
ephemeral_agent._post_prompt_llm_params = copy.deepcopy(self._post_prompt_llm_params)
|
|
1249
|
+
|
|
1250
|
+
# Copy internal fillers if they exist
|
|
1251
|
+
if hasattr(self, '_internal_fillers'):
|
|
1252
|
+
ephemeral_agent._internal_fillers = copy.deepcopy(self._internal_fillers)
|
|
1253
|
+
|
|
1254
|
+
# Don't deep copy _contexts_builder - it has a circular reference to the agent
|
|
1255
|
+
# The contexts are already copied via _prompt_manager._contexts (below)
|
|
1256
|
+
# Just copy the flag indicating contexts are defined
|
|
1257
|
+
if hasattr(self, '_contexts_defined'):
|
|
1258
|
+
ephemeral_agent._contexts_defined = self._contexts_defined
|
|
1259
|
+
|
|
1260
|
+
# Deep copy the POM object if it exists to prevent sharing prompt sections
|
|
1261
|
+
if hasattr(self, 'pom') and self.pom:
|
|
1262
|
+
ephemeral_agent.pom = copy.deepcopy(self.pom)
|
|
1263
|
+
# Handle native_functions which might be stored as an attribute or property
|
|
1264
|
+
if hasattr(self, '_native_functions'):
|
|
1265
|
+
ephemeral_agent._native_functions = copy.deepcopy(self._native_functions)
|
|
1266
|
+
elif hasattr(self, 'native_functions'):
|
|
1267
|
+
ephemeral_agent.native_functions = copy.deepcopy(self.native_functions)
|
|
1268
|
+
ephemeral_agent._swaig_query_params = copy.deepcopy(self._swaig_query_params)
|
|
1269
|
+
|
|
1270
|
+
# Create new manager instances that point to the ephemeral agent
|
|
1271
|
+
# This breaks the circular reference and allows independent modification
|
|
1272
|
+
from signalwire_agents.core.agent.prompt.manager import PromptManager
|
|
1273
|
+
from signalwire_agents.core.agent.tools.registry import ToolRegistry
|
|
1274
|
+
|
|
1275
|
+
# Create new prompt manager for the ephemeral agent
|
|
1276
|
+
ephemeral_agent._prompt_manager = PromptManager(ephemeral_agent)
|
|
1277
|
+
# Copy ALL PromptManager state
|
|
1278
|
+
if hasattr(self._prompt_manager, '_sections'):
|
|
1279
|
+
ephemeral_agent._prompt_manager._sections = copy.deepcopy(self._prompt_manager._sections)
|
|
1280
|
+
ephemeral_agent._prompt_manager._prompt_text = copy.deepcopy(self._prompt_manager._prompt_text)
|
|
1281
|
+
ephemeral_agent._prompt_manager._post_prompt_text = copy.deepcopy(self._prompt_manager._post_prompt_text)
|
|
1282
|
+
ephemeral_agent._prompt_manager._contexts = copy.deepcopy(self._prompt_manager._contexts)
|
|
1283
|
+
|
|
1284
|
+
# Create new tool registry for the ephemeral agent
|
|
1285
|
+
ephemeral_agent._tool_registry = ToolRegistry(ephemeral_agent)
|
|
1286
|
+
# Copy the SWAIG functions - we need a shallow copy here because
|
|
1287
|
+
# the functions themselves can be shared, we just need a new dict
|
|
1288
|
+
if hasattr(self._tool_registry, '_swaig_functions'):
|
|
1289
|
+
ephemeral_agent._tool_registry._swaig_functions = self._tool_registry._swaig_functions.copy()
|
|
1290
|
+
if hasattr(self._tool_registry, '_tool_instances'):
|
|
1291
|
+
ephemeral_agent._tool_registry._tool_instances = self._tool_registry._tool_instances.copy()
|
|
1292
|
+
|
|
1293
|
+
# Create a new skill manager for the ephemeral agent
|
|
1294
|
+
# This is important because skills register tools with the agent's registry
|
|
1295
|
+
from signalwire_agents.core.skill_manager import SkillManager
|
|
1296
|
+
ephemeral_agent.skill_manager = SkillManager(ephemeral_agent)
|
|
1297
|
+
|
|
1298
|
+
# Copy any already loaded skills from the original agent
|
|
1299
|
+
# This ensures skills loaded during __init__ are available in the ephemeral agent
|
|
1300
|
+
if hasattr(self.skill_manager, 'loaded_skills'):
|
|
1301
|
+
for skill_key, skill_instance in self.skill_manager.loaded_skills.items():
|
|
1302
|
+
# Re-load the skill in the ephemeral agent's context
|
|
1303
|
+
# We need to get the skill name and params from the existing instance
|
|
1304
|
+
skill_name = skill_instance.SKILL_NAME
|
|
1305
|
+
skill_params = getattr(skill_instance, 'params', {})
|
|
1306
|
+
try:
|
|
1307
|
+
ephemeral_agent.skill_manager.load_skill(skill_name, type(skill_instance), skill_params)
|
|
1308
|
+
except Exception as e:
|
|
1309
|
+
self.log.warning("failed_to_copy_skill_to_ephemeral",
|
|
1310
|
+
skill_name=skill_name,
|
|
1311
|
+
error=str(e))
|
|
3332
1312
|
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
- None if normal processing should continue
|
|
1313
|
+
# Re-bind the tool decorator method to the new instance
|
|
1314
|
+
ephemeral_agent.tool = ephemeral_agent._tool_decorator
|
|
3336
1315
|
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
path: The path where this callback should be registered (default: "/sip")
|
|
3340
|
-
"""
|
|
3341
|
-
# Normalize the path (remove trailing slash)
|
|
3342
|
-
normalized_path = path.rstrip("/")
|
|
3343
|
-
if not normalized_path.startswith("/"):
|
|
3344
|
-
normalized_path = f"/{normalized_path}"
|
|
3345
|
-
|
|
3346
|
-
# Store the callback with the normalized path (without trailing slash)
|
|
3347
|
-
self.log.info("registering_routing_callback", path=normalized_path)
|
|
3348
|
-
if not hasattr(self, '_routing_callbacks'):
|
|
3349
|
-
self._routing_callbacks = {}
|
|
3350
|
-
self._routing_callbacks[normalized_path] = callback_fn
|
|
3351
|
-
|
|
3352
|
-
def set_dynamic_config_callback(self, callback: Callable[[dict, dict, dict, EphemeralAgentConfig], None]) -> 'AgentBase':
|
|
3353
|
-
"""
|
|
3354
|
-
Set a callback function for dynamic agent configuration
|
|
1316
|
+
# Share the logger but bind it to indicate ephemeral copy
|
|
1317
|
+
ephemeral_agent.log = self.log.bind(ephemeral=True)
|
|
3355
1318
|
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
the agent's voice, prompt, parameters, etc. based on request data.
|
|
1319
|
+
# Mark this as an ephemeral agent to prevent double application of dynamic config
|
|
1320
|
+
ephemeral_agent._is_ephemeral = True
|
|
3359
1321
|
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
- agent_config.add_language(...)
|
|
3364
|
-
- agent_config.prompt_add_section(...)
|
|
3365
|
-
- agent_config.set_params(...)
|
|
3366
|
-
- agent_config.set_global_data(...)
|
|
3367
|
-
|
|
3368
|
-
Example:
|
|
3369
|
-
def my_config(query_params, body_params, headers, agent):
|
|
3370
|
-
if query_params.get('tier') == 'premium':
|
|
3371
|
-
agent.add_language("English", "en-US", "premium_voice")
|
|
3372
|
-
agent.set_params({"end_of_speech_timeout": 500})
|
|
3373
|
-
agent.set_global_data({"tier": query_params.get('tier', 'standard')})
|
|
3374
|
-
|
|
3375
|
-
my_agent.set_dynamic_config_callback(my_config)
|
|
3376
|
-
"""
|
|
3377
|
-
self._dynamic_config_callback = callback
|
|
3378
|
-
return self
|
|
3379
|
-
|
|
3380
|
-
def manual_set_proxy_url(self, proxy_url: str) -> 'AgentBase':
|
|
1322
|
+
return ephemeral_agent
|
|
1323
|
+
|
|
1324
|
+
async def _handle_request(self, request: Request, response: Response):
|
|
3381
1325
|
"""
|
|
3382
|
-
|
|
1326
|
+
Override SWMLService's _handle_request to use AgentBase's _render_swml
|
|
3383
1327
|
|
|
3384
|
-
This
|
|
3385
|
-
|
|
3386
|
-
Args:
|
|
3387
|
-
proxy_url: The base URL to use for webhooks (e.g., https://example.ngrok.io)
|
|
3388
|
-
|
|
3389
|
-
Returns:
|
|
3390
|
-
Self for method chaining
|
|
1328
|
+
This ensures that when routes are handled by SWMLService's router,
|
|
1329
|
+
they still use AgentBase's SWML rendering logic.
|
|
3391
1330
|
"""
|
|
3392
|
-
if
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
1331
|
+
# Use WebMixin's implementation if available
|
|
1332
|
+
if hasattr(super(), '_handle_root_request'):
|
|
1333
|
+
return await self._handle_root_request(request)
|
|
1334
|
+
|
|
1335
|
+
# Fallback to basic implementation
|
|
1336
|
+
try:
|
|
1337
|
+
# Parse body if POST request
|
|
1338
|
+
body = {}
|
|
1339
|
+
if request.method == "POST":
|
|
1340
|
+
try:
|
|
1341
|
+
body = await request.json()
|
|
1342
|
+
except:
|
|
1343
|
+
pass
|
|
3396
1344
|
|
|
3397
|
-
#
|
|
3398
|
-
|
|
3399
|
-
super()._proxy_url_base = self._proxy_url_base
|
|
3400
|
-
if hasattr(super(), '_proxy_detection_done'):
|
|
3401
|
-
super()._proxy_detection_done = True
|
|
3402
|
-
|
|
3403
|
-
self.log.info("proxy_url_manually_set", proxy_url_base=self._proxy_url_base)
|
|
1345
|
+
# Get call_id
|
|
1346
|
+
call_id = body.get("call_id") if body else request.query_params.get("call_id")
|
|
3404
1347
|
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
Add a skill to this agent
|
|
3414
|
-
|
|
3415
|
-
Args:
|
|
3416
|
-
skill_name: Name of the skill to add
|
|
3417
|
-
params: Optional parameters to pass to the skill for configuration
|
|
1348
|
+
# Check auth
|
|
1349
|
+
if not self._check_basic_auth(request):
|
|
1350
|
+
return Response(
|
|
1351
|
+
content=json.dumps({"error": "Unauthorized"}),
|
|
1352
|
+
status_code=401,
|
|
1353
|
+
headers={"WWW-Authenticate": "Basic"},
|
|
1354
|
+
media_type="application/json"
|
|
1355
|
+
)
|
|
3418
1356
|
|
|
3419
|
-
|
|
3420
|
-
|
|
1357
|
+
# Render SWML using AgentBase's method
|
|
1358
|
+
swml = self._render_swml(call_id)
|
|
3421
1359
|
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
return self
|
|
3434
|
-
|
|
3435
|
-
def list_skills(self) -> List[str]:
|
|
3436
|
-
"""List currently loaded skills"""
|
|
3437
|
-
return self.skill_manager.list_loaded_skills()
|
|
3438
|
-
|
|
3439
|
-
def has_skill(self, skill_name: str) -> bool:
|
|
3440
|
-
"""Check if skill is loaded"""
|
|
3441
|
-
return self.skill_manager.has_skill(skill_name)
|
|
1360
|
+
return Response(
|
|
1361
|
+
content=swml,
|
|
1362
|
+
media_type="application/json"
|
|
1363
|
+
)
|
|
1364
|
+
except Exception as e:
|
|
1365
|
+
return Response(
|
|
1366
|
+
content=json.dumps({"error": str(e)}),
|
|
1367
|
+
status_code=500,
|
|
1368
|
+
media_type="application/json"
|
|
1369
|
+
)
|
|
1370
|
+
|