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.
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/CHANGELOG.md +4 -0
- {signalwire_agents-0.1.28/signalwire_agents.egg-info → signalwire_agents-0.1.29}/PKG-INFO +1 -1
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/pyproject.toml +1 -1
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/__init__.py +1 -1
- signalwire_agents-0.1.29/signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents-0.1.29/signalwire_agents/core/config_loader.py +259 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/contexts.py +75 -0
- signalwire_agents-0.1.29/signalwire_agents/core/security_config.py +333 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_service.py +19 -25
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/search_service.py +200 -11
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29/signalwire_agents.egg-info}/PKG-INFO +1 -1
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/SOURCES.txt +3 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/LICENSE +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/requirements-dev.txt +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/requirements.txt +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/schema.json +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/setup.cfg +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/setup.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/agent_server.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/build_search.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/config.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/agent_loader.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/argparse_helpers.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/core/dynamic_config.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/execution/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/execution/datamap_exec.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/execution/webhook_exec.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/output/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/output/output_formatter.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/output/swml_dump.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/data_generation.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/data_overrides.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/simulation/mock_env.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/test_swaig.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/cli/types.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/config/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/deployment/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/deployment/handlers/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/prompt/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/prompt/manager.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/routing/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/security/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/swml/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/tools/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/tools/decorator.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent/tools/registry.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/agent_base.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/data_map.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/function_result.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/logging_config.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/ai_config_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/auth_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/prompt_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/serverless_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/skill_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/state_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/tool_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/mixins/web_mixin.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/pom_builder.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/security/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/security/session_manager.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/skill_base.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/skill_manager.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swaig_function.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_builder.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_handler.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/core/swml_renderer.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/concierge.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/faq_bot.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/info_gatherer.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/receptionist.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/prefabs/survey.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/schema.json +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/document_processor.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/index_builder.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/query_processor.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/search/search_engine.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/api_ninjas_trivia/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/api_ninjas_trivia/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/api_ninjas_trivia/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere_serverless/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere_serverless/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datasphere_serverless/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datetime/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datetime/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/datetime/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/joke/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/joke/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/joke/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/math/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/math/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/math/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/mcp_gateway/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/mcp_gateway/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/mcp_gateway/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/native_vector_search/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/native_vector_search/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/play_background_file/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/play_background_file/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/play_background_file/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/registry.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/spider/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/spider/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/spider/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/swml_transfer/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/swml_transfer/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/swml_transfer/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/weather_api/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/weather_api/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/weather_api/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/web_search/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/web_search/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/web_search/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/wikipedia_search/README.md +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/wikipedia_search/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/skills/wikipedia_search/skill.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/__init__.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/pom_utils.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/schema_utils.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/token_generators.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents/utils/validators.py +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/dependency_links.txt +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/entry_points.txt +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/requires.txt +0 -0
- {signalwire_agents-0.1.28 → signalwire_agents-0.1.29}/signalwire_agents.egg-info/top_level.txt +0 -0
@@ -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.
|
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
|
|