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
@@ -188,7 +188,7 @@ class SwaigFunctionResult:
188
188
  self.action.append(swml_action)
189
189
  return self
190
190
 
191
- def swml_transfer(self, dest: str, ai_response: str) -> 'SwaigFunctionResult':
191
+ def swml_transfer(self, dest: str, ai_response: str, final: bool = True) -> 'SwaigFunctionResult':
192
192
  """
193
193
  Add a SWML transfer action with AI response setup for when transfer completes.
194
194
 
@@ -202,22 +202,28 @@ class SwaigFunctionResult:
202
202
  Args:
203
203
  dest: Destination URL for the transfer (SWML endpoint, SIP address, etc.)
204
204
  ai_response: Message the AI should say when transfer completes and control returns
205
+ final: Whether this is a permanent transfer (True) or temporary (False).
206
+ Defaults to True for permanent transfers (same as connect method).
205
207
 
206
208
  Returns:
207
209
  Self for method chaining
208
210
 
209
211
  Example:
210
- # Transfer with post-processing (speak first, then transfer)
212
+ # Permanent transfer (default)
211
213
  result = (
212
214
  SwaigFunctionResult("I'm transferring you to support", post_process=True)
213
215
  .swml_transfer(
214
216
  "https://support.example.com/swml",
215
- "The support call is complete. How else can I help?"
217
+ "Goodbye!" # Won't be used since final=True by default
216
218
  )
217
219
  )
218
220
 
219
- # Or enable post-processing with method chaining
220
- result.swml_transfer(dest, ai_response).set_post_process(True)
221
+ # Temporary transfer with return
222
+ result.swml_transfer(
223
+ dest,
224
+ "The support call is complete. How else can I help?",
225
+ final=False
226
+ )
221
227
  """
222
228
  # Create the SWML action structure directly
223
229
  swml_action = {
@@ -229,7 +235,8 @@ class SwaigFunctionResult:
229
235
  {"transfer": {"dest": dest}}
230
236
  ]
231
237
  }
232
- }
238
+ },
239
+ "transfer": str(final).lower() # Convert boolean to "true"/"false" string
233
240
  }
234
241
 
235
242
  # Add to actions list directly
@@ -251,9 +258,89 @@ class SwaigFunctionResult:
251
258
  Returns:
252
259
  self for method chaining
253
260
  """
254
- action = {"set_global_data": data}
255
- return self.add_action("set_global_data", action)
261
+ return self.add_action("set_global_data", data)
256
262
 
263
+ def swml_user_event(self, event_data: Dict[str, Any]) -> 'SwaigFunctionResult':
264
+ """
265
+ Send a user event through SWML to update the client UI.
266
+
267
+ This is a convenience method for sending user events to connected clients,
268
+ commonly used for real-time UI updates in interactive applications.
269
+
270
+ Args:
271
+ event_data: Dictionary containing the event type and any associated data
272
+ Example: {"type": "cards_dealt", "player_hand": [...], "score": 21}
273
+
274
+ Returns:
275
+ Self for method chaining
276
+
277
+ Example:
278
+ result = (
279
+ SwaigFunctionResult("You have blackjack!")
280
+ .swml_user_event({
281
+ "type": "cards_dealt",
282
+ "player_hand": player_cards,
283
+ "dealer_hand": dealer_cards,
284
+ "player_score": 21
285
+ })
286
+ )
287
+ """
288
+ swml_action = {
289
+ "sections": {
290
+ "main": [{
291
+ "user_event": {
292
+ "event": event_data
293
+ }
294
+ }]
295
+ },
296
+ "version": "1.0.0"
297
+ }
298
+
299
+ return self.add_action("SWML", swml_action)
300
+
301
+ def swml_change_step(self, step_name: str) -> 'SwaigFunctionResult':
302
+ """
303
+ Change the conversation step in the AI agent's workflow.
304
+
305
+ This is a convenience method for transitioning between conversation steps,
306
+ allowing dynamic workflow control based on user interactions or game state.
307
+
308
+ Args:
309
+ step_name: Name of the step to transition to (e.g., "betting", "playing", "hand_complete")
310
+
311
+ Returns:
312
+ Self for method chaining
313
+
314
+ Example:
315
+ result = (
316
+ SwaigFunctionResult("Starting a new hand")
317
+ .swml_change_step("betting")
318
+ .swml_user_event({"type": "game_reset", "chips": 1000})
319
+ )
320
+ """
321
+ return self.add_action("change_step", step_name)
322
+
323
+ def swml_change_context(self, context_name: str) -> 'SwaigFunctionResult':
324
+ """
325
+ Change the conversation context in the AI agent's workflow.
326
+
327
+ This is a convenience method for switching between different conversation contexts,
328
+ useful for agents that handle multiple distinct workflows or service modes.
329
+
330
+ Args:
331
+ context_name: Name of the context to transition to (e.g., "support", "sales", "technical")
332
+
333
+ Returns:
334
+ Self for method chaining
335
+
336
+ Example:
337
+ result = (
338
+ SwaigFunctionResult("Transferring you to technical support")
339
+ .swml_change_context("technical_support")
340
+ )
341
+ """
342
+ return self.add_action("change_context", context_name)
343
+
257
344
  def execute_swml(self, swml_content, transfer: bool = False) -> 'SwaigFunctionResult':
258
345
  """
259
346
  Execute SWML content with optional transfer behavior.
@@ -281,7 +368,7 @@ class SwaigFunctionResult:
281
368
  else:
282
369
  raise TypeError("swml_content must be string, dict, or SWML object")
283
370
 
284
- action = {"SWML": swml_data}
371
+ action = swml_data
285
372
  if transfer:
286
373
  action["transfer"] = "true"
287
374
 
@@ -294,8 +381,7 @@ class SwaigFunctionResult:
294
381
  Returns:
295
382
  self for method chaining
296
383
  """
297
- action = {"hangup": True}
298
- return self.add_action("hangup", action)
384
+ return self.add_action("hangup", True)
299
385
 
300
386
  def hold(self, timeout: int = 300) -> 'SwaigFunctionResult':
301
387
  """
@@ -309,8 +395,7 @@ class SwaigFunctionResult:
309
395
  """
310
396
  # Clamp timeout to valid range
311
397
  timeout = max(0, min(timeout, 900))
312
- action = {"hold": timeout}
313
- return self.add_action("hold", action)
398
+ return self.add_action("hold", timeout)
314
399
 
315
400
  def wait_for_user(self, enabled: Optional[bool] = None, timeout: Optional[int] = None, answer_first: bool = False) -> 'SwaigFunctionResult':
316
401
  """
@@ -333,8 +418,7 @@ class SwaigFunctionResult:
333
418
  else:
334
419
  wait_value = True
335
420
 
336
- action = {"wait_for_user": wait_value}
337
- return self.add_action("wait_for_user", action)
421
+ return self.add_action("wait_for_user", wait_value)
338
422
 
339
423
  def stop(self) -> 'SwaigFunctionResult':
340
424
  """
@@ -343,8 +427,7 @@ class SwaigFunctionResult:
343
427
  Returns:
344
428
  self for method chaining
345
429
  """
346
- action = {"stop": True}
347
- return self.add_action("stop", action)
430
+ return self.add_action("stop", True)
348
431
 
349
432
  def say(self, text: str) -> 'SwaigFunctionResult':
350
433
  """
@@ -356,8 +439,7 @@ class SwaigFunctionResult:
356
439
  Returns:
357
440
  self for method chaining
358
441
  """
359
- action = {"say": text}
360
- return self.add_action("say", action)
442
+ return self.add_action("say", text)
361
443
 
362
444
  def play_background_file(self, filename: str, wait: bool = False) -> 'SwaigFunctionResult':
363
445
  """
@@ -395,8 +477,7 @@ class SwaigFunctionResult:
395
477
  Returns:
396
478
  self for method chaining
397
479
  """
398
- action = {"end_of_speech_timeout": milliseconds}
399
- return self.add_action("end_of_speech_timeout", action)
480
+ return self.add_action("end_of_speech_timeout", milliseconds)
400
481
 
401
482
  def set_speech_event_timeout(self, milliseconds: int) -> 'SwaigFunctionResult':
402
483
  """
@@ -409,8 +490,7 @@ class SwaigFunctionResult:
409
490
  Returns:
410
491
  self for method chaining
411
492
  """
412
- action = {"speech_event_timeout": milliseconds}
413
- return self.add_action("speech_event_timeout", action)
493
+ return self.add_action("speech_event_timeout", milliseconds)
414
494
 
415
495
  def remove_global_data(self, keys: Union[str, List[str]]) -> 'SwaigFunctionResult':
416
496
  """
@@ -422,8 +502,7 @@ class SwaigFunctionResult:
422
502
  Returns:
423
503
  self for method chaining
424
504
  """
425
- action = {"unset_global_data": keys}
426
- return self.add_action("unset_global_data", action)
505
+ return self.add_action("unset_global_data", keys)
427
506
 
428
507
  def set_metadata(self, data: Dict[str, Any]) -> 'SwaigFunctionResult':
429
508
  """
@@ -435,8 +514,7 @@ class SwaigFunctionResult:
435
514
  Returns:
436
515
  self for method chaining
437
516
  """
438
- action = {"set_meta_data": data}
439
- return self.add_action("set_meta_data", action)
517
+ return self.add_action("set_meta_data", data)
440
518
 
441
519
  def remove_metadata(self, keys: Union[str, List[str]]) -> 'SwaigFunctionResult':
442
520
  """
@@ -448,8 +526,7 @@ class SwaigFunctionResult:
448
526
  Returns:
449
527
  self for method chaining
450
528
  """
451
- action = {"unset_meta_data": keys}
452
- return self.add_action("unset_meta_data", action)
529
+ return self.add_action("unset_meta_data", keys)
453
530
 
454
531
  def toggle_functions(self, function_toggles: List[Dict[str, Any]]) -> 'SwaigFunctionResult':
455
532
  """
@@ -461,8 +538,7 @@ class SwaigFunctionResult:
461
538
  Returns:
462
539
  self for method chaining
463
540
  """
464
- action = {"toggle_functions": function_toggles}
465
- return self.add_action("toggle_functions", action)
541
+ return self.add_action("toggle_functions", function_toggles)
466
542
 
467
543
  def enable_functions_on_timeout(self, enabled: bool = True) -> 'SwaigFunctionResult':
468
544
  """
@@ -474,8 +550,7 @@ class SwaigFunctionResult:
474
550
  Returns:
475
551
  self for method chaining
476
552
  """
477
- action = {"functions_on_speaker_timeout": enabled}
478
- return self.add_action("functions_on_speaker_timeout", action)
553
+ return self.add_action("functions_on_speaker_timeout", enabled)
479
554
 
480
555
  def enable_extensive_data(self, enabled: bool = True) -> 'SwaigFunctionResult':
481
556
  """
@@ -488,8 +563,7 @@ class SwaigFunctionResult:
488
563
  Returns:
489
564
  self for method chaining
490
565
  """
491
- action = {"extensive_data": enabled}
492
- return self.add_action("extensive_data", action)
566
+ return self.add_action("extensive_data", enabled)
493
567
 
494
568
  def update_settings(self, settings: Dict[str, Any]) -> 'SwaigFunctionResult':
495
569
  """
@@ -510,8 +584,7 @@ class SwaigFunctionResult:
510
584
  Returns:
511
585
  self for method chaining
512
586
  """
513
- action = {"settings": settings}
514
- return self.add_action("settings", action)
587
+ return self.add_action("settings", settings)
515
588
 
516
589
  def switch_context(self, system_prompt: Optional[str] = None, user_prompt: Optional[str] = None,
517
590
  consolidate: bool = False, full_reset: bool = False) -> 'SwaigFunctionResult':
@@ -529,7 +602,7 @@ class SwaigFunctionResult:
529
602
  """
530
603
  if system_prompt and not user_prompt and not consolidate and not full_reset:
531
604
  # Simple string context switch
532
- action = {"context_switch": system_prompt}
605
+ return self.add_action("context_switch", system_prompt)
533
606
  else:
534
607
  # Advanced object context switch
535
608
  context_data = {}
@@ -541,9 +614,7 @@ class SwaigFunctionResult:
541
614
  context_data["consolidate"] = True
542
615
  if full_reset:
543
616
  context_data["full_reset"] = True
544
- action = {"context_switch": context_data}
545
-
546
- return self.add_action("context_switch", action)
617
+ return self.add_action("context_switch", context_data)
547
618
 
548
619
  def simulate_user_input(self, text: str) -> 'SwaigFunctionResult':
549
620
  """
@@ -555,8 +626,7 @@ class SwaigFunctionResult:
555
626
  Returns:
556
627
  self for method chaining
557
628
  """
558
- action = {"user_input": text}
559
- return self.add_action("user_input", action)
629
+ return self.add_action("user_input", text)
560
630
 
561
631
  def send_sms(self, to_number: str, from_number: str, body: Optional[str] = None,
562
632
  media: Optional[List[str]] = None, tags: Optional[List[str]] = None,
@@ -622,7 +692,8 @@ class SwaigFunctionResult:
622
692
  currency: str = "usd", language: str = "en-US", voice: str = "woman",
623
693
  description: Optional[str] = None, valid_card_types: str = "visa mastercard amex",
624
694
  parameters: Optional[List[Dict[str, str]]] = None,
625
- prompts: Optional[List[Dict[str, Any]]] = None) -> 'SwaigFunctionResult':
695
+ prompts: Optional[List[Dict[str, Any]]] = None,
696
+ ai_response: Optional[str] = "The payment status is ${pay_result}, do not mention anything else about collecting payment if successful.") -> 'SwaigFunctionResult':
626
697
  """
627
698
  Process payment using SWML pay action.
628
699
 
@@ -690,6 +761,7 @@ class SwaigFunctionResult:
690
761
  "version": "1.0.0",
691
762
  "sections": {
692
763
  "main": [
764
+ {"set": {"ai_response": ai_response}},
693
765
  {"pay": pay_params}
694
766
  ]
695
767
  }
@@ -89,28 +89,85 @@ class StructuredLoggerWrapper:
89
89
  # Also support the 'warn' alias
90
90
  warn = warning
91
91
 
92
+ def bind(self, **kwargs) -> 'StructuredLoggerWrapper':
93
+ """
94
+ Create a new logger instance with bound context data
95
+
96
+ This maintains compatibility with structlog's bind() method.
97
+ The bound data will be included in all subsequent log messages.
98
+ """
99
+ # Create a new wrapper that includes the bound context
100
+ return BoundStructuredLoggerWrapper(self._logger, kwargs)
101
+
92
102
  # Support direct access to underlying logger attributes if needed
93
103
  def __getattr__(self, name: str) -> Any:
94
104
  """Delegate any unknown attributes to the underlying logger"""
95
105
  return getattr(self._logger, name)
96
106
 
97
107
 
98
- def get_execution_mode() -> str:
108
+ class BoundStructuredLoggerWrapper(StructuredLoggerWrapper):
109
+ """
110
+ A structured logger wrapper that includes bound context data in all messages
111
+ """
112
+
113
+ def __init__(self, logger: logging.Logger, bound_data: Dict[str, Any]):
114
+ super().__init__(logger)
115
+ self._bound_data = bound_data
116
+
117
+ def _format_structured_message(self, message: str, **kwargs) -> str:
118
+ """Format a message with both bound data and additional keyword arguments"""
119
+ # Combine bound data with additional kwargs
120
+ all_kwargs = {**self._bound_data, **kwargs}
121
+ return super()._format_structured_message(message, **all_kwargs)
122
+
123
+ def bind(self, **kwargs) -> 'BoundStructuredLoggerWrapper':
124
+ """Create a new logger with additional bound context"""
125
+ # Combine existing bound data with new data
126
+ new_bound_data = {**self._bound_data, **kwargs}
127
+ return BoundStructuredLoggerWrapper(self._logger, new_bound_data)
128
+
129
+
130
+ def get_execution_mode():
99
131
  """
100
132
  Determine the execution mode based on environment variables
101
133
 
102
134
  Returns:
103
- 'cgi' if running in CGI mode
104
- 'lambda' if running in AWS Lambda
105
- 'server' for normal server mode
135
+ str: 'server', 'cgi', 'lambda', 'google_cloud_function', 'azure_function', or 'unknown'
106
136
  """
137
+ # Check for CGI environment
107
138
  if os.getenv('GATEWAY_INTERFACE'):
108
139
  return 'cgi'
140
+
141
+ # Check for AWS Lambda environment
109
142
  if os.getenv('AWS_LAMBDA_FUNCTION_NAME') or os.getenv('LAMBDA_TASK_ROOT'):
110
143
  return 'lambda'
144
+
145
+ # Check for Google Cloud Functions environment
146
+ if (os.getenv('FUNCTION_TARGET') or
147
+ os.getenv('K_SERVICE') or
148
+ os.getenv('GOOGLE_CLOUD_PROJECT')):
149
+ return 'google_cloud_function'
150
+
151
+ # Check for Azure Functions environment
152
+ if (os.getenv('AZURE_FUNCTIONS_ENVIRONMENT') or
153
+ os.getenv('FUNCTIONS_WORKER_RUNTIME') or
154
+ os.getenv('AzureWebJobsStorage')):
155
+ return 'azure_function'
156
+
157
+ # Default to server mode
111
158
  return 'server'
112
159
 
113
160
 
161
+ def reset_logging_configuration():
162
+ """
163
+ Reset the logging configuration flag to allow reconfiguration
164
+
165
+ This is useful when environment variables change after initial configuration.
166
+ """
167
+ global _logging_configured
168
+ _logging_configured = False
169
+
170
+
114
171
  def configure_logging():
115
172
  """
116
173
  Configure logging system once, globally, based on environment variables
@@ -182,31 +239,39 @@ def _configure_off_mode():
182
239
 
183
240
 
184
241
  def _configure_stderr_mode(log_level: str):
185
- """Configure logging to stderr"""
242
+ """Configure logging to stderr with colored formatting"""
186
243
  # Clear existing handlers
187
244
  logging.getLogger().handlers.clear()
188
245
 
189
246
  # Convert log level
190
247
  numeric_level = getattr(logging, log_level.upper(), logging.INFO)
191
248
 
192
- # Configure to stderr
193
- logging.basicConfig(
194
- stream=sys.stderr,
195
- level=numeric_level,
196
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
197
- )
249
+ # Create handler with colored formatter
250
+ handler = logging.StreamHandler(sys.stderr)
251
+ handler.setFormatter(ColoredFormatter())
252
+
253
+ # Configure root logger
254
+ root_logger = logging.getLogger()
255
+ root_logger.setLevel(numeric_level)
256
+ root_logger.addHandler(handler)
198
257
 
199
258
 
200
259
  def _configure_default_mode(log_level: str):
201
- """Configure standard logging behavior"""
260
+ """Configure standard logging behavior with colored formatting"""
261
+ # Clear existing handlers
262
+ logging.getLogger().handlers.clear()
263
+
202
264
  # Convert log level
203
265
  numeric_level = getattr(logging, log_level.upper(), logging.INFO)
204
266
 
205
- # Configure standard logging
206
- logging.basicConfig(
207
- level=numeric_level,
208
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
209
- )
267
+ # Create handler with colored formatter
268
+ handler = logging.StreamHandler()
269
+ handler.setFormatter(ColoredFormatter())
270
+
271
+ # Configure root logger
272
+ root_logger = logging.getLogger()
273
+ root_logger.setLevel(numeric_level)
274
+ root_logger.addHandler(handler)
210
275
 
211
276
 
212
277
  def get_logger(name: str) -> StructuredLoggerWrapper:
@@ -229,4 +294,83 @@ def get_logger(name: str) -> StructuredLoggerWrapper:
229
294
  python_logger = logging.getLogger(name)
230
295
 
231
296
  # Wrap it with our structured logging interface
232
- return StructuredLoggerWrapper(python_logger)
297
+ return StructuredLoggerWrapper(python_logger)
298
+
299
+
300
+ class ColoredFormatter(logging.Formatter):
301
+ """
302
+ A beautiful colored logging formatter that makes logs easy to read and visually appealing
303
+ """
304
+
305
+ # ANSI color codes
306
+ COLORS = {
307
+ 'DEBUG': '\033[36m', # Cyan
308
+ 'INFO': '\033[32m', # Green
309
+ 'WARNING': '\033[33m', # Yellow
310
+ 'ERROR': '\033[31m', # Red
311
+ 'CRITICAL': '\033[35m', # Magenta
312
+ 'RESET': '\033[0m', # Reset
313
+ 'BOLD': '\033[1m', # Bold
314
+ 'DIM': '\033[2m', # Dim
315
+ 'WHITE': '\033[37m', # White
316
+ 'BLUE': '\033[34m', # Blue
317
+ 'BLACK': '\033[30m', # Black (for brackets)
318
+ }
319
+
320
+ def __init__(self):
321
+ super().__init__()
322
+
323
+ def format(self, record):
324
+ # Check if we should use colors (not in raw mode, and stdout is a tty)
325
+ use_colors = (
326
+ hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() and
327
+ os.getenv('SIGNALWIRE_LOG_MODE') != 'off' and
328
+ '--raw' not in sys.argv and '--dump-swml' not in sys.argv
329
+ )
330
+
331
+ if use_colors:
332
+ # Get colors
333
+ level_color = self.COLORS.get(record.levelname, self.COLORS['WHITE'])
334
+ reset = self.COLORS['RESET']
335
+ dim = self.COLORS['DIM']
336
+ bold = self.COLORS['BOLD']
337
+ blue = self.COLORS['BLUE']
338
+ black = self.COLORS['BLACK']
339
+
340
+ # Format timestamp in a compact, readable way
341
+ timestamp = self.formatTime(record, '%H:%M:%S')
342
+
343
+ # Format level with appropriate color and consistent width
344
+ level_name = f"{level_color}{record.levelname:<8}{reset}"
345
+
346
+ # Format logger name - keep it short and readable
347
+ logger_name = record.name
348
+ if len(logger_name) > 15:
349
+ # Truncate long logger names but keep the end (most specific part)
350
+ logger_name = "..." + logger_name[-12:]
351
+
352
+ # Get function and line info if available
353
+ func_info = ""
354
+ if hasattr(record, 'funcName') and hasattr(record, 'lineno'):
355
+ func_name = getattr(record, 'funcName', '')
356
+ line_no = getattr(record, 'lineno', 0)
357
+ if func_name and func_name != '<module>':
358
+ func_info = f" {dim}({func_name}:{line_no}){reset}"
359
+
360
+ # Format the message
361
+ message = record.getMessage()
362
+
363
+ # Create the final formatted message with a clean, readable layout
364
+ formatted = (
365
+ f"{black}[{reset}{dim}{timestamp}{reset}{black}]{reset} "
366
+ f"{level_name} "
367
+ f"{blue}{logger_name:<15}{reset}"
368
+ f"{func_info} "
369
+ f"{message}"
370
+ )
371
+
372
+ return formatted
373
+ else:
374
+ # Non-colored format (fallback for files, pipes, etc.)
375
+ timestamp = self.formatTime(record, '%Y-%m-%d %H:%M:%S')
376
+ return f"{timestamp} {record.levelname:<8} {record.name} {record.getMessage()}"
@@ -0,0 +1,28 @@
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 .prompt_mixin import PromptMixin
11
+ from .tool_mixin import ToolMixin
12
+ from .web_mixin import WebMixin
13
+ from .auth_mixin import AuthMixin
14
+ from .skill_mixin import SkillMixin
15
+ from .ai_config_mixin import AIConfigMixin
16
+ from .serverless_mixin import ServerlessMixin
17
+ from .state_mixin import StateMixin
18
+
19
+ __all__ = [
20
+ 'PromptMixin',
21
+ 'ToolMixin',
22
+ 'WebMixin',
23
+ 'AuthMixin',
24
+ 'SkillMixin',
25
+ 'AIConfigMixin',
26
+ 'ServerlessMixin',
27
+ 'StateMixin'
28
+ ]