signalwire-agents 0.1.13__py3-none-any.whl → 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +176 -23
- 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/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +1225 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +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 +825 -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 +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1134 -0
- signalwire_agents/core/security_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 +167 -200
- 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 +752 -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 +10 -1
- 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.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/METADATA +344 -215
- signalwire_agents-1.0.7.dist-info/RECORD +142 -0
- signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/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.7.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.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
|
|
|
@@ -276,20 +112,19 @@ class AgentBase(SWMLService):
|
|
|
276
112
|
port: int = 3000,
|
|
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,24 @@ 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
|
+
final_port = port if port != 3000 else service_config.get('port', port)
|
|
166
|
+
final_name = service_config.get('name', name)
|
|
167
|
+
|
|
325
168
|
# Initialize the SWMLService base class
|
|
326
169
|
super().__init__(
|
|
327
|
-
name=
|
|
328
|
-
route=
|
|
329
|
-
host=
|
|
330
|
-
port=
|
|
170
|
+
name=final_name,
|
|
171
|
+
route=final_route,
|
|
172
|
+
host=final_host,
|
|
173
|
+
port=final_port,
|
|
331
174
|
basic_auth=basic_auth,
|
|
332
|
-
schema_path=schema_path
|
|
175
|
+
schema_path=schema_path,
|
|
176
|
+
config_file=config_file
|
|
333
177
|
)
|
|
334
178
|
|
|
335
179
|
# Log the schema path if found and not suppressing logs
|
|
@@ -352,8 +196,6 @@ class AgentBase(SWMLService):
|
|
|
352
196
|
|
|
353
197
|
# Initialize prompt handling
|
|
354
198
|
self._use_pom = use_pom
|
|
355
|
-
self._raw_prompt = None
|
|
356
|
-
self._post_prompt = None
|
|
357
199
|
|
|
358
200
|
# Initialize POM if needed
|
|
359
201
|
if self._use_pom:
|
|
@@ -369,11 +211,9 @@ class AgentBase(SWMLService):
|
|
|
369
211
|
self.pom = None
|
|
370
212
|
|
|
371
213
|
# Initialize tool registry (separate from SWMLService verb registry)
|
|
372
|
-
self._swaig_functions = {}
|
|
373
214
|
|
|
374
215
|
# Initialize session manager
|
|
375
216
|
self._session_manager = SessionManager(token_expiry_secs=token_expiry_secs)
|
|
376
|
-
self._enable_state_tracking = enable_state_tracking
|
|
377
217
|
|
|
378
218
|
# URL override variables
|
|
379
219
|
self._web_hook_url_override = None
|
|
@@ -388,21 +228,20 @@ class AgentBase(SWMLService):
|
|
|
388
228
|
self._record_format = record_format
|
|
389
229
|
self._record_stereo = record_stereo
|
|
390
230
|
|
|
231
|
+
# Initialize refactored managers early
|
|
232
|
+
self._prompt_manager = PromptManager(self)
|
|
233
|
+
self._tool_registry = ToolRegistry(self)
|
|
234
|
+
|
|
391
235
|
# Process declarative PROMPT_SECTIONS if defined in subclass
|
|
392
236
|
self._process_prompt_sections()
|
|
393
237
|
|
|
394
|
-
# Initialize state manager
|
|
395
|
-
self._state_manager = state_manager or FileStateManager()
|
|
396
238
|
|
|
397
239
|
# Process class-decorated tools (using @AgentBase.tool)
|
|
398
|
-
self.
|
|
240
|
+
self._tool_registry.register_class_decorated_tools()
|
|
399
241
|
|
|
400
242
|
# Add native_functions parameter
|
|
401
243
|
self.native_functions = native_functions or []
|
|
402
244
|
|
|
403
|
-
# Register state tracking tools if enabled
|
|
404
|
-
if enable_state_tracking:
|
|
405
|
-
self._register_state_tracking_tools()
|
|
406
245
|
|
|
407
246
|
# Initialize new configuration containers
|
|
408
247
|
self._hints = []
|
|
@@ -411,6 +250,9 @@ class AgentBase(SWMLService):
|
|
|
411
250
|
self._params = {}
|
|
412
251
|
self._global_data = {}
|
|
413
252
|
self._function_includes = []
|
|
253
|
+
# Initialize LLM params as empty - only send if explicitly set
|
|
254
|
+
self._prompt_llm_params = {}
|
|
255
|
+
self._post_prompt_llm_params = {}
|
|
414
256
|
|
|
415
257
|
# Dynamic configuration callback
|
|
416
258
|
self._dynamic_config_callback = None
|
|
@@ -422,1045 +264,447 @@ class AgentBase(SWMLService):
|
|
|
422
264
|
self._contexts_builder = None
|
|
423
265
|
self._contexts_defined = False
|
|
424
266
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
267
|
+
# Initialize SWAIG query params for dynamic config
|
|
268
|
+
self._swaig_query_params = {}
|
|
269
|
+
|
|
270
|
+
# Initialize verb insertion points for call flow customization
|
|
271
|
+
self._pre_answer_verbs = [] # Verbs to run before answer (e.g., ringback, screening)
|
|
272
|
+
self._answer_config = {} # Configuration for the answer verb
|
|
273
|
+
self._post_answer_verbs = [] # Verbs to run after answer, before AI (e.g., announcements)
|
|
274
|
+
self._post_ai_verbs = [] # Verbs to run after AI ends (e.g., cleanup, transfers)
|
|
275
|
+
|
|
276
|
+
# Verb categories for pre-answer validation
|
|
277
|
+
_PRE_ANSWER_SAFE_VERBS = {
|
|
278
|
+
"transfer", "execute", "return", "label", "goto", "request",
|
|
279
|
+
"switch", "cond", "if", "eval", "set", "unset", "hangup",
|
|
280
|
+
"send_sms", "sleep", "stop_record_call", "stop_denoise", "stop_tap"
|
|
281
|
+
}
|
|
282
|
+
_AUTO_ANSWER_VERBS = {"play", "connect"}
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _load_service_config(config_file: Optional[str], service_name: str) -> dict:
|
|
286
|
+
"""Load service configuration from config file if available"""
|
|
287
|
+
from signalwire_agents.core.config_loader import ConfigLoader
|
|
288
|
+
|
|
289
|
+
# Find config file
|
|
290
|
+
if not config_file:
|
|
291
|
+
config_file = ConfigLoader.find_config_file(service_name)
|
|
292
|
+
|
|
293
|
+
if not config_file:
|
|
294
|
+
return {}
|
|
295
|
+
|
|
296
|
+
# Load config
|
|
297
|
+
config_loader = ConfigLoader([config_file])
|
|
298
|
+
if not config_loader.has_config():
|
|
299
|
+
return {}
|
|
428
300
|
|
|
301
|
+
# Get service section
|
|
302
|
+
service_config = config_loader.get_section('service')
|
|
303
|
+
if service_config:
|
|
304
|
+
return service_config
|
|
305
|
+
|
|
306
|
+
return {}
|
|
429
307
|
|
|
430
|
-
def
|
|
308
|
+
def get_name(self) -> str:
|
|
431
309
|
"""
|
|
432
|
-
|
|
310
|
+
Get agent name
|
|
433
311
|
|
|
434
|
-
|
|
435
|
-
|
|
312
|
+
Returns:
|
|
313
|
+
Agent name
|
|
436
314
|
"""
|
|
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
|
-
# ----------------------------------------------------------------------
|
|
315
|
+
return self.name
|
|
530
316
|
|
|
531
|
-
def
|
|
317
|
+
def get_full_url(self, include_auth: bool = False) -> str:
|
|
532
318
|
"""
|
|
533
|
-
|
|
319
|
+
Get the full URL for this agent's endpoint
|
|
534
320
|
|
|
535
|
-
|
|
536
|
-
|
|
321
|
+
Args:
|
|
322
|
+
include_auth: Whether to include authentication credentials in the URL
|
|
537
323
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
that you can't mix POM sections with raw text in the main prompt.
|
|
324
|
+
Returns:
|
|
325
|
+
Full URL including host, port, and route (with auth if requested)
|
|
541
326
|
"""
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
327
|
+
mode = get_execution_mode()
|
|
328
|
+
|
|
329
|
+
if mode == 'cgi':
|
|
330
|
+
protocol = 'https' if os.getenv('HTTPS') == 'on' else 'http'
|
|
331
|
+
host = os.getenv('HTTP_HOST') or os.getenv('SERVER_NAME') or 'localhost'
|
|
332
|
+
script_name = os.getenv('SCRIPT_NAME', '')
|
|
333
|
+
base_url = f"{protocol}://{host}{script_name}"
|
|
334
|
+
elif mode == 'lambda':
|
|
335
|
+
# AWS Lambda Function URL format
|
|
336
|
+
lambda_url = os.getenv('AWS_LAMBDA_FUNCTION_URL')
|
|
337
|
+
if lambda_url:
|
|
338
|
+
base_url = lambda_url.rstrip('/')
|
|
339
|
+
else:
|
|
340
|
+
# Fallback construction for Lambda
|
|
341
|
+
region = os.getenv('AWS_REGION', 'us-east-1')
|
|
342
|
+
function_name = os.getenv('AWS_LAMBDA_FUNCTION_NAME', 'unknown')
|
|
343
|
+
base_url = f"https://{function_name}.lambda-url.{region}.on.aws"
|
|
344
|
+
elif mode == 'google_cloud_function':
|
|
345
|
+
# Google Cloud Functions URL format
|
|
346
|
+
project_id = os.getenv('GOOGLE_CLOUD_PROJECT') or os.getenv('GCP_PROJECT')
|
|
347
|
+
region = os.getenv('FUNCTION_REGION') or os.getenv('GOOGLE_CLOUD_REGION', 'us-central1')
|
|
348
|
+
service_name = os.getenv('K_SERVICE') or os.getenv('FUNCTION_TARGET', 'unknown')
|
|
349
|
+
|
|
350
|
+
if project_id:
|
|
351
|
+
base_url = f"https://{region}-{project_id}.cloudfunctions.net/{service_name}"
|
|
352
|
+
else:
|
|
353
|
+
# Fallback for local testing or incomplete environment
|
|
354
|
+
base_url = f"https://localhost:8080"
|
|
355
|
+
elif mode == 'azure_function':
|
|
356
|
+
# Azure Functions URL format
|
|
357
|
+
function_app_name = os.getenv('WEBSITE_SITE_NAME') or os.getenv('AZURE_FUNCTIONS_APP_NAME')
|
|
358
|
+
function_name = os.getenv('AZURE_FUNCTION_NAME', 'unknown')
|
|
359
|
+
|
|
360
|
+
if function_app_name:
|
|
361
|
+
base_url = f"https://{function_app_name}.azurewebsites.net/api/{function_name}"
|
|
362
|
+
else:
|
|
363
|
+
# Fallback for local testing
|
|
364
|
+
base_url = f"https://localhost:7071/api/{function_name}"
|
|
365
|
+
else:
|
|
366
|
+
# Server mode - use the SWMLService's unified URL building
|
|
367
|
+
# Build the full URL using the parent's method
|
|
368
|
+
base_url = self._build_full_url(endpoint="", include_auth=include_auth)
|
|
369
|
+
return base_url
|
|
370
|
+
|
|
371
|
+
# For serverless modes, add authentication if requested
|
|
372
|
+
if include_auth:
|
|
373
|
+
username, password = self.get_basic_auth_credentials()
|
|
374
|
+
if username and password:
|
|
375
|
+
# Parse URL to insert auth
|
|
376
|
+
from urllib.parse import urlparse, urlunparse
|
|
377
|
+
parsed = urlparse(base_url)
|
|
378
|
+
# Reconstruct with auth
|
|
379
|
+
base_url = urlunparse((
|
|
380
|
+
parsed.scheme,
|
|
381
|
+
f"{username}:{password}@{parsed.netloc}",
|
|
382
|
+
parsed.path,
|
|
383
|
+
parsed.params,
|
|
384
|
+
parsed.query,
|
|
385
|
+
parsed.fragment
|
|
386
|
+
))
|
|
387
|
+
|
|
388
|
+
# Add route for serverless modes
|
|
389
|
+
if self.route and self.route != "/" and not base_url.endswith(self.route):
|
|
390
|
+
base_url = f"{base_url}/{self.route.lstrip('/')}"
|
|
548
391
|
|
|
549
|
-
return
|
|
392
|
+
return base_url
|
|
550
393
|
|
|
551
|
-
def
|
|
394
|
+
def on_summary(self, summary: Optional[Dict[str, Any]], raw_data: Optional[Dict[str, Any]] = None) -> None:
|
|
552
395
|
"""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
396
|
+
Called when a post-prompt summary is received
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
summary: The summary object or None if no summary was found
|
|
400
|
+
raw_data: The complete raw POST data from the request
|
|
401
|
+
"""
|
|
402
|
+
# Default implementation does nothing
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
# ==================== Call Flow Verb Insertion Methods ====================
|
|
406
|
+
|
|
407
|
+
def add_pre_answer_verb(self, verb_name: str, config: Dict[str, Any]) -> 'AgentBase':
|
|
408
|
+
"""
|
|
409
|
+
Add a verb to run before the call is answered.
|
|
410
|
+
|
|
411
|
+
Pre-answer verbs execute while the call is still ringing. Only certain
|
|
412
|
+
verbs are safe to use before answering:
|
|
413
|
+
|
|
414
|
+
Safe verbs: transfer, execute, return, label, goto, request, switch,
|
|
415
|
+
cond, if, eval, set, unset, hangup, send_sms, sleep,
|
|
416
|
+
stop_record_call, stop_denoise, stop_tap
|
|
417
|
+
|
|
418
|
+
Verbs with auto_answer option (play, connect): Must include
|
|
419
|
+
"auto_answer": False in config to prevent automatic answering.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
verb_name: The SWML verb name (e.g., "play", "sleep", "request")
|
|
423
|
+
config: Verb configuration dictionary
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Self for method chaining
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
ValueError: If verb is not safe for pre-answer use
|
|
430
|
+
|
|
431
|
+
Example:
|
|
432
|
+
# Play ringback tone before answering
|
|
433
|
+
agent.add_pre_answer_verb("play", {
|
|
434
|
+
"urls": ["ring:us"],
|
|
435
|
+
"auto_answer": False
|
|
436
|
+
})
|
|
556
437
|
"""
|
|
557
|
-
#
|
|
558
|
-
if
|
|
438
|
+
# Validate verb is safe for pre-answer use
|
|
439
|
+
if verb_name in self._AUTO_ANSWER_VERBS:
|
|
440
|
+
if not config.get("auto_answer") is False:
|
|
441
|
+
self.log.warning(
|
|
442
|
+
"pre_answer_verb_will_answer",
|
|
443
|
+
verb=verb_name,
|
|
444
|
+
hint=f"Add 'auto_answer': False to prevent {verb_name} from answering the call"
|
|
445
|
+
)
|
|
446
|
+
elif verb_name not in self._PRE_ANSWER_SAFE_VERBS:
|
|
559
447
|
raise ValueError(
|
|
560
|
-
"
|
|
561
|
-
"
|
|
448
|
+
f"Verb '{verb_name}' is not safe for pre-answer use. "
|
|
449
|
+
f"Safe verbs: {', '.join(sorted(self._PRE_ANSWER_SAFE_VERBS))}"
|
|
562
450
|
)
|
|
563
|
-
|
|
564
|
-
|
|
451
|
+
|
|
452
|
+
self._pre_answer_verbs.append((verb_name, config))
|
|
453
|
+
return self
|
|
454
|
+
|
|
455
|
+
def add_answer_verb(self, config: Optional[Dict[str, Any]] = None) -> 'AgentBase':
|
|
565
456
|
"""
|
|
566
|
-
|
|
567
|
-
|
|
457
|
+
Configure the answer verb.
|
|
458
|
+
|
|
459
|
+
The answer verb connects the call. Use this method to customize
|
|
460
|
+
answer behavior, such as setting max_duration.
|
|
461
|
+
|
|
568
462
|
Args:
|
|
569
|
-
|
|
570
|
-
|
|
463
|
+
config: Optional answer verb configuration (e.g., {"max_duration": 3600})
|
|
464
|
+
|
|
571
465
|
Returns:
|
|
572
466
|
Self for method chaining
|
|
467
|
+
|
|
468
|
+
Example:
|
|
469
|
+
# Set maximum call duration to 1 hour
|
|
470
|
+
agent.add_answer_verb({"max_duration": 3600})
|
|
573
471
|
"""
|
|
574
|
-
self.
|
|
575
|
-
self._raw_prompt = text
|
|
472
|
+
self._answer_config = config or {}
|
|
576
473
|
return self
|
|
577
|
-
|
|
578
|
-
def
|
|
474
|
+
|
|
475
|
+
def add_post_answer_verb(self, verb_name: str, config: Dict[str, Any]) -> 'AgentBase':
|
|
579
476
|
"""
|
|
580
|
-
|
|
581
|
-
|
|
477
|
+
Add a verb to run after the call is answered but before the AI starts.
|
|
478
|
+
|
|
479
|
+
Post-answer verbs run after the call is connected. Common uses include
|
|
480
|
+
welcome messages, legal disclaimers, and hold music.
|
|
481
|
+
|
|
582
482
|
Args:
|
|
583
|
-
|
|
584
|
-
|
|
483
|
+
verb_name: The SWML verb name (e.g., "play", "sleep")
|
|
484
|
+
config: Verb configuration dictionary
|
|
485
|
+
|
|
585
486
|
Returns:
|
|
586
487
|
Self for method chaining
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
# Play welcome message
|
|
491
|
+
agent.add_post_answer_verb("play", {
|
|
492
|
+
"url": "say:Welcome to Acme Corporation."
|
|
493
|
+
})
|
|
494
|
+
# Brief pause
|
|
495
|
+
agent.add_post_answer_verb("sleep", {"time": 500})
|
|
587
496
|
"""
|
|
588
|
-
self.
|
|
497
|
+
self._post_answer_verbs.append((verb_name, config))
|
|
589
498
|
return self
|
|
590
|
-
|
|
591
|
-
def
|
|
499
|
+
|
|
500
|
+
def add_post_ai_verb(self, verb_name: str, config: Dict[str, Any]) -> 'AgentBase':
|
|
592
501
|
"""
|
|
593
|
-
|
|
594
|
-
|
|
502
|
+
Add a verb to run after the AI conversation ends.
|
|
503
|
+
|
|
504
|
+
Post-AI verbs run when the AI completes its conversation. Common uses
|
|
505
|
+
include clean disconnects, transfers, and logging.
|
|
506
|
+
|
|
595
507
|
Args:
|
|
596
|
-
|
|
597
|
-
|
|
508
|
+
verb_name: The SWML verb name (e.g., "hangup", "transfer", "request")
|
|
509
|
+
config: Verb configuration dictionary
|
|
510
|
+
|
|
598
511
|
Returns:
|
|
599
512
|
Self for method chaining
|
|
513
|
+
|
|
514
|
+
Example:
|
|
515
|
+
# Log call completion and hang up
|
|
516
|
+
agent.add_post_ai_verb("request", {
|
|
517
|
+
"url": "https://api.example.com/call-complete",
|
|
518
|
+
"method": "POST"
|
|
519
|
+
})
|
|
520
|
+
agent.add_post_ai_verb("hangup", {})
|
|
600
521
|
"""
|
|
601
|
-
|
|
602
|
-
self.pom = pom
|
|
603
|
-
else:
|
|
604
|
-
raise ValueError("use_pom must be True to use set_prompt_pom")
|
|
522
|
+
self._post_ai_verbs.append((verb_name, config))
|
|
605
523
|
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':
|
|
524
|
+
|
|
525
|
+
def clear_pre_answer_verbs(self) -> 'AgentBase':
|
|
616
526
|
"""
|
|
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
|
-
|
|
527
|
+
Remove all pre-answer verbs.
|
|
528
|
+
|
|
627
529
|
Returns:
|
|
628
530
|
Self for method chaining
|
|
629
531
|
"""
|
|
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
|
-
|
|
532
|
+
self._pre_answer_verbs = []
|
|
662
533
|
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':
|
|
534
|
+
|
|
535
|
+
def clear_post_answer_verbs(self) -> 'AgentBase':
|
|
671
536
|
"""
|
|
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
|
-
|
|
537
|
+
Remove all post-answer verbs.
|
|
538
|
+
|
|
680
539
|
Returns:
|
|
681
540
|
Self for method chaining
|
|
682
541
|
"""
|
|
683
|
-
|
|
684
|
-
self.pom.add_to_section(
|
|
685
|
-
title=title,
|
|
686
|
-
body=body,
|
|
687
|
-
bullet=bullet,
|
|
688
|
-
bullets=bullets
|
|
689
|
-
)
|
|
542
|
+
self._post_answer_verbs = []
|
|
690
543
|
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':
|
|
544
|
+
|
|
545
|
+
def clear_post_ai_verbs(self) -> 'AgentBase':
|
|
699
546
|
"""
|
|
700
|
-
|
|
547
|
+
Remove all post-AI verbs.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Self for method chaining
|
|
551
|
+
"""
|
|
552
|
+
self._post_ai_verbs = []
|
|
553
|
+
return self
|
|
554
|
+
|
|
555
|
+
# ==================== End Call Flow Verb Insertion Methods ====================
|
|
556
|
+
|
|
557
|
+
def enable_sip_routing(self, auto_map: bool = True, path: str = "/sip") -> 'AgentBase':
|
|
558
|
+
"""
|
|
559
|
+
Enable SIP-based routing for this agent
|
|
560
|
+
|
|
561
|
+
This allows the agent to automatically route SIP requests based on SIP usernames.
|
|
562
|
+
When enabled, an endpoint at the specified path is automatically created
|
|
563
|
+
that will handle SIP requests and deliver them to this agent.
|
|
701
564
|
|
|
702
565
|
Args:
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
566
|
+
auto_map: Whether to automatically map common SIP usernames to this agent
|
|
567
|
+
(based on the agent name and route path)
|
|
568
|
+
path: The path to register the SIP routing endpoint (default: "/sip")
|
|
569
|
+
|
|
708
570
|
Returns:
|
|
709
571
|
Self for method chaining
|
|
710
572
|
"""
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
|
573
|
+
# Create a routing callback that handles SIP usernames
|
|
574
|
+
def sip_routing_callback(request: Request, body: Dict[str, Any]) -> Optional[str]:
|
|
575
|
+
# Extract SIP username from the request body
|
|
576
|
+
sip_username = self.extract_sip_username(body)
|
|
721
577
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
578
|
+
if sip_username:
|
|
579
|
+
self.log.info("sip_username_extracted", username=sip_username)
|
|
580
|
+
|
|
581
|
+
# Check if this username is registered with this agent
|
|
582
|
+
if hasattr(self, '_sip_usernames') and sip_username.lower() in self._sip_usernames:
|
|
583
|
+
self.log.info("sip_username_matched", username=sip_username)
|
|
584
|
+
# This route is already being handled by the agent, no need to redirect
|
|
585
|
+
return None
|
|
586
|
+
else:
|
|
587
|
+
self.log.info("sip_username_not_matched", username=sip_username)
|
|
588
|
+
# Not registered with this agent, let routing continue
|
|
589
|
+
|
|
590
|
+
return None
|
|
725
591
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
)
|
|
592
|
+
# Register the callback with the SWMLService, specifying the path
|
|
593
|
+
self.register_routing_callback(sip_routing_callback, path=path)
|
|
594
|
+
|
|
595
|
+
# Auto-map common usernames if requested
|
|
596
|
+
if auto_map:
|
|
597
|
+
self.auto_map_sip_usernames()
|
|
732
598
|
|
|
733
599
|
return self
|
|
734
600
|
|
|
735
|
-
|
|
736
|
-
# Tool/Function Management
|
|
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':
|
|
601
|
+
def register_sip_username(self, sip_username: str) -> 'AgentBase':
|
|
750
602
|
"""
|
|
751
|
-
|
|
603
|
+
Register a SIP username that should be routed to this agent
|
|
752
604
|
|
|
753
605
|
Args:
|
|
754
|
-
|
|
755
|
-
description: Function description for the AI
|
|
756
|
-
parameters: JSON Schema of parameters
|
|
757
|
-
handler: Function to call when invoked
|
|
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
|
|
606
|
+
sip_username: SIP username to register
|
|
762
607
|
|
|
763
608
|
Returns:
|
|
764
609
|
Self for method chaining
|
|
765
610
|
"""
|
|
766
|
-
if
|
|
767
|
-
|
|
611
|
+
if not hasattr(self, '_sip_usernames'):
|
|
612
|
+
self._sip_usernames = set()
|
|
768
613
|
|
|
769
|
-
self.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
parameters=parameters,
|
|
773
|
-
handler=handler,
|
|
774
|
-
secure=secure,
|
|
775
|
-
fillers=fillers,
|
|
776
|
-
webhook_url=webhook_url,
|
|
777
|
-
**swaig_fields
|
|
778
|
-
)
|
|
614
|
+
self._sip_usernames.add(sip_username.lower())
|
|
615
|
+
self.log.info("sip_username_registered", username=sip_username)
|
|
616
|
+
|
|
779
617
|
return self
|
|
780
618
|
|
|
781
|
-
def
|
|
619
|
+
def auto_map_sip_usernames(self) -> 'AgentBase':
|
|
782
620
|
"""
|
|
783
|
-
|
|
621
|
+
Automatically register common SIP usernames based on this agent's
|
|
622
|
+
name and route
|
|
784
623
|
|
|
785
|
-
Args:
|
|
786
|
-
function_dict: Complete SWAIG function definition dictionary
|
|
787
|
-
|
|
788
624
|
Returns:
|
|
789
625
|
Self for method chaining
|
|
790
626
|
"""
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
627
|
+
# Register username based on agent name
|
|
628
|
+
clean_name = re.sub(r'[^a-z0-9_]', '', self.name.lower())
|
|
629
|
+
if clean_name:
|
|
630
|
+
self.register_sip_username(clean_name)
|
|
794
631
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
632
|
+
# Register username based on route (without slashes)
|
|
633
|
+
clean_route = re.sub(r'[^a-z0-9_]', '', self.route.lower())
|
|
634
|
+
if clean_route and clean_route != clean_name:
|
|
635
|
+
self.register_sip_username(clean_route)
|
|
636
|
+
|
|
637
|
+
# Register common variations if they make sense
|
|
638
|
+
if len(clean_name) > 3:
|
|
639
|
+
# Register without vowels
|
|
640
|
+
no_vowels = re.sub(r'[aeiou]', '', clean_name)
|
|
641
|
+
if no_vowels != clean_name and len(no_vowels) > 2:
|
|
642
|
+
self.register_sip_username(no_vowels)
|
|
643
|
+
|
|
801
644
|
return self
|
|
802
645
|
|
|
803
|
-
def
|
|
646
|
+
def set_web_hook_url(self, url: str) -> 'AgentBase':
|
|
804
647
|
"""
|
|
805
|
-
|
|
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):
|
|
838
|
-
"""
|
|
839
|
-
Register tools defined with @AgentBase.tool class decorator
|
|
840
|
-
|
|
841
|
-
This method scans the class for methods decorated with @AgentBase.tool
|
|
842
|
-
and registers them automatically.
|
|
843
|
-
"""
|
|
844
|
-
# Get the class of this instance
|
|
845
|
-
cls = self.__class__
|
|
846
|
-
|
|
847
|
-
# Loop through all attributes in the class
|
|
848
|
-
for name in dir(cls):
|
|
849
|
-
# Get the attribute
|
|
850
|
-
attr = getattr(cls, name)
|
|
851
|
-
|
|
852
|
-
# Check if it's a method decorated with @AgentBase.tool
|
|
853
|
-
if inspect.ismethod(attr) or inspect.isfunction(attr):
|
|
854
|
-
if hasattr(attr, "_is_tool") and getattr(attr, "_is_tool", False):
|
|
855
|
-
# Extract tool information
|
|
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)
|
|
995
|
-
|
|
996
|
-
self._app = app
|
|
997
|
-
|
|
998
|
-
return self._app
|
|
999
|
-
|
|
1000
|
-
def get_prompt(self) -> Union[str, List[Dict[str, Any]]]:
|
|
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
|
|
1038
|
-
|
|
1039
|
-
# Return raw text (either explicitly set or default)
|
|
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
|
|
1080
|
-
|
|
1081
|
-
def on_function_call(self, name: str, args: Dict[str, Any], raw_data: Optional[Dict[str, Any]] = None) -> Any:
|
|
1082
|
-
"""
|
|
1083
|
-
Called when a SWAIG function is invoked
|
|
648
|
+
Override the default web_hook_url with a supplied URL string
|
|
1084
649
|
|
|
1085
650
|
Args:
|
|
1086
|
-
|
|
1087
|
-
args: Function arguments
|
|
1088
|
-
raw_data: Raw request data
|
|
651
|
+
url: The URL to use for SWAIG function webhooks
|
|
1089
652
|
|
|
1090
653
|
Returns:
|
|
1091
|
-
|
|
654
|
+
Self for method chaining
|
|
1092
655
|
"""
|
|
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)}"}
|
|
656
|
+
self._web_hook_url_override = url
|
|
657
|
+
return self
|
|
1122
658
|
|
|
1123
|
-
def
|
|
659
|
+
def set_post_prompt_url(self, url: str) -> 'AgentBase':
|
|
1124
660
|
"""
|
|
1125
|
-
|
|
661
|
+
Override the default post_prompt_url with a supplied URL string
|
|
1126
662
|
|
|
1127
663
|
Args:
|
|
1128
|
-
|
|
1129
|
-
password: Password from request
|
|
664
|
+
url: The URL to use for post-prompt summary delivery
|
|
1130
665
|
|
|
1131
666
|
Returns:
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
This method can be overridden by subclasses.
|
|
667
|
+
Self for method chaining
|
|
1135
668
|
"""
|
|
1136
|
-
|
|
669
|
+
self._post_prompt_url_override = url
|
|
670
|
+
return self
|
|
1137
671
|
|
|
1138
|
-
def
|
|
672
|
+
def add_swaig_query_params(self, params: Dict[str, str]) -> 'AgentBase':
|
|
1139
673
|
"""
|
|
1140
|
-
|
|
674
|
+
Add query parameters that will be included in all SWAIG webhook URLs
|
|
1141
675
|
|
|
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
|
|
676
|
+
This is particularly useful for preserving dynamic configuration state
|
|
677
|
+
across SWAIG callbacks. For example, if your dynamic config adds skills
|
|
678
|
+
based on query parameters, you can pass those same parameters through
|
|
679
|
+
to the SWAIG webhook so the same configuration is applied.
|
|
1164
680
|
|
|
1165
681
|
Args:
|
|
1166
|
-
|
|
1167
|
-
token: Token to validate
|
|
1168
|
-
call_id: Call ID for the session
|
|
682
|
+
params: Dictionary of query parameters to add to SWAIG URLs
|
|
1169
683
|
|
|
1170
684
|
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
|
|
685
|
+
Self for method chaining
|
|
1281
686
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
(
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
687
|
+
Example:
|
|
688
|
+
def dynamic_config(query_params, body_params, headers, agent):
|
|
689
|
+
if query_params.get('tier') == 'premium':
|
|
690
|
+
agent.add_skill('advanced_search')
|
|
691
|
+
# Preserve the tier param so SWAIG callbacks work
|
|
692
|
+
agent.add_swaig_query_params({'tier': 'premium'})
|
|
1288
693
|
"""
|
|
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)
|
|
694
|
+
if params and isinstance(params, dict):
|
|
695
|
+
self._swaig_query_params.update(params)
|
|
696
|
+
return self
|
|
1307
697
|
|
|
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:
|
|
698
|
+
def clear_swaig_query_params(self) -> 'AgentBase':
|
|
1375
699
|
"""
|
|
1376
|
-
|
|
700
|
+
Clear all SWAIG query parameters
|
|
1377
701
|
|
|
1378
|
-
Args:
|
|
1379
|
-
endpoint: The endpoint path (e.g., "swaig", "post_prompt")
|
|
1380
|
-
query_params: Optional query parameters to append
|
|
1381
|
-
|
|
1382
702
|
Returns:
|
|
1383
|
-
|
|
703
|
+
Self for method chaining
|
|
1384
704
|
"""
|
|
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
|
-
|
|
705
|
+
self._swaig_query_params = {}
|
|
706
|
+
return self
|
|
707
|
+
|
|
1464
708
|
def _render_swml(self, call_id: str = None, modifications: Optional[dict] = None) -> str:
|
|
1465
709
|
"""
|
|
1466
710
|
Render the complete SWML document using SWMLService methods
|
|
@@ -1472,52 +716,114 @@ class AgentBase(SWMLService):
|
|
|
1472
716
|
Returns:
|
|
1473
717
|
SWML document as a string
|
|
1474
718
|
"""
|
|
719
|
+
self.log.debug("_render_swml_called",
|
|
720
|
+
call_id=call_id,
|
|
721
|
+
has_modifications=bool(modifications),
|
|
722
|
+
use_ephemeral=bool(modifications and modifications.get("__use_ephemeral_agent")),
|
|
723
|
+
has_dynamic_callback=bool(self._dynamic_config_callback))
|
|
724
|
+
|
|
725
|
+
# Check if we need to use an ephemeral agent for dynamic configuration
|
|
726
|
+
agent_to_use = self
|
|
727
|
+
if modifications and modifications.get("__use_ephemeral_agent"):
|
|
728
|
+
# Create an ephemeral copy for this request
|
|
729
|
+
self.log.debug("creating_ephemeral_agent",
|
|
730
|
+
original_sections=len(self._prompt_manager._sections) if hasattr(self._prompt_manager, '_sections') else 0)
|
|
731
|
+
agent_to_use = self._create_ephemeral_copy()
|
|
732
|
+
self.log.debug("ephemeral_agent_created",
|
|
733
|
+
ephemeral_sections=len(agent_to_use._prompt_manager._sections) if hasattr(agent_to_use._prompt_manager, '_sections') else 0)
|
|
734
|
+
|
|
735
|
+
# Extract the request data
|
|
736
|
+
request = modifications.get("__request")
|
|
737
|
+
request_data = modifications.get("__request_data", {})
|
|
738
|
+
|
|
739
|
+
if self._dynamic_config_callback:
|
|
740
|
+
try:
|
|
741
|
+
# Extract request data
|
|
742
|
+
if request:
|
|
743
|
+
query_params = dict(request.query_params)
|
|
744
|
+
headers = dict(request.headers)
|
|
745
|
+
else:
|
|
746
|
+
# No request object - use empty defaults
|
|
747
|
+
query_params = {}
|
|
748
|
+
headers = {}
|
|
749
|
+
body_params = request_data
|
|
750
|
+
|
|
751
|
+
# Call the dynamic config callback with the ephemeral agent
|
|
752
|
+
# This allows FULL dynamic configuration including adding skills
|
|
753
|
+
self.log.debug("calling_dynamic_config_on_ephemeral", has_request=bool(request))
|
|
754
|
+
self._dynamic_config_callback(query_params, body_params, headers, agent_to_use)
|
|
755
|
+
self.log.debug("dynamic_config_complete",
|
|
756
|
+
ephemeral_sections_after=len(agent_to_use._prompt_manager._sections) if hasattr(agent_to_use._prompt_manager, '_sections') else 0)
|
|
757
|
+
|
|
758
|
+
except Exception as e:
|
|
759
|
+
self.log.error("dynamic_config_error", error=str(e))
|
|
760
|
+
|
|
761
|
+
# Clear the special markers so they don't affect rendering
|
|
762
|
+
modifications = None
|
|
763
|
+
|
|
1475
764
|
# Reset the document to a clean state
|
|
1476
|
-
|
|
765
|
+
agent_to_use.reset_document()
|
|
1477
766
|
|
|
1478
767
|
# Get prompt
|
|
1479
|
-
prompt =
|
|
768
|
+
prompt = agent_to_use.get_prompt()
|
|
1480
769
|
prompt_is_pom = isinstance(prompt, list)
|
|
1481
770
|
|
|
1482
771
|
# Get post-prompt
|
|
1483
|
-
post_prompt =
|
|
772
|
+
post_prompt = agent_to_use.get_post_prompt()
|
|
1484
773
|
|
|
1485
774
|
# Generate a call ID if needed
|
|
1486
|
-
if
|
|
1487
|
-
call_id =
|
|
775
|
+
if call_id is None:
|
|
776
|
+
call_id = agent_to_use._session_manager.create_session()
|
|
777
|
+
self.log.debug("generated_call_id", call_id=call_id)
|
|
778
|
+
else:
|
|
779
|
+
self.log.debug("using_provided_call_id", call_id=call_id)
|
|
1488
780
|
|
|
1489
|
-
#
|
|
1490
|
-
query_params = {}
|
|
781
|
+
# Start with any SWAIG query params that were set
|
|
782
|
+
query_params = agent_to_use._swaig_query_params.copy() if agent_to_use._swaig_query_params else {}
|
|
1491
783
|
|
|
1492
784
|
# Get the default webhook URL with auth
|
|
1493
|
-
default_webhook_url =
|
|
785
|
+
default_webhook_url = agent_to_use._build_webhook_url("swaig", query_params)
|
|
1494
786
|
|
|
1495
787
|
# Use override if set
|
|
1496
|
-
if hasattr(
|
|
1497
|
-
default_webhook_url =
|
|
788
|
+
if hasattr(agent_to_use, '_web_hook_url_override') and agent_to_use._web_hook_url_override:
|
|
789
|
+
default_webhook_url = agent_to_use._web_hook_url_override
|
|
1498
790
|
|
|
1499
791
|
# Prepare SWAIG object (correct format)
|
|
1500
792
|
swaig_obj = {}
|
|
1501
793
|
|
|
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
794
|
# Add native_functions if any are defined
|
|
1509
|
-
if
|
|
1510
|
-
swaig_obj["native_functions"] =
|
|
795
|
+
if agent_to_use.native_functions:
|
|
796
|
+
swaig_obj["native_functions"] = agent_to_use.native_functions
|
|
1511
797
|
|
|
1512
798
|
# Add includes if any are defined
|
|
1513
|
-
if
|
|
1514
|
-
swaig_obj["includes"] =
|
|
799
|
+
if agent_to_use._function_includes:
|
|
800
|
+
swaig_obj["includes"] = agent_to_use._function_includes
|
|
801
|
+
|
|
802
|
+
# Add internal_fillers if any are defined
|
|
803
|
+
if hasattr(agent_to_use, '_internal_fillers') and agent_to_use._internal_fillers:
|
|
804
|
+
swaig_obj["internal_fillers"] = agent_to_use._internal_fillers
|
|
1515
805
|
|
|
1516
806
|
# Create functions array
|
|
1517
807
|
functions = []
|
|
1518
808
|
|
|
809
|
+
# Debug logging to see what functions we have
|
|
810
|
+
self.log.debug("checking_swaig_functions",
|
|
811
|
+
agent_name=agent_to_use.name,
|
|
812
|
+
is_ephemeral=getattr(agent_to_use, '_is_ephemeral', False),
|
|
813
|
+
registry_id=id(agent_to_use._tool_registry),
|
|
814
|
+
agent_id=id(agent_to_use),
|
|
815
|
+
function_count=len(agent_to_use._tool_registry._swaig_functions) if hasattr(agent_to_use._tool_registry, '_swaig_functions') else 0,
|
|
816
|
+
functions=list(agent_to_use._tool_registry._swaig_functions.keys()) if hasattr(agent_to_use._tool_registry, '_swaig_functions') else [])
|
|
817
|
+
|
|
1519
818
|
# Add each function to the functions array
|
|
1520
|
-
|
|
819
|
+
# Check if the registry has the _swaig_functions attribute
|
|
820
|
+
if not hasattr(agent_to_use._tool_registry, '_swaig_functions'):
|
|
821
|
+
self.log.warning("tool_registry_missing_swaig_functions",
|
|
822
|
+
registry_id=id(agent_to_use._tool_registry),
|
|
823
|
+
agent_id=id(agent_to_use))
|
|
824
|
+
agent_to_use._tool_registry._swaig_functions = {}
|
|
825
|
+
|
|
826
|
+
for name, func in agent_to_use._tool_registry._swaig_functions.items():
|
|
1521
827
|
if isinstance(func, dict):
|
|
1522
828
|
# For raw dictionaries (DataMap functions), use the entire dictionary as-is
|
|
1523
829
|
# This preserves data_map and any other special fields
|
|
@@ -1531,76 +837,133 @@ class AgentBase(SWMLService):
|
|
|
1531
837
|
# Check if it's secure and get token for secure functions when we have a call_id
|
|
1532
838
|
token = None
|
|
1533
839
|
if func.secure and call_id:
|
|
1534
|
-
token =
|
|
840
|
+
token = agent_to_use._create_tool_token(tool_name=name, call_id=call_id)
|
|
841
|
+
self.log.debug("created_token_for_function", function=name, call_id=call_id, token_prefix=token[:20] if token else None)
|
|
1535
842
|
|
|
1536
843
|
# Prepare function entry
|
|
1537
844
|
function_entry = {
|
|
1538
845
|
"function": name,
|
|
1539
846
|
"description": func.description,
|
|
1540
|
-
"parameters":
|
|
1541
|
-
"type": "object",
|
|
1542
|
-
"properties": func.parameters
|
|
1543
|
-
}
|
|
847
|
+
"parameters": func._ensure_parameter_structure()
|
|
1544
848
|
}
|
|
1545
849
|
|
|
1546
|
-
# Add
|
|
1547
|
-
if func.
|
|
850
|
+
# Add wait_file if present (audio/video file URL)
|
|
851
|
+
if hasattr(func, 'wait_file') and func.wait_file:
|
|
852
|
+
wait_file_url = func.wait_file
|
|
853
|
+
# If wait_file is a relative URL, convert it to absolute using agent's base URL
|
|
854
|
+
if wait_file_url and not wait_file_url.startswith(('http://', 'https://', '//')):
|
|
855
|
+
# Build full URL using the agent's base URL
|
|
856
|
+
base_url = agent_to_use._get_base_url(include_auth=False)
|
|
857
|
+
# Handle relative paths appropriately
|
|
858
|
+
if not wait_file_url.startswith('/'):
|
|
859
|
+
wait_file_url = '/' + wait_file_url
|
|
860
|
+
wait_file_url = f"{base_url}{wait_file_url}"
|
|
861
|
+
function_entry["wait_file"] = wait_file_url
|
|
862
|
+
|
|
863
|
+
# Add fillers if present (text phrases to say while processing)
|
|
864
|
+
if hasattr(func, 'fillers') and func.fillers:
|
|
1548
865
|
function_entry["fillers"] = func.fillers
|
|
1549
866
|
|
|
867
|
+
# Add wait_file_loops if present
|
|
868
|
+
if hasattr(func, 'wait_file_loops') and func.wait_file_loops is not None:
|
|
869
|
+
function_entry["wait_file_loops"] = func.wait_file_loops
|
|
870
|
+
|
|
1550
871
|
# Handle webhook URL
|
|
1551
872
|
if hasattr(func, 'webhook_url') and func.webhook_url:
|
|
1552
873
|
# External webhook function - use the provided URL directly
|
|
1553
874
|
function_entry["web_hook_url"] = func.webhook_url
|
|
1554
|
-
elif token:
|
|
1555
|
-
# Local function with token - build local webhook URL
|
|
1556
|
-
|
|
1557
|
-
|
|
875
|
+
elif token or agent_to_use._swaig_query_params:
|
|
876
|
+
# Local function with token OR SWAIG query params - build local webhook URL
|
|
877
|
+
# Start with SWAIG query params
|
|
878
|
+
url_params = agent_to_use._swaig_query_params.copy() if agent_to_use._swaig_query_params else {}
|
|
879
|
+
if token:
|
|
880
|
+
url_params["__token"] = token # Use __token to avoid collision
|
|
881
|
+
function_entry["web_hook_url"] = agent_to_use._build_webhook_url("swaig", url_params)
|
|
1558
882
|
|
|
1559
883
|
functions.append(function_entry)
|
|
1560
884
|
|
|
1561
885
|
# Add functions array to SWAIG object if we have any
|
|
1562
886
|
if functions:
|
|
1563
887
|
swaig_obj["functions"] = functions
|
|
888
|
+
# Add defaults section now that we know we have functions
|
|
889
|
+
if "defaults" not in swaig_obj:
|
|
890
|
+
swaig_obj["defaults"] = {
|
|
891
|
+
"web_hook_url": default_webhook_url
|
|
892
|
+
}
|
|
1564
893
|
|
|
1565
894
|
# Add post-prompt URL with token if we have a post-prompt
|
|
1566
895
|
post_prompt_url = None
|
|
1567
896
|
if post_prompt:
|
|
1568
897
|
# Create a token for post_prompt if we have a call_id
|
|
1569
|
-
|
|
1570
|
-
if
|
|
898
|
+
# Start with SWAIG query params
|
|
899
|
+
query_params = agent_to_use._swaig_query_params.copy() if agent_to_use._swaig_query_params else {}
|
|
900
|
+
if call_id and hasattr(agent_to_use, '_session_manager'):
|
|
1571
901
|
try:
|
|
1572
|
-
token =
|
|
902
|
+
token = agent_to_use._session_manager.create_tool_token("post_prompt", call_id)
|
|
1573
903
|
if token:
|
|
1574
|
-
query_params["
|
|
904
|
+
query_params["__token"] = token # Use __token to avoid collision
|
|
1575
905
|
except Exception as e:
|
|
1576
|
-
|
|
906
|
+
agent_to_use.log.error("post_prompt_token_creation_error", error=str(e))
|
|
1577
907
|
|
|
1578
908
|
# Build the URL with the token (if any)
|
|
1579
|
-
post_prompt_url =
|
|
909
|
+
post_prompt_url = agent_to_use._build_webhook_url("post_prompt", query_params)
|
|
1580
910
|
|
|
1581
911
|
# Use override if set
|
|
1582
|
-
if hasattr(
|
|
1583
|
-
post_prompt_url =
|
|
912
|
+
if hasattr(agent_to_use, '_post_prompt_url_override') and agent_to_use._post_prompt_url_override:
|
|
913
|
+
post_prompt_url = agent_to_use._post_prompt_url_override
|
|
1584
914
|
|
|
1585
|
-
#
|
|
1586
|
-
|
|
915
|
+
# ========== PHASE 1: PRE-ANSWER VERBS ==========
|
|
916
|
+
# These run while the call is still ringing
|
|
917
|
+
for verb_name, verb_config in agent_to_use._pre_answer_verbs:
|
|
918
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
919
|
+
|
|
920
|
+
# ========== PHASE 2: ANSWER VERB ==========
|
|
921
|
+
# Only add answer verb if auto_answer is enabled
|
|
922
|
+
if agent_to_use._auto_answer:
|
|
923
|
+
agent_to_use.add_verb("answer", agent_to_use._answer_config)
|
|
924
|
+
|
|
925
|
+
# ========== PHASE 3: POST-ANSWER VERBS ==========
|
|
926
|
+
# These run after answer but before AI
|
|
927
|
+
|
|
928
|
+
# Add recording if enabled (this is a post-answer verb)
|
|
929
|
+
if agent_to_use._record_call:
|
|
930
|
+
agent_to_use.add_verb("record_call", {
|
|
931
|
+
"format": agent_to_use._record_format,
|
|
932
|
+
"stereo": agent_to_use._record_stereo
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
# Add user-defined post-answer verbs
|
|
936
|
+
for verb_name, verb_config in agent_to_use._post_answer_verbs:
|
|
937
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1587
938
|
|
|
1588
939
|
# Use the AI verb handler to build and validate the AI verb config
|
|
1589
940
|
ai_config = {}
|
|
1590
941
|
|
|
1591
942
|
# Get the AI verb handler
|
|
1592
|
-
ai_handler =
|
|
943
|
+
ai_handler = agent_to_use.verb_registry.get_handler("ai")
|
|
1593
944
|
if ai_handler:
|
|
1594
945
|
try:
|
|
1595
946
|
# Check if we're in contexts mode
|
|
1596
|
-
if
|
|
1597
|
-
# Generate contexts
|
|
1598
|
-
contexts_dict =
|
|
947
|
+
if agent_to_use._contexts_defined and agent_to_use._contexts_builder:
|
|
948
|
+
# Generate contexts and combine with base prompt
|
|
949
|
+
contexts_dict = agent_to_use._contexts_builder.to_dict()
|
|
1599
950
|
|
|
1600
|
-
#
|
|
951
|
+
# Determine base prompt (required when using contexts)
|
|
952
|
+
base_prompt_text = None
|
|
953
|
+
base_prompt_pom = None
|
|
954
|
+
|
|
955
|
+
if prompt_is_pom:
|
|
956
|
+
base_prompt_pom = prompt
|
|
957
|
+
elif prompt:
|
|
958
|
+
base_prompt_text = prompt
|
|
959
|
+
else:
|
|
960
|
+
# Provide default base prompt if none exists
|
|
961
|
+
base_prompt_text = f"You are {agent_to_use.name}, a helpful AI assistant that follows structured workflows."
|
|
962
|
+
|
|
963
|
+
# Build AI config with base prompt + contexts
|
|
1601
964
|
ai_config = ai_handler.build_config(
|
|
1602
|
-
prompt_text=
|
|
1603
|
-
prompt_pom=
|
|
965
|
+
prompt_text=base_prompt_text,
|
|
966
|
+
prompt_pom=base_prompt_pom,
|
|
1604
967
|
contexts=contexts_dict,
|
|
1605
968
|
post_prompt=post_prompt,
|
|
1606
969
|
post_prompt_url=post_prompt_url,
|
|
@@ -1619,28 +982,54 @@ class AgentBase(SWMLService):
|
|
|
1619
982
|
# Add new configuration parameters to the AI config
|
|
1620
983
|
|
|
1621
984
|
# Add hints if any
|
|
1622
|
-
if
|
|
1623
|
-
ai_config["hints"] =
|
|
985
|
+
if agent_to_use._hints:
|
|
986
|
+
ai_config["hints"] = agent_to_use._hints
|
|
1624
987
|
|
|
1625
988
|
# Add languages if any
|
|
1626
|
-
if
|
|
1627
|
-
ai_config["languages"] =
|
|
989
|
+
if agent_to_use._languages:
|
|
990
|
+
ai_config["languages"] = agent_to_use._languages
|
|
1628
991
|
|
|
1629
992
|
# Add pronunciation rules if any
|
|
1630
|
-
if
|
|
1631
|
-
ai_config["pronounce"] =
|
|
993
|
+
if agent_to_use._pronounce:
|
|
994
|
+
ai_config["pronounce"] = agent_to_use._pronounce
|
|
1632
995
|
|
|
1633
996
|
# Add params if any
|
|
1634
|
-
if
|
|
1635
|
-
ai_config["params"] =
|
|
997
|
+
if agent_to_use._params:
|
|
998
|
+
ai_config["params"] = agent_to_use._params
|
|
1636
999
|
|
|
1637
1000
|
# Add global_data if any
|
|
1638
|
-
if
|
|
1639
|
-
ai_config["global_data"] =
|
|
1001
|
+
if agent_to_use._global_data:
|
|
1002
|
+
ai_config["global_data"] = agent_to_use._global_data
|
|
1003
|
+
|
|
1004
|
+
# Always add LLM parameters to prompt
|
|
1005
|
+
if "prompt" in ai_config:
|
|
1006
|
+
# Only add LLM params if explicitly set
|
|
1007
|
+
if agent_to_use._prompt_llm_params:
|
|
1008
|
+
if isinstance(ai_config["prompt"], dict):
|
|
1009
|
+
ai_config["prompt"].update(agent_to_use._prompt_llm_params)
|
|
1010
|
+
elif isinstance(ai_config["prompt"], str):
|
|
1011
|
+
# Convert string prompt to dict format
|
|
1012
|
+
ai_config["prompt"] = {
|
|
1013
|
+
"text": ai_config["prompt"],
|
|
1014
|
+
**agent_to_use._prompt_llm_params
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
# Only add LLM parameters to post_prompt if explicitly set
|
|
1018
|
+
if post_prompt and "post_prompt" in ai_config:
|
|
1019
|
+
# Only add LLM params if explicitly set
|
|
1020
|
+
if agent_to_use._post_prompt_llm_params:
|
|
1021
|
+
if isinstance(ai_config["post_prompt"], dict):
|
|
1022
|
+
ai_config["post_prompt"].update(agent_to_use._post_prompt_llm_params)
|
|
1023
|
+
elif isinstance(ai_config["post_prompt"], str):
|
|
1024
|
+
# Convert string post_prompt to dict format
|
|
1025
|
+
ai_config["post_prompt"] = {
|
|
1026
|
+
"text": ai_config["post_prompt"],
|
|
1027
|
+
**agent_to_use._post_prompt_llm_params
|
|
1028
|
+
}
|
|
1640
1029
|
|
|
1641
1030
|
except ValueError as e:
|
|
1642
|
-
if not
|
|
1643
|
-
|
|
1031
|
+
if not agent_to_use._suppress_logs:
|
|
1032
|
+
agent_to_use.log.error("ai_verb_config_error", error=str(e))
|
|
1644
1033
|
else:
|
|
1645
1034
|
# Fallback if no handler (shouldn't happen but just in case)
|
|
1646
1035
|
ai_config = {
|
|
@@ -1658,24 +1047,30 @@ class AgentBase(SWMLService):
|
|
|
1658
1047
|
ai_config["SWAIG"] = swaig_obj
|
|
1659
1048
|
|
|
1660
1049
|
# Add the new configurations if not already added by the handler
|
|
1661
|
-
if
|
|
1662
|
-
ai_config["hints"] =
|
|
1050
|
+
if agent_to_use._hints and "hints" not in ai_config:
|
|
1051
|
+
ai_config["hints"] = agent_to_use._hints
|
|
1663
1052
|
|
|
1664
|
-
if
|
|
1665
|
-
ai_config["languages"] =
|
|
1053
|
+
if agent_to_use._languages and "languages" not in ai_config:
|
|
1054
|
+
ai_config["languages"] = agent_to_use._languages
|
|
1666
1055
|
|
|
1667
|
-
if
|
|
1668
|
-
ai_config["pronounce"] =
|
|
1056
|
+
if agent_to_use._pronounce and "pronounce" not in ai_config:
|
|
1057
|
+
ai_config["pronounce"] = agent_to_use._pronounce
|
|
1669
1058
|
|
|
1670
|
-
if
|
|
1671
|
-
ai_config["params"] =
|
|
1672
|
-
|
|
1673
|
-
if self._global_data and "global_data" not in ai_config:
|
|
1674
|
-
ai_config["global_data"] = self._global_data
|
|
1059
|
+
if agent_to_use._params and "params" not in ai_config:
|
|
1060
|
+
ai_config["params"] = agent_to_use._params
|
|
1675
1061
|
|
|
1062
|
+
if agent_to_use._global_data and "global_data" not in ai_config:
|
|
1063
|
+
ai_config["global_data"] = agent_to_use._global_data
|
|
1064
|
+
|
|
1065
|
+
# ========== PHASE 4: AI VERB ==========
|
|
1676
1066
|
# Add the AI verb to the document
|
|
1677
|
-
|
|
1678
|
-
|
|
1067
|
+
agent_to_use.add_verb("ai", ai_config)
|
|
1068
|
+
|
|
1069
|
+
# ========== PHASE 5: POST-AI VERBS ==========
|
|
1070
|
+
# These run after the AI conversation ends
|
|
1071
|
+
for verb_name, verb_config in agent_to_use._post_ai_verbs:
|
|
1072
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1073
|
+
|
|
1679
1074
|
# Apply any modifications from the callback to agent state
|
|
1680
1075
|
if modifications and isinstance(modifications, dict):
|
|
1681
1076
|
# Handle global_data modifications by updating the AI config directly
|
|
@@ -1690,1457 +1085,74 @@ class AgentBase(SWMLService):
|
|
|
1690
1085
|
ai_config[key] = value
|
|
1691
1086
|
|
|
1692
1087
|
# 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")
|
|
1088
|
+
agent_to_use.reset_document()
|
|
1089
|
+
|
|
1090
|
+
# Rebuild with 5-phase approach
|
|
1091
|
+
# PHASE 1: Pre-answer verbs
|
|
1092
|
+
for verb_name, verb_config in agent_to_use._pre_answer_verbs:
|
|
1093
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1094
|
+
|
|
1095
|
+
# PHASE 2: Answer verb (if auto_answer enabled)
|
|
1096
|
+
if agent_to_use._auto_answer:
|
|
1097
|
+
agent_to_use.add_verb("answer", agent_to_use._answer_config)
|
|
1098
|
+
|
|
1099
|
+
# PHASE 3: Post-answer verbs
|
|
1100
|
+
if agent_to_use._record_call:
|
|
1101
|
+
agent_to_use.add_verb("record_call", {
|
|
1102
|
+
"format": agent_to_use._record_format,
|
|
1103
|
+
"stereo": agent_to_use._record_stereo
|
|
1104
|
+
})
|
|
1105
|
+
for verb_name, verb_config in agent_to_use._post_answer_verbs:
|
|
1106
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
1107
|
+
|
|
1108
|
+
# PHASE 4: AI verb
|
|
1109
|
+
agent_to_use.add_verb("ai", ai_config)
|
|
1110
|
+
|
|
1111
|
+
# PHASE 5: Post-AI verbs
|
|
1112
|
+
for verb_name, verb_config in agent_to_use._post_ai_verbs:
|
|
1113
|
+
agent_to_use.add_verb(verb_name, verb_config)
|
|
2909
1114
|
|
|
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
|
-
)
|
|
1115
|
+
# Return the rendered document as a string
|
|
1116
|
+
return agent_to_use.render_document()
|
|
2967
1117
|
|
|
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")
|
|
1118
|
+
def _build_webhook_url(self, endpoint: str, query_params: Optional[Dict[str, str]] = None) -> str:
|
|
1119
|
+
"""
|
|
1120
|
+
Helper method to build webhook URLs consistently
|
|
2979
1121
|
|
|
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
|
|
1122
|
+
Args:
|
|
1123
|
+
endpoint: The endpoint path (e.g., "swaig", "post_prompt")
|
|
1124
|
+
query_params: Optional query parameters to append
|
|
3014
1125
|
|
|
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
|
-
)
|
|
1126
|
+
Returns:
|
|
1127
|
+
Fully constructed webhook URL
|
|
1128
|
+
"""
|
|
1129
|
+
# Check for serverless environment and use appropriate URL generation
|
|
1130
|
+
mode = get_execution_mode()
|
|
1131
|
+
|
|
1132
|
+
if mode != 'server':
|
|
1133
|
+
# In serverless mode, use the serverless-appropriate URL with auth
|
|
1134
|
+
base_url = self.get_full_url(include_auth=True)
|
|
3043
1135
|
|
|
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 = {}
|
|
1136
|
+
# Ensure the endpoint has a trailing slash to prevent redirects
|
|
1137
|
+
if endpoint in ["swaig", "post_prompt"]:
|
|
1138
|
+
endpoint = f"{endpoint}/"
|
|
3060
1139
|
|
|
3061
|
-
#
|
|
3062
|
-
|
|
1140
|
+
# Build the full webhook URL
|
|
1141
|
+
url = f"{base_url}/{endpoint}"
|
|
3063
1142
|
|
|
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))
|
|
1143
|
+
# Add query parameters if any (only if they have values)
|
|
1144
|
+
if query_params:
|
|
1145
|
+
# Remove any call_id from query params
|
|
1146
|
+
filtered_params = {k: v for k, v in query_params.items() if k != "call_id" and v}
|
|
1147
|
+
if filtered_params:
|
|
1148
|
+
params = "&".join([f"{k}={v}" for k, v in filtered_params.items()])
|
|
1149
|
+
url = f"{url}?{params}"
|
|
3075
1150
|
|
|
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")
|
|
1151
|
+
return url
|
|
3096
1152
|
|
|
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
|
-
)
|
|
1153
|
+
# Server mode - use the parent class's implementation from SWMLService
|
|
1154
|
+
# which properly handles SWML_PROXY_URL_BASE environment variable
|
|
1155
|
+
return super()._build_webhook_url(endpoint, query_params)
|
|
3144
1156
|
|
|
3145
1157
|
def _find_summary_in_post_data(self, body, logger):
|
|
3146
1158
|
"""
|
|
@@ -3174,268 +1186,165 @@ class AgentBase(SWMLService):
|
|
|
3174
1186
|
return pdata["raw"]
|
|
3175
1187
|
|
|
3176
1188
|
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
1189
|
|
|
3224
|
-
def
|
|
1190
|
+
def _create_ephemeral_copy(self):
|
|
3225
1191
|
"""
|
|
3226
|
-
|
|
1192
|
+
Create a lightweight copy of this agent for ephemeral configuration.
|
|
3227
1193
|
|
|
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
|
|
1194
|
+
This creates a partial copy that shares most resources but has independent
|
|
1195
|
+
configuration for SWML generation. Used when dynamic configuration callbacks
|
|
1196
|
+
need to modify the agent without affecting the persistent state.
|
|
3270
1197
|
|
|
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
1198
|
Returns:
|
|
3277
|
-
|
|
1199
|
+
A lightweight copy of the agent suitable for ephemeral modifications
|
|
3278
1200
|
"""
|
|
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))
|
|
1201
|
+
import copy
|
|
3319
1202
|
|
|
3320
|
-
#
|
|
3321
|
-
|
|
1203
|
+
# Create a new instance of the same class
|
|
1204
|
+
cls = self.__class__
|
|
1205
|
+
ephemeral_agent = cls.__new__(cls)
|
|
1206
|
+
|
|
1207
|
+
# Copy all attributes as shallow references first
|
|
1208
|
+
for key, value in self.__dict__.items():
|
|
1209
|
+
setattr(ephemeral_agent, key, value)
|
|
1210
|
+
|
|
1211
|
+
# Deep copy only the configuration that affects SWML generation
|
|
1212
|
+
# These are the parts that dynamic config might modify
|
|
1213
|
+
ephemeral_agent._params = copy.deepcopy(self._params)
|
|
1214
|
+
ephemeral_agent._hints = copy.deepcopy(self._hints)
|
|
1215
|
+
ephemeral_agent._languages = copy.deepcopy(self._languages)
|
|
1216
|
+
ephemeral_agent._pronounce = copy.deepcopy(self._pronounce)
|
|
1217
|
+
ephemeral_agent._global_data = copy.deepcopy(self._global_data)
|
|
1218
|
+
ephemeral_agent._function_includes = copy.deepcopy(self._function_includes)
|
|
3322
1219
|
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
1220
|
+
# Deep copy verb insertion points for call flow customization
|
|
1221
|
+
ephemeral_agent._pre_answer_verbs = copy.deepcopy(self._pre_answer_verbs)
|
|
1222
|
+
ephemeral_agent._answer_config = copy.deepcopy(self._answer_config)
|
|
1223
|
+
ephemeral_agent._post_answer_verbs = copy.deepcopy(self._post_answer_verbs)
|
|
1224
|
+
ephemeral_agent._post_ai_verbs = copy.deepcopy(self._post_ai_verbs)
|
|
1225
|
+
|
|
1226
|
+
# Deep copy LLM parameters
|
|
1227
|
+
ephemeral_agent._prompt_llm_params = copy.deepcopy(self._prompt_llm_params)
|
|
1228
|
+
ephemeral_agent._post_prompt_llm_params = copy.deepcopy(self._post_prompt_llm_params)
|
|
1229
|
+
|
|
1230
|
+
# Copy internal fillers if they exist
|
|
1231
|
+
if hasattr(self, '_internal_fillers'):
|
|
1232
|
+
ephemeral_agent._internal_fillers = copy.deepcopy(self._internal_fillers)
|
|
1233
|
+
|
|
1234
|
+
# Don't deep copy _contexts_builder - it has a circular reference to the agent
|
|
1235
|
+
# The contexts are already copied via _prompt_manager._contexts (below)
|
|
1236
|
+
# Just copy the flag indicating contexts are defined
|
|
1237
|
+
if hasattr(self, '_contexts_defined'):
|
|
1238
|
+
ephemeral_agent._contexts_defined = self._contexts_defined
|
|
1239
|
+
|
|
1240
|
+
# Deep copy the POM object if it exists to prevent sharing prompt sections
|
|
1241
|
+
if hasattr(self, 'pom') and self.pom:
|
|
1242
|
+
ephemeral_agent.pom = copy.deepcopy(self.pom)
|
|
1243
|
+
# Handle native_functions which might be stored as an attribute or property
|
|
1244
|
+
if hasattr(self, '_native_functions'):
|
|
1245
|
+
ephemeral_agent._native_functions = copy.deepcopy(self._native_functions)
|
|
1246
|
+
elif hasattr(self, 'native_functions'):
|
|
1247
|
+
ephemeral_agent.native_functions = copy.deepcopy(self.native_functions)
|
|
1248
|
+
ephemeral_agent._swaig_query_params = copy.deepcopy(self._swaig_query_params)
|
|
1249
|
+
|
|
1250
|
+
# Create new manager instances that point to the ephemeral agent
|
|
1251
|
+
# This breaks the circular reference and allows independent modification
|
|
1252
|
+
from signalwire_agents.core.agent.prompt.manager import PromptManager
|
|
1253
|
+
from signalwire_agents.core.agent.tools.registry import ToolRegistry
|
|
1254
|
+
|
|
1255
|
+
# Create new prompt manager for the ephemeral agent
|
|
1256
|
+
ephemeral_agent._prompt_manager = PromptManager(ephemeral_agent)
|
|
1257
|
+
# Copy ALL PromptManager state
|
|
1258
|
+
if hasattr(self._prompt_manager, '_sections'):
|
|
1259
|
+
ephemeral_agent._prompt_manager._sections = copy.deepcopy(self._prompt_manager._sections)
|
|
1260
|
+
ephemeral_agent._prompt_manager._prompt_text = copy.deepcopy(self._prompt_manager._prompt_text)
|
|
1261
|
+
ephemeral_agent._prompt_manager._post_prompt_text = copy.deepcopy(self._prompt_manager._post_prompt_text)
|
|
1262
|
+
ephemeral_agent._prompt_manager._contexts = copy.deepcopy(self._prompt_manager._contexts)
|
|
1263
|
+
|
|
1264
|
+
# Create new tool registry for the ephemeral agent
|
|
1265
|
+
ephemeral_agent._tool_registry = ToolRegistry(ephemeral_agent)
|
|
1266
|
+
# Copy the SWAIG functions - we need a shallow copy here because
|
|
1267
|
+
# the functions themselves can be shared, we just need a new dict
|
|
1268
|
+
if hasattr(self._tool_registry, '_swaig_functions'):
|
|
1269
|
+
ephemeral_agent._tool_registry._swaig_functions = self._tool_registry._swaig_functions.copy()
|
|
1270
|
+
if hasattr(self._tool_registry, '_tool_instances'):
|
|
1271
|
+
ephemeral_agent._tool_registry._tool_instances = self._tool_registry._tool_instances.copy()
|
|
1272
|
+
|
|
1273
|
+
# Create a new skill manager for the ephemeral agent
|
|
1274
|
+
# This is important because skills register tools with the agent's registry
|
|
1275
|
+
from signalwire_agents.core.skill_manager import SkillManager
|
|
1276
|
+
ephemeral_agent.skill_manager = SkillManager(ephemeral_agent)
|
|
1277
|
+
|
|
1278
|
+
# Copy any already loaded skills from the original agent
|
|
1279
|
+
# This ensures skills loaded during __init__ are available in the ephemeral agent
|
|
1280
|
+
if hasattr(self.skill_manager, 'loaded_skills'):
|
|
1281
|
+
for skill_key, skill_instance in self.skill_manager.loaded_skills.items():
|
|
1282
|
+
# Re-load the skill in the ephemeral agent's context
|
|
1283
|
+
# We need to get the skill name and params from the existing instance
|
|
1284
|
+
skill_name = skill_instance.SKILL_NAME
|
|
1285
|
+
skill_params = getattr(skill_instance, 'params', {})
|
|
1286
|
+
try:
|
|
1287
|
+
ephemeral_agent.skill_manager.load_skill(skill_name, type(skill_instance), skill_params)
|
|
1288
|
+
except Exception as e:
|
|
1289
|
+
self.log.warning("failed_to_copy_skill_to_ephemeral",
|
|
1290
|
+
skill_name=skill_name,
|
|
1291
|
+
error=str(e))
|
|
3332
1292
|
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
- None if normal processing should continue
|
|
1293
|
+
# Re-bind the tool decorator method to the new instance
|
|
1294
|
+
ephemeral_agent.tool = ephemeral_agent._tool_decorator
|
|
3336
1295
|
|
|
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
|
|
1296
|
+
# Share the logger but bind it to indicate ephemeral copy
|
|
1297
|
+
ephemeral_agent.log = self.log.bind(ephemeral=True)
|
|
3355
1298
|
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
the agent's voice, prompt, parameters, etc. based on request data.
|
|
1299
|
+
# Mark this as an ephemeral agent to prevent double application of dynamic config
|
|
1300
|
+
ephemeral_agent._is_ephemeral = True
|
|
3359
1301
|
|
|
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':
|
|
1302
|
+
return ephemeral_agent
|
|
1303
|
+
|
|
1304
|
+
async def _handle_request(self, request: Request, response: Response):
|
|
3381
1305
|
"""
|
|
3382
|
-
|
|
1306
|
+
Override SWMLService's _handle_request to use AgentBase's _render_swml
|
|
3383
1307
|
|
|
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
|
|
1308
|
+
This ensures that when routes are handled by SWMLService's router,
|
|
1309
|
+
they still use AgentBase's SWML rendering logic.
|
|
3391
1310
|
"""
|
|
3392
|
-
if
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
1311
|
+
# Use WebMixin's implementation if available
|
|
1312
|
+
if hasattr(super(), '_handle_root_request'):
|
|
1313
|
+
return await self._handle_root_request(request)
|
|
1314
|
+
|
|
1315
|
+
# Fallback to basic implementation
|
|
1316
|
+
try:
|
|
1317
|
+
# Parse body if POST request
|
|
1318
|
+
body = {}
|
|
1319
|
+
if request.method == "POST":
|
|
1320
|
+
try:
|
|
1321
|
+
body = await request.json()
|
|
1322
|
+
except:
|
|
1323
|
+
pass
|
|
3396
1324
|
|
|
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)
|
|
1325
|
+
# Get call_id
|
|
1326
|
+
call_id = body.get("call_id") if body else request.query_params.get("call_id")
|
|
3404
1327
|
|
|
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
|
|
1328
|
+
# Check auth
|
|
1329
|
+
if not self._check_basic_auth(request):
|
|
1330
|
+
return Response(
|
|
1331
|
+
content=json.dumps({"error": "Unauthorized"}),
|
|
1332
|
+
status_code=401,
|
|
1333
|
+
headers={"WWW-Authenticate": "Basic"},
|
|
1334
|
+
media_type="application/json"
|
|
1335
|
+
)
|
|
3418
1336
|
|
|
3419
|
-
|
|
3420
|
-
|
|
1337
|
+
# Render SWML using AgentBase's method
|
|
1338
|
+
swml = self._render_swml(call_id)
|
|
3421
1339
|
|
|
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)
|
|
1340
|
+
return Response(
|
|
1341
|
+
content=swml,
|
|
1342
|
+
media_type="application/json"
|
|
1343
|
+
)
|
|
1344
|
+
except Exception as e:
|
|
1345
|
+
return Response(
|
|
1346
|
+
content=json.dumps({"error": str(e)}),
|
|
1347
|
+
status_code=500,
|
|
1348
|
+
media_type="application/json"
|
|
1349
|
+
)
|
|
1350
|
+
|