mirror of
https://github.com/imartinez/privateGPT.git
synced 2026-07-17 01:48:03 +00:00
166 lines
5.1 KiB
Python
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
|