signalwire-agents 0.1.23__py3-none-any.whl → 0.1.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- signalwire_agents/__init__.py +1 -1
- signalwire_agents/agent_server.py +2 -1
- signalwire_agents/cli/config.py +61 -0
- signalwire_agents/cli/core/__init__.py +1 -0
- signalwire_agents/cli/core/agent_loader.py +254 -0
- signalwire_agents/cli/core/argparse_helpers.py +164 -0
- signalwire_agents/cli/core/dynamic_config.py +62 -0
- signalwire_agents/cli/execution/__init__.py +1 -0
- signalwire_agents/cli/execution/datamap_exec.py +437 -0
- signalwire_agents/cli/execution/webhook_exec.py +125 -0
- signalwire_agents/cli/output/__init__.py +1 -0
- signalwire_agents/cli/output/output_formatter.py +132 -0
- signalwire_agents/cli/output/swml_dump.py +177 -0
- signalwire_agents/cli/simulation/__init__.py +1 -0
- signalwire_agents/cli/simulation/data_generation.py +365 -0
- signalwire_agents/cli/simulation/data_overrides.py +187 -0
- signalwire_agents/cli/simulation/mock_env.py +271 -0
- signalwire_agents/cli/test_swaig.py +522 -2539
- signalwire_agents/cli/types.py +72 -0
- signalwire_agents/core/agent/__init__.py +1 -3
- signalwire_agents/core/agent/config/__init__.py +1 -3
- signalwire_agents/core/agent/prompt/manager.py +25 -7
- signalwire_agents/core/agent/tools/decorator.py +2 -0
- signalwire_agents/core/agent/tools/registry.py +8 -0
- signalwire_agents/core/agent_base.py +492 -3053
- signalwire_agents/core/function_result.py +31 -42
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
- signalwire_agents/core/mixins/auth_mixin.py +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +345 -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 +219 -0
- signalwire_agents/core/mixins/tool_mixin.py +295 -0
- signalwire_agents/core/mixins/web_mixin.py +1130 -0
- signalwire_agents/core/skill_manager.py +3 -1
- signalwire_agents/core/swaig_function.py +10 -1
- signalwire_agents/core/swml_service.py +140 -58
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/native_vector_search/skill.py +33 -13
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +4 -0
- signalwire_agents/skills/spider/skill.py +479 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +1 -0
- signalwire_agents/skills/swml_transfer/skill.py +257 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/METADATA +47 -2
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/RECORD +62 -22
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/entry_points.txt +1 -1
- signalwire_agents/core/agent/config/ephemeral.py +0 -176
- signalwire_agents-0.1.23.data/data/schema.json +0 -5611
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,368 @@
|
|
1
|
+
"""
|
2
|
+
Copyright (c) 2025 SignalWire
|
3
|
+
|
4
|
+
This file is part of the SignalWire AI Agents SDK.
|
5
|
+
|
6
|
+
Licensed under the MIT License.
|
7
|
+
See LICENSE file in the project root for full license information.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
import json
|
12
|
+
import sys
|
13
|
+
from typing import Optional, Dict, Any
|
14
|
+
|
15
|
+
from signalwire_agents.core.logging_config import get_execution_mode
|
16
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
17
|
+
|
18
|
+
|
19
|
+
class ServerlessMixin:
|
20
|
+
"""
|
21
|
+
Mixin class containing all serverless/cloud platform methods for AgentBase
|
22
|
+
"""
|
23
|
+
|
24
|
+
def handle_serverless_request(self, event=None, context=None, mode=None):
|
25
|
+
"""
|
26
|
+
Handle serverless environment requests (CGI, Lambda, Cloud Functions)
|
27
|
+
|
28
|
+
Args:
|
29
|
+
event: Serverless event object (Lambda, Cloud Functions)
|
30
|
+
context: Serverless context object (Lambda, Cloud Functions)
|
31
|
+
mode: Override execution mode (from force_mode in run())
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
Response appropriate for the serverless platform
|
35
|
+
"""
|
36
|
+
if mode is None:
|
37
|
+
mode = get_execution_mode()
|
38
|
+
|
39
|
+
try:
|
40
|
+
if mode == 'cgi':
|
41
|
+
# Check authentication in CGI mode
|
42
|
+
if not self._check_cgi_auth():
|
43
|
+
return self._send_cgi_auth_challenge()
|
44
|
+
|
45
|
+
path_info = os.getenv('PATH_INFO', '').strip('/')
|
46
|
+
if not path_info:
|
47
|
+
return self._render_swml()
|
48
|
+
else:
|
49
|
+
# Parse CGI request for SWAIG function call
|
50
|
+
args = {}
|
51
|
+
call_id = None
|
52
|
+
raw_data = None
|
53
|
+
|
54
|
+
# Try to parse POST data from stdin for CGI
|
55
|
+
import sys
|
56
|
+
content_length = os.getenv('CONTENT_LENGTH')
|
57
|
+
if content_length and content_length.isdigit():
|
58
|
+
try:
|
59
|
+
post_data = sys.stdin.read(int(content_length))
|
60
|
+
if post_data:
|
61
|
+
raw_data = json.loads(post_data)
|
62
|
+
call_id = raw_data.get("call_id")
|
63
|
+
|
64
|
+
# Extract arguments like the FastAPI handler does
|
65
|
+
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
66
|
+
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
67
|
+
args = raw_data["argument"]["parsed"][0]
|
68
|
+
elif "raw" in raw_data["argument"]:
|
69
|
+
try:
|
70
|
+
args = json.loads(raw_data["argument"]["raw"])
|
71
|
+
except Exception:
|
72
|
+
pass
|
73
|
+
except Exception:
|
74
|
+
# If parsing fails, continue with empty args
|
75
|
+
pass
|
76
|
+
|
77
|
+
return self._execute_swaig_function(path_info, args, call_id, raw_data)
|
78
|
+
|
79
|
+
elif mode == 'lambda':
|
80
|
+
# Check authentication in Lambda mode
|
81
|
+
if not self._check_lambda_auth(event):
|
82
|
+
return self._send_lambda_auth_challenge()
|
83
|
+
|
84
|
+
if event:
|
85
|
+
path = event.get('pathParameters', {}).get('proxy', '') if event.get('pathParameters') else ''
|
86
|
+
if not path:
|
87
|
+
swml_response = self._render_swml()
|
88
|
+
return {
|
89
|
+
"statusCode": 200,
|
90
|
+
"headers": {"Content-Type": "application/json"},
|
91
|
+
"body": swml_response
|
92
|
+
}
|
93
|
+
else:
|
94
|
+
# Parse Lambda event for SWAIG function call
|
95
|
+
args = {}
|
96
|
+
call_id = None
|
97
|
+
raw_data = None
|
98
|
+
|
99
|
+
# Parse request body if present
|
100
|
+
body_content = event.get('body')
|
101
|
+
if body_content:
|
102
|
+
try:
|
103
|
+
if isinstance(body_content, str):
|
104
|
+
raw_data = json.loads(body_content)
|
105
|
+
else:
|
106
|
+
raw_data = body_content
|
107
|
+
|
108
|
+
call_id = raw_data.get("call_id")
|
109
|
+
|
110
|
+
# Extract arguments like the FastAPI handler does
|
111
|
+
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
112
|
+
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
113
|
+
args = raw_data["argument"]["parsed"][0]
|
114
|
+
elif "raw" in raw_data["argument"]:
|
115
|
+
try:
|
116
|
+
args = json.loads(raw_data["argument"]["raw"])
|
117
|
+
except Exception:
|
118
|
+
pass
|
119
|
+
except Exception:
|
120
|
+
# If parsing fails, continue with empty args
|
121
|
+
pass
|
122
|
+
|
123
|
+
result = self._execute_swaig_function(path, args, call_id, raw_data)
|
124
|
+
return {
|
125
|
+
"statusCode": 200,
|
126
|
+
"headers": {"Content-Type": "application/json"},
|
127
|
+
"body": json.dumps(result) if isinstance(result, dict) else str(result)
|
128
|
+
}
|
129
|
+
else:
|
130
|
+
# Handle case when event is None (direct Lambda call with no event)
|
131
|
+
swml_response = self._render_swml()
|
132
|
+
return {
|
133
|
+
"statusCode": 200,
|
134
|
+
"headers": {"Content-Type": "application/json"},
|
135
|
+
"body": swml_response
|
136
|
+
}
|
137
|
+
|
138
|
+
elif mode == 'google_cloud_function':
|
139
|
+
# Check authentication in Google Cloud Functions mode
|
140
|
+
if not self._check_google_cloud_function_auth(event):
|
141
|
+
return self._send_google_cloud_function_auth_challenge()
|
142
|
+
|
143
|
+
return self._handle_google_cloud_function_request(event)
|
144
|
+
|
145
|
+
elif mode == 'azure_function':
|
146
|
+
# Check authentication in Azure Functions mode
|
147
|
+
if not self._check_azure_function_auth(event):
|
148
|
+
return self._send_azure_function_auth_challenge()
|
149
|
+
|
150
|
+
return self._handle_azure_function_request(event)
|
151
|
+
|
152
|
+
|
153
|
+
except Exception as e:
|
154
|
+
import logging
|
155
|
+
logging.error(f"Error in serverless request handler: {e}")
|
156
|
+
if mode == 'lambda':
|
157
|
+
return {
|
158
|
+
"statusCode": 500,
|
159
|
+
"headers": {"Content-Type": "application/json"},
|
160
|
+
"body": json.dumps({"error": str(e)})
|
161
|
+
}
|
162
|
+
else:
|
163
|
+
raise
|
164
|
+
|
165
|
+
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):
|
166
|
+
"""
|
167
|
+
Execute a SWAIG function in serverless context
|
168
|
+
|
169
|
+
Args:
|
170
|
+
function_name: Name of the function to execute
|
171
|
+
args: Function arguments dictionary
|
172
|
+
call_id: Optional call ID
|
173
|
+
raw_data: Optional raw request data
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Function execution result
|
177
|
+
"""
|
178
|
+
# Use the existing logger
|
179
|
+
req_log = self.log.bind(
|
180
|
+
endpoint="serverless_swaig",
|
181
|
+
function=function_name
|
182
|
+
)
|
183
|
+
|
184
|
+
if call_id:
|
185
|
+
req_log = req_log.bind(call_id=call_id)
|
186
|
+
|
187
|
+
req_log.debug("serverless_function_call_received")
|
188
|
+
|
189
|
+
try:
|
190
|
+
# Validate function exists
|
191
|
+
if function_name not in self._tool_registry._swaig_functions:
|
192
|
+
req_log.warning("function_not_found", available_functions=list(self._tool_registry._swaig_functions.keys()))
|
193
|
+
return {"error": f"Function '{function_name}' not found"}
|
194
|
+
|
195
|
+
# Use empty args if not provided
|
196
|
+
if args is None:
|
197
|
+
args = {}
|
198
|
+
|
199
|
+
# Use empty raw_data if not provided, but include function call structure
|
200
|
+
if raw_data is None:
|
201
|
+
raw_data = {
|
202
|
+
"function": function_name,
|
203
|
+
"argument": {
|
204
|
+
"parsed": [args] if args else [],
|
205
|
+
"raw": json.dumps(args) if args else "{}"
|
206
|
+
}
|
207
|
+
}
|
208
|
+
if call_id:
|
209
|
+
raw_data["call_id"] = call_id
|
210
|
+
|
211
|
+
req_log.debug("executing_function", args=json.dumps(args))
|
212
|
+
|
213
|
+
# Call the function using the existing on_function_call method
|
214
|
+
result = self.on_function_call(function_name, args, raw_data)
|
215
|
+
|
216
|
+
# Convert result to dict if needed (same logic as in _handle_swaig_request)
|
217
|
+
if isinstance(result, SwaigFunctionResult):
|
218
|
+
result_dict = result.to_dict()
|
219
|
+
elif isinstance(result, dict):
|
220
|
+
result_dict = result
|
221
|
+
else:
|
222
|
+
result_dict = {"response": str(result)}
|
223
|
+
|
224
|
+
req_log.info("serverless_function_executed_successfully")
|
225
|
+
req_log.debug("function_result", result=json.dumps(result_dict))
|
226
|
+
return result_dict
|
227
|
+
|
228
|
+
except Exception as e:
|
229
|
+
req_log.error("serverless_function_execution_error", error=str(e))
|
230
|
+
return {"error": str(e), "function": function_name}
|
231
|
+
|
232
|
+
def _handle_google_cloud_function_request(self, request):
|
233
|
+
"""
|
234
|
+
Handle Google Cloud Functions specific requests
|
235
|
+
|
236
|
+
Args:
|
237
|
+
request: Flask request object from Google Cloud Functions
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
Flask response object
|
241
|
+
"""
|
242
|
+
try:
|
243
|
+
# Get the path from the request
|
244
|
+
path = request.path.strip('/')
|
245
|
+
|
246
|
+
if not path:
|
247
|
+
# Root request - return SWML
|
248
|
+
swml_response = self._render_swml()
|
249
|
+
from flask import Response
|
250
|
+
return Response(
|
251
|
+
response=swml_response,
|
252
|
+
status=200,
|
253
|
+
headers={"Content-Type": "application/json"}
|
254
|
+
)
|
255
|
+
else:
|
256
|
+
# SWAIG function call
|
257
|
+
args = {}
|
258
|
+
call_id = None
|
259
|
+
raw_data = None
|
260
|
+
|
261
|
+
# Parse request data
|
262
|
+
if request.method == 'POST':
|
263
|
+
try:
|
264
|
+
if request.is_json:
|
265
|
+
raw_data = request.get_json()
|
266
|
+
else:
|
267
|
+
raw_data = json.loads(request.get_data(as_text=True))
|
268
|
+
|
269
|
+
call_id = raw_data.get("call_id")
|
270
|
+
|
271
|
+
# Extract arguments like the FastAPI handler does
|
272
|
+
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
273
|
+
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
274
|
+
args = raw_data["argument"]["parsed"][0]
|
275
|
+
elif "raw" in raw_data["argument"]:
|
276
|
+
try:
|
277
|
+
args = json.loads(raw_data["argument"]["raw"])
|
278
|
+
except Exception:
|
279
|
+
pass
|
280
|
+
except Exception:
|
281
|
+
# If parsing fails, continue with empty args
|
282
|
+
pass
|
283
|
+
|
284
|
+
result = self._execute_swaig_function(path, args, call_id, raw_data)
|
285
|
+
from flask import Response
|
286
|
+
return Response(
|
287
|
+
response=json.dumps(result) if isinstance(result, dict) else str(result),
|
288
|
+
status=200,
|
289
|
+
headers={"Content-Type": "application/json"}
|
290
|
+
)
|
291
|
+
|
292
|
+
except Exception as e:
|
293
|
+
import logging
|
294
|
+
logging.error(f"Error in Google Cloud Function request handler: {e}")
|
295
|
+
from flask import Response
|
296
|
+
return Response(
|
297
|
+
response=json.dumps({"error": str(e)}),
|
298
|
+
status=500,
|
299
|
+
headers={"Content-Type": "application/json"}
|
300
|
+
)
|
301
|
+
|
302
|
+
def _handle_azure_function_request(self, req):
|
303
|
+
"""
|
304
|
+
Handle Azure Functions specific requests
|
305
|
+
|
306
|
+
Args:
|
307
|
+
req: Azure Functions HttpRequest object
|
308
|
+
|
309
|
+
Returns:
|
310
|
+
Azure Functions HttpResponse object
|
311
|
+
"""
|
312
|
+
try:
|
313
|
+
import azure.functions as func
|
314
|
+
|
315
|
+
# Get the path from the request
|
316
|
+
path = req.url.split('/')[-1] if req.url else ''
|
317
|
+
|
318
|
+
if not path or path == 'api':
|
319
|
+
# Root request - return SWML
|
320
|
+
swml_response = self._render_swml()
|
321
|
+
return func.HttpResponse(
|
322
|
+
body=swml_response,
|
323
|
+
status_code=200,
|
324
|
+
headers={"Content-Type": "application/json"}
|
325
|
+
)
|
326
|
+
else:
|
327
|
+
# SWAIG function call
|
328
|
+
args = {}
|
329
|
+
call_id = None
|
330
|
+
raw_data = None
|
331
|
+
|
332
|
+
# Parse request data
|
333
|
+
if req.method == 'POST':
|
334
|
+
try:
|
335
|
+
body = req.get_body()
|
336
|
+
if body:
|
337
|
+
raw_data = json.loads(body.decode('utf-8'))
|
338
|
+
call_id = raw_data.get("call_id")
|
339
|
+
|
340
|
+
# Extract arguments like the FastAPI handler does
|
341
|
+
if "argument" in raw_data and isinstance(raw_data["argument"], dict):
|
342
|
+
if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
|
343
|
+
args = raw_data["argument"]["parsed"][0]
|
344
|
+
elif "raw" in raw_data["argument"]:
|
345
|
+
try:
|
346
|
+
args = json.loads(raw_data["argument"]["raw"])
|
347
|
+
except Exception:
|
348
|
+
pass
|
349
|
+
except Exception:
|
350
|
+
# If parsing fails, continue with empty args
|
351
|
+
pass
|
352
|
+
|
353
|
+
result = self._execute_swaig_function(path, args, call_id, raw_data)
|
354
|
+
return func.HttpResponse(
|
355
|
+
body=json.dumps(result) if isinstance(result, dict) else str(result),
|
356
|
+
status_code=200,
|
357
|
+
headers={"Content-Type": "application/json"}
|
358
|
+
)
|
359
|
+
|
360
|
+
except Exception as e:
|
361
|
+
import logging
|
362
|
+
logging.error(f"Error in Azure Function request handler: {e}")
|
363
|
+
import azure.functions as func
|
364
|
+
return func.HttpResponse(
|
365
|
+
body=json.dumps({"error": str(e)}),
|
366
|
+
status_code=500,
|
367
|
+
headers={"Content-Type": "application/json"}
|
368
|
+
)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
"""
|
2
|
+
Copyright (c) 2025 SignalWire
|
3
|
+
|
4
|
+
This file is part of the SignalWire AI Agents SDK.
|
5
|
+
|
6
|
+
Licensed under the MIT License.
|
7
|
+
See LICENSE file in the project root for full license information.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from typing import List, Dict, Any, Optional
|
11
|
+
|
12
|
+
|
13
|
+
class SkillMixin:
|
14
|
+
"""
|
15
|
+
Mixin class containing all skill management methods for AgentBase
|
16
|
+
"""
|
17
|
+
|
18
|
+
def add_skill(self, skill_name: str, params: Optional[Dict[str, Any]] = None) -> 'AgentBase':
|
19
|
+
"""
|
20
|
+
Add a skill to this agent
|
21
|
+
|
22
|
+
Args:
|
23
|
+
skill_name: Name of the skill to add
|
24
|
+
params: Optional parameters to pass to the skill for configuration
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Self for method chaining
|
28
|
+
|
29
|
+
Raises:
|
30
|
+
ValueError: If skill not found or failed to load with detailed error message
|
31
|
+
"""
|
32
|
+
# Debug logging
|
33
|
+
self.log.debug("add_skill_called",
|
34
|
+
skill_name=skill_name,
|
35
|
+
agent_id=id(self),
|
36
|
+
registry_id=id(self._tool_registry) if hasattr(self, '_tool_registry') else None,
|
37
|
+
is_ephemeral=getattr(self, '_is_ephemeral', False))
|
38
|
+
|
39
|
+
success, error_message = self.skill_manager.load_skill(skill_name, params=params)
|
40
|
+
if not success:
|
41
|
+
raise ValueError(f"Failed to load skill '{skill_name}': {error_message}")
|
42
|
+
return self
|
43
|
+
|
44
|
+
def remove_skill(self, skill_name: str) -> 'AgentBase':
|
45
|
+
"""Remove a skill from this agent"""
|
46
|
+
self.skill_manager.unload_skill(skill_name)
|
47
|
+
return self
|
48
|
+
|
49
|
+
def list_skills(self) -> List[str]:
|
50
|
+
"""List currently loaded skills"""
|
51
|
+
return self.skill_manager.list_loaded_skills()
|
52
|
+
|
53
|
+
def has_skill(self, skill_name: str) -> bool:
|
54
|
+
"""Check if skill is loaded"""
|
55
|
+
return self.skill_manager.has_skill(skill_name)
|
@@ -0,0 +1,219 @@
|
|
1
|
+
"""
|
2
|
+
Copyright (c) 2025 SignalWire
|
3
|
+
|
4
|
+
This file is part of the SignalWire AI Agents SDK.
|
5
|
+
|
6
|
+
Licensed under the MIT License.
|
7
|
+
See LICENSE file in the project root for full license information.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from typing import Callable
|
11
|
+
|
12
|
+
from signalwire_agents.core.function_result import SwaigFunctionResult
|
13
|
+
|
14
|
+
|
15
|
+
class StateMixin:
|
16
|
+
"""
|
17
|
+
Mixin class containing all state and session management methods for AgentBase
|
18
|
+
"""
|
19
|
+
|
20
|
+
def _create_tool_token(self, tool_name: str, call_id: str) -> str:
|
21
|
+
"""
|
22
|
+
Create a secure token for a tool call
|
23
|
+
|
24
|
+
Args:
|
25
|
+
tool_name: Name of the tool
|
26
|
+
call_id: Call ID for this session
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Secure token string
|
30
|
+
"""
|
31
|
+
try:
|
32
|
+
# Ensure we have a session manager
|
33
|
+
if not hasattr(self, '_session_manager'):
|
34
|
+
self.log.error("no_session_manager")
|
35
|
+
return ""
|
36
|
+
|
37
|
+
# Create the token using the session manager
|
38
|
+
return self._session_manager.create_tool_token(tool_name, call_id)
|
39
|
+
except Exception as e:
|
40
|
+
self.log.error("token_creation_error", error=str(e), tool=tool_name, call_id=call_id)
|
41
|
+
return ""
|
42
|
+
|
43
|
+
def validate_tool_token(self, function_name: str, token: str, call_id: str) -> bool:
|
44
|
+
"""
|
45
|
+
Validate a tool token
|
46
|
+
|
47
|
+
Args:
|
48
|
+
function_name: Name of the function/tool
|
49
|
+
token: Token to validate
|
50
|
+
call_id: Call ID for the session
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
True if token is valid, False otherwise
|
54
|
+
"""
|
55
|
+
try:
|
56
|
+
# Skip validation for non-secure tools
|
57
|
+
if function_name not in self._tool_registry._swaig_functions:
|
58
|
+
self.log.warning("unknown_function", function=function_name)
|
59
|
+
return False
|
60
|
+
|
61
|
+
# Get the function and check if it's secure
|
62
|
+
func = self._tool_registry._swaig_functions[function_name]
|
63
|
+
is_secure = True # Default to secure
|
64
|
+
|
65
|
+
if isinstance(func, dict):
|
66
|
+
# For raw dictionaries (DataMap functions), they're always secure
|
67
|
+
is_secure = True
|
68
|
+
else:
|
69
|
+
# For SWAIGFunction objects, check the secure attribute
|
70
|
+
is_secure = func.secure
|
71
|
+
|
72
|
+
# Always allow non-secure functions
|
73
|
+
if not is_secure:
|
74
|
+
self.log.debug("non_secure_function_allowed", function=function_name)
|
75
|
+
return True
|
76
|
+
|
77
|
+
# Check if we have a session manager
|
78
|
+
if not hasattr(self, '_session_manager'):
|
79
|
+
self.log.error("no_session_manager")
|
80
|
+
return False
|
81
|
+
|
82
|
+
# Handle missing token
|
83
|
+
if not token:
|
84
|
+
self.log.warning("missing_token", function=function_name)
|
85
|
+
return False
|
86
|
+
|
87
|
+
# For debugging: Log token details
|
88
|
+
try:
|
89
|
+
# Capture original parameters
|
90
|
+
self.log.debug("token_validate_input",
|
91
|
+
function=function_name,
|
92
|
+
call_id=call_id,
|
93
|
+
token_length=len(token))
|
94
|
+
|
95
|
+
# Try to decode token for debugging
|
96
|
+
if hasattr(self._session_manager, 'debug_token'):
|
97
|
+
debug_info = self._session_manager.debug_token(token)
|
98
|
+
self.log.debug("token_debug", debug_info=debug_info)
|
99
|
+
|
100
|
+
# Extract token components
|
101
|
+
if debug_info.get("valid_format") and "components" in debug_info:
|
102
|
+
components = debug_info["components"]
|
103
|
+
token_call_id = components.get("call_id")
|
104
|
+
token_function = components.get("function")
|
105
|
+
token_expiry = components.get("expiry")
|
106
|
+
|
107
|
+
# Log parameter mismatches
|
108
|
+
if token_function != function_name:
|
109
|
+
self.log.warning("token_function_mismatch",
|
110
|
+
expected=function_name,
|
111
|
+
actual=token_function)
|
112
|
+
|
113
|
+
if token_call_id != call_id:
|
114
|
+
self.log.warning("token_call_id_mismatch",
|
115
|
+
expected=call_id,
|
116
|
+
actual=token_call_id)
|
117
|
+
|
118
|
+
# Check expiration
|
119
|
+
if debug_info.get("status", {}).get("is_expired"):
|
120
|
+
self.log.warning("token_expired",
|
121
|
+
expires_in=debug_info["status"].get("expires_in_seconds"))
|
122
|
+
except Exception as e:
|
123
|
+
self.log.error("token_debug_error", error=str(e))
|
124
|
+
|
125
|
+
# Use call_id from token if the provided one is empty
|
126
|
+
if not call_id and hasattr(self._session_manager, 'debug_token'):
|
127
|
+
try:
|
128
|
+
debug_info = self._session_manager.debug_token(token)
|
129
|
+
if debug_info.get("valid_format") and "components" in debug_info:
|
130
|
+
token_call_id = debug_info["components"].get("call_id")
|
131
|
+
if token_call_id:
|
132
|
+
self.log.debug("using_call_id_from_token", call_id=token_call_id)
|
133
|
+
is_valid = self._session_manager.validate_tool_token(function_name, token, token_call_id)
|
134
|
+
if is_valid:
|
135
|
+
self.log.debug("token_valid_with_extracted_call_id")
|
136
|
+
return True
|
137
|
+
except Exception as e:
|
138
|
+
self.log.error("error_using_call_id_from_token", error=str(e))
|
139
|
+
|
140
|
+
# Normal validation with provided call_id
|
141
|
+
is_valid = self._session_manager.validate_tool_token(function_name, token, call_id)
|
142
|
+
|
143
|
+
if is_valid:
|
144
|
+
self.log.debug("token_valid", function=function_name)
|
145
|
+
else:
|
146
|
+
self.log.warning("token_invalid", function=function_name)
|
147
|
+
|
148
|
+
return is_valid
|
149
|
+
except Exception as e:
|
150
|
+
self.log.error("token_validation_error", error=str(e), function=function_name)
|
151
|
+
return False
|
152
|
+
|
153
|
+
# Note: set_dynamic_config_callback is implemented in WebMixin
|
154
|
+
|
155
|
+
def _register_state_tracking_tools(self):
|
156
|
+
"""
|
157
|
+
Register special tools for state tracking
|
158
|
+
|
159
|
+
This adds startup_hook and hangup_hook SWAIG functions that automatically
|
160
|
+
activate and deactivate the session when called. These are useful for
|
161
|
+
tracking call state and cleaning up resources when a call ends.
|
162
|
+
"""
|
163
|
+
# Register startup hook to activate session
|
164
|
+
self.define_tool(
|
165
|
+
name="startup_hook",
|
166
|
+
description="Called when a new conversation starts to initialize state",
|
167
|
+
parameters={},
|
168
|
+
handler=lambda args, raw_data: self._handle_startup_hook(args, raw_data),
|
169
|
+
secure=False # No auth needed for this system function
|
170
|
+
)
|
171
|
+
|
172
|
+
# Register hangup hook to end session
|
173
|
+
self.define_tool(
|
174
|
+
name="hangup_hook",
|
175
|
+
description="Called when conversation ends to clean up resources",
|
176
|
+
parameters={},
|
177
|
+
handler=lambda args, raw_data: self._handle_hangup_hook(args, raw_data),
|
178
|
+
secure=False # No auth needed for this system function
|
179
|
+
)
|
180
|
+
|
181
|
+
def _handle_startup_hook(self, args, raw_data):
|
182
|
+
"""
|
183
|
+
Handle the startup hook function call
|
184
|
+
|
185
|
+
Args:
|
186
|
+
args: Function arguments (empty for this hook)
|
187
|
+
raw_data: Raw request data containing call_id
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
Success response
|
191
|
+
"""
|
192
|
+
call_id = raw_data.get("call_id") if raw_data else None
|
193
|
+
if call_id:
|
194
|
+
self.log.info("session_activated", call_id=call_id)
|
195
|
+
self._session_manager.activate_session(call_id)
|
196
|
+
return SwaigFunctionResult("Session activated")
|
197
|
+
else:
|
198
|
+
self.log.warning("session_activation_failed", error="No call_id provided")
|
199
|
+
return SwaigFunctionResult("Failed to activate session: No call_id provided")
|
200
|
+
|
201
|
+
def _handle_hangup_hook(self, args, raw_data):
|
202
|
+
"""
|
203
|
+
Handle the hangup hook function call
|
204
|
+
|
205
|
+
Args:
|
206
|
+
args: Function arguments (empty for this hook)
|
207
|
+
raw_data: Raw request data containing call_id
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
Success response
|
211
|
+
"""
|
212
|
+
call_id = raw_data.get("call_id") if raw_data else None
|
213
|
+
if call_id:
|
214
|
+
self.log.info("session_ended", call_id=call_id)
|
215
|
+
self._session_manager.end_session(call_id)
|
216
|
+
return SwaigFunctionResult("Session ended")
|
217
|
+
else:
|
218
|
+
self.log.warning("session_end_failed", error="No call_id provided")
|
219
|
+
return SwaigFunctionResult("Failed to end session: No call_id provided")
|