llm-api/plan_router.py aktualisiert
All checks were successful
Deploy Trainer_LLM to llm-node / deploy (push) Successful in 2s

This commit is contained in:
Lars 2025-08-12 12:46:48 +02:00
parent 798e103eb8
commit 81473e20eb

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
plan_router.py v0.9.0 (WP-15) plan_router.py v0.10.0 (WP-15)
Minimal-CRUD für Plan-Templates & Pläne (POST/GET) + Idempotenz via Fingerprint. Minimal-CRUD für Plan-Templates & Pläne (POST/GET) + Idempotenz via Fingerprint.
Keine bestehenden API-Signaturen geändert. Qdrant-Client-Stil wie exercise_router. Erweiterung ggü. v0.9.0: optionale Section-Felder ideal/supplement + materialisierte
Facettenfelder für Qdrant-Indizes; robustere PayloadModel-Konvertierung.
""" """
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -33,6 +34,8 @@ class TemplateSection(BaseModel):
name: str name: str
target_minutes: int target_minutes: int
must_keywords: List[str] = [] must_keywords: List[str] = []
ideal_keywords: List[str] = [] # NEU
supplement_keywords: List[str] = [] # NEU
forbid_keywords: List[str] = [] forbid_keywords: List[str] = []
capability_targets: Dict[str, int] = {} capability_targets: Dict[str, int] = {}
@ -145,6 +148,14 @@ def _get_by_field(collection: str, key: str, value: Any) -> Optional[Dict[str, A
payload.setdefault("id", str(pts[0].id)) payload.setdefault("id", str(pts[0].id))
return payload return payload
def _as_model(model_cls, payload: Dict[str, Any]):
"""Filtert unbekannte Felder heraus (Pydantic v1/v2 kompatibel)."""
fields = getattr(model_cls, "model_fields", None) or getattr(model_cls, "__fields__", {})
allowed = set(fields.keys())
data = {k: payload[k] for k in payload.keys() if k in allowed}
return model_cls(**data)
# ----------------- # -----------------
# Endpoints # Endpoints
# ----------------- # -----------------
@ -152,10 +163,22 @@ def _get_by_field(collection: str, key: str, value: Any) -> Optional[Dict[str, A
def create_plan_template(t: PlanTemplate): def create_plan_template(t: PlanTemplate):
_ensure_collection(PLAN_TEMPLATE_COLLECTION) _ensure_collection(PLAN_TEMPLATE_COLLECTION)
payload = t.model_dump() payload = t.model_dump()
# Normalisierung
payload["goals"] = _norm_list(payload.get("goals")) payload["goals"] = _norm_list(payload.get("goals"))
for s in payload.get("sections", []) or []: sections = payload.get("sections", []) or []
for s in sections:
s["must_keywords"] = _norm_list(s.get("must_keywords") or []) s["must_keywords"] = _norm_list(s.get("must_keywords") or [])
s["ideal_keywords"] = _norm_list(s.get("ideal_keywords") or []) # NEU
s["supplement_keywords"] = _norm_list(s.get("supplement_keywords") or []) # NEU
s["forbid_keywords"] = _norm_list(s.get("forbid_keywords") or []) s["forbid_keywords"] = _norm_list(s.get("forbid_keywords") or [])
# Materialisierte Facettenfelder (stabile KEYWORD-Indizes in Qdrant)
payload["section_names"] = _norm_list([s.get("name", "") for s in sections])
payload["section_must_keywords"] = _norm_list([kw for s in sections for kw in (s.get("must_keywords") or [])])
payload["section_ideal_keywords"] = _norm_list([kw for s in sections for kw in (s.get("ideal_keywords") or [])])
payload["section_supplement_keywords"] = _norm_list([kw for s in sections for kw in (s.get("supplement_keywords") or [])])
payload["section_forbid_keywords"] = _norm_list([kw for s in sections for kw in (s.get("forbid_keywords") or [])])
vec = _embed(_template_embed_text(t)) vec = _embed(_template_embed_text(t))
qdrant.upsert(collection_name=PLAN_TEMPLATE_COLLECTION, points=[PointStruct(id=str(t.id), vector=vec, payload=payload)]) qdrant.upsert(collection_name=PLAN_TEMPLATE_COLLECTION, points=[PointStruct(id=str(t.id), vector=vec, payload=payload)])
return t return t
@ -167,7 +190,7 @@ def get_plan_template(tpl_id: str):
found = _get_by_field(PLAN_TEMPLATE_COLLECTION, "id", tpl_id) found = _get_by_field(PLAN_TEMPLATE_COLLECTION, "id", tpl_id)
if not found: if not found:
raise HTTPException(status_code=404, detail="not found") raise HTTPException(status_code=404, detail="not found")
return PlanTemplate(**found) return _as_model(PlanTemplate, found)
@router.post("/plan", response_model=Plan) @router.post("/plan", response_model=Plan)
@ -180,7 +203,7 @@ def create_plan(p: Plan):
# Idempotenz # Idempotenz
existing = _get_by_field(PLAN_COLLECTION, "fingerprint", p.fingerprint) existing = _get_by_field(PLAN_COLLECTION, "fingerprint", p.fingerprint)
if existing: if existing:
return Plan(**existing) return _as_model(Plan, existing)
# Normalisieren # Normalisieren
p.goals = _norm_list(p.goals) p.goals = _norm_list(p.goals)
@ -206,4 +229,4 @@ def get_plan(plan_id: str):
found["created_at"] = datetime.fromisoformat(found["created_at"]) found["created_at"] = datetime.fromisoformat(found["created_at"])
except Exception: except Exception:
pass pass
return Plan(**found) return _as_model(Plan, found)