From 81473e20eb091f589d953971ce08a6dc9d453c88 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 12 Aug 2025 12:46:48 +0200 Subject: [PATCH] llm-api/plan_router.py aktualisiert --- llm-api/plan_router.py | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/llm-api/plan_router.py b/llm-api/plan_router.py index 33da8b9..884b02e 100644 --- a/llm-api/plan_router.py +++ b/llm-api/plan_router.py @@ -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. -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 Payload→Model-Konvertierung. """ from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field @@ -33,6 +34,8 @@ class TemplateSection(BaseModel): name: str target_minutes: int must_keywords: List[str] = [] + ideal_keywords: List[str] = [] # NEU + supplement_keywords: List[str] = [] # NEU forbid_keywords: List[str] = [] 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)) 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 # ----------------- @@ -152,10 +163,22 @@ def _get_by_field(collection: str, key: str, value: Any) -> Optional[Dict[str, A def create_plan_template(t: PlanTemplate): _ensure_collection(PLAN_TEMPLATE_COLLECTION) payload = t.model_dump() + # Normalisierung 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["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 []) + + # 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)) qdrant.upsert(collection_name=PLAN_TEMPLATE_COLLECTION, points=[PointStruct(id=str(t.id), vector=vec, payload=payload)]) return t @@ -167,7 +190,7 @@ def get_plan_template(tpl_id: str): found = _get_by_field(PLAN_TEMPLATE_COLLECTION, "id", tpl_id) if not found: raise HTTPException(status_code=404, detail="not found") - return PlanTemplate(**found) + return _as_model(PlanTemplate, found) @router.post("/plan", response_model=Plan) @@ -180,7 +203,7 @@ def create_plan(p: Plan): # Idempotenz existing = _get_by_field(PLAN_COLLECTION, "fingerprint", p.fingerprint) if existing: - return Plan(**existing) + return _as_model(Plan, existing) # Normalisieren p.goals = _norm_list(p.goals) @@ -206,4 +229,4 @@ def get_plan(plan_id: str): found["created_at"] = datetime.fromisoformat(found["created_at"]) except Exception: pass - return Plan(**found) \ No newline at end of file + return _as_model(Plan, found) \ No newline at end of file