atlas-chat 0.1.0__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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized configuration management using Pydantic models.
|
|
3
|
+
|
|
4
|
+
This module provides a unified configuration system that:
|
|
5
|
+
- Uses Pydantic for type validation and environment variable loading
|
|
6
|
+
- Replaces the duplicate config loading logic in config_utils.py
|
|
7
|
+
- Provides proper error handling with logging tracebacks
|
|
8
|
+
- Supports both .env files and direct environment variables
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
|
|
20
|
+
from pydantic_settings import BaseSettings
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_env_var(value: Optional[str], required: bool = True) -> Optional[str]:
|
|
26
|
+
"""
|
|
27
|
+
Resolve environment variables in config values.
|
|
28
|
+
|
|
29
|
+
Supports patterns like:
|
|
30
|
+
- "${ENV_VAR_NAME}" -> replaced with os.environ.get("ENV_VAR_NAME")
|
|
31
|
+
- "literal-string" -> returned as-is
|
|
32
|
+
- None -> returned as-is
|
|
33
|
+
|
|
34
|
+
Note: Only complete env var patterns are resolved. Values like "prefix-${VAR}"
|
|
35
|
+
or "${VAR}-suffix" are treated as literals and returned unchanged.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
value: Config value that may contain env var pattern
|
|
39
|
+
required: If True (default), raises ValueError if env var is not set.
|
|
40
|
+
If False, returns None when env var is not set.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Resolved value with env vars substituted, or None if value is None
|
|
44
|
+
or if env var is not set and required=False
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If env var pattern is found but variable is not set and required=True
|
|
48
|
+
"""
|
|
49
|
+
if value is None:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
# Pattern: ${VAR_NAME}
|
|
53
|
+
# Uses fullmatch() to ensure the entire string is an env var pattern.
|
|
54
|
+
# Patterns like "${VAR}-suffix" or "prefix-${VAR}" are treated as literals.
|
|
55
|
+
pattern = r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}'
|
|
56
|
+
match = re.fullmatch(pattern, value)
|
|
57
|
+
|
|
58
|
+
if match:
|
|
59
|
+
env_var_name = match.group(1)
|
|
60
|
+
env_value = os.environ.get(env_var_name)
|
|
61
|
+
|
|
62
|
+
if env_value is None:
|
|
63
|
+
if required:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Environment variable '{env_var_name}' is not set but required in config"
|
|
66
|
+
)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return env_value
|
|
70
|
+
|
|
71
|
+
# Return literal string if no pattern found
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ModelConfig(BaseModel):
|
|
76
|
+
"""Configuration for a single LLM model."""
|
|
77
|
+
model_name: str
|
|
78
|
+
model_url: str
|
|
79
|
+
api_key: str
|
|
80
|
+
description: Optional[str] = None
|
|
81
|
+
max_tokens: Optional[int] = 10000
|
|
82
|
+
temperature: Optional[float] = 0.7
|
|
83
|
+
# Optional extra HTTP headers (e.g. for providers like OpenRouter)
|
|
84
|
+
extra_headers: Optional[Dict[str, str]] = None
|
|
85
|
+
# Compliance/security level (e.g., "External", "Internal", "Public")
|
|
86
|
+
compliance_level: Optional[str] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LLMConfig(BaseModel):
|
|
90
|
+
"""Configuration for all LLM models."""
|
|
91
|
+
models: Dict[str, ModelConfig]
|
|
92
|
+
|
|
93
|
+
@field_validator('models', mode='before')
|
|
94
|
+
@classmethod
|
|
95
|
+
def validate_models(cls, v):
|
|
96
|
+
"""Convert dict values to ModelConfig objects."""
|
|
97
|
+
if isinstance(v, dict):
|
|
98
|
+
return {name: ModelConfig(**config) if isinstance(config, dict) else config
|
|
99
|
+
for name, config in v.items()}
|
|
100
|
+
return v
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class OAuthConfig(BaseModel):
|
|
104
|
+
"""OAuth 2.1 configuration for MCP server authentication.
|
|
105
|
+
|
|
106
|
+
Supports the OAuth 2.1 Authorization Code Grant with PKCE as implemented
|
|
107
|
+
by FastMCP. See https://gofastmcp.com/clients/auth/oauth for details.
|
|
108
|
+
"""
|
|
109
|
+
scopes: Optional[List[str]] = None # OAuth scopes to request (e.g., ["read", "write"])
|
|
110
|
+
client_name: str = "Atlas UI" # Client name for dynamic registration
|
|
111
|
+
callback_port: Optional[int] = None # Fixed port for OAuth callback (default: random)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MCPServerConfig(BaseModel):
|
|
115
|
+
"""Configuration for a single MCP server."""
|
|
116
|
+
description: Optional[str] = None
|
|
117
|
+
author: Optional[str] = None # Author of the MCP server
|
|
118
|
+
short_description: Optional[str] = None # Short description for marketplace display
|
|
119
|
+
help_email: Optional[str] = None # Contact email for help/support
|
|
120
|
+
groups: List[str] = Field(default_factory=list)
|
|
121
|
+
enabled: bool = True
|
|
122
|
+
command: Optional[List[str]] = None # Command to run server (for stdio servers)
|
|
123
|
+
cwd: Optional[str] = None # Working directory for command
|
|
124
|
+
env: Optional[Dict[str, str]] = None # Environment variables for stdio servers
|
|
125
|
+
url: Optional[str] = None # URL for HTTP servers
|
|
126
|
+
type: str = "stdio" # Server type: "stdio" or "http" (deprecated, use transport)
|
|
127
|
+
transport: Optional[str] = None # Explicit transport: "stdio", "http", "sse" - takes priority over auto-detection
|
|
128
|
+
# Authentication configuration
|
|
129
|
+
auth_type: str = "none" # Authentication type: "none", "api_key", "bearer", "jwt", "oauth"
|
|
130
|
+
auth_token: Optional[str] = None # Bearer token for MCP server authentication (supports ${ENV_VAR})
|
|
131
|
+
oauth_config: Optional[OAuthConfig] = None # OAuth 2.1 configuration (when auth_type="oauth")
|
|
132
|
+
compliance_level: Optional[str] = None # Compliance/security level (e.g., "SOC2", "HIPAA", "Public")
|
|
133
|
+
require_approval: List[str] = Field(default_factory=list) # List of tool names (without server prefix) requiring approval
|
|
134
|
+
allow_edit: List[str] = Field(default_factory=list) # LEGACY. List of tool names (without server prefix) allowing argument editing
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class MCPConfig(BaseModel):
|
|
138
|
+
"""Configuration for all MCP servers."""
|
|
139
|
+
servers: Dict[str, MCPServerConfig] = Field(default_factory=dict)
|
|
140
|
+
|
|
141
|
+
@field_validator('servers', mode='before')
|
|
142
|
+
@classmethod
|
|
143
|
+
def validate_servers(cls, v):
|
|
144
|
+
"""Convert dict values to MCPServerConfig objects."""
|
|
145
|
+
if isinstance(v, dict):
|
|
146
|
+
return {name: MCPServerConfig(**config) if isinstance(config, dict) else config
|
|
147
|
+
for name, config in v.items()}
|
|
148
|
+
return v
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class RAGSourceConfig(BaseModel):
|
|
152
|
+
"""Configuration for a single RAG source (MCP or HTTP-based).
|
|
153
|
+
|
|
154
|
+
Supports two types:
|
|
155
|
+
- "mcp": MCP-based RAG server that exposes rag_discover_resources tool
|
|
156
|
+
- "http": HTTP REST API RAG server (like ATLAS RAG API)
|
|
157
|
+
"""
|
|
158
|
+
type: Literal["mcp", "http"] = "mcp"
|
|
159
|
+
|
|
160
|
+
# Common fields
|
|
161
|
+
display_name: Optional[str] = None # UI display name
|
|
162
|
+
description: Optional[str] = None
|
|
163
|
+
icon: Optional[str] = None # UI icon
|
|
164
|
+
groups: List[str] = Field(default_factory=list) # Access groups
|
|
165
|
+
compliance_level: Optional[str] = None
|
|
166
|
+
enabled: bool = True
|
|
167
|
+
|
|
168
|
+
# MCP-specific fields (type="mcp")
|
|
169
|
+
command: Optional[List[str]] = None # Command for stdio MCP servers
|
|
170
|
+
cwd: Optional[str] = None # Working directory
|
|
171
|
+
env: Optional[Dict[str, str]] = None # Environment variables
|
|
172
|
+
url: Optional[str] = None # URL for HTTP/SSE MCP servers
|
|
173
|
+
transport: Optional[str] = None # "stdio", "http", "sse"
|
|
174
|
+
auth_token: Optional[str] = None # MCP server auth token
|
|
175
|
+
|
|
176
|
+
# HTTP REST API fields (type="http")
|
|
177
|
+
bearer_token: Optional[str] = None # Bearer token for HTTP RAG API
|
|
178
|
+
default_model: Optional[str] = None # Model for RAG queries
|
|
179
|
+
top_k: int = 4 # Number of documents to retrieve
|
|
180
|
+
timeout: float = 60.0 # Request timeout in seconds
|
|
181
|
+
|
|
182
|
+
# API endpoint customization (HTTP type)
|
|
183
|
+
discovery_endpoint: str = "/discover/datasources"
|
|
184
|
+
query_endpoint: str = "/rag/completions"
|
|
185
|
+
|
|
186
|
+
@model_validator(mode='after')
|
|
187
|
+
def validate_type_specific_fields(self):
|
|
188
|
+
"""Validate that required fields are present based on type."""
|
|
189
|
+
if self.type == "mcp":
|
|
190
|
+
# MCP type requires either command (stdio) or url (http/sse)
|
|
191
|
+
if not self.command and not self.url:
|
|
192
|
+
raise ValueError("MCP RAG source requires either 'command' or 'url'")
|
|
193
|
+
elif self.type == "http":
|
|
194
|
+
# HTTP type requires url
|
|
195
|
+
if not self.url:
|
|
196
|
+
raise ValueError("HTTP RAG source requires 'url'")
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class RAGSourcesConfig(BaseModel):
|
|
201
|
+
"""Configuration for all RAG sources."""
|
|
202
|
+
sources: Dict[str, RAGSourceConfig] = Field(default_factory=dict)
|
|
203
|
+
|
|
204
|
+
@field_validator('sources', mode='before')
|
|
205
|
+
@classmethod
|
|
206
|
+
def validate_sources(cls, v):
|
|
207
|
+
"""Convert dict values to RAGSourceConfig objects."""
|
|
208
|
+
if isinstance(v, dict):
|
|
209
|
+
return {name: RAGSourceConfig(**config) if isinstance(config, dict) else config
|
|
210
|
+
for name, config in v.items()}
|
|
211
|
+
return v
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ToolApprovalConfig(BaseModel):
|
|
215
|
+
"""Configuration for a single tool's approval settings."""
|
|
216
|
+
require_approval: bool = False
|
|
217
|
+
allow_edit: bool = True
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ToolApprovalsConfig(BaseModel):
|
|
221
|
+
"""Configuration for tool approvals."""
|
|
222
|
+
require_approval_by_default: bool = False
|
|
223
|
+
tools: Dict[str, ToolApprovalConfig] = Field(default_factory=dict)
|
|
224
|
+
|
|
225
|
+
@field_validator('tools', mode='before')
|
|
226
|
+
@classmethod
|
|
227
|
+
def validate_tools(cls, v):
|
|
228
|
+
"""Convert dict values to ToolApprovalConfig objects."""
|
|
229
|
+
if isinstance(v, dict):
|
|
230
|
+
return {name: ToolApprovalConfig(**config) if isinstance(config, dict) else config
|
|
231
|
+
for name, config in v.items()}
|
|
232
|
+
return v
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class FileExtractorConfig(BaseModel):
|
|
236
|
+
"""Configuration for a single file content extractor service."""
|
|
237
|
+
url: str
|
|
238
|
+
method: str = "POST"
|
|
239
|
+
timeout_seconds: int = 30
|
|
240
|
+
max_file_size_mb: int = 50
|
|
241
|
+
preview_chars: Optional[int] = 2000
|
|
242
|
+
request_format: str = "base64" # "base64", "multipart", or "url"
|
|
243
|
+
form_field_name: str = "file" # Field name for multipart form uploads
|
|
244
|
+
response_field: str = "text"
|
|
245
|
+
enabled: bool = True
|
|
246
|
+
# API key for authentication (supports ${ENV_VAR} syntax)
|
|
247
|
+
api_key: Optional[str] = None
|
|
248
|
+
# Additional HTTP headers (values support ${ENV_VAR} syntax)
|
|
249
|
+
headers: Optional[Dict[str, str]] = None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class FileExtractorsConfig(BaseModel):
|
|
253
|
+
"""Configuration for file content extraction services."""
|
|
254
|
+
enabled: bool = True
|
|
255
|
+
default_behavior: str = "full" # "full" | "preview" | "none"
|
|
256
|
+
extractors: Dict[str, FileExtractorConfig] = Field(default_factory=dict)
|
|
257
|
+
extension_mapping: Dict[str, str] = Field(default_factory=dict)
|
|
258
|
+
mime_mapping: Dict[str, str] = Field(default_factory=dict)
|
|
259
|
+
|
|
260
|
+
@field_validator('default_behavior', mode='before')
|
|
261
|
+
@classmethod
|
|
262
|
+
def normalize_default_behavior(cls, v):
|
|
263
|
+
"""Normalize legacy values to new 3-mode scheme."""
|
|
264
|
+
legacy_map = {"extract": "full", "attach_only": "none"}
|
|
265
|
+
return legacy_map.get(v, v)
|
|
266
|
+
|
|
267
|
+
@field_validator('extractors', mode='before')
|
|
268
|
+
@classmethod
|
|
269
|
+
def validate_extractors(cls, v):
|
|
270
|
+
"""Convert dict values to FileExtractorConfig objects."""
|
|
271
|
+
if isinstance(v, dict):
|
|
272
|
+
return {name: FileExtractorConfig(**config) if isinstance(config, dict) else config
|
|
273
|
+
for name, config in v.items()}
|
|
274
|
+
return v
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class AppSettings(BaseSettings):
|
|
278
|
+
"""Main application settings loaded from environment variables."""
|
|
279
|
+
|
|
280
|
+
# Application settings
|
|
281
|
+
app_name: str = "Chat UI"
|
|
282
|
+
port: int = 8000
|
|
283
|
+
debug_mode: bool = False
|
|
284
|
+
# Logging settings
|
|
285
|
+
log_level: str = "INFO" # Override default logging level (DEBUG, INFO, WARNING, ERROR)
|
|
286
|
+
feature_metrics_logging_enabled: bool = Field(
|
|
287
|
+
False,
|
|
288
|
+
description="Enable metrics logging for user activities (LLM calls, tool calls, file uploads, errors)",
|
|
289
|
+
validation_alias=AliasChoices("FEATURE_METRICS_LOGGING_ENABLED"),
|
|
290
|
+
)
|
|
291
|
+
# Suppress LiteLLM verbose logging (independent of log_level)
|
|
292
|
+
feature_suppress_litellm_logging: bool = Field(
|
|
293
|
+
default=True,
|
|
294
|
+
description="Suppress LiteLLM verbose stdout/debug output by setting LITELLM_LOG=ERROR",
|
|
295
|
+
validation_alias=AliasChoices("FEATURE_SUPPRESS_LITELLM_LOGGING"),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# RAG Feature Flag
|
|
299
|
+
# When enabled, RAG sources are configured in config/overrides/rag-sources.json
|
|
300
|
+
# See docs/admin/external-rag-api.md for configuration details
|
|
301
|
+
feature_rag_enabled: bool = Field(
|
|
302
|
+
False,
|
|
303
|
+
description="Enable RAG (Retrieval-Augmented Generation). Configure sources in rag-sources.json",
|
|
304
|
+
validation_alias=AliasChoices("FEATURE_RAG_ENABLED"),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Banner settings
|
|
308
|
+
banner_enabled: bool = False
|
|
309
|
+
|
|
310
|
+
# Splash screen settings
|
|
311
|
+
feature_splash_screen_enabled: bool = Field(
|
|
312
|
+
False,
|
|
313
|
+
description="Enable startup splash screen for displaying policies and information",
|
|
314
|
+
validation_alias=AliasChoices("FEATURE_SPLASH_SCREEN_ENABLED", "SPLASH_SCREEN_ENABLED"),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Agent settings
|
|
318
|
+
# Renamed to feature_agent_mode_available to align with other FEATURE_* flags.
|
|
319
|
+
feature_agent_mode_available: bool = Field(
|
|
320
|
+
True,
|
|
321
|
+
description="Agent mode availability feature flag",
|
|
322
|
+
validation_alias=AliasChoices("FEATURE_AGENT_MODE_AVAILABLE", "AGENT_MODE_AVAILABLE")
|
|
323
|
+
) # Accept both old and new env var names
|
|
324
|
+
agent_max_steps: int = 10
|
|
325
|
+
agent_loop_strategy: str = Field(
|
|
326
|
+
default="think-act",
|
|
327
|
+
description="Agent loop strategy selector (react, think-act)",
|
|
328
|
+
validation_alias=AliasChoices("AGENT_LOOP_STRATEGY"),
|
|
329
|
+
)
|
|
330
|
+
# Backward compatibility: support old AGENT_MODE_AVAILABLE env if present
|
|
331
|
+
@property
|
|
332
|
+
def agent_mode_available(self) -> bool:
|
|
333
|
+
"""Maintain backward compatibility for code still referencing agent_mode_available."""
|
|
334
|
+
return self.feature_agent_mode_available
|
|
335
|
+
|
|
336
|
+
# Tool approval settings
|
|
337
|
+
require_tool_approval_by_default: bool = False
|
|
338
|
+
# When true, all tools require approval (admin-enforced), overriding per-tool and default settings
|
|
339
|
+
force_tool_approval_globally: bool = Field(default=False, validation_alias="FORCE_TOOL_APPROVAL_GLOBALLY")
|
|
340
|
+
|
|
341
|
+
# LLM Health Check settings
|
|
342
|
+
llm_health_check_interval: int = 5 # minutes
|
|
343
|
+
|
|
344
|
+
# MCP Health Check settings
|
|
345
|
+
mcp_health_check_interval: int = 300 # seconds (5 minutes)
|
|
346
|
+
|
|
347
|
+
# MCP Auto-Reconnect settings
|
|
348
|
+
feature_mcp_auto_reconnect_enabled: bool = Field(
|
|
349
|
+
False,
|
|
350
|
+
description="Enable automatic reconnection to failed MCP servers with exponential backoff",
|
|
351
|
+
validation_alias=AliasChoices("FEATURE_MCP_AUTO_RECONNECT_ENABLED"),
|
|
352
|
+
)
|
|
353
|
+
mcp_reconnect_interval: int = Field(
|
|
354
|
+
default=60,
|
|
355
|
+
description="Base interval in seconds between MCP reconnect attempts",
|
|
356
|
+
validation_alias="MCP_RECONNECT_INTERVAL"
|
|
357
|
+
)
|
|
358
|
+
mcp_reconnect_max_interval: int = Field(
|
|
359
|
+
default=300,
|
|
360
|
+
description="Maximum interval in seconds between MCP reconnect attempts (caps exponential backoff)",
|
|
361
|
+
validation_alias="MCP_RECONNECT_MAX_INTERVAL"
|
|
362
|
+
)
|
|
363
|
+
mcp_reconnect_backoff_multiplier: float = Field(
|
|
364
|
+
default=2.0,
|
|
365
|
+
description="Multiplier for exponential backoff between reconnect attempts",
|
|
366
|
+
validation_alias="MCP_RECONNECT_BACKOFF_MULTIPLIER"
|
|
367
|
+
)
|
|
368
|
+
mcp_discovery_timeout: int = Field(
|
|
369
|
+
default=30,
|
|
370
|
+
description="Timeout in seconds for MCP discovery calls (list_tools, list_prompts)",
|
|
371
|
+
validation_alias="MCP_DISCOVERY_TIMEOUT"
|
|
372
|
+
)
|
|
373
|
+
mcp_call_timeout: int = Field(
|
|
374
|
+
default=120,
|
|
375
|
+
description="Timeout in seconds for MCP tool calls (call_tool)",
|
|
376
|
+
validation_alias="MCP_CALL_TIMEOUT"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# MCP Token Storage settings
|
|
380
|
+
mcp_token_storage_dir: Optional[str] = Field(
|
|
381
|
+
default=None,
|
|
382
|
+
description="Directory for storing encrypted user tokens. Defaults to config/secure/",
|
|
383
|
+
validation_alias="MCP_TOKEN_STORAGE_DIR"
|
|
384
|
+
)
|
|
385
|
+
mcp_token_encryption_key: Optional[str] = Field(
|
|
386
|
+
default=None,
|
|
387
|
+
description="Encryption key for user tokens. If not set, tokens won't persist across restarts",
|
|
388
|
+
validation_alias="MCP_TOKEN_ENCRYPTION_KEY"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Admin settings
|
|
392
|
+
admin_group: str = "admin"
|
|
393
|
+
test_user: str = "test@test.com" # Test user for development
|
|
394
|
+
auth_group_check_url: Optional[str] = Field(default=None, validation_alias="AUTH_GROUP_CHECK_URL")
|
|
395
|
+
auth_group_check_api_key: Optional[str] = Field(default=None, validation_alias="AUTH_GROUP_CHECK_API_KEY")
|
|
396
|
+
|
|
397
|
+
# Authentication header configuration
|
|
398
|
+
auth_user_header: str = Field(
|
|
399
|
+
default="X-User-Email",
|
|
400
|
+
description="HTTP header name to extract authenticated username from reverse proxy",
|
|
401
|
+
validation_alias="AUTH_USER_HEADER"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Authentication header configuration
|
|
405
|
+
auth_user_header_type: str = Field(
|
|
406
|
+
default="email-string",
|
|
407
|
+
description="The datatype stored in AUTH_USER_HEADER",
|
|
408
|
+
validation_alias="AUTH_USER_HEADER_TYPE"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Authentication AWS expected ALB ARN
|
|
412
|
+
auth_aws_expected_alb_arn: str = Field(
|
|
413
|
+
default="",
|
|
414
|
+
description="The expected AWS ALB ARN",
|
|
415
|
+
validation_alias="AUTH_AWS_EXPECTED_ALB_ARN"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Authentication AWS region
|
|
419
|
+
auth_aws_region: str = Field(
|
|
420
|
+
default="us-east-1",
|
|
421
|
+
description="The AWS region",
|
|
422
|
+
validation_alias="AUTH_AWS_REGION"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Proxy secret authentication configuration
|
|
426
|
+
feature_proxy_secret_enabled: bool = Field(
|
|
427
|
+
default=False,
|
|
428
|
+
description="Enable proxy secret validation to ensure requests come from trusted reverse proxy",
|
|
429
|
+
validation_alias="FEATURE_PROXY_SECRET_ENABLED"
|
|
430
|
+
)
|
|
431
|
+
proxy_secret_header: str = Field(
|
|
432
|
+
default="X-Proxy-Secret",
|
|
433
|
+
description="HTTP header name for proxy secret validation",
|
|
434
|
+
validation_alias="PROXY_SECRET_HEADER"
|
|
435
|
+
)
|
|
436
|
+
proxy_secret: Optional[str] = Field(
|
|
437
|
+
default=None,
|
|
438
|
+
description="Secret value that must be sent by reverse proxy for validation",
|
|
439
|
+
validation_alias="PROXY_SECRET"
|
|
440
|
+
)
|
|
441
|
+
auth_redirect_url: str = Field(
|
|
442
|
+
default="/auth",
|
|
443
|
+
description="URL to redirect to when authentication fails",
|
|
444
|
+
validation_alias="AUTH_REDIRECT_URL"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# S3/MinIO storage settings
|
|
448
|
+
use_mock_s3: bool = False # Use in-process S3 mock (no Docker required)
|
|
449
|
+
s3_endpoint: str = "http://localhost:9000"
|
|
450
|
+
s3_bucket_name: str = "atlas-files"
|
|
451
|
+
s3_access_key: str = "minioadmin"
|
|
452
|
+
s3_secret_key: str = "minioadmin"
|
|
453
|
+
s3_region: str = "us-east-1"
|
|
454
|
+
s3_timeout: int = 30
|
|
455
|
+
s3_use_ssl: bool = False
|
|
456
|
+
|
|
457
|
+
# Feature flags
|
|
458
|
+
feature_workspaces_enabled: bool = False
|
|
459
|
+
feature_tools_enabled: bool = False
|
|
460
|
+
feature_marketplace_enabled: bool = False
|
|
461
|
+
feature_files_panel_enabled: bool = False
|
|
462
|
+
feature_chat_history_enabled: bool = False
|
|
463
|
+
# Compliance level filtering feature gate
|
|
464
|
+
feature_compliance_levels_enabled: bool = Field(
|
|
465
|
+
False,
|
|
466
|
+
description="Enable compliance level filtering for MCP servers and data sources",
|
|
467
|
+
validation_alias=AliasChoices("FEATURE_COMPLIANCE_LEVELS_ENABLED"),
|
|
468
|
+
)
|
|
469
|
+
# Email domain whitelist feature gate
|
|
470
|
+
feature_domain_whitelist_enabled: bool = Field(
|
|
471
|
+
False,
|
|
472
|
+
description="Enable email domain whitelist restriction (configured in domain-whitelist.json)",
|
|
473
|
+
validation_alias=AliasChoices("FEATURE_DOMAIN_WHITELIST_ENABLED", "FEATURE_DOE_LAB_CHECK_ENABLED"),
|
|
474
|
+
)
|
|
475
|
+
# File content extraction feature gate
|
|
476
|
+
feature_file_content_extraction_enabled: bool = Field(
|
|
477
|
+
False,
|
|
478
|
+
description="Enable automatic content extraction from uploaded files (PDFs, images)",
|
|
479
|
+
validation_alias=AliasChoices("FEATURE_FILE_CONTENT_EXTRACTION_ENABLED"),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Capability tokens (for headless access to downloads/iframes)
|
|
483
|
+
capability_token_secret: str = ""
|
|
484
|
+
capability_token_ttl_seconds: int = 3600
|
|
485
|
+
|
|
486
|
+
# Backend URL configuration for MCP server file access
|
|
487
|
+
# This should be the publicly accessible URL of the backend API
|
|
488
|
+
# Example: "https://atlas-ui.example.com" or "http://localhost:8000"
|
|
489
|
+
# If not set, relative URLs will be used (only works for local/stdio servers)
|
|
490
|
+
backend_public_url: Optional[str] = Field(
|
|
491
|
+
default=None,
|
|
492
|
+
description="Public URL of the backend API for file downloads by remote MCP servers",
|
|
493
|
+
validation_alias="BACKEND_PUBLIC_URL",
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Whether to include base64 file content as fallback in tool arguments
|
|
497
|
+
# This allows MCP servers to access files even if they cannot reach the backend URL
|
|
498
|
+
# WARNING: Enabling this can significantly increase message sizes for large files
|
|
499
|
+
include_file_content_base64: bool = Field(
|
|
500
|
+
default=False,
|
|
501
|
+
description="Include base64 encoded file content in tool arguments as fallback",
|
|
502
|
+
validation_alias="INCLUDE_FILE_CONTENT_BASE64",
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Rate limiting (global middleware)
|
|
506
|
+
rate_limit_rpm: int = Field(default=600, validation_alias="RATE_LIMIT_RPM")
|
|
507
|
+
rate_limit_window_seconds: int = Field(default=60, validation_alias="RATE_LIMIT_WINDOW_SECONDS")
|
|
508
|
+
rate_limit_per_path: bool = Field(default=False, validation_alias="RATE_LIMIT_PER_PATH")
|
|
509
|
+
|
|
510
|
+
# Security headers toggles (HSTS intentionally omitted)
|
|
511
|
+
security_csp_enabled: bool = Field(default=True, validation_alias="SECURITY_CSP_ENABLED")
|
|
512
|
+
security_csp_value: str | None = Field(
|
|
513
|
+
default="default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'self'",
|
|
514
|
+
validation_alias="SECURITY_CSP_VALUE",
|
|
515
|
+
)
|
|
516
|
+
security_xfo_enabled: bool = Field(default=True, validation_alias="SECURITY_XFO_ENABLED")
|
|
517
|
+
security_xfo_value: str = Field(default="SAMEORIGIN", validation_alias="SECURITY_XFO_VALUE")
|
|
518
|
+
security_nosniff_enabled: bool = Field(default=True, validation_alias="SECURITY_NOSNIFF_ENABLED")
|
|
519
|
+
security_referrer_policy_enabled: bool = Field(default=True, validation_alias="SECURITY_REFERRER_POLICY_ENABLED")
|
|
520
|
+
security_referrer_policy_value: str = Field(default="no-referrer", validation_alias="SECURITY_REFERRER_POLICY_VALUE")
|
|
521
|
+
|
|
522
|
+
# Prompt / template settings
|
|
523
|
+
prompt_base_path: str = "prompts" # Relative or absolute path to directory containing prompt templates
|
|
524
|
+
system_prompt_filename: str = "system_prompt.md" # Filename for system prompt template
|
|
525
|
+
tool_synthesis_prompt_filename: str = "tool_synthesis_prompt.md" # Filename for tool synthesis prompt template
|
|
526
|
+
# Agent prompts
|
|
527
|
+
agent_reason_prompt_filename: str = "agent_reason_prompt.md" # Filename for agent reason phase
|
|
528
|
+
agent_observe_prompt_filename: str = "agent_observe_prompt.md" # Filename for agent observe phase
|
|
529
|
+
|
|
530
|
+
# Config file names (can be overridden via environment variables)
|
|
531
|
+
mcp_config_file: str = Field(default="mcp.json", validation_alias="MCP_CONFIG_FILE")
|
|
532
|
+
rag_sources_config_file: str = Field(default="rag-sources.json", validation_alias="RAG_SOURCES_CONFIG_FILE")
|
|
533
|
+
llm_config_file: str = Field(default="llmconfig.yml", validation_alias="LLM_CONFIG_FILE")
|
|
534
|
+
help_config_file: str = Field(default="help-config.json", validation_alias="HELP_CONFIG_FILE")
|
|
535
|
+
messages_config_file: str = Field(default="messages.txt", validation_alias="MESSAGES_CONFIG_FILE")
|
|
536
|
+
tool_approvals_config_file: str = Field(default="tool-approvals.json", validation_alias="TOOL_APPROVALS_CONFIG_FILE")
|
|
537
|
+
splash_config_file: str = Field(default="splash-config.json", validation_alias="SPLASH_CONFIG_FILE")
|
|
538
|
+
file_extractors_config_file: str = Field(default="file-extractors.json", validation_alias="FILE_EXTRACTORS_CONFIG_FILE")
|
|
539
|
+
|
|
540
|
+
# Config directory paths
|
|
541
|
+
app_config_overrides: str = Field(default="config/overrides", validation_alias="APP_CONFIG_OVERRIDES")
|
|
542
|
+
app_config_defaults: str = Field(default="config/defaults", validation_alias="APP_CONFIG_DEFAULTS")
|
|
543
|
+
|
|
544
|
+
# Logging directory
|
|
545
|
+
app_log_dir: Optional[str] = Field(default=None, validation_alias="APP_LOG_DIR")
|
|
546
|
+
|
|
547
|
+
# Environment mode
|
|
548
|
+
environment: str = Field(default="production", validation_alias="ENVIRONMENT")
|
|
549
|
+
|
|
550
|
+
# Prompt injection risk thresholds
|
|
551
|
+
# NOT USED RIGHT NOW.
|
|
552
|
+
pi_threshold_low: int = Field(default=30, validation_alias="PI_THRESHOLD_LOW")
|
|
553
|
+
pi_threshold_medium: int = Field(default=50, validation_alias="PI_THRESHOLD_MEDIUM")
|
|
554
|
+
pi_threshold_high: int = Field(default=80, validation_alias="PI_THRESHOLD_HIGH")
|
|
555
|
+
|
|
556
|
+
# Runtime directories (relative to project root, not backend/)
|
|
557
|
+
runtime_feedback_dir: str = Field(default="../runtime/feedback", validation_alias="RUNTIME_FEEDBACK_DIR")
|
|
558
|
+
|
|
559
|
+
@model_validator(mode='after')
|
|
560
|
+
def validate_aws_alb_config(self):
|
|
561
|
+
"""Validate that AWS ALB ARN is properly configured when using aws-alb-jwt auth."""
|
|
562
|
+
if self.auth_user_header_type == "aws-alb-jwt":
|
|
563
|
+
placeholder = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/your-alb-name/..."
|
|
564
|
+
if not self.auth_aws_expected_alb_arn or self.auth_aws_expected_alb_arn == placeholder:
|
|
565
|
+
raise ValueError(
|
|
566
|
+
"auth_aws_expected_alb_arn must be set to a valid AWS ALB ARN when auth_user_header_type is 'aws-alb-jwt'. "
|
|
567
|
+
"Current value is empty or a placeholder. Set AUTH_AWS_EXPECTED_ALB_ARN environment variable."
|
|
568
|
+
)
|
|
569
|
+
return self
|
|
570
|
+
|
|
571
|
+
model_config = {
|
|
572
|
+
"env_file": "../.env",
|
|
573
|
+
"env_file_encoding": "utf-8",
|
|
574
|
+
"extra": "ignore",
|
|
575
|
+
"env_prefix": "",
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class ConfigManager:
|
|
580
|
+
"""Centralized configuration manager with proper error handling."""
|
|
581
|
+
|
|
582
|
+
def __init__(self, backend_root: Optional[Path] = None):
|
|
583
|
+
self._backend_root = backend_root or Path(__file__).parent.parent.parent
|
|
584
|
+
self._app_settings: Optional[AppSettings] = None
|
|
585
|
+
self._llm_config: Optional[LLMConfig] = None
|
|
586
|
+
self._mcp_config: Optional[MCPConfig] = None
|
|
587
|
+
self._rag_mcp_config: Optional[MCPConfig] = None
|
|
588
|
+
self._rag_sources_config: Optional[RAGSourcesConfig] = None
|
|
589
|
+
self._tool_approvals_config: Optional[ToolApprovalsConfig] = None
|
|
590
|
+
self._file_extractors_config: Optional[FileExtractorsConfig] = None
|
|
591
|
+
|
|
592
|
+
def _search_paths(self, file_name: str) -> List[Path]:
|
|
593
|
+
"""Generate common search paths for a configuration file.
|
|
594
|
+
|
|
595
|
+
Preferred layout uses project_root/config/overrides and project_root/config/defaults.
|
|
596
|
+
The backend process often runs with CWD=backend/, so relative paths like
|
|
597
|
+
"config/overrides" incorrectly resolve to backend/config/overrides (which doesn't exist).
|
|
598
|
+
|
|
599
|
+
Configuration settings can override these directories:
|
|
600
|
+
app_config_overrides, app_config_defaults (can be absolute or relative to project root)
|
|
601
|
+
|
|
602
|
+
Legacy fallbacks (backend/configfilesadmin, backend/configfiles) are preserved.
|
|
603
|
+
"""
|
|
604
|
+
project_root = self._backend_root.parent # /workspaces/atlas-ui-3-11
|
|
605
|
+
|
|
606
|
+
# Use app_settings for config paths
|
|
607
|
+
overrides_env = self.app_settings.app_config_overrides
|
|
608
|
+
defaults_env = self.app_settings.app_config_defaults
|
|
609
|
+
|
|
610
|
+
overrides_root = Path(overrides_env)
|
|
611
|
+
defaults_root = Path(defaults_env)
|
|
612
|
+
|
|
613
|
+
# If provided paths are relative, interpret them relative to project root first.
|
|
614
|
+
if not overrides_root.is_absolute():
|
|
615
|
+
overrides_root_project = project_root / overrides_root
|
|
616
|
+
else:
|
|
617
|
+
overrides_root_project = overrides_root
|
|
618
|
+
if not defaults_root.is_absolute():
|
|
619
|
+
defaults_root_project = project_root / defaults_root
|
|
620
|
+
else:
|
|
621
|
+
defaults_root_project = defaults_root
|
|
622
|
+
|
|
623
|
+
# Legacy locations (inside backend)
|
|
624
|
+
legacy_admin = self._backend_root / "configfilesadmin" / file_name
|
|
625
|
+
legacy_defaults = self._backend_root / "configfiles" / file_name
|
|
626
|
+
|
|
627
|
+
# Build list including both CWD-relative (for backwards compat if running from project root)
|
|
628
|
+
# and project-root-relative variants. Deduplicate while preserving order.
|
|
629
|
+
candidates: List[Path] = [
|
|
630
|
+
overrides_root / file_name,
|
|
631
|
+
defaults_root / file_name,
|
|
632
|
+
overrides_root_project / file_name,
|
|
633
|
+
defaults_root_project / file_name,
|
|
634
|
+
legacy_admin,
|
|
635
|
+
legacy_defaults,
|
|
636
|
+
Path(file_name), # CWD
|
|
637
|
+
Path(f"../{file_name}"), # parent of CWD
|
|
638
|
+
project_root / file_name,
|
|
639
|
+
self._backend_root / file_name,
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
seen = set()
|
|
643
|
+
search_paths: List[Path] = []
|
|
644
|
+
for p in candidates:
|
|
645
|
+
if p not in seen:
|
|
646
|
+
seen.add(p)
|
|
647
|
+
search_paths.append(p)
|
|
648
|
+
|
|
649
|
+
logger.debug(
|
|
650
|
+
"Config search paths for %s: %s", file_name, [str(p) for p in search_paths]
|
|
651
|
+
)
|
|
652
|
+
return search_paths
|
|
653
|
+
|
|
654
|
+
def _load_file_with_error_handling(self, file_paths: List[Path], file_type: str) -> Optional[Dict[str, Any]]:
|
|
655
|
+
"""Load a file with comprehensive error handling and logging."""
|
|
656
|
+
for path in file_paths:
|
|
657
|
+
try:
|
|
658
|
+
if not path.exists():
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
logger.info(f"Found {file_type} config at: {path.absolute()}")
|
|
662
|
+
|
|
663
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
664
|
+
if file_type.lower() == "yaml":
|
|
665
|
+
data = yaml.safe_load(f)
|
|
666
|
+
elif file_type.lower() == "json":
|
|
667
|
+
data = json.load(f)
|
|
668
|
+
else:
|
|
669
|
+
raise ValueError(f"Unsupported file type: {file_type}")
|
|
670
|
+
|
|
671
|
+
if not isinstance(data, dict):
|
|
672
|
+
logger.error(
|
|
673
|
+
f"Invalid {file_type} format in {path}: expected dict, got {type(data)}",
|
|
674
|
+
exc_info=True
|
|
675
|
+
)
|
|
676
|
+
continue
|
|
677
|
+
|
|
678
|
+
logger.info(f"Successfully loaded {file_type} config from {path}")
|
|
679
|
+
return data
|
|
680
|
+
|
|
681
|
+
except (yaml.YAMLError, json.JSONDecodeError) as e:
|
|
682
|
+
logger.error(f"{file_type} parsing error in {path}: {e}", exc_info=True)
|
|
683
|
+
continue
|
|
684
|
+
except Exception as e:
|
|
685
|
+
logger.error(f"Unexpected error reading {path}: {e}", exc_info=True)
|
|
686
|
+
continue
|
|
687
|
+
|
|
688
|
+
logger.warning(f"{file_type} config not found in any of these locations: {[str(p) for p in file_paths]}")
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
@property
|
|
692
|
+
def app_settings(self) -> AppSettings:
|
|
693
|
+
"""Get application settings (cached)."""
|
|
694
|
+
if self._app_settings is None:
|
|
695
|
+
try:
|
|
696
|
+
self._app_settings = AppSettings()
|
|
697
|
+
logger.info("Application settings loaded successfully")
|
|
698
|
+
except Exception as e:
|
|
699
|
+
logger.error(f"Failed to load application settings: {e}", exc_info=True)
|
|
700
|
+
# Create default settings as fallback
|
|
701
|
+
self._app_settings = AppSettings()
|
|
702
|
+
return self._app_settings
|
|
703
|
+
|
|
704
|
+
@property
|
|
705
|
+
def llm_config(self) -> LLMConfig:
|
|
706
|
+
"""Get LLM configuration (cached)."""
|
|
707
|
+
if self._llm_config is None:
|
|
708
|
+
try:
|
|
709
|
+
# Use config filename from app settings
|
|
710
|
+
llm_filename = self.app_settings.llm_config_file
|
|
711
|
+
file_paths = self._search_paths(llm_filename)
|
|
712
|
+
data = self._load_file_with_error_handling(file_paths, "YAML")
|
|
713
|
+
|
|
714
|
+
if data:
|
|
715
|
+
self._llm_config = LLMConfig(**data)
|
|
716
|
+
# Validate compliance levels
|
|
717
|
+
self._validate_llm_compliance_levels()
|
|
718
|
+
logger.info(f"Loaded {len(self._llm_config.models)} models from LLM config")
|
|
719
|
+
else:
|
|
720
|
+
self._llm_config = LLMConfig(models={})
|
|
721
|
+
logger.info("Created empty LLM config (no configuration file found)")
|
|
722
|
+
|
|
723
|
+
except Exception as e:
|
|
724
|
+
logger.error(f"Failed to parse LLM configuration: {e}", exc_info=True)
|
|
725
|
+
self._llm_config = LLMConfig(models={})
|
|
726
|
+
|
|
727
|
+
return self._llm_config
|
|
728
|
+
|
|
729
|
+
def _validate_llm_compliance_levels(self):
|
|
730
|
+
"""Validate compliance levels for all LLM models."""
|
|
731
|
+
try:
|
|
732
|
+
# Standardize on running from atlas/ directory (agent_start.sh)
|
|
733
|
+
# Use non-prefixed imports so they resolve when cwd=backend
|
|
734
|
+
from atlas.core.compliance import get_compliance_manager
|
|
735
|
+
compliance_mgr = get_compliance_manager()
|
|
736
|
+
|
|
737
|
+
for model_name, model_config in self._llm_config.models.items():
|
|
738
|
+
if model_config.compliance_level:
|
|
739
|
+
validated = compliance_mgr.validate_compliance_level(
|
|
740
|
+
model_config.compliance_level,
|
|
741
|
+
context=f"for LLM model '{model_name}'"
|
|
742
|
+
)
|
|
743
|
+
# Update to canonical name or None if invalid
|
|
744
|
+
model_config.compliance_level = validated
|
|
745
|
+
except Exception as e:
|
|
746
|
+
logger.warning(f"Could not validate LLM compliance levels: {e}")
|
|
747
|
+
|
|
748
|
+
@property
|
|
749
|
+
def mcp_config(self) -> MCPConfig:
|
|
750
|
+
"""Get MCP configuration (cached)."""
|
|
751
|
+
if self._mcp_config is None:
|
|
752
|
+
try:
|
|
753
|
+
# Use config filename from app settings
|
|
754
|
+
mcp_filename = self.app_settings.mcp_config_file
|
|
755
|
+
file_paths = self._search_paths(mcp_filename)
|
|
756
|
+
data = self._load_file_with_error_handling(file_paths, "JSON")
|
|
757
|
+
|
|
758
|
+
if data:
|
|
759
|
+
# Convert flat structure to nested structure for Pydantic
|
|
760
|
+
servers_data = {"servers": data}
|
|
761
|
+
self._mcp_config = MCPConfig(**servers_data)
|
|
762
|
+
# Validate compliance levels
|
|
763
|
+
self._validate_mcp_compliance_levels(self._mcp_config, "MCP")
|
|
764
|
+
logger.info(f"Loaded MCP config with {len(self._mcp_config.servers)} servers: {list(self._mcp_config.servers.keys())}")
|
|
765
|
+
else:
|
|
766
|
+
self._mcp_config = MCPConfig()
|
|
767
|
+
logger.info("Created empty MCP config (no configuration file found)")
|
|
768
|
+
|
|
769
|
+
except Exception as e:
|
|
770
|
+
logger.error(f"Failed to parse MCP configuration: {e}", exc_info=True)
|
|
771
|
+
self._mcp_config = MCPConfig()
|
|
772
|
+
|
|
773
|
+
return self._mcp_config
|
|
774
|
+
|
|
775
|
+
@property
|
|
776
|
+
def rag_mcp_config(self) -> MCPConfig:
|
|
777
|
+
"""Get RAG MCP configuration (cached) derived from rag-sources.json.
|
|
778
|
+
|
|
779
|
+
Extracts MCP-type sources from rag_sources_config and converts them
|
|
780
|
+
to MCPServerConfig format for compatibility with RAGMCPService.
|
|
781
|
+
Returns an empty config when FEATURE_RAG_ENABLED is false.
|
|
782
|
+
"""
|
|
783
|
+
if not self.app_settings.feature_rag_enabled:
|
|
784
|
+
if self._rag_mcp_config is None:
|
|
785
|
+
self._rag_mcp_config = MCPConfig()
|
|
786
|
+
return self._rag_mcp_config
|
|
787
|
+
|
|
788
|
+
if self._rag_mcp_config is None:
|
|
789
|
+
try:
|
|
790
|
+
# Get all RAG sources and filter to MCP type only
|
|
791
|
+
rag_sources = self.rag_sources_config
|
|
792
|
+
mcp_servers: Dict[str, MCPServerConfig] = {}
|
|
793
|
+
|
|
794
|
+
for name, source in rag_sources.sources.items():
|
|
795
|
+
if source.type != "mcp":
|
|
796
|
+
continue
|
|
797
|
+
if not source.enabled:
|
|
798
|
+
continue
|
|
799
|
+
|
|
800
|
+
# Convert RAGSourceConfig to MCPServerConfig
|
|
801
|
+
mcp_servers[name] = MCPServerConfig(
|
|
802
|
+
description=source.description,
|
|
803
|
+
groups=source.groups,
|
|
804
|
+
enabled=source.enabled,
|
|
805
|
+
command=source.command,
|
|
806
|
+
cwd=source.cwd,
|
|
807
|
+
env=source.env,
|
|
808
|
+
url=source.url,
|
|
809
|
+
transport=source.transport,
|
|
810
|
+
auth_token=source.auth_token,
|
|
811
|
+
compliance_level=source.compliance_level,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
self._rag_mcp_config = MCPConfig(servers=mcp_servers)
|
|
815
|
+
|
|
816
|
+
if mcp_servers:
|
|
817
|
+
# Validate compliance levels
|
|
818
|
+
self._validate_mcp_compliance_levels(self._rag_mcp_config, "RAG MCP")
|
|
819
|
+
logger.info(
|
|
820
|
+
"Loaded RAG MCP config with %d servers from rag-sources.json: %s",
|
|
821
|
+
len(mcp_servers),
|
|
822
|
+
list(mcp_servers.keys())
|
|
823
|
+
)
|
|
824
|
+
else:
|
|
825
|
+
logger.info("No MCP-type RAG sources found in rag-sources.json")
|
|
826
|
+
|
|
827
|
+
except Exception as e:
|
|
828
|
+
logger.error("Failed to build RAG MCP configuration: %s", e, exc_info=True)
|
|
829
|
+
self._rag_mcp_config = MCPConfig()
|
|
830
|
+
|
|
831
|
+
return self._rag_mcp_config
|
|
832
|
+
|
|
833
|
+
@property
|
|
834
|
+
def rag_sources_config(self) -> RAGSourcesConfig:
|
|
835
|
+
"""Get unified RAG sources configuration (cached) from rag-sources.json.
|
|
836
|
+
|
|
837
|
+
This config supports both MCP-based and HTTP REST API RAG sources.
|
|
838
|
+
Returns an empty config when FEATURE_RAG_ENABLED is false.
|
|
839
|
+
"""
|
|
840
|
+
if not self.app_settings.feature_rag_enabled:
|
|
841
|
+
if self._rag_sources_config is None:
|
|
842
|
+
self._rag_sources_config = RAGSourcesConfig()
|
|
843
|
+
logger.info("RAG sources config skipped (FEATURE_RAG_ENABLED=false)")
|
|
844
|
+
return self._rag_sources_config
|
|
845
|
+
|
|
846
|
+
if self._rag_sources_config is None:
|
|
847
|
+
try:
|
|
848
|
+
rag_filename = self.app_settings.rag_sources_config_file
|
|
849
|
+
file_paths = self._search_paths(rag_filename)
|
|
850
|
+
data = self._load_file_with_error_handling(file_paths, "JSON")
|
|
851
|
+
|
|
852
|
+
if data:
|
|
853
|
+
sources_data = {"sources": data}
|
|
854
|
+
self._rag_sources_config = RAGSourcesConfig(**sources_data)
|
|
855
|
+
# Validate compliance levels
|
|
856
|
+
self._validate_rag_sources_compliance_levels(self._rag_sources_config)
|
|
857
|
+
logger.info(
|
|
858
|
+
"Loaded RAG sources config with %d sources: %s",
|
|
859
|
+
len(self._rag_sources_config.sources),
|
|
860
|
+
list(self._rag_sources_config.sources.keys())
|
|
861
|
+
)
|
|
862
|
+
else:
|
|
863
|
+
self._rag_sources_config = RAGSourcesConfig()
|
|
864
|
+
logger.info("Created empty RAG sources config (no configuration file found)")
|
|
865
|
+
|
|
866
|
+
except Exception as e:
|
|
867
|
+
logger.error("Failed to parse RAG sources configuration: %s", e, exc_info=True)
|
|
868
|
+
self._rag_sources_config = RAGSourcesConfig()
|
|
869
|
+
|
|
870
|
+
return self._rag_sources_config
|
|
871
|
+
|
|
872
|
+
def _validate_rag_sources_compliance_levels(self, config: RAGSourcesConfig) -> None:
|
|
873
|
+
"""Validate that RAG source compliance levels are defined."""
|
|
874
|
+
from atlas.core.compliance import get_compliance_manager
|
|
875
|
+
try:
|
|
876
|
+
compliance_mgr = get_compliance_manager()
|
|
877
|
+
for source_name, source_config in config.sources.items():
|
|
878
|
+
level = source_config.compliance_level
|
|
879
|
+
if level and not compliance_mgr.is_valid_level(level):
|
|
880
|
+
logger.warning(
|
|
881
|
+
"RAG source '%s' has unknown compliance level: %s",
|
|
882
|
+
source_name,
|
|
883
|
+
level
|
|
884
|
+
)
|
|
885
|
+
except Exception as e:
|
|
886
|
+
logger.debug("Compliance validation skipped for RAG sources: %s", e)
|
|
887
|
+
|
|
888
|
+
@property
|
|
889
|
+
def tool_approvals_config(self) -> ToolApprovalsConfig:
|
|
890
|
+
"""Get tool approvals configuration built from mcp.json and env variables (cached)."""
|
|
891
|
+
if self._tool_approvals_config is None:
|
|
892
|
+
try:
|
|
893
|
+
# Get default from environment
|
|
894
|
+
default_require_approval = self.app_settings.require_tool_approval_by_default
|
|
895
|
+
|
|
896
|
+
# Build tool-specific configs from MCP servers (Option B):
|
|
897
|
+
# Only include entries explicitly listed under require_approval.
|
|
898
|
+
tools_config: Dict[str, ToolApprovalConfig] = {}
|
|
899
|
+
|
|
900
|
+
for server_name, server_config in self.mcp_config.servers.items():
|
|
901
|
+
require_approval_list = server_config.require_approval or []
|
|
902
|
+
|
|
903
|
+
for tool_name in require_approval_list:
|
|
904
|
+
full_tool_name = f"{server_name}_{tool_name}"
|
|
905
|
+
# Mark as explicitly requiring approval; allow_edit is moot for requirement
|
|
906
|
+
tools_config[full_tool_name] = ToolApprovalConfig(
|
|
907
|
+
require_approval=True,
|
|
908
|
+
allow_edit=True # UI always allows edits; keep True for compatibility
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
self._tool_approvals_config = ToolApprovalsConfig(
|
|
912
|
+
require_approval_by_default=default_require_approval,
|
|
913
|
+
tools=tools_config
|
|
914
|
+
)
|
|
915
|
+
logger.info(f"Built tool approvals config from mcp.json with {len(tools_config)} tool-specific settings (default: {default_require_approval})")
|
|
916
|
+
|
|
917
|
+
except Exception as e:
|
|
918
|
+
logger.error(f"Failed to build tool approvals configuration: {e}", exc_info=True)
|
|
919
|
+
self._tool_approvals_config = ToolApprovalsConfig()
|
|
920
|
+
|
|
921
|
+
return self._tool_approvals_config
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def file_extractors_config(self) -> FileExtractorsConfig:
|
|
925
|
+
"""Get file extractors configuration (cached)."""
|
|
926
|
+
if self._file_extractors_config is None:
|
|
927
|
+
try:
|
|
928
|
+
extractors_filename = self.app_settings.file_extractors_config_file
|
|
929
|
+
file_paths = self._search_paths(extractors_filename)
|
|
930
|
+
data = self._load_file_with_error_handling(file_paths, "JSON")
|
|
931
|
+
|
|
932
|
+
if data:
|
|
933
|
+
self._file_extractors_config = FileExtractorsConfig(**data)
|
|
934
|
+
# Resolve environment variables in extractor configs
|
|
935
|
+
self._resolve_file_extractor_env_vars()
|
|
936
|
+
logger.info(
|
|
937
|
+
f"Loaded file extractors config with {len(self._file_extractors_config.extractors)} extractors"
|
|
938
|
+
)
|
|
939
|
+
else:
|
|
940
|
+
# Return disabled config if file not found
|
|
941
|
+
self._file_extractors_config = FileExtractorsConfig(enabled=False)
|
|
942
|
+
logger.info("File extractors config not found, using disabled defaults")
|
|
943
|
+
|
|
944
|
+
except Exception as e:
|
|
945
|
+
logger.error(f"Failed to parse file extractors configuration: {e}", exc_info=True)
|
|
946
|
+
self._file_extractors_config = FileExtractorsConfig(enabled=False)
|
|
947
|
+
|
|
948
|
+
return self._file_extractors_config
|
|
949
|
+
|
|
950
|
+
def _resolve_file_extractor_env_vars(self) -> None:
|
|
951
|
+
"""Resolve environment variables in file extractor configurations.
|
|
952
|
+
|
|
953
|
+
Supports ${ENV_VAR} syntax for:
|
|
954
|
+
- url: Extractor service URL (required - extractor disabled if not set)
|
|
955
|
+
- api_key: Authentication API key (optional - None if not set)
|
|
956
|
+
- headers: Header values (optional - omitted if not set)
|
|
957
|
+
"""
|
|
958
|
+
if self._file_extractors_config is None:
|
|
959
|
+
return
|
|
960
|
+
|
|
961
|
+
for extractor_name, extractor in self._file_extractors_config.extractors.items():
|
|
962
|
+
try:
|
|
963
|
+
# Resolve URL if it contains env var pattern (required)
|
|
964
|
+
if extractor.url:
|
|
965
|
+
resolved_url = resolve_env_var(extractor.url, required=True)
|
|
966
|
+
if resolved_url != extractor.url:
|
|
967
|
+
extractor.url = resolved_url
|
|
968
|
+
logger.debug(f"Resolved URL env var for extractor '{extractor_name}'")
|
|
969
|
+
|
|
970
|
+
except ValueError as e:
|
|
971
|
+
logger.error(f"Failed to resolve URL env var for extractor '{extractor_name}': {e}")
|
|
972
|
+
# Disable the extractor if URL env var resolution fails
|
|
973
|
+
extractor.enabled = False
|
|
974
|
+
continue
|
|
975
|
+
|
|
976
|
+
# Resolve API key if it contains env var pattern (optional)
|
|
977
|
+
if extractor.api_key:
|
|
978
|
+
resolved_key = resolve_env_var(extractor.api_key, required=False)
|
|
979
|
+
if resolved_key is None:
|
|
980
|
+
logger.debug(f"API key env var not set for extractor '{extractor_name}', will make unauthenticated requests")
|
|
981
|
+
extractor.api_key = None
|
|
982
|
+
elif resolved_key != extractor.api_key:
|
|
983
|
+
extractor.api_key = resolved_key
|
|
984
|
+
logger.debug(f"Resolved API key env var for extractor '{extractor_name}'")
|
|
985
|
+
|
|
986
|
+
# Resolve header values if they contain env var patterns (optional)
|
|
987
|
+
if extractor.headers:
|
|
988
|
+
resolved_headers = {}
|
|
989
|
+
for header_name, header_value in extractor.headers.items():
|
|
990
|
+
resolved_value = resolve_env_var(header_value, required=False)
|
|
991
|
+
if resolved_value is not None:
|
|
992
|
+
resolved_headers[header_name] = resolved_value
|
|
993
|
+
if resolved_value != header_value:
|
|
994
|
+
logger.debug(f"Resolved header '{header_name}' env var for extractor '{extractor_name}'")
|
|
995
|
+
else:
|
|
996
|
+
logger.debug(f"Header '{header_name}' env var not set for extractor '{extractor_name}', omitting header")
|
|
997
|
+
extractor.headers = resolved_headers if resolved_headers else None
|
|
998
|
+
|
|
999
|
+
def _validate_mcp_compliance_levels(self, config: MCPConfig, config_type: str):
|
|
1000
|
+
"""Validate compliance levels for all MCP servers."""
|
|
1001
|
+
try:
|
|
1002
|
+
# Standardize on running from atlas/ directory (agent_start.sh)
|
|
1003
|
+
from atlas.core.compliance import get_compliance_manager
|
|
1004
|
+
compliance_mgr = get_compliance_manager()
|
|
1005
|
+
|
|
1006
|
+
for server_name, server_config in config.servers.items():
|
|
1007
|
+
if server_config.compliance_level:
|
|
1008
|
+
validated = compliance_mgr.validate_compliance_level(
|
|
1009
|
+
server_config.compliance_level,
|
|
1010
|
+
context=f"for {config_type} server '{server_name}'"
|
|
1011
|
+
)
|
|
1012
|
+
# Update to canonical name or None if invalid
|
|
1013
|
+
server_config.compliance_level = validated
|
|
1014
|
+
except Exception as e:
|
|
1015
|
+
logger.warning(f"Could not validate {config_type} compliance levels: {e}")
|
|
1016
|
+
|
|
1017
|
+
def reload_configs(self) -> None:
|
|
1018
|
+
"""Reload all configurations from files."""
|
|
1019
|
+
self._app_settings = None
|
|
1020
|
+
self._llm_config = None
|
|
1021
|
+
self._mcp_config = None
|
|
1022
|
+
self._rag_mcp_config = None
|
|
1023
|
+
self._rag_sources_config = None
|
|
1024
|
+
self._tool_approvals_config = None
|
|
1025
|
+
self._file_extractors_config = None
|
|
1026
|
+
logger.info("Configuration cache cleared, will reload on next access")
|
|
1027
|
+
|
|
1028
|
+
def reload_mcp_config(self) -> MCPConfig:
|
|
1029
|
+
"""Reload MCP configuration from disk.
|
|
1030
|
+
|
|
1031
|
+
This clears the cached MCP config and forces a reload from the config file.
|
|
1032
|
+
Used for hot-reloading MCP server configuration without restarting the application.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
The newly loaded MCPConfig
|
|
1036
|
+
"""
|
|
1037
|
+
self._mcp_config = None
|
|
1038
|
+
self._tool_approvals_config = None # Also clear tool approvals since they depend on MCP
|
|
1039
|
+
logger.info("MCP configuration cache cleared, reloading from disk")
|
|
1040
|
+
return self.mcp_config
|
|
1041
|
+
|
|
1042
|
+
def validate_config(self) -> Dict[str, bool]:
|
|
1043
|
+
"""Validate all configurations and return status."""
|
|
1044
|
+
status = {}
|
|
1045
|
+
|
|
1046
|
+
try:
|
|
1047
|
+
self.app_settings
|
|
1048
|
+
status["app_settings"] = True
|
|
1049
|
+
except Exception as e:
|
|
1050
|
+
logger.error(f"App settings validation failed: {e}", exc_info=True)
|
|
1051
|
+
status["app_settings"] = False
|
|
1052
|
+
|
|
1053
|
+
try:
|
|
1054
|
+
llm_config = self.llm_config
|
|
1055
|
+
status["llm_config"] = len(llm_config.models) > 0
|
|
1056
|
+
if not status["llm_config"]:
|
|
1057
|
+
logger.warning("LLM config is valid but contains no models")
|
|
1058
|
+
except Exception as e:
|
|
1059
|
+
logger.error(f"LLM config validation failed: {e}", exc_info=True)
|
|
1060
|
+
status["llm_config"] = False
|
|
1061
|
+
|
|
1062
|
+
try:
|
|
1063
|
+
mcp_config = self.mcp_config
|
|
1064
|
+
status["mcp_config"] = len(mcp_config.servers) > 0
|
|
1065
|
+
if not status["mcp_config"]:
|
|
1066
|
+
logger.warning("MCP config is valid but contains no servers")
|
|
1067
|
+
except Exception as e:
|
|
1068
|
+
logger.error(f"MCP config validation failed: {e}", exc_info=True)
|
|
1069
|
+
status["mcp_config"] = False
|
|
1070
|
+
|
|
1071
|
+
return status
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
# Global configuration manager instance
|
|
1075
|
+
config_manager = ConfigManager()
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
# Convenience functions for easy access
|
|
1079
|
+
def get_app_settings() -> AppSettings:
|
|
1080
|
+
"""Get application settings."""
|
|
1081
|
+
return config_manager.app_settings
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def get_llm_config() -> LLMConfig:
|
|
1085
|
+
"""Get LLM configuration."""
|
|
1086
|
+
return config_manager.llm_config
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def get_mcp_config() -> MCPConfig:
|
|
1090
|
+
"""Get MCP configuration."""
|
|
1091
|
+
return config_manager.mcp_config
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def get_file_extractors_config() -> FileExtractorsConfig:
|
|
1095
|
+
"""Get file extractors configuration."""
|
|
1096
|
+
return config_manager.file_extractors_config
|