llm-api/plan_router.py aktualisiert
All checks were successful
Deploy Trainer_LLM to llm-node / deploy (push) Successful in 2s
All checks were successful
Deploy Trainer_LLM to llm-node / deploy (push) Successful in 2s
This commit is contained in:
parent
798e103eb8
commit
81473e20eb
|
|
@ -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 Payload→Model-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)
|
||||||
Loading…
Reference in New Issue
Block a user