feat(07-03): classifier wired to load_provider_config + ai_config stub removed — D-04/D-06
- Remove _ProviderConfigStub from services/ai_config.py (replaced by real ProviderConfig) - Add module-level import: from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS - load_provider_config() now returns ProviderConfig (no lazy import inside function body) - classifier.py: replace inline _settings dict with load_provider_config(session) call (D-06) - Per-user override path builds ProviderConfig from PROVIDER_DEFAULTS (no api_key — T-07-06) - Fallback to app_settings defaults when load_provider_config returns None (D-15) - Truncation delegated to provider._truncate() — no more text slices in classifier (D-12/D-13) - Promote test_api_key_encrypt_decrypt to passing (round-trip + domain salt isolation) - Update test_classifier.py: test_per_user_provider + test_default_provider_fallback use ProviderConfig assertions
This commit is contained in:
@@ -22,38 +22,21 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS
|
||||
from config import settings
|
||||
|
||||
# ProviderConfig redefined in ai/provider_config.py during Plan 02 —
|
||||
# load_provider_config will re-import and return that class once Plan 02 lands.
|
||||
# The stub below is removed in Plan 03 once the classifier consumes the real class.
|
||||
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# ── Stub ProviderConfig for Plan 01 (replaced by ai/provider_config.py in Plan 02) ──
|
||||
|
||||
class _ProviderConfigStub(BaseModel):
|
||||
"""Minimal provider config placeholder until Plan 02 creates ai/provider_config.py."""
|
||||
|
||||
provider_id: str
|
||||
api_key: str = ""
|
||||
base_url: Optional[str] = None
|
||||
model: str = ""
|
||||
context_chars: int = 8000
|
||||
|
||||
|
||||
# ── HKDF key derivation ───────────────────────────────────────────────────────
|
||||
|
||||
def _derive_ai_settings_key(master_key: bytes, provider_id: str) -> Fernet:
|
||||
@@ -125,31 +108,19 @@ def decrypt_api_key(master_key: bytes, provider_id: str, api_key_enc: str) -> st
|
||||
|
||||
# ── Provider config loader ────────────────────────────────────────────────────
|
||||
|
||||
async def load_provider_config(session: AsyncSession) -> Optional[_ProviderConfigStub]:
|
||||
async def load_provider_config(session: AsyncSession) -> Optional[ProviderConfig]:
|
||||
"""Load the active AI provider config from the system_settings table.
|
||||
|
||||
Returns a _ProviderConfigStub built from the row where is_active=True,
|
||||
Returns a ProviderConfig built from the row where is_active=True,
|
||||
decrypting api_key_enc when present. Returns None when no active row exists.
|
||||
|
||||
Note: In Plan 02, this function will try to import and return ProviderConfig
|
||||
from ai/provider_config.py instead of the stub. The import is done lazily
|
||||
inside the function body so that Plan 01 does not depend on files that don't
|
||||
exist yet.
|
||||
|
||||
Args:
|
||||
session: An open AsyncSession.
|
||||
|
||||
Returns:
|
||||
A _ProviderConfigStub (or the real ProviderConfig from Plan 02+) if an
|
||||
active row exists; None if the table is empty or no row is marked active.
|
||||
A ProviderConfig if an active row exists; None if the table is empty or
|
||||
no row is marked active.
|
||||
"""
|
||||
# Lazy import: try to use the real ProviderConfig once Plan 02 lands
|
||||
try:
|
||||
from ai.provider_config import ProviderConfig as _RealProviderConfig # type: ignore[import]
|
||||
config_cls = _RealProviderConfig
|
||||
except ImportError:
|
||||
config_cls = _ProviderConfigStub # type: ignore[assignment]
|
||||
|
||||
from db.models import SystemSettings # local import to avoid circular deps
|
||||
|
||||
stmt = select(SystemSettings).where(SystemSettings.is_active.is_(True))
|
||||
@@ -171,7 +142,7 @@ async def load_provider_config(session: AsyncSession) -> Optional[_ProviderConfi
|
||||
provider_id=row.provider_id,
|
||||
)
|
||||
|
||||
return config_cls(
|
||||
return ProviderConfig(
|
||||
provider_id=row.provider_id,
|
||||
api_key=api_key,
|
||||
base_url=row.base_url,
|
||||
@@ -208,11 +179,14 @@ async def seed_system_settings_from_env(session: AsyncSession) -> None:
|
||||
# Row already exists — never overwrite (idempotent)
|
||||
return
|
||||
|
||||
# Use PROVIDER_DEFAULTS context_chars for the provider, fallback to 8000
|
||||
context_chars = PROVIDER_DEFAULTS.get(provider_id, {}).get("context_chars", 8000)
|
||||
|
||||
# Insert default row with no API key (local providers like Ollama don't need one)
|
||||
row = SystemSettings(
|
||||
provider_id=provider_id,
|
||||
model_name=model_name,
|
||||
context_chars=8000,
|
||||
context_chars=context_chars,
|
||||
is_active=True,
|
||||
api_key_enc=None,
|
||||
base_url=None,
|
||||
|
||||
Reference in New Issue
Block a user