-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconfig.py
More file actions
183 lines (145 loc) · 6 KB
/
config.py
File metadata and controls
183 lines (145 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"""
Configuration management for CodeBuddy2API
Implements a multi-layered configuration system with hot-reloading.
Priority order:
1. In-memory config (for hot-settings from the UI)
2. config.json file (for persisted user overrides)
3. Environment variables (for deployment, e.g., Docker)
4. Hard-coded defaults
"""
import os
import json
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
# --- Private State ---
_config_cache: Dict[str, Any] = {}
_CONFIG_JSON_PATH = 'config/config.json' # Use a path inside a directory
_DEFAULT_CONFIG = {
"CODEBUDDY_HOST": "127.0.0.1",
"CODEBUDDY_PORT": 8001,
"CODEBUDDY_PASSWORD": None,
"CODEBUDDY_API_ENDPOINT": "https://www.codebuddy.ai",
"CODEBUDDY_CREDS_DIR": "data/.codebuddy_creds",
"CODEBUDDY_LOG_LEVEL": "INFO",
"CODEBUDDY_MODELS": "claude-4.0,claude-3.7,gpt-5,gpt-5-mini,gpt-5-nano,o4-mini,gemini-2.5-flash,gemini-2.5-pro,auto-chat",
"CODEBUDDY_ROTATION_COUNT": 1,
"CODEBUDDY_PROMPT_ENHANCE": "true",
"AUTO_DELETE_EXHAUSTED": False,
}
# --- Core Functions ---
def load_config():
"""
Loads configuration from all sources into the in-memory cache.
This should be called once at application startup.
"""
global _config_cache
config = _DEFAULT_CONFIG.copy()
try:
from dotenv import load_dotenv
load_dotenv()
logger.info("Loaded environment variables from .env file.")
except ImportError:
logger.warning("python-dotenv not installed, skipping .env file loading.")
for key in config:
env_value = os.getenv(key)
if env_value is not None:
config[key] = env_value
if os.path.exists(_CONFIG_JSON_PATH):
try:
with open(_CONFIG_JSON_PATH, 'r', encoding='utf-8') as f:
content = f.read()
if content:
persisted_config = json.loads(content)
config.update(persisted_config)
logger.info(f"Loaded and merged persisted settings from {_CONFIG_JSON_PATH}.")
except Exception as e:
logger.error(f"Error loading {_CONFIG_JSON_PATH}: {e}")
_config_cache = config
logger.info("Configuration loaded successfully.")
def _get_config_value(key: str) -> Any:
return _config_cache.get(key, _DEFAULT_CONFIG.get(key))
def _update_config_value(key: str, value: Any):
global _config_cache
_config_cache[key] = value
# Downgrade to debug to avoid verbose logging in production
logger.debug(f"Hot-reloaded setting '{key}' to new value.")
def save_config_to_json():
"""
Saves the entire current in-memory configuration to config.json.
This is simpler and more robust, ensuring a complete snapshot is always saved.
This will create the file if it doesn't exist.
"""
try:
config_dir = os.path.dirname(_CONFIG_JSON_PATH)
if not os.path.exists(config_dir):
os.makedirs(config_dir)
logger.info(f"Created config directory at {config_dir}")
config_to_save = {key: _config_cache.get(key) for key in _DEFAULT_CONFIG}
import tempfile
fd, tmp_path = tempfile.mkstemp(dir=config_dir or '.', suffix='.tmp')
try:
with os.fdopen(fd, 'w', encoding='utf-8') as f:
json.dump(config_to_save, f, indent=4)
os.replace(tmp_path, _CONFIG_JSON_PATH)
except BaseException:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
logger.info(f"Settings successfully persisted to {_CONFIG_JSON_PATH}.")
except Exception as e:
logger.error(f"Failed to save config to {_CONFIG_JSON_PATH}: {e}")
raise
# --- Public Getter Functions ---
def get_active_config() -> Dict[str, Any]:
return {key: _config_cache.get(key) for key in _DEFAULT_CONFIG}
def get_server_host() -> str:
return str(_get_config_value("CODEBUDDY_HOST"))
def get_server_port() -> int:
return int(_get_config_value("CODEBUDDY_PORT"))
def get_server_password() -> Optional[str]:
return _get_config_value("CODEBUDDY_PASSWORD")
def get_codebuddy_api_endpoint() -> str:
return str(_get_config_value("CODEBUDDY_API_ENDPOINT"))
def get_codebuddy_creds_dir() -> str:
return str(_get_config_value("CODEBUDDY_CREDS_DIR"))
def get_log_level() -> str:
return str(_get_config_value("CODEBUDDY_LOG_LEVEL")).upper()
def get_available_models() -> list:
models_str = str(_get_config_value("CODEBUDDY_MODELS"))
return [model.strip() for model in models_str.split(",")]
def get_rotation_count() -> int:
return int(_get_config_value("CODEBUDDY_ROTATION_COUNT"))
def get_prompt_enhance_enabled() -> bool:
val = str(_get_config_value("CODEBUDDY_PROMPT_ENHANCE")).lower()
return val in ('true', '1', 'yes', 'on')
def get_auto_delete_exhausted() -> bool:
val = _get_config_value("AUTO_DELETE_EXHAUSTED")
if isinstance(val, bool):
return val
return str(val).lower() in ('true', '1', 'yes', 'on')
def get_config() -> Dict[str, Any]:
"""Return full config as dict (lowercase keys for compatibility)."""
return {
k.lower(): v for k, v in _config_cache.items()
}
# --- Public Setter for Hot-Reload ---
def update_settings(new_settings: Dict[str, Any]):
"""Updates the live config and persists it to config.json."""
for key, value in new_settings.items():
if key in _config_cache:
original_type = type(_DEFAULT_CONFIG.get(key, value))
try:
if original_type is bool:
typed_value = str(value).lower() in ('true', '1', 't', 'y', 'yes')
else:
typed_value = original_type(value)
_update_config_value(key, typed_value)
except (ValueError, TypeError):
logger.warning(f"Could not cast new value for '{key}' to {original_type}. Using as string.")
_update_config_value(key, value)
save_config_to_json()
# --- Initial Load ---
load_config()