root c6164d946c feat(model): wählbares LLM-Modell pro User & pro Job
Mac-Worker:
- /api/summarize, /api/protocol, /api/preload akzeptieren optionales
  model-Feld → überschreibt den .env-Default pro Call, kein Restart nötig
- /api/models listet die lokal verfügbaren Ollama-Modelle + Default

LXC:
- User.default_model + Job.model (Migration für SQLite)
- /api/auth/me liefert default_model, PATCH speichert ihn sofort
- /api/mac/models proxiet die Liste an die UI
- Pipeline reicht job.model an Mac-Worker durch; leer = Worker-Default
- create_job & reprocess akzeptieren model-Override
- Snapshot-Meta nutzt job.model (genauer als /health-Snapshot)

UI:
- Header-Dropdown "Modell:" neben "Profil:" — sofort-speichert User-Default
- Jeder Job zeigt sein Modell als Tag (lila); leer bedeutet Worker-Default
- Reprocess öffnet einen Modal-Picker (statt simplem confirm), Auswahl
  aller verfügbaren Modelle inkl. "Worker-Default"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:48:11 +00:00

81 lines
3.5 KiB
Python

"""Leichte In-Place-Migrationen für SQLite (kein Alembic).
Wird beim App-Start nach create_all() ausgeführt. Idempotent.
"""
from __future__ import annotations
import logging
from sqlalchemy import text
from sqlmodel import Session
from app.core.db import engine
log = logging.getLogger("voice-agent.migrate")
def _existing_columns(conn, table: str) -> set[str]:
rows = conn.execute(text(f"PRAGMA table_info({table})")).all()
return {r[1] for r in rows} # row[1] = column name
def run_migrations() -> None:
with engine.begin() as conn:
# 1) Job.owner_id Spalte nachrüsten, falls die Tabelle aus alter Version kommt.
cols = _existing_columns(conn, "job")
if "owner_id" not in cols:
log.info("Migration: füge job.owner_id hinzu")
conn.execute(text("ALTER TABLE job ADD COLUMN owner_id INTEGER"))
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_job_owner_id ON job(owner_id)"))
# 2) Job.profile Spalte nachrüsten (Profil-System).
cols = _existing_columns(conn, "job")
if "profile" not in cols:
log.info("Migration: füge job.profile hinzu (default 'meeting')")
conn.execute(text("ALTER TABLE job ADD COLUMN profile VARCHAR DEFAULT 'meeting'"))
# 3) User.default_profile nachrüsten.
user_cols = _existing_columns(conn, "user")
if user_cols and "default_profile" not in user_cols:
log.info("Migration: füge user.default_profile hinzu (default 'meeting')")
conn.execute(text("ALTER TABLE user ADD COLUMN default_profile VARCHAR DEFAULT 'meeting'"))
# 4) Job.pdf_path nachrüsten (PDF-Export zusätzlich zu DOCX).
cols = _existing_columns(conn, "job")
if "pdf_path" not in cols:
log.info("Migration: füge job.pdf_path hinzu")
conn.execute(text("ALTER TABLE job ADD COLUMN pdf_path VARCHAR DEFAULT ''"))
# 5) Diagnose-Felder: step_started_at, last_heartbeat_at
cols = _existing_columns(conn, "job")
if "step_started_at" not in cols:
log.info("Migration: füge job.step_started_at hinzu")
conn.execute(text("ALTER TABLE job ADD COLUMN step_started_at DATETIME"))
if "last_heartbeat_at" not in cols:
log.info("Migration: füge job.last_heartbeat_at hinzu")
conn.execute(text("ALTER TABLE job ADD COLUMN last_heartbeat_at DATETIME"))
# 6) Modell-Auswahl: user.default_model + job.model
user_cols = _existing_columns(conn, "user")
if user_cols and "default_model" not in user_cols:
log.info("Migration: füge user.default_model hinzu (leer = Worker-Default)")
conn.execute(text("ALTER TABLE user ADD COLUMN default_model VARCHAR DEFAULT ''"))
cols = _existing_columns(conn, "job")
if "model" not in cols:
log.info("Migration: füge job.model hinzu (leer = Worker-Default)")
conn.execute(text("ALTER TABLE job ADD COLUMN model VARCHAR DEFAULT ''"))
def assign_orphan_jobs_to(user_id: int) -> int:
"""Weist alle Jobs ohne owner_id dem angegebenen User zu. Gibt Anzahl zurück."""
with Session(engine) as session:
result = session.execute(
text("UPDATE job SET owner_id = :uid WHERE owner_id IS NULL"),
{"uid": user_id},
)
session.commit()
n = result.rowcount or 0
if n:
log.info("Migration: %d Bestand-Jobs an user_id=%d zugeordnet", n, user_id)
return n