signalwire-agents 0.1.28__tar.gz → 0.1.29__tar.gz

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 (139) hide show
  1. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/CHANGELOG.md +4 -0
  2. {signalwire_agents-0.1.28/signalwire_agents.egg-info → signalwire_agents-0.1.29}/PKG-INFO +1 -1
  3. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/pyproject.toml +1 -1
  4. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/__init__.py +1 -1
  5. signalwire_agents-0.1.29/signalwire_agents/core/auth_handler.py +233 -0
  6. signalwire_agents-0.1.29/signalwire_agents/core/config_loader.py +259 -0
  7. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/contexts.py +75 -0
  8. signalwire_agents-0.1.29/signalwire_agents/core/security_config.py +333 -0
  9. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_service.py +19 -25
  10. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/search_service.py +200 -11
  11. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29/signalwire_agents.egg-info}/PKG-INFO +1 -1
  12. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/SOURCES.txt +3 -0
  13. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/LICENSE +0 -0
  14. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/README.md +0 -0
  15. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/requirements-dev.txt +0 -0
  16. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/requirements.txt +0 -0
  17. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/schema.json +0 -0
  18. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/setup.cfg +0 -0
  19. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/setup.py +0 -0
  20. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/agent_server.py +0 -0
  21. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/__init__.py +0 -0
  22. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/build_search.py +0 -0
  23. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/config.py +0 -0
  24. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/__init__.py +0 -0
  25. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/agent_loader.py +0 -0
  26. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/argparse_helpers.py +0 -0
  27. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/dynamic_config.py +0 -0
  28. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/execution/__init__.py +0 -0
  29. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/execution/datamap_exec.py +0 -0
  30. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/execution/webhook_exec.py +0 -0
  31. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/output/__init__.py +0 -0
  32. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/output/output_formatter.py +0 -0
  33. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/output/swml_dump.py +0 -0
  34. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/__init__.py +0 -0
  35. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/data_generation.py +0 -0
  36. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/data_overrides.py +0 -0
  37. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/mock_env.py +0 -0
  38. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/test_swaig.py +0 -0
  39. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/types.py +0 -0
  40. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/__init__.py +0 -0
  41. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/__init__.py +0 -0
  42. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/config/__init__.py +0 -0
  43. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/deployment/__init__.py +0 -0
  44. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/deployment/handlers/__init__.py +0 -0
  45. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/prompt/__init__.py +0 -0
  46. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/prompt/manager.py +0 -0
  47. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/routing/__init__.py +0 -0
  48. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/security/__init__.py +0 -0
  49. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/swml/__init__.py +0 -0
  50. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/tools/__init__.py +0 -0
  51. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/tools/decorator.py +0 -0
  52. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/tools/registry.py +0 -0
  53. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent_base.py +0 -0
  54. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/data_map.py +0 -0
  55. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/function_result.py +0 -0
  56. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/logging_config.py +0 -0
  57. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/__init__.py +0 -0
  58. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/ai_config_mixin.py +0 -0
  59. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/auth_mixin.py +0 -0
  60. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/prompt_mixin.py +0 -0
  61. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/serverless_mixin.py +0 -0
  62. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/skill_mixin.py +0 -0
  63. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/state_mixin.py +0 -0
  64. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/tool_mixin.py +0 -0
  65. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/web_mixin.py +0 -0
  66. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/pom_builder.py +0 -0
  67. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/security/__init__.py +0 -0
  68. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/security/session_manager.py +0 -0
  69. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/skill_base.py +0 -0
  70. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/skill_manager.py +0 -0
  71. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swaig_function.py +0 -0
  72. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_builder.py +0 -0
  73. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_handler.py +0 -0
  74. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_renderer.py +0 -0
  75. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/__init__.py +0 -0
  76. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/concierge.py +0 -0
  77. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/faq_bot.py +0 -0
  78. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/info_gatherer.py +0 -0
  79. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/receptionist.py +0 -0
  80. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/survey.py +0 -0
  81. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/schema.json +0 -0
  82. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/__init__.py +0 -0
  83. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/document_processor.py +0 -0
  84. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/index_builder.py +0 -0
  85. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/query_processor.py +0 -0
  86. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/search_engine.py +0 -0
  87. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/README.md +0 -0
  88. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/__init__.py +0 -0
  89. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/api_ninjas_trivia/README.md +0 -0
  90. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/api_ninjas_trivia/__init__.py +0 -0
  91. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/api_ninjas_trivia/skill.py +0 -0
  92. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere/README.md +0 -0
  93. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere/__init__.py +0 -0
  94. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere/skill.py +0 -0
  95. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere_serverless/README.md +0 -0
  96. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere_serverless/__init__.py +0 -0
  97. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere_serverless/skill.py +0 -0
  98. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datetime/README.md +0 -0
  99. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datetime/__init__.py +0 -0
  100. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datetime/skill.py +0 -0
  101. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/joke/README.md +0 -0
  102. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/joke/__init__.py +0 -0
  103. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/joke/skill.py +0 -0
  104. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/math/README.md +0 -0
  105. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/math/__init__.py +0 -0
  106. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/math/skill.py +0 -0
  107. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/mcp_gateway/README.md +0 -0
  108. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/mcp_gateway/__init__.py +0 -0
  109. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/mcp_gateway/skill.py +0 -0
  110. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/native_vector_search/__init__.py +0 -0
  111. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/native_vector_search/skill.py +0 -0
  112. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/play_background_file/README.md +0 -0
  113. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/play_background_file/__init__.py +0 -0
  114. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/play_background_file/skill.py +0 -0
  115. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/registry.py +0 -0
  116. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/spider/README.md +0 -0
  117. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/spider/__init__.py +0 -0
  118. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/spider/skill.py +0 -0
  119. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/swml_transfer/README.md +0 -0
  120. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/swml_transfer/__init__.py +0 -0
  121. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/swml_transfer/skill.py +0 -0
  122. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/weather_api/README.md +0 -0
  123. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/weather_api/__init__.py +0 -0
  124. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/weather_api/skill.py +0 -0
  125. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/web_search/README.md +0 -0
  126. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/web_search/__init__.py +0 -0
  127. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/web_search/skill.py +0 -0
  128. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/wikipedia_search/README.md +0 -0
  129. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/wikipedia_search/__init__.py +0 -0
  130. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/wikipedia_search/skill.py +0 -0
  131. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/__init__.py +0 -0
  132. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/pom_utils.py +0 -0
  133. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/schema_utils.py +0 -0
  134. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/token_generators.py +0 -0
  135. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/validators.py +0 -0
  136. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/dependency_links.txt +0 -0
  137. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/entry_points.txt +0 -0
  138. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/requires.txt +0 -0
  139. {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/top_level.txt +0 -0
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.29] - 2025-06-23
4
+
5
+ - Version bump
6
+
3
7
  ## [0.1.28] - 2025-06-23
4
8
 
5
9
  - Version bump
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire_agents
3
- Version: 0.1.28
3
+ Version: 0.1.29
4
4
  Summary: SignalWire AI Agents SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "signalwire_agents"
7
- version = "0.1.28"
7
+ version = "0.1.29"
8
8
  description = "SignalWire AI Agents SDK"
9
9
  authors = [
10
10
  {name = "SignalWire Team", email = "info@signalwire.com"}
@@ -18,7 +18,7 @@ A package for building AI agents using SignalWire's AI and SWML capabilities.
18
18
  from .core.logging_config import configure_logging
19
19
  configure_logging()
20
20
 
21
- __version__ = "0.1.28"
21
+ __version__ = "0.1.29"
22
22
 
23
23
  # Import core classes for easier access
24
24
  from .core.agent_base import AgentBase
@@ -0,0 +1,233 @@
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 secrets
11
+ from typing import Optional, Tuple, Dict, Any, Callable
12
+ from functools import wraps
13
+
14
+ try:
15
+ from fastapi import HTTPException, Depends
16
+ from fastapi.security import HTTPBasic, HTTPBasicCredentials, HTTPBearer, HTTPAuthorizationCredentials
17
+ except ImportError:
18
+ HTTPException = None
19
+ Depends = None
20
+ HTTPBasic = None
21
+ HTTPBasicCredentials = None
22
+ HTTPBearer = None
23
+ HTTPAuthorizationCredentials = None
24
+
25
+ from signalwire_agents.core.logging_config import get_logger
26
+
27
+ logger = get_logger("auth_handler")
28
+
29
+
30
+ class AuthHandler:
31
+ """
32
+ Unified authentication handler supporting multiple auth methods.
33
+
34
+ This class provides a clean pattern for handling Basic Auth, Bearer tokens,
35
+ and API keys across all SignalWire services.
36
+ """
37
+
38
+ def __init__(self, security_config: 'SecurityConfig'):
39
+ """
40
+ Initialize auth handler with security configuration.
41
+
42
+ Args:
43
+ security_config: SecurityConfig instance with auth settings
44
+ """
45
+ self.security_config = security_config
46
+ self.basic_auth = HTTPBasic() if HTTPBasic else None
47
+ self.bearer_auth = HTTPBearer(auto_error=False) if HTTPBearer else None
48
+
49
+ # Get auth methods from config
50
+ self._setup_auth_methods()
51
+
52
+ def _setup_auth_methods(self):
53
+ """Setup enabled authentication methods from config"""
54
+ self.auth_methods = {}
55
+
56
+ # Basic auth (always available for backward compatibility)
57
+ username, password = self.security_config.get_basic_auth()
58
+ self.auth_methods['basic'] = {
59
+ 'enabled': True,
60
+ 'username': username,
61
+ 'password': password
62
+ }
63
+
64
+ # Bearer token (if configured)
65
+ bearer_token = getattr(self.security_config, 'bearer_token', None)
66
+ if bearer_token:
67
+ self.auth_methods['bearer'] = {
68
+ 'enabled': True,
69
+ 'token': bearer_token
70
+ }
71
+
72
+ # API key (if configured)
73
+ api_key = getattr(self.security_config, 'api_key', None)
74
+ if api_key:
75
+ self.auth_methods['api_key'] = {
76
+ 'enabled': True,
77
+ 'key': api_key,
78
+ 'header': getattr(self.security_config, 'api_key_header', 'X-API-Key')
79
+ }
80
+
81
+ def verify_basic_auth(self, credentials: HTTPBasicCredentials) -> bool:
82
+ """Verify basic auth credentials"""
83
+ if not self.auth_methods.get('basic', {}).get('enabled'):
84
+ return False
85
+
86
+ basic_config = self.auth_methods['basic']
87
+ username_correct = secrets.compare_digest(
88
+ credentials.username, basic_config['username']
89
+ )
90
+ password_correct = secrets.compare_digest(
91
+ credentials.password, basic_config['password']
92
+ )
93
+
94
+ return username_correct and password_correct
95
+
96
+ def verify_bearer_token(self, credentials: HTTPAuthorizationCredentials) -> bool:
97
+ """Verify bearer token"""
98
+ if not self.auth_methods.get('bearer', {}).get('enabled'):
99
+ return False
100
+
101
+ bearer_config = self.auth_methods['bearer']
102
+ return secrets.compare_digest(
103
+ credentials.credentials, bearer_config['token']
104
+ )
105
+
106
+ def verify_api_key(self, api_key: str) -> bool:
107
+ """Verify API key"""
108
+ if not self.auth_methods.get('api_key', {}).get('enabled'):
109
+ return False
110
+
111
+ api_config = self.auth_methods['api_key']
112
+ return secrets.compare_digest(api_key, api_config['key'])
113
+
114
+ def get_fastapi_dependency(self, optional: bool = False):
115
+ """
116
+ Get FastAPI dependency for authentication.
117
+
118
+ Args:
119
+ optional: If True, authentication is optional
120
+
121
+ Returns:
122
+ FastAPI dependency function
123
+ """
124
+ if not Depends:
125
+ return None
126
+
127
+ async def auth_dependency(
128
+ basic_credentials: Optional[HTTPBasicCredentials] = Depends(self.basic_auth) if self.basic_auth else None,
129
+ bearer_credentials: Optional[HTTPAuthorizationCredentials] = Depends(self.bearer_auth) if self.bearer_auth else None,
130
+ api_key: Optional[str] = None # Get from header in request
131
+ ):
132
+ # Try each auth method
133
+ authenticated = False
134
+ auth_method = None
135
+
136
+ # Try bearer token first (if provided)
137
+ if bearer_credentials and self.verify_bearer_token(bearer_credentials):
138
+ authenticated = True
139
+ auth_method = 'bearer'
140
+
141
+ # Try basic auth
142
+ elif basic_credentials and self.verify_basic_auth(basic_credentials):
143
+ authenticated = True
144
+ auth_method = 'basic'
145
+
146
+ # Try API key (would need to be extracted from request headers)
147
+ # This is a simplified version - in practice, you'd get it from request
148
+
149
+ if not authenticated and not optional:
150
+ raise HTTPException(
151
+ status_code=401,
152
+ detail="Invalid authentication credentials",
153
+ headers={"WWW-Authenticate": "Basic"},
154
+ )
155
+
156
+ return {'authenticated': authenticated, 'method': auth_method}
157
+
158
+ return auth_dependency
159
+
160
+ def flask_decorator(self, f: Callable) -> Callable:
161
+ """
162
+ Flask decorator for authentication.
163
+
164
+ This provides compatibility with Flask-based services like MCP Gateway.
165
+ """
166
+ @wraps(f)
167
+ def decorated(*args, **kwargs):
168
+ from flask import request, Response
169
+
170
+ # Try Bearer token first
171
+ auth_header = request.headers.get('Authorization', '')
172
+
173
+ if auth_header.startswith('Bearer ') and self.auth_methods.get('bearer', {}).get('enabled'):
174
+ token = auth_header[7:]
175
+ if secrets.compare_digest(token, self.auth_methods['bearer']['token']):
176
+ return f(*args, **kwargs)
177
+
178
+ # Try API key
179
+ if self.auth_methods.get('api_key', {}).get('enabled'):
180
+ api_config = self.auth_methods['api_key']
181
+ api_key = request.headers.get(api_config['header'])
182
+ if api_key and secrets.compare_digest(api_key, api_config['key']):
183
+ return f(*args, **kwargs)
184
+
185
+ # Fall back to Basic auth
186
+ auth = request.authorization
187
+ if auth and self.auth_methods.get('basic', {}).get('enabled'):
188
+ basic_config = self.auth_methods['basic']
189
+ if auth.username == basic_config['username'] and \
190
+ auth.password == basic_config['password']:
191
+ return f(*args, **kwargs)
192
+
193
+ # Authentication failed
194
+ logger.warning(
195
+ "auth_failed",
196
+ ip=request.remote_addr,
197
+ method=request.method,
198
+ path=request.path
199
+ )
200
+
201
+ return Response(
202
+ 'Authentication required',
203
+ 401,
204
+ {'WWW-Authenticate': 'Basic realm="SignalWire Service"'}
205
+ )
206
+
207
+ return decorated
208
+
209
+ def get_auth_info(self) -> Dict[str, Any]:
210
+ """Get information about configured auth methods"""
211
+ info = {}
212
+
213
+ if self.auth_methods.get('basic', {}).get('enabled'):
214
+ info['basic'] = {
215
+ 'enabled': True,
216
+ 'username': self.auth_methods['basic']['username']
217
+ }
218
+
219
+ if self.auth_methods.get('bearer', {}).get('enabled'):
220
+ info['bearer'] = {
221
+ 'enabled': True,
222
+ 'hint': 'Use Authorization: Bearer <token>'
223
+ }
224
+
225
+ if self.auth_methods.get('api_key', {}).get('enabled'):
226
+ api_config = self.auth_methods['api_key']
227
+ info['api_key'] = {
228
+ 'enabled': True,
229
+ 'header': api_config['header'],
230
+ 'hint': f'Use {api_config["header"]}: <key>'
231
+ }
232
+
233
+ return info
@@ -0,0 +1,259 @@
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 re
12
+ import json
13
+ from typing import Any, Dict, List, Optional, Union
14
+ from signalwire_agents.core.logging_config import get_logger
15
+
16
+ logger = get_logger("config_loader")
17
+
18
+
19
+ class ConfigLoader:
20
+ """
21
+ Configuration loader with environment variable substitution.
22
+
23
+ Supports ${VAR|default} syntax for referencing environment variables
24
+ within JSON configuration files. This provides a clean pattern for
25
+ configuration across all SignalWire services.
26
+ """
27
+
28
+ def __init__(self, config_paths: Optional[List[str]] = None):
29
+ """
30
+ Initialize config loader.
31
+
32
+ Args:
33
+ config_paths: Optional list of config file paths to check.
34
+ If not provided, uses default search paths.
35
+ """
36
+ self.config_paths = config_paths or self._get_default_paths()
37
+ self._config = None
38
+ self._config_file = None
39
+ self._load_config()
40
+
41
+ def _get_default_paths(self) -> List[str]:
42
+ """Get default configuration file search paths."""
43
+ return [
44
+ "config.json",
45
+ "agent_config.json",
46
+ "swml_config.json",
47
+ ".swml/config.json",
48
+ os.path.expanduser("~/.swml/config.json"),
49
+ "/etc/swml/config.json"
50
+ ]
51
+
52
+ def _load_config(self) -> None:
53
+ """Load configuration from the first available config file."""
54
+ for path in self.config_paths:
55
+ if os.path.exists(path):
56
+ try:
57
+ with open(path, 'r') as f:
58
+ self._config = json.load(f)
59
+ self._config_file = path
60
+ logger.info("config_loaded", path=path)
61
+ break
62
+ except Exception as e:
63
+ logger.error("config_load_error", path=path, error=str(e))
64
+
65
+ def has_config(self) -> bool:
66
+ """Check if a configuration was loaded."""
67
+ return self._config is not None
68
+
69
+ def get_config_file(self) -> Optional[str]:
70
+ """Get the path of the loaded config file."""
71
+ return self._config_file
72
+
73
+ def get_config(self) -> Dict[str, Any]:
74
+ """Get the raw configuration (before substitution)."""
75
+ return self._config or {}
76
+
77
+ def substitute_vars(self, value: Any) -> Any:
78
+ """
79
+ Recursively substitute environment variables in configuration values.
80
+
81
+ Supports ${VAR|default} syntax where:
82
+ - VAR is the environment variable name
83
+ - default is the fallback value if VAR is not set
84
+
85
+ Args:
86
+ value: The value to process (can be string, dict, list, etc.)
87
+
88
+ Returns:
89
+ The value with all environment variables substituted
90
+ """
91
+ if isinstance(value, str):
92
+ # Pattern to match ${VAR} or ${VAR|default}
93
+ pattern = r'\$\{([^}|]+)(?:\|([^}]*))?\}'
94
+
95
+ def replacer(match):
96
+ var_name = match.group(1)
97
+ default = match.group(2) if match.group(2) is not None else ''
98
+ return os.environ.get(var_name, default)
99
+
100
+ # Substitute all variables
101
+ result = re.sub(pattern, replacer, value)
102
+
103
+ # Try to parse as JSON to get proper types
104
+ if result.lower() in ('true', 'false'):
105
+ return result.lower() == 'true'
106
+ elif result.isdigit():
107
+ return int(result)
108
+ elif result.replace('.', '', 1).isdigit():
109
+ return float(result)
110
+ else:
111
+ return result
112
+
113
+ elif isinstance(value, dict):
114
+ # Recursively process dictionary
115
+ return {k: self.substitute_vars(v) for k, v in value.items()}
116
+
117
+ elif isinstance(value, list):
118
+ # Recursively process list
119
+ return [self.substitute_vars(item) for item in value]
120
+
121
+ else:
122
+ # Return other types as-is
123
+ return value
124
+
125
+ def get(self, key_path: str, default: Any = None) -> Any:
126
+ """
127
+ Get a configuration value by dot-notation path.
128
+
129
+ Args:
130
+ key_path: Dot-separated path (e.g., "security.ssl_enabled")
131
+ default: Default value if path not found
132
+
133
+ Returns:
134
+ The configuration value with variables substituted
135
+ """
136
+ if not self._config:
137
+ return default
138
+
139
+ # Navigate through the config using the dot path
140
+ keys = key_path.split('.')
141
+ value = self._config
142
+
143
+ for key in keys:
144
+ if isinstance(value, dict) and key in value:
145
+ value = value[key]
146
+ else:
147
+ return default
148
+
149
+ # Substitute variables before returning
150
+ return self.substitute_vars(value)
151
+
152
+ def get_section(self, section: str) -> Dict[str, Any]:
153
+ """
154
+ Get an entire configuration section.
155
+
156
+ Args:
157
+ section: The section name (e.g., "security", "server")
158
+
159
+ Returns:
160
+ The configuration section with all variables substituted
161
+ """
162
+ if not self._config or section not in self._config:
163
+ return {}
164
+
165
+ return self.substitute_vars(self._config[section])
166
+
167
+ def merge_with_env(self, env_prefix: str = "SWML_") -> Dict[str, Any]:
168
+ """
169
+ Merge configuration with environment variables.
170
+
171
+ Config file takes precedence over environment variables,
172
+ but config can reference env vars via substitution.
173
+
174
+ Args:
175
+ env_prefix: Prefix for environment variables to consider
176
+
177
+ Returns:
178
+ Merged configuration dictionary
179
+ """
180
+ # Start with substituted config
181
+ result = self.substitute_vars(self._config) if self._config else {}
182
+
183
+ # Only add env vars that aren't already in config
184
+ # This preserves config file precedence
185
+ for key, value in os.environ.items():
186
+ if key.startswith(env_prefix):
187
+ # Convert SWML_SSL_ENABLED to ssl_enabled
188
+ config_key = key[len(env_prefix):].lower()
189
+
190
+ # Only set if not already in config
191
+ if not self._has_nested_key(result, config_key):
192
+ self._set_nested_key(result, config_key, value)
193
+
194
+ return result
195
+
196
+ def _has_nested_key(self, data: Dict, key_path: str) -> bool:
197
+ """Check if a nested key exists in dictionary."""
198
+ keys = key_path.split('_')
199
+ current = data
200
+
201
+ for key in keys:
202
+ if isinstance(current, dict) and key in current:
203
+ current = current[key]
204
+ else:
205
+ return False
206
+ return True
207
+
208
+ def _set_nested_key(self, data: Dict, key_path: str, value: Any) -> None:
209
+ """Set a value in dictionary using underscore-separated path."""
210
+ keys = key_path.split('_')
211
+ current = data
212
+
213
+ for key in keys[:-1]:
214
+ if key not in current:
215
+ current[key] = {}
216
+ current = current[key]
217
+
218
+ current[keys[-1]] = value
219
+
220
+ @staticmethod
221
+ def find_config_file(service_name: Optional[str] = None,
222
+ additional_paths: Optional[List[str]] = None) -> Optional[str]:
223
+ """
224
+ Static method to find a config file for a service.
225
+
226
+ Args:
227
+ service_name: Optional service name for service-specific config
228
+ additional_paths: Additional paths to check
229
+
230
+ Returns:
231
+ Path to the first config file found, or None
232
+ """
233
+ paths = []
234
+
235
+ # Service-specific config
236
+ if service_name:
237
+ paths.extend([
238
+ f"{service_name}_config.json",
239
+ f".swml/{service_name}_config.json"
240
+ ])
241
+
242
+ # Additional paths
243
+ if additional_paths:
244
+ paths.extend(additional_paths)
245
+
246
+ # Default paths
247
+ paths.extend([
248
+ "config.json",
249
+ "agent_config.json",
250
+ ".swml/config.json",
251
+ os.path.expanduser("~/.swml/config.json"),
252
+ "/etc/swml/config.json"
253
+ ])
254
+
255
+ for path in paths:
256
+ if os.path.exists(path):
257
+ return path
258
+
259
+ return None
@@ -259,6 +259,10 @@ class Context:
259
259
  # Context prompt (separate from system_prompt)
260
260
  self._prompt_text: Optional[str] = None
261
261
  self._prompt_sections: List[Dict[str, Any]] = []
262
+
263
+ # Context fillers
264
+ self._enter_fillers: Optional[Dict[str, List[str]]] = None
265
+ self._exit_fillers: Optional[Dict[str, List[str]]] = None
262
266
 
263
267
  def add_step(self, name: str) -> Step:
264
268
  """
@@ -450,6 +454,70 @@ class Context:
450
454
  self._prompt_sections.append({"title": title, "bullets": bullets})
451
455
  return self
452
456
 
457
+ def set_enter_fillers(self, enter_fillers: Dict[str, List[str]]) -> 'Context':
458
+ """
459
+ Set fillers that the AI says when entering this context
460
+
461
+ Args:
462
+ enter_fillers: Dictionary mapping language codes (or "default") to lists of filler phrases
463
+ Example: {"en-US": ["Welcome...", "Hello..."], "default": ["Entering..."]}
464
+
465
+ Returns:
466
+ Self for method chaining
467
+ """
468
+ if enter_fillers and isinstance(enter_fillers, dict):
469
+ self._enter_fillers = enter_fillers
470
+ return self
471
+
472
+ def set_exit_fillers(self, exit_fillers: Dict[str, List[str]]) -> 'Context':
473
+ """
474
+ Set fillers that the AI says when exiting this context
475
+
476
+ Args:
477
+ exit_fillers: Dictionary mapping language codes (or "default") to lists of filler phrases
478
+ Example: {"en-US": ["Goodbye...", "Thank you..."], "default": ["Exiting..."]}
479
+
480
+ Returns:
481
+ Self for method chaining
482
+ """
483
+ if exit_fillers and isinstance(exit_fillers, dict):
484
+ self._exit_fillers = exit_fillers
485
+ return self
486
+
487
+ def add_enter_filler(self, language_code: str, fillers: List[str]) -> 'Context':
488
+ """
489
+ Add enter fillers for a specific language
490
+
491
+ Args:
492
+ language_code: Language code (e.g., "en-US", "es") or "default" for catch-all
493
+ fillers: List of filler phrases for entering this context
494
+
495
+ Returns:
496
+ Self for method chaining
497
+ """
498
+ if language_code and fillers and isinstance(fillers, list):
499
+ if self._enter_fillers is None:
500
+ self._enter_fillers = {}
501
+ self._enter_fillers[language_code] = fillers
502
+ return self
503
+
504
+ def add_exit_filler(self, language_code: str, fillers: List[str]) -> 'Context':
505
+ """
506
+ Add exit fillers for a specific language
507
+
508
+ Args:
509
+ language_code: Language code (e.g., "en-US", "es") or "default" for catch-all
510
+ fillers: List of filler phrases for exiting this context
511
+
512
+ Returns:
513
+ Self for method chaining
514
+ """
515
+ if language_code and fillers and isinstance(fillers, list):
516
+ if self._exit_fillers is None:
517
+ self._exit_fillers = {}
518
+ self._exit_fillers[language_code] = fillers
519
+ return self
520
+
453
521
  def _render_prompt(self) -> Optional[str]:
454
522
  """Render the context's prompt text"""
455
523
  if self._prompt_text is not None:
@@ -533,6 +601,13 @@ class Context:
533
601
  elif self._prompt_text is not None:
534
602
  # Use string format
535
603
  context_dict["prompt"] = self._prompt_text
604
+
605
+ # Add enter and exit fillers if defined
606
+ if self._enter_fillers is not None:
607
+ context_dict["enter_fillers"] = self._enter_fillers
608
+
609
+ if self._exit_fillers is not None:
610
+ context_dict["exit_fillers"] = self._exit_fillers
536
611
 
537
612
  return context_dict
538
613