138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
"""In-memory + JSON-file LLM profile manager.
|
|
|
|
Profiles are kept in a dict keyed by profile_id and written to a JSON file
|
|
on every mutation, so they survive server restarts. The pattern mirrors
|
|
TaskManager but without threading concerns beyond a simple lock (profiles
|
|
are only mutated by API calls in FastAPI request handlers).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import threading
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from webapp.models import LLMProfile
|
|
|
|
|
|
_DEFAULT_STORE = Path(__file__).resolve().parents[2] / "configs" / "llm_profiles.json"
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
class ProfileManager:
|
|
"""Manages LLM profiles with in-memory cache and JSON file persistence."""
|
|
|
|
def __init__(self, store_path: Path = _DEFAULT_STORE) -> None:
|
|
self._store_path = store_path
|
|
self._lock = threading.Lock()
|
|
self._profiles: dict[str, LLMProfile] = {}
|
|
self._load()
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Public API
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def list_all(self) -> list[LLMProfile]:
|
|
"""Return all profiles sorted by creation time."""
|
|
with self._lock:
|
|
return sorted(self._profiles.values(), key=lambda p: p.created_at)
|
|
|
|
def get(self, profile_id: str) -> LLMProfile | None:
|
|
"""Return one profile by id, or None if not found."""
|
|
with self._lock:
|
|
return self._profiles.get(profile_id)
|
|
|
|
def create(
|
|
self,
|
|
name: str,
|
|
model: str,
|
|
base_url: str,
|
|
api_key: str,
|
|
timeout_seconds: int = 30,
|
|
) -> LLMProfile:
|
|
"""Create and persist a new profile, returning it."""
|
|
now = _now_iso()
|
|
profile = LLMProfile(
|
|
profile_id=uuid.uuid4().hex[:12],
|
|
name=name,
|
|
model=model,
|
|
base_url=base_url,
|
|
api_key=api_key,
|
|
timeout_seconds=timeout_seconds,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
with self._lock:
|
|
self._profiles[profile.profile_id] = profile
|
|
self._persist()
|
|
return profile
|
|
|
|
def update(
|
|
self,
|
|
profile_id: str,
|
|
name: str,
|
|
model: str,
|
|
base_url: str,
|
|
api_key: str,
|
|
timeout_seconds: int = 30,
|
|
) -> LLMProfile | None:
|
|
"""Update an existing profile in-place; returns None if not found."""
|
|
with self._lock:
|
|
existing = self._profiles.get(profile_id)
|
|
if existing is None:
|
|
return None
|
|
updated = existing.model_copy(update={
|
|
"name": name,
|
|
"model": model,
|
|
"base_url": base_url,
|
|
"api_key": api_key,
|
|
"timeout_seconds": timeout_seconds,
|
|
"updated_at": _now_iso(),
|
|
})
|
|
self._profiles[profile_id] = updated
|
|
self._persist()
|
|
return updated
|
|
|
|
def delete(self, profile_id: str) -> bool:
|
|
"""Remove a profile; returns True if deleted, False if not found."""
|
|
with self._lock:
|
|
if profile_id not in self._profiles:
|
|
return False
|
|
del self._profiles[profile_id]
|
|
self._persist()
|
|
return True
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Persistence helpers
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def _load(self) -> None:
|
|
"""Load profiles from the JSON store file, ignoring missing/corrupt files."""
|
|
if not self._store_path.exists():
|
|
return
|
|
try:
|
|
data = json.loads(self._store_path.read_text(encoding="utf-8"))
|
|
for raw in data.get("profiles", []):
|
|
p = LLMProfile.model_validate(raw)
|
|
self._profiles[p.profile_id] = p
|
|
except Exception: # noqa: BLE001
|
|
pass # Corrupt store — start fresh
|
|
|
|
def _persist(self) -> None:
|
|
"""Write current profiles to the JSON store file (called under lock)."""
|
|
self._store_path.parent.mkdir(parents=True, exist_ok=True)
|
|
payload = {"profiles": [p.model_dump() for p in self._profiles.values()]}
|
|
self._store_path.write_text(
|
|
json.dumps(payload, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
# Module-level singleton shared by FastAPI routes.
|
|
profile_manager = ProfileManager()
|