signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,460 @@
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
+ # Support both HTTP API (v2) and REST API (v1) payload formats
86
+ # HTTP API v2 uses rawPath, REST API v1 uses pathParameters.proxy
87
+ path = event.get('rawPath', '').strip('/')
88
+ if not path and event.get('pathParameters'):
89
+ path = event.get('pathParameters', {}).get('proxy', '')
90
+
91
+ # Parse request body if present
92
+ args = {}
93
+ call_id = None
94
+ raw_data = None
95
+ function_name = None
96
+
97
+ body_content = event.get('body')
98
+ if body_content:
99
+ try:
100
+ if event.get('isBase64Encoded'):
101
+ import base64
102
+ body_content = base64.b64decode(body_content).decode('utf-8')
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
+ function_name = raw_data.get("function")
110
+
111
+ # Extract arguments like the FastAPI handler does
112
+ if "argument" in raw_data and isinstance(raw_data["argument"], dict):
113
+ if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
114
+ args = raw_data["argument"]["parsed"][0]
115
+ elif "raw" in raw_data["argument"]:
116
+ try:
117
+ args = json.loads(raw_data["argument"]["raw"])
118
+ except Exception:
119
+ pass
120
+ except Exception:
121
+ # If parsing fails, continue with empty args
122
+ pass
123
+
124
+ # Determine if this is a SWAIG function call
125
+ # Case 1: /swaig endpoint with function name in body
126
+ # Case 2: /{function_name} path-based routing
127
+ # Case 3: Root path - return SWML
128
+
129
+ if path in ('swaig', 'swaig/') and function_name:
130
+ # /swaig endpoint with function name in body
131
+ result = self._execute_swaig_function(function_name, args, call_id, raw_data)
132
+ return {
133
+ "statusCode": 200,
134
+ "headers": {"Content-Type": "application/json"},
135
+ "body": json.dumps(result) if isinstance(result, dict) else str(result)
136
+ }
137
+ elif path and path not in ('', 'swaig', 'swaig/'):
138
+ # Path-based function routing (e.g., /say_hello)
139
+ result = self._execute_swaig_function(path, args, call_id, raw_data)
140
+ return {
141
+ "statusCode": 200,
142
+ "headers": {"Content-Type": "application/json"},
143
+ "body": json.dumps(result) if isinstance(result, dict) else str(result)
144
+ }
145
+ else:
146
+ # Root path or /swaig without function - return SWML
147
+ swml_response = self._render_swml()
148
+ return {
149
+ "statusCode": 200,
150
+ "headers": {"Content-Type": "application/json"},
151
+ "body": swml_response
152
+ }
153
+ else:
154
+ # Handle case when event is None (direct Lambda call with no event)
155
+ swml_response = self._render_swml()
156
+ return {
157
+ "statusCode": 200,
158
+ "headers": {"Content-Type": "application/json"},
159
+ "body": swml_response
160
+ }
161
+
162
+ elif mode == 'google_cloud_function':
163
+ # Check authentication in Google Cloud Functions mode
164
+ if not self._check_google_cloud_function_auth(event):
165
+ return self._send_google_cloud_function_auth_challenge()
166
+
167
+ return self._handle_google_cloud_function_request(event)
168
+
169
+ elif mode == 'azure_function':
170
+ # Check authentication in Azure Functions mode
171
+ if not self._check_azure_function_auth(event):
172
+ return self._send_azure_function_auth_challenge()
173
+
174
+ return self._handle_azure_function_request(event)
175
+
176
+
177
+ except Exception as e:
178
+ import logging
179
+ import traceback
180
+ logging.error(f"Error in serverless request handler: {e}")
181
+ logging.error(f"Traceback: {traceback.format_exc()}")
182
+ if mode == 'lambda':
183
+ return {
184
+ "statusCode": 500,
185
+ "headers": {"Content-Type": "application/json"},
186
+ "body": json.dumps({"error": str(e)})
187
+ }
188
+ else:
189
+ raise
190
+
191
+ 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):
192
+ """
193
+ Execute a SWAIG function in serverless context
194
+
195
+ Args:
196
+ function_name: Name of the function to execute
197
+ args: Function arguments dictionary
198
+ call_id: Optional call ID
199
+ raw_data: Optional raw request data
200
+
201
+ Returns:
202
+ Function execution result
203
+ """
204
+ # Use the existing logger
205
+ req_log = self.log.bind(
206
+ endpoint="serverless_swaig",
207
+ function=function_name
208
+ )
209
+
210
+ if call_id:
211
+ req_log = req_log.bind(call_id=call_id)
212
+
213
+ req_log.debug("serverless_function_call_received")
214
+
215
+ try:
216
+ # Validate function exists
217
+ if function_name not in self._tool_registry._swaig_functions:
218
+ req_log.warning("function_not_found", available_functions=list(self._tool_registry._swaig_functions.keys()))
219
+ return {"error": f"Function '{function_name}' not found"}
220
+
221
+ # Use empty args if not provided
222
+ if args is None:
223
+ args = {}
224
+
225
+ # Use empty raw_data if not provided, but include function call structure
226
+ if raw_data is None:
227
+ raw_data = {
228
+ "function": function_name,
229
+ "argument": {
230
+ "parsed": [args] if args else [],
231
+ "raw": json.dumps(args) if args else "{}"
232
+ }
233
+ }
234
+ if call_id:
235
+ raw_data["call_id"] = call_id
236
+
237
+ req_log.debug("executing_function", args=json.dumps(args))
238
+
239
+ # Call the function using the existing on_function_call method
240
+ result = self.on_function_call(function_name, args, raw_data)
241
+
242
+ # Convert result to dict if needed (same logic as in _handle_swaig_request)
243
+ if isinstance(result, SwaigFunctionResult):
244
+ result_dict = result.to_dict()
245
+ elif isinstance(result, dict):
246
+ result_dict = result
247
+ else:
248
+ result_dict = {"response": str(result)}
249
+
250
+ req_log.info("serverless_function_executed_successfully")
251
+ req_log.debug("function_result", result=json.dumps(result_dict))
252
+ return result_dict
253
+
254
+ except Exception as e:
255
+ req_log.error("serverless_function_execution_error", error=str(e))
256
+ return {"error": str(e), "function": function_name}
257
+
258
+ def _handle_google_cloud_function_request(self, request):
259
+ """
260
+ Handle Google Cloud Functions specific requests
261
+
262
+ Args:
263
+ request: Flask request object from Google Cloud Functions
264
+
265
+ Returns:
266
+ Flask response object
267
+ """
268
+ try:
269
+ from urllib.parse import urlparse
270
+
271
+ # Get the path from the request
272
+ path = request.path.strip('/')
273
+
274
+ # Try to detect and set the base URL from the request for webhook URLs
275
+ base_url = None
276
+ if hasattr(request, 'url') and request.url:
277
+ parsed = urlparse(request.url)
278
+ # Get the base URL without the path
279
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
280
+
281
+ # Set the proxy URL base so SWML renders correct webhook URLs
282
+ if base_url and not getattr(self, '_proxy_url_base_from_env', False):
283
+ self._proxy_url_base = base_url
284
+
285
+ # Parse request body if present
286
+ args = {}
287
+ call_id = None
288
+ raw_data = None
289
+ function_name = None
290
+
291
+ if request.method == 'POST':
292
+ try:
293
+ if request.is_json:
294
+ raw_data = request.get_json()
295
+ else:
296
+ raw_data = json.loads(request.get_data(as_text=True))
297
+
298
+ call_id = raw_data.get("call_id")
299
+ function_name = raw_data.get("function")
300
+
301
+ # Extract arguments like the FastAPI handler does
302
+ if "argument" in raw_data and isinstance(raw_data["argument"], dict):
303
+ if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
304
+ args = raw_data["argument"]["parsed"][0]
305
+ elif "raw" in raw_data["argument"]:
306
+ try:
307
+ args = json.loads(raw_data["argument"]["raw"])
308
+ except Exception:
309
+ pass
310
+ except Exception:
311
+ # If parsing fails, continue with empty args
312
+ pass
313
+
314
+ # Determine if this is a SWAIG function call
315
+ # Case 1: /swaig endpoint with function name in body
316
+ # Case 2: /{function_name} path-based routing
317
+ # Case 3: Root path - return SWML
318
+
319
+ from flask import Response
320
+
321
+ if path in ('swaig', 'swaig/') and function_name:
322
+ # /swaig endpoint with function name in body
323
+ result = self._execute_swaig_function(function_name, args, call_id, raw_data)
324
+ return Response(
325
+ response=json.dumps(result) if isinstance(result, dict) else str(result),
326
+ status=200,
327
+ headers={"Content-Type": "application/json"}
328
+ )
329
+ elif path and path not in ('', 'swaig', 'swaig/'):
330
+ # Path-based function routing (e.g., /say_hello)
331
+ result = self._execute_swaig_function(path, args, call_id, raw_data)
332
+ return Response(
333
+ response=json.dumps(result) if isinstance(result, dict) else str(result),
334
+ status=200,
335
+ headers={"Content-Type": "application/json"}
336
+ )
337
+ else:
338
+ # Root path or /swaig without function - return SWML
339
+ swml_response = self._render_swml()
340
+ return Response(
341
+ response=swml_response,
342
+ status=200,
343
+ headers={"Content-Type": "application/json"}
344
+ )
345
+
346
+ except Exception as e:
347
+ import logging
348
+ logging.error(f"Error in Google Cloud Function request handler: {e}")
349
+ from flask import Response
350
+ return Response(
351
+ response=json.dumps({"error": str(e)}),
352
+ status=500,
353
+ headers={"Content-Type": "application/json"}
354
+ )
355
+
356
+ def _handle_azure_function_request(self, req):
357
+ """
358
+ Handle Azure Functions specific requests
359
+
360
+ Args:
361
+ req: Azure Functions HttpRequest object
362
+
363
+ Returns:
364
+ Azure Functions HttpResponse object
365
+ """
366
+ try:
367
+ import azure.functions as func
368
+ from urllib.parse import urlparse
369
+
370
+ # Get the path from the request URL
371
+ # Azure Functions URLs look like: https://app.azurewebsites.net/api/function_name/path
372
+ path = ''
373
+ base_url = None
374
+ if req.url:
375
+ parsed = urlparse(req.url)
376
+ # Full path after /api/ e.g. "function_app" or "function_app/swaig"
377
+ url_parts = req.url.split('/api/')
378
+ if len(url_parts) > 1:
379
+ full_path = url_parts[1].strip('/')
380
+ # Split into function name and sub-path
381
+ path_parts = full_path.split('/', 1)
382
+ function_app_name = path_parts[0] if path_parts else ''
383
+ path = path_parts[1] if len(path_parts) > 1 else ''
384
+
385
+ # Base URL includes the function app name for webhook URLs
386
+ # e.g., https://app.azurewebsites.net/api/function_app
387
+ base_url = f"{parsed.scheme}://{parsed.netloc}/api/{function_app_name}"
388
+ else:
389
+ base_url = f"{parsed.scheme}://{parsed.netloc}/api"
390
+
391
+ # Set the proxy URL base so SWML renders correct webhook URLs
392
+ if base_url and not getattr(self, '_proxy_url_base_from_env', False):
393
+ self._proxy_url_base = base_url
394
+
395
+ # Parse request body if present
396
+ args = {}
397
+ call_id = None
398
+ raw_data = None
399
+ function_name = None
400
+
401
+ if req.method == 'POST':
402
+ try:
403
+ body = req.get_body()
404
+ if body:
405
+ raw_data = json.loads(body.decode('utf-8'))
406
+ call_id = raw_data.get("call_id")
407
+ function_name = raw_data.get("function")
408
+
409
+ # Extract arguments like the FastAPI handler does
410
+ if "argument" in raw_data and isinstance(raw_data["argument"], dict):
411
+ if "parsed" in raw_data["argument"] and isinstance(raw_data["argument"]["parsed"], list) and raw_data["argument"]["parsed"]:
412
+ args = raw_data["argument"]["parsed"][0]
413
+ elif "raw" in raw_data["argument"]:
414
+ try:
415
+ args = json.loads(raw_data["argument"]["raw"])
416
+ except Exception:
417
+ pass
418
+ except Exception:
419
+ # If parsing fails, continue with empty args
420
+ pass
421
+
422
+ # Determine if this is a SWAIG function call
423
+ # Case 1: /swaig endpoint with function name in body
424
+ # Case 2: /{function_name} path-based routing
425
+ # Case 3: Root path - return SWML
426
+
427
+ if path in ('swaig', 'swaig/') and function_name:
428
+ # /swaig endpoint with function name in body
429
+ result = self._execute_swaig_function(function_name, args, call_id, raw_data)
430
+ return func.HttpResponse(
431
+ body=json.dumps(result) if isinstance(result, dict) else str(result),
432
+ status_code=200,
433
+ headers={"Content-Type": "application/json"}
434
+ )
435
+ elif path and path not in ('', 'api', 'swaig', 'swaig/'):
436
+ # Path-based function routing (e.g., /say_hello)
437
+ result = self._execute_swaig_function(path, args, call_id, raw_data)
438
+ return func.HttpResponse(
439
+ body=json.dumps(result) if isinstance(result, dict) else str(result),
440
+ status_code=200,
441
+ headers={"Content-Type": "application/json"}
442
+ )
443
+ else:
444
+ # Root path or /swaig without function - return SWML
445
+ swml_response = self._render_swml()
446
+ return func.HttpResponse(
447
+ body=swml_response,
448
+ status_code=200,
449
+ headers={"Content-Type": "application/json"}
450
+ )
451
+
452
+ except Exception as e:
453
+ import logging
454
+ logging.error(f"Error in Azure Function request handler: {e}")
455
+ import azure.functions as func
456
+ return func.HttpResponse(
457
+ body=json.dumps({"error": str(e)}),
458
+ status_code=500,
459
+ headers={"Content-Type": "application/json"}
460
+ )
@@ -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,153 @@
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