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.
Files changed (64) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +2 -1
  3. signalwire_agents/cli/config.py +61 -0
  4. signalwire_agents/cli/core/__init__.py +1 -0
  5. signalwire_agents/cli/core/agent_loader.py +254 -0
  6. signalwire_agents/cli/core/argparse_helpers.py +164 -0
  7. signalwire_agents/cli/core/dynamic_config.py +62 -0
  8. signalwire_agents/cli/execution/__init__.py +1 -0
  9. signalwire_agents/cli/execution/datamap_exec.py +437 -0
  10. signalwire_agents/cli/execution/webhook_exec.py +125 -0
  11. signalwire_agents/cli/output/__init__.py +1 -0
  12. signalwire_agents/cli/output/output_formatter.py +132 -0
  13. signalwire_agents/cli/output/swml_dump.py +177 -0
  14. signalwire_agents/cli/simulation/__init__.py +1 -0
  15. signalwire_agents/cli/simulation/data_generation.py +365 -0
  16. signalwire_agents/cli/simulation/data_overrides.py +187 -0
  17. signalwire_agents/cli/simulation/mock_env.py +271 -0
  18. signalwire_agents/cli/test_swaig.py +522 -2539
  19. signalwire_agents/cli/types.py +72 -0
  20. signalwire_agents/core/agent/__init__.py +1 -3
  21. signalwire_agents/core/agent/config/__init__.py +1 -3
  22. signalwire_agents/core/agent/prompt/manager.py +25 -7
  23. signalwire_agents/core/agent/tools/decorator.py +2 -0
  24. signalwire_agents/core/agent/tools/registry.py +8 -0
  25. signalwire_agents/core/agent_base.py +492 -3053
  26. signalwire_agents/core/function_result.py +31 -42
  27. signalwire_agents/core/mixins/__init__.py +28 -0
  28. signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
  29. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  30. signalwire_agents/core/mixins/prompt_mixin.py +345 -0
  31. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  32. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  33. signalwire_agents/core/mixins/state_mixin.py +219 -0
  34. signalwire_agents/core/mixins/tool_mixin.py +295 -0
  35. signalwire_agents/core/mixins/web_mixin.py +1130 -0
  36. signalwire_agents/core/skill_manager.py +3 -1
  37. signalwire_agents/core/swaig_function.py +10 -1
  38. signalwire_agents/core/swml_service.py +140 -58
  39. signalwire_agents/skills/README.md +452 -0
  40. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  41. signalwire_agents/skills/datasphere/README.md +210 -0
  42. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  43. signalwire_agents/skills/datetime/README.md +132 -0
  44. signalwire_agents/skills/joke/README.md +149 -0
  45. signalwire_agents/skills/math/README.md +161 -0
  46. signalwire_agents/skills/native_vector_search/skill.py +33 -13
  47. signalwire_agents/skills/play_background_file/README.md +218 -0
  48. signalwire_agents/skills/spider/README.md +236 -0
  49. signalwire_agents/skills/spider/__init__.py +4 -0
  50. signalwire_agents/skills/spider/skill.py +479 -0
  51. signalwire_agents/skills/swml_transfer/README.md +395 -0
  52. signalwire_agents/skills/swml_transfer/__init__.py +1 -0
  53. signalwire_agents/skills/swml_transfer/skill.py +257 -0
  54. signalwire_agents/skills/weather_api/README.md +178 -0
  55. signalwire_agents/skills/web_search/README.md +163 -0
  56. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  57. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/METADATA +47 -2
  58. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/RECORD +62 -22
  59. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/entry_points.txt +1 -1
  60. signalwire_agents/core/agent/config/ephemeral.py +0 -176
  61. signalwire_agents-0.1.23.data/data/schema.json +0 -5611
  62. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/WHEEL +0 -0
  63. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/licenses/LICENSE +0 -0
  64. {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")