135 lines
4.3 KiB
Python
135 lines
4.3 KiB
Python
"""Configuration resolution utilities for hierarchical settings."""
|
|
|
|
import logging
|
|
from typing import Any, cast
|
|
|
|
from src import models
|
|
from src.config import settings
|
|
from src.schemas import (
|
|
MessageConfiguration,
|
|
ResolvedConfiguration,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def deep_update(base: dict[str, Any], update: dict[str, Any]) -> None:
|
|
"""
|
|
Recursive update of a dictionary.
|
|
Skips None values in the update dictionary.
|
|
"""
|
|
for key, value in update.items():
|
|
if value is None:
|
|
continue
|
|
|
|
if isinstance(value, dict) and key in base and isinstance(base[key], dict):
|
|
deep_update(cast(dict[str, Any], base[key]), cast(dict[str, Any], value))
|
|
else:
|
|
base[key] = value
|
|
|
|
|
|
def normalize_configuration_dict(raw: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Normalize a workspace/session/message configuration dict to match the current
|
|
`ResolvedConfiguration` schema.
|
|
|
|
This function exists to preserve backwards compatibility with older clients/tests
|
|
that used legacy configuration keys (e.g. `deriver.enabled` or `skip_deriver`).
|
|
|
|
Behavior:
|
|
- If `reasoning.enabled` is not explicitly set, but `deriver.enabled` is present,
|
|
`reasoning.enabled` is derived from `deriver.enabled`.
|
|
- If `skip_deriver` is explicitly `True`, `reasoning.enabled` is forced to `False`
|
|
unless `reasoning.enabled` was already explicitly set.
|
|
- Legacy keys are removed from the returned dict to avoid polluting the resolved
|
|
configuration with unused fields.
|
|
"""
|
|
normalized: dict[str, Any] = dict(raw)
|
|
|
|
reasoning_raw = normalized.get("reasoning")
|
|
reasoning_present = "reasoning" in normalized
|
|
reasoning: dict[str, Any]
|
|
if isinstance(reasoning_raw, dict):
|
|
reasoning = dict(cast(dict[str, Any], reasoning_raw))
|
|
else:
|
|
reasoning = {}
|
|
reasoning_enabled_explicit = reasoning.get("enabled") is not None
|
|
|
|
if not reasoning_enabled_explicit:
|
|
deriver_raw = normalized.get("deriver")
|
|
deriver: dict[str, Any]
|
|
if isinstance(deriver_raw, dict):
|
|
deriver = dict(cast(dict[str, Any], deriver_raw))
|
|
else:
|
|
deriver = {}
|
|
if deriver.get("enabled") is not None:
|
|
reasoning["enabled"] = bool(deriver["enabled"])
|
|
|
|
if not reasoning_enabled_explicit and normalized.get("skip_deriver") is True:
|
|
reasoning["enabled"] = False
|
|
|
|
if reasoning_present or reasoning:
|
|
normalized["reasoning"] = reasoning
|
|
|
|
normalized.pop("deriver", None)
|
|
normalized.pop("skip_deriver", None)
|
|
|
|
return normalized
|
|
|
|
|
|
def get_configuration(
|
|
message_configuration: MessageConfiguration | None,
|
|
session: models.Session | None,
|
|
workspace: models.Workspace | None = None,
|
|
) -> ResolvedConfiguration:
|
|
"""
|
|
Resolve session configuration with hierarchical fallback.
|
|
|
|
Resolution hierarchy:
|
|
1. Message configuration
|
|
2. Session configuration
|
|
3. Workspace configuration
|
|
4. Global defaults from settings
|
|
|
|
Args:
|
|
message_configuration: Optional message configuration
|
|
session: Optional session model
|
|
workspace: Optional workspace model
|
|
|
|
Returns:
|
|
ResolvedConfiguration
|
|
"""
|
|
# Start with defaults
|
|
config_dict: dict[str, Any] = {
|
|
"reasoning": {"enabled": settings.DERIVER.ENABLED},
|
|
"peer_card": {
|
|
"use": settings.PEER_CARD.ENABLED,
|
|
"create": settings.PEER_CARD.ENABLED,
|
|
},
|
|
"summary": {
|
|
"enabled": settings.SUMMARY.ENABLED,
|
|
"messages_per_short_summary": settings.SUMMARY.MESSAGES_PER_SHORT_SUMMARY,
|
|
"messages_per_long_summary": settings.SUMMARY.MESSAGES_PER_LONG_SUMMARY,
|
|
},
|
|
"dream": {"enabled": settings.DREAM.ENABLED},
|
|
}
|
|
|
|
# Apply overrides in order (Workspace -> Session -> Message)
|
|
# Note: deep_update modifies config_dict in place
|
|
|
|
if workspace is not None:
|
|
deep_update(config_dict, normalize_configuration_dict(workspace.configuration))
|
|
|
|
if session is not None:
|
|
deep_update(config_dict, normalize_configuration_dict(session.configuration))
|
|
|
|
if message_configuration is not None:
|
|
deep_update(
|
|
config_dict,
|
|
normalize_configuration_dict(
|
|
message_configuration.model_dump(exclude_none=True)
|
|
),
|
|
)
|
|
|
|
return ResolvedConfiguration(**config_dict)
|