Files
privateGPT/private_gpt/settings/settings_loader.py
2026-07-16 13:36:11 +02:00

166 lines
5.1 KiB
Python

import functools
import logging
import os
import sys
import typing
from collections.abc import Iterable
from pathlib import Path
from typing import Any
from pydantic.v1.utils import deep_update, unique_list
from private_gpt.constants import PROJECT_ROOT_PATH
from private_gpt.settings.yaml import load_yaml_with_envvars
logger = logging.getLogger(__name__)
_settings_folder_str = os.environ.get("PGPT_SETTINGS_FOLDER", str(PROJECT_ROOT_PATH))
_settings_folders = unique_list(
[item.strip() for item in _settings_folder_str.split(",") if item.strip()]
)
# if running in unittest, use the test profile
_test_profile = ["test"] if "tests.fixtures" in sys.modules else []
active_profiles: list[str] = unique_list(
["default"]
# try to load override profile if the file exist
+ (
["override"]
if any(
(Path(folder) / "settings.override.yaml").is_file()
for folder in _settings_folders
)
else []
)
+ [
item.strip()
for item in os.environ.get("PGPT_PROFILES", "").split(",")
if item.strip()
]
+ _test_profile
)
def merge_settings(settings: Iterable[dict[str, Any]]) -> dict[str, Any]:
return functools.reduce(deep_update, settings, {})
def load_settings_from_profile(profile: str) -> dict[str, Any]:
if profile == "default":
profile_file_name = "settings.yaml"
elif profile == "override":
profile_file_name = "settings.override.yaml"
else:
profile_file_name = f"settings-{profile}.yaml"
config: dict[str, Any] = {}
found = False
for settings_folder in _settings_folders:
path = Path(settings_folder) / profile_file_name
if not path.is_file():
continue
with Path(path).open("r") as f:
raw = load_yaml_with_envvars(f)
if not isinstance(raw, dict):
raise TypeError(f"Config file has no top-level mapping: {path}")
config = raw
found = True
break
if not found:
raise FileNotFoundError(
f"Settings file not found for profile '{profile}'. "
f"Searched in folders: {_settings_folders} with file name '{profile_file_name}'"
)
return config
@typing.no_type_check
def discover_models_from_environment(
environ: dict[str, Any] = os.environ,
) -> list[dict[str, Any]]:
"""Discover model configurations from environment variables.
This function parses environment variables with the pattern:
PGPT_MODELS_<MODEL_ID>_<PARAMETER>[__<NESTED_PARAM>]
Examples:
PGPT_MODELS_GPT4_API_KEY=sk-123 ->
{"gpt4": {"api_key": "sk-123", "name": "gpt4"}}
PGPT_MODELS_CLAUDE_CONFIG__TEMPERATURE=0.7 ->
{"claude": {"config": {"temperature": "0.7"}, "name": "claude"}}
Args:
environ: Dictionary of environment variables (defaults to os.environ)
Returns:
Dictionary mapping model IDs to their configuration dictionaries.
Each model config includes a "name" field set to the model ID.
Raises:
ValueError: When there's a conflict between scalar and nested parameter values
Notes:
- Model IDs are automatically lowercased
- Parameter names are automatically lowercased
- Double underscores (__) create nested parameter structures
- Each model automatically gets a "name" field with its ID
"""
models = {}
def _set_nested_param(
config: dict[str, Any],
param_keys: list[str],
value: str,
model_id: str,
env_key: str,
) -> None:
"""Set a nested parameter value, creating intermediate dictionaries."""
current = config
for key in param_keys[:-1]:
if key not in current:
current[key] = {}
elif not isinstance(current[key], dict):
raise ValueError(
f"Conflict setting environment variable for model {model_id}: {env_key}. "
f"Parameter '{key}' is already set as a scalar value."
)
current = current[key]
current[param_keys[-1]] = value
for key, value in environ.items():
if key.startswith("PGPT_MODELS_") and key.count("_") >= 3:
_, _, model_id, param = key.split("_", 3)
model_id = model_id.lower()
if model_id not in models:
models[model_id] = {"name": model_id}
param_lower = param.lower()
param_keys = param_lower.split("__")
_set_nested_param(models[model_id], param_keys, value, model_id, key)
return list(models.values())
def load_active_settings() -> dict[str, Any]:
"""Load active profiles and merge them."""
logger.debug("Loading settings with profiles=%s", active_profiles)
loaded_profiles = [
load_settings_from_profile(profile) for profile in active_profiles
]
merged: dict[str, Any] = merge_settings(loaded_profiles)
discovered_models = discover_models_from_environment()
if discovered_models:
merged["models"] = [
*(merged.get("models", []) or []),
*discovered_models,
]
return merged