diff --git a/.claude/docs/functional/DOMAIN_MODEL.md b/.claude/docs/functional/DOMAIN_MODEL.md index b240724..5f0a99a 100644 --- a/.claude/docs/functional/DOMAIN_MODEL.md +++ b/.claude/docs/functional/DOMAIN_MODEL.md @@ -465,7 +465,7 @@ skill_level_definitions ( **Fachliche Grenze aktuell:** Mehrere gleichwertige „Pakete“ paralleler Alternativen sind **modellierbar** (mehrere ausgehende Kanten), aber noch **nicht** über eine dedizierte „Alternativgruppe“ in der UI trivial pflegbar; siehe `technical/TRAINING_FRAMEWORK_SPEC.md` §4. -**KI-Planung (Workbench, Stand 0.8.233):** Am Graph können Trainer neben Kanten ein **`planning_roadmap`**-Artefakt (Curriculum-Stufen) und **`planning_catalog_context`** (Primärfokus, Stilrichtung, Trainingsstil, Zielgruppe aus den Katalog-Dimensionen §1) pflegen. Die Roadmap-first-Pipeline matcht Übungen pro Stufe; Didaktik und Reihenfolge kommen aus Roadmap + QS, nicht aus Technik-Hardcoding. Technische Details: **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**. Für **Trainingsplanung** (Einheit, Abschnitt, Rahmen-Slot) gelten dieselben Katalog- und Retrieval-Bausteine mit anderen Scopes — Phase G, siehe Roadmap **`PLANNING_KI_ROADMAP.md`**. +**KI-Planung (Workbench, Stand 0.8.233):** Am Graph können Trainer neben Kanten ein **`planning_roadmap`**-Artefakt (Curriculum-Stufen) und **`planning_catalog_context`** (Primärfokus, Stilrichtung, Trainingsstil, Zielgruppe aus den Katalog-Dimensionen §1) pflegen. Die Roadmap-first-Pipeline matcht Übungen pro Stufe; Didaktik und Reihenfolge kommen aus Roadmap + QS, nicht aus Technik-Hardcoding. **Geplant (H1):** Katalog-Dimensionen zusätzlich als **Prompt-Snippets** in LLM-Aufrufen (Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`**. Technische Details: **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**. Für **Trainingsplanung** (Einheit, Abschnitt, Rahmen-Slot) gelten dieselben Katalog- und Retrieval-Bausteine mit anderen Scopes — Phase G, siehe Roadmap **`PLANNING_KI_ROADMAP.md`**. ### Trainingsrahmen‑Vorlage (Rahmenprogramm, CURR‑002 Stufe 2 / CURR‑009) diff --git a/backend/planning_exercise_path_builder.py b/backend/planning_exercise_path_builder.py index 8084c99..aa56214 100644 --- a/backend/planning_exercise_path_builder.py +++ b/backend/planning_exercise_path_builder.py @@ -2208,20 +2208,120 @@ def _normalize_slot_title(title: Optional[str]) -> str: return (title or "").strip().casefold() -def _filter_trivial_slot_diffs(diffs: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]: - """Gleicher sichtbarer Titel = kein inhaltlicher Wechsel (nur ID-Doppel in der Bibliothek).""" +def _annotate_slot_diffs( + diffs: Sequence[Mapping[str, Any]], +) -> List[Dict[str, Any]]: + """Kennzeichnet reine ID-Tausche (gleicher Titel) — bleiben sichtbar, zählen aber nicht als inhaltlich.""" out: List[Dict[str, Any]] = [] for raw in diffs or []: if not isinstance(raw, dict): continue - bt = _normalize_slot_title(raw.get("baseline_title")) - pt = _normalize_slot_title(raw.get("proposed_title")) - if bt and pt and bt == pt: - continue - out.append(dict(raw)) + entry = dict(raw) + bt = _normalize_slot_title(entry.get("baseline_title")) + pt = _normalize_slot_title(entry.get("proposed_title")) + entry["trivial_id_swap"] = bool(bt and pt and bt == pt) + out.append(entry) return out +def _actionable_slot_diffs(diffs: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]: + return [d for d in diffs if not d.get("trivial_id_swap")] + + +def _last_rematch_replacements_by_slot( + rematch_log: Sequence[Mapping[str, Any]], +) -> Dict[int, Mapping[str, Any]]: + """Letzter erfolgreicher Replace je Slot (Multi-Runden-Rematch).""" + out: Dict[int, Mapping[str, Any]] = {} + for entry in rematch_log or []: + if not isinstance(entry, dict): + continue + if str(entry.get("action") or "") != "replaced": + continue + if entry.get("new_exercise_id") is None: + continue + midx = entry.get("roadmap_major_step_index") + if midx is None: + continue + out[int(midx)] = entry + return out + + +def _build_rematch_suggestion_diffs( + baseline_steps: Sequence[Mapping[str, Any]], + rematch_log: Sequence[Mapping[str, Any]], +) -> List[Dict[str, Any]]: + """Vorschläge aus Rematch-Protokoll, wenn End-Pfad vs. Baseline identisch wirkt.""" + base_by = _steps_by_major_index(baseline_steps) + replacements = _last_rematch_replacements_by_slot(rematch_log) + diffs: List[Dict[str, Any]] = [] + for midx, entry in sorted(replacements.items()): + base = base_by.get(midx, {}) + base_id = base.get("exercise_id") + new_id = entry.get("new_exercise_id") + base_title = (base.get("title") or "").strip() or None + new_title = (entry.get("new_title") or "").strip() or None + same_id = False + if base_id is not None and new_id is not None: + try: + same_id = int(base_id) == int(new_id) + except (TypeError, ValueError): + same_id = False + if same_id: + bt = _normalize_slot_title(base_title) + pt = _normalize_slot_title(new_title) + if bt and pt and bt == pt: + continue + diffs.append( + { + "roadmap_major_step_index": midx, + "baseline_exercise_id": int(base_id) if base_id is not None else None, + "baseline_title": base_title, + "proposed_exercise_id": int(new_id) if new_id is not None else None, + "proposed_title": new_title, + "baseline_slot_status": base.get("slot_status"), + "proposed_slot_status": "matched", + "changed": True, + "from_rematch_log": True, + } + ) + return diffs + + +def _overlay_rematch_suggestions_on_steps( + proposed_steps: Sequence[Mapping[str, Any]], + suggestion_diffs: Sequence[Mapping[str, Any]], +) -> List[Dict[str, Any]]: + """Ergänzt proposed_steps um Rematch-Kandidaten (für selektive Übernahme).""" + if not suggestion_diffs: + return list(proposed_steps or []) + prop_by = _steps_by_major_index(proposed_steps) + for diff in suggestion_diffs: + if not isinstance(diff, dict) or not diff.get("from_rematch_log"): + continue + midx = diff.get("roadmap_major_step_index") + new_id = diff.get("proposed_exercise_id") + if midx is None or new_id is None: + continue + existing = dict(prop_by.get(int(midx), {})) + existing.update( + { + "exercise_id": int(new_id), + "title": diff.get("proposed_title") or existing.get("title"), + "variant_id": existing.get("variant_id"), + "roadmap_major_step_index": int(midx), + "is_ai_proposal": False, + "slot_status": "matched", + "roadmap_match_source": "rematch_suggestion", + } + ) + prop_by[int(midx)] = existing + ordered: List[Dict[str, Any]] = [] + for midx in sorted(prop_by.keys()): + ordered.append(prop_by[midx]) + return ordered + + def _build_progression_slot_diffs( baseline_steps: Sequence[Mapping[str, Any]], proposed_steps: Sequence[Mapping[str, Any]], @@ -2269,24 +2369,46 @@ def _build_progression_compare_response( if isinstance(proposed_eval, dict) and isinstance(proposed_eval.get("path_qa"), dict) else pipeline_qa ) - slot_diffs = _filter_trivial_slot_diffs( + slot_diffs = _annotate_slot_diffs( _build_progression_slot_diffs(baseline_steps, proposed_steps), ) + actionable_diffs = _actionable_slot_diffs(slot_diffs) + slot_diffs_source = "steps" + rematch_log = ( + pipeline_qa.get("rematch_log") + if isinstance(pipeline_qa.get("rematch_log"), list) + else [] + ) + apply_steps = list(proposed_steps) + if not actionable_diffs and rematch_log: + rematch_raw = _build_rematch_suggestion_diffs(baseline_steps, rematch_log) + rematch_diffs = _annotate_slot_diffs(rematch_raw) + rematch_actionable = _actionable_slot_diffs(rematch_diffs) + if rematch_actionable: + actionable_diffs = rematch_actionable + slot_diffs = rematch_diffs + slot_diffs_source = "rematch_log" + apply_steps = _overlay_rematch_suggestions_on_steps(proposed_steps, rematch_actionable) return { **dict(proposed), "comparison_mode": True, "baseline_steps": baseline_steps, "baseline_path_qa": baseline_qa, - "proposed_steps": proposed_steps, + "proposed_steps": apply_steps, + "proposed_steps_pipeline": proposed_steps, "proposed_path_qa": fair_qa, "proposed_path_qa_pipeline": pipeline_qa, "slot_diffs": slot_diffs, - "slot_diff_count": len(slot_diffs), + "slot_diffs_actionable": actionable_diffs, + "slot_diff_count": len(actionable_diffs), + "slot_diff_count_including_trivial": len(slot_diffs), + "slot_diffs_source": slot_diffs_source, + "optimization_actionable": len(actionable_diffs) > 0, "baseline_quality_score": _path_qa_quality_score(baseline_qa), "proposed_quality_score": _path_qa_quality_score(fair_qa), "proposed_pipeline_quality_score": _path_qa_quality_score(pipeline_qa), "path_qa": fair_qa, - "steps": proposed_steps, + "steps": apply_steps, } diff --git a/backend/tests/test_planning_compare_slot_diffs.py b/backend/tests/test_planning_compare_slot_diffs.py index cef4d85..aa86d86 100644 --- a/backend/tests/test_planning_compare_slot_diffs.py +++ b/backend/tests/test_planning_compare_slot_diffs.py @@ -1,11 +1,14 @@ -"""Tests Vergleichs-Diffs (triviale ID-Tausche ausfiltern).""" +"""Tests Vergleichs-Diffs (triviale ID-Tausche markieren, Rematch-Vorschläge).""" from planning_exercise_path_builder import ( + _actionable_slot_diffs, + _annotate_slot_diffs, + _build_progression_compare_response, _build_progression_slot_diffs, - _filter_trivial_slot_diffs, + _build_rematch_suggestion_diffs, ) -def test_filter_trivial_slot_diffs_same_title_different_id(): +def test_annotate_trivial_id_swap(): diffs = [ { "roadmap_major_step_index": 1, @@ -15,10 +18,13 @@ def test_filter_trivial_slot_diffs_same_title_different_id(): "proposed_title": "Rhythmuswechsel in der Kumite-Beinarbeit", } ] - assert _filter_trivial_slot_diffs(diffs) == [] + annotated = _annotate_slot_diffs(diffs) + assert len(annotated) == 1 + assert annotated[0]["trivial_id_swap"] is True + assert _actionable_slot_diffs(annotated) == [] -def test_filter_trivial_slot_diffs_keeps_real_title_change(): +def test_annotate_keeps_real_title_change(): diffs = [ { "roadmap_major_step_index": 1, @@ -28,12 +34,12 @@ def test_filter_trivial_slot_diffs_keeps_real_title_change(): "proposed_title": "Neu", } ] - filtered = _filter_trivial_slot_diffs(diffs) - assert len(filtered) == 1 - assert filtered[0]["proposed_title"] == "Neu" + annotated = _annotate_slot_diffs(diffs) + assert annotated[0]["trivial_id_swap"] is False + assert len(_actionable_slot_diffs(annotated)) == 1 -def test_build_slot_diffs_then_filter(): +def test_build_slot_diffs_then_annotate(): baseline = [ {"roadmap_major_step_index": 0, "exercise_id": 1, "title": "A"}, {"roadmap_major_step_index": 1, "exercise_id": 10, "title": "Gleich"}, @@ -43,5 +49,54 @@ def test_build_slot_diffs_then_filter(): {"roadmap_major_step_index": 1, "exercise_id": 77, "title": "Gleich"}, ] raw = _build_progression_slot_diffs(baseline, proposed) - assert len(raw) == 1 - assert _filter_trivial_slot_diffs(raw) == [] + annotated = _annotate_slot_diffs(raw) + assert len(annotated) == 1 + assert annotated[0]["trivial_id_swap"] is True + assert _actionable_slot_diffs(annotated) == [] + + +def test_rematch_suggestion_diffs_when_end_path_matches_baseline(): + baseline = [ + {"roadmap_major_step_index": 1, "exercise_id": None, "title": "Lernziel Slot 2"}, + {"roadmap_major_step_index": 4, "exercise_id": 50, "title": "Bestehend"}, + ] + proposed = list(baseline) + rematch_log = [ + { + "roadmap_major_step_index": 1, + "action": "replaced", + "round": 1, + "new_exercise_id": 101, + "new_title": "Rhythmuswechsel in der Kumite-Beinarbeit", + "replaced_exercise_id": None, + "replaced_title": None, + }, + { + "roadmap_major_step_index": 1, + "action": "replaced", + "round": 3, + "new_exercise_id": 102, + "new_title": "Kumite Beinarbeit — vertiefung", + "replaced_exercise_id": 101, + "replaced_title": "Rhythmuswechsel in der Kumite-Beinarbeit", + }, + ] + diffs = _build_rematch_suggestion_diffs(baseline, rematch_log) + assert len(diffs) == 1 + assert diffs[0]["proposed_exercise_id"] == 102 + assert diffs[0]["from_rematch_log"] is True + + compare = _build_progression_compare_response( + {"steps": baseline, "path_qa": {"overall_ok": True, "quality_score": 0.88}}, + { + "steps": proposed, + "path_qa": { + "overall_ok": False, + "quality_score": 0.65, + "rematch_log": rematch_log, + }, + }, + ) + assert compare["slot_diffs_source"] == "rematch_log" + assert compare["slot_diff_count"] == 1 + assert compare["proposed_steps"][0]["exercise_id"] == 102 diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index b672d48..a9d5820 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -37,6 +37,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl | Überblick DB | `.claude/docs/technical/DATABASE_SCHEMA.md` | | Domäne | `.claude/docs/functional/DOMAIN_MODEL.md` | | **Gewichtetes Fähigkeiten-Scoring (Phase 3)** | `.claude/docs/technical/SKILL_SCORING_SPEC.md` | +| **Planungs-KI — Katalog-Prompt-Snippets (H1)** | **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** | | **Lieferliste inkl. Medien & Formular-UX** | `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §6, §16 | | **Fachlicher Nutzerüberblick (Design/Product)** | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** | @@ -113,8 +114,9 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl | **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ **0.8.231–0.8.232** | | **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** | | **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** | +| **H1** | Katalog-Prompt-Snippets (modulare LLM-Anweisungen) | 🔲 Spec **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** | -**Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16. +**Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet; **H1:** zusätzlich Prompt-Snippets), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16. **Validierung (Mae Geri, Härtetest):** Pfad-QS vor Optimierung ~65 % → nach Trainer-Roadmap + KI-Gap-Fill **~88 % OK**. Workbench ist **universell** gedacht; Mae Geri war Referenzfall, kein Sonder-Patch. @@ -267,7 +269,8 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl ### Planungs-KI (priorisiert) -1. **Dev-Regression:** Katalog-Match für Gewaltschutz, Breitensport, Kinder — nicht nur Mae-Geri-Härtetest. +1. **H1 Katalog-Prompt-Snippets:** modulare LLM-Anweisungen — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung). +2. **Dev-Regression:** Katalog-Match für Gewaltschutz, Breitensport, Kinder — nicht nur Mae-Geri-Härtetest. 2. **PathBuilder-Parität:** `planning_catalog_context`-Dropdowns auch in `ExerciseProgressionPathBuilder`. 3. **QS-UI:** positive LLM-Empfehlungen als Highlights statt nur Optimierungspotenziale. 4. **UI-Wizard:** 4 Schritte (Ziel & Katalog → Roadmap → Match → Lücken); Backend-Pipeline unverändert. diff --git a/docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md b/docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md new file mode 100644 index 0000000..61dacdd --- /dev/null +++ b/docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md @@ -0,0 +1,229 @@ +# Planungs-KI — Katalog-Snippets für modulare Prompts + +**Stand:** 2026-05-22 +**Status:** Spezifikation (Phase **H1** — Umsetzung offen) +**Bezüge:** `PLANNING_PROGRESSION_GRAPH_KI.md` §4.4 · `AI_PROMPT_TARGET_ARCHITECTURE.md` §2.4 · `planning_catalog_context.py` + +--- + +## 1. Problem + +Seit **F13 (0.8.233)** fließen Primärfokus, Trainingsstil, Zielgruppe und Stilrichtung als `planning_catalog_context` in **Retrieval und Scoring** (`PlanningTargetProfile`). + +Die **LLM-Prompts** (Roadmap, Stufen-Spec, Pfad-QS, Gap-Fill) erhalten diese Dimensionen **nicht** als differenzierte Bewertungs- und Planungslogik — höchstens indirekt über Freitext oder JSON-Kataloglisten in Intent-Prompts. + +**Folge:** Ein Breitensport-Pfad kann mit Leistungsgruppen-Kriterien bewertet werden; Gewaltschutz wird wie Technik-Curriculum behandelt; QS-Hinweise und Rematch-Vorschläge passen fachlich nicht zum gewählten Kontext. + +**Ziel:** Gleiche **Prompt-Basis**, aber **kaskadierte Snippet-Blöcke** pro Katalog-Ausprägung — keine Matrix aus Voll-Prompt-Kopien. + +--- + +## 2. Priorität der Dimensionen (absteigend) + +Verbindliche Reihenfolge bei Konflikten und beim Rollout: + +| Rang | Dimension | DB-Tabelle | Snippet-Rolle | +|------|-----------|------------|----------------| +| **1** | **Primärfokus** | `focus_areas` | Definiert *worüber* geplant und bewertet wird (Technik-Curriculum vs. Gewaltschutz vs. Fitness …). **Dominant.** | +| **2** | **Trainingsstil** | `training_types` | Definiert *wie* trainiert wird (Methodik, Belastungsaufbau, Wettkampf vs. Breitenansatz im Training). | +| **3** | **Zielgruppe** | `target_groups` | Definiert *für wen* (Kinder, Breitensport, Leistungsgruppe) — Tempo, Komplexität, Sicherheit. | +| **4** | **Stilrichtung** | `style_directions` | Vereins-/Stil-Linie (Shotokan, WKF …) — **Nuancen**, kein neuer Planungstyp. | + +**Kaskaden-Regel:** Bei widersprüchlichen Snippet-Aussagen gilt: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung. + +--- + +## 3. Architektur — drei Schichten (Erinnerung) + +| Schicht | Heute | Mit H1 | +|---------|-------|--------| +| **Retrieval** | Katalog-Gewichte in `merge_catalog_context_into_target` | unverändert | +| **Technik-Gates** | `planning_exercise_semantics.py`, nur `topic_type=technique` | unverändert | +| **LLM-Prompts** | kaum Katalog-spezifisch | **`catalog_guidance_block`** pro Aufruf | + +Snippets **ersetzen** keine Technik-Disambiguierung und **duplizieren** keine Retrieval-Gewichte — sie steuern **Didaktik, Bewertungsmaßstäbe und Formulierung** für das Modell. + +--- + +## 4. Snippet-Modell + +### 4.1 Lookup-Schlüssel + +Pro Katalog-Eintrag ein stabiler **`snippet_key`** (nicht nur numerische ID — IDs können sich in Dev/Import unterscheiden): + +``` +focus:{slug} z. B. focus:gewaltschutz +training_type:{slug} z. B. training_type:kumite +target_group:{slug} z. B. target_group:breitensport +style:{slug} z. B. style:shotokan +``` + +**Primär** aus `slug` der DB-Zeile; Fallback normalisierter `name` (ASCII, lower, `_`). + +Mehrfachauswahl im UI: pro Dimension **höchstens ein Snippet** — die Zeile mit `is_primary: true`, sonst erste Zeile, sonst höchste `weight`. + +### 4.2 Snippet-Inhalt (Struktur) + +Jedes Snippet liefert strukturierte Textbausteine (Deutsch, für LLM): + +| Feld | Pflicht | Inhalt | +|------|---------|--------| +| `planning_lens` | ja | 2–4 Sätze: Was ist das Planungsziel in dieser Dimension? | +| `qa_criteria` | ja | Bullet-artige Kriterien für Pfad-QS (was ist „gut“, was ist kein Mangel) | +| `roadmap_hints` | empfohlen | Stufenlogik, typische Phasen, was vermeiden | +| `anti_patterns` | optional | Explizite Fehlbewertungen vermeiden (z. B. „keine Wettkampf-Tiefe verlangen“) | +| `rematch_guard` | optional | Wann **kein** Auto-Rematch sinnvoll (Breitensport: keine Perfektions-Slots erzwingen) | + +Phase **H1:** flache Markdown-Strings im Code-Modul. +Phase **H2 (optional):** Tabelle `planning_catalog_prompt_snippets` oder JSONB an Katalog-Zeilen, Admin-editierbar. + +### 4.3 Platzhalter in `ai_prompts` + +Neue **gemeinsame** Platzhalter (Mustache), in alle betroffenen Prompts einfügen: + +| Platzhalter | Bedeutung | +|-------------|-----------| +| `{{catalog_guidance_block}}` | Gerenderter Gesamttext (alle aktiven Snippets, kaskadiert) | +| `{{catalog_context_json}}` | Kompakte JSON-Zusammenfassung der gewählten IDs/Namen (Audit) | +| `{{#has_catalog_guidance}}` … `{{/has_catalog_guidance}}` | Block nur wenn mindestens ein Snippet aktiv | + +**Optional fein (später):** `{{catalog_focus_snippet}}`, `{{catalog_training_type_snippet}}`, … — Phase H1 nur `_block` + JSON. + +### 4.4 Betroffene Prompt-Slugs (Reihenfolge Einbindung) + +| Priorität | Slug | Migration | Wirkung | +|-----------|------|-----------|---------| +| 1 | `planning_exercise_path_qa` | bestehend | Pfad-QS, `quality_score`, Empfehlungen | +| 2 | `planning_progression_roadmap` | 078 | Major Steps, Didaktik | +| 3 | `planning_progression_stage_spec` | 079 | Stufen-Gates, Erfolgskriterien | +| 4 | `planning_progression_start_target` | 087 | Start/Ziel-Extraktion | +| 5 | `planning_progression_goal_analysis` | 078 | Zielanalyse | +| 6 | Gap-Fill / Übungs-KI | 085+ | `planning_context` ergänzen | + +Intent-Prompts (`planning_exercise_search_intent`, …) **optional** Phase H1.5 — dort bereits Katalog-JSON. + +--- + +## 5. Builder (Backend) + +**Neues Modul:** `backend/planning_catalog_prompt_snippets.py` + +```python +def build_catalog_guidance_for_prompt( + cur, + catalog: Optional[ProgressionPlanningCatalogContext], +) -> Dict[str, str]: + """ + Returns: + catalog_guidance_block: str + catalog_context_json: str + has_catalog_guidance: bool + snippet_keys: list[str] # Metadaten für Logs/Tests + """ +``` + +**Ablauf:** + +1. `catalog` aus Request oder `planning_roadmap.planning_catalog_context` (wie F13). +2. Pro Dimension aktives Item auflösen → `snippet_key` → Text aus Registry. +3. Snippets in **Prioritätsreihenfolge** §2 zu `_block` zusammenfügen (Überschriften: „Primärfokus“, „Trainingsstil“, …). +4. Fehlende Snippets: Dimension **weglassen** (kein Default-Text) — besser kein Snippet als falscher. + +**Einbindung:** Orchestratoren rufen Builder auf und mergen in `variables` vor `load_and_render_ai_prompt`: + +- `planning_exercise_path_qa.py` → `try_llm_qa_progression_path` +- `planning_progression_roadmap.py` (Roadmap-/Stage-Pipeline) +- `planning_exercise_path_builder.py` (catalog an QA/Match durchreichen) + +`ProgressionPathSuggestRequest` trägt `planning_catalog_context` bereits — kein neues API-Feld nötig. + +--- + +## 6. Beispiel-Snippets (Review-Entwurf) + +### 6.1 Primärfokus — Gewaltschutz (`focus:gewaltschutz`) + +**planning_lens:** Planung zielt auf Prävention, Deeskalation, Grenzen und sichere Übungsformen — nicht auf Wettkampf-Perfektion oder Technik-Show. + +**qa_criteria:** Gute Pfade bauen Sicherheit, Kommunikation und Alternativen auf; „Lücken“ sind fehlende Deeskalations- oder Rollenspiel-Stufen, nicht fehlende Kick-Varianten. + +**anti_patterns:** Nicht nach Kumite-Tiefe, Explosivität oder Wettkampf-Belastung bewerten. + +### 6.2 Primärfokus — Technik / Kumite-Beinarbeit (`focus:kumite` o. ä.) + +**planning_lens:** Curriculum für eine Technik oder Kumite-Teilaspekt; aufeinander aufbauende Belastung und Anwendungsnähe sind erwünscht. + +**qa_criteria:** Kohärente Progression Grundlagen → Anwendung → Vertiefung; Übergänge ohne Sprünge; themenfremde Kraft-/Ausdauer-Inseln abwerten. + +### 6.3 Trainingsstil — Breitensport (`training_type:breitensport` o. Name-Match) + +**planning_lens:** Partizipation, Verständlichkeit, Freude am Bewegen; weniger maximale Spezialisierung. + +**qa_criteria:** Hohe OK-Rate bei moderatem Schwierigkeitsanstieg; „Perfektion“-Stufen nur optional, nicht als Pflicht-Lücke. + +**rematch_guard:** Keine leeren Slots erzwingen, nur um eine Leistungs-Perfektionsstufe zu füllen. + +### 6.4 Zielgruppe — Leistungsgruppe (`target_group:leistungsgruppe`) + +**qa_criteria:** Höhere Anspruchskurven, Belastungs- und Kombinationsprogressionen sind relevant; Lücken in Spezialisierung können echte Hinweise sein. + +*(Weitere Snippets iterativ ergänzen — nicht alle Katalog-Zeilen sofort.)* + +--- + +## 7. Rollout-Phasen + +### H1 — Minimal viable (Progressionsgraph) + +- [ ] Modul `planning_catalog_prompt_snippets.py` + Registry (5–8 Snippets: 2–3 Foki, 2 Trainingsstile, 2 Zielgruppen) +- [ ] Einbindung in **`planning_exercise_path_qa`** + **`planning_progression_roadmap`** + **`planning_progression_stage_spec`** +- [ ] Migration Prompt-Templates: Abschnitt `{{#has_catalog_guidance}}…{{/has_catalog_guidance}}` +- [ ] Tests: gleicher Pfad + unterschiedlicher Katalog → unterschiedlicher `catalog_guidance_block`; Snapshot QA-Variablen +- [ ] Dev-Regression: Gewaltschutz, Breitensport, Kinder — **Hinweistexte** müssen zum Kontext passen (nicht Mae-Geri-Kriterien) + +### H1.5 + +- [ ] Rematch/Refine: `rematch_guard` aus Snippets respektieren (weniger Ping-Pong bei Breitensport) +- [ ] Intent-Prompts + Gap-Fill-Kontext + +### H2 — Betrieb + +- [ ] Snippets in DB, Admin-UI oder Markdown-Import +- [ ] Versionierung / Audit wie `ai_prompts` + +### H3 — Phase G (Trainingsplanung) + +- [ ] Gleicher Builder, anderer Orchestrator (Abschnitts-QS, Slot-Suggest) + +--- + +## 8. Tests & Akzeptanz + +| Test | Erwartung | +|------|-----------| +| `test_catalog_prompt_snippets_priority` | Bei Konflikt gewinnt Fokus-Snippet-Text in `_block`-Reihenfolge | +| `test_path_qa_variables_include_guidance` | Mit Gewaltschutz-Kontext enthält gerendeter Prompt „Deeskalation“ o. ä., nicht „Kumite-Perfektion“ | +| `test_path_qa_no_snippet_without_catalog` | Ohne Katalog: `has_catalog_guidance=false`, Prompt unverändert wie heute | +| Manuell | Mae-Geri-Pfad + Breitensport-Kontext: QS-Highlights ohne Leistungs-Belastungs-Forderungen | + +**Nicht Ziel von H1:** Retrieval-Gewichte neu kalibrieren; Technik-Tuples externalisieren (separates Backlog **H** in Roadmap). + +--- + +## 9. Abgrenzung zu anderen Fixes + +| Thema | Dokument / Fix | +|-------|----------------| +| 88 % vs. 65 % falsche Pipeline | Evaluate-only für Pfad-QS; fairer Compare — Code-Stand 2026-05-22 | +| Triviale ID-Tausche im Dialog | `_filter_trivial_slot_diffs` | +| Katalog nur im Retrieval | F13 — bleibt, Snippets ergänzen LLM-Schicht | + +Snippets lösen **fachliche Fehlbewertung** — nicht Pipeline-Inkonsistenz allein. + +--- + +## 10. Changelog + +| Datum | Änderung | +|-------|----------| +| 2026-05-22 | Erstfassung — Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung; H1–H3 Rollout | diff --git a/docs/architecture/PLANNING_KI_ROADMAP.md b/docs/architecture/PLANNING_KI_ROADMAP.md index de73bbf..98b9cfb 100644 --- a/docs/architecture/PLANNING_KI_ROADMAP.md +++ b/docs/architecture/PLANNING_KI_ROADMAP.md @@ -156,6 +156,20 @@ Details: **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16 · Domäne **`DOMAIN_MODEL. --- +## Phase H1 — Katalog-Prompt-Snippets (Spez geplant) + +**Spec:** **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** + +Modulare Textbausteine pro Katalog-Ausprägung in LLM-Prompts (Roadmap, Pfad-QS, Stufen-Spec) — **nicht** neue Retrieval-Welt. + +**Priorität (absteigend):** Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung. + +- [ ] `planning_catalog_prompt_snippets.py` + Registry (5–8 Snippets) +- [ ] Platzhalter `{{catalog_guidance_block}}` in Pfad-QS + Roadmap-Prompts +- [ ] Dev-Regression: Gewaltschutz / Breitensport / Kinder — QS-Hinweise passend zum Kontext + +--- + ## Phase H — Plattform (Backlog) - [ ] Technik-Disambiguierung konfigurierbar (DB statt `_GERI_TECHNIQUES` in Code) @@ -167,7 +181,7 @@ Details: **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16 · Domäne **`DOMAIN_MODEL. | Von | Nach | Hinweis | |-----|------|---------| -| F13 | G0 | Katalog-Kontext in Einheitsplanung | +| F13 | G0, **H1** | Katalog-Kontext in Einheitsplanung; Snippets in LLM-Prompts | | F7, F11 | G1, G2 | Skill-Expectations + QS-Muster | | F4, F9 | G3 | Graph-Roadmap kann Rahmen referenzieren | | G | H | Workflow-Engine lohnt bei verzweigten Planungsflows | diff --git a/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md b/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md index 55d2c9b..647b4e2 100644 --- a/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md +++ b/docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md @@ -10,6 +10,7 @@ `.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md` (Zielarchitektur) · `.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md` (Retrieval/Scoring) · `.claude/docs/technical/SKILL_SCORING_SPEC.md` (Fähigkeiten-Scoring) · +**`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** (H1 — modulare Katalog-Prompts) · `docs/architecture/PLANNING_KI_ROADMAP.md` (Produkt-Roadmap Phase G+) --- @@ -183,6 +184,8 @@ Shinkan unterscheidet **drei Schichten** (kein monolithisches „Vokabular“): **Seit 0.8.233:** `planning_catalog_context` im Request und im Graph-Artefakt (`planning_catalog_context` JSON). Fließt in `PlanningTargetProfile` → Hybrid-Retrieval (`score_exercise_against_target`: „Fokusbereich passend“, …). Zusätzlich additive Text-Signale aus Anfrage + Start/Ziel + Notizen (`planning_exercise_text_signals`). +**Geplant (H1):** dieselben Dimensionen als **kaskadierte Prompt-Snippets** in Roadmap-, Stufen-Spec- und Pfad-QS-Prompts — Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung — siehe **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`**. + **Technik-Gates** (`technique_scope`, Geschwister-Ausschluss) nur bei `topic_type == "technique"` — Fokus-Pfade (Gewaltschutz, Fitness, …) werden nicht wie Mae-Geri-Pfade behandelt. Fallback: fehlt `planning_catalog_context` im Request, wird aus gespeichertem `planning_roadmap` am Graph geladen. @@ -376,6 +379,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js` | **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ | 0.8.231–0.8.232 | | **F13** | **Katalog-Kontext** (`planning_catalog_context`) im Match + Graph-Artefakt | ✅ | **0.8.233** | | **F14** | `ProgressionGraphEditor` Slot-UI + Planungskontext-Dropdowns | ✅ | 0.8.233 | +| **H1** | **Katalog-Prompt-Snippets** — modulare LLM-Anweisungen pro Dimension | 🔲 | Spec **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** | | **G** | Trainingsplanung: eigene Pipeline + Wiederverwendung Bausteine (§16) | 🔲 | — | | **UX** | Wizard/Stepper; PathBuilder-Parität Katalog | 🔲 | — | | **H** | Technik-Disambiguierung konfigurierbar (DB statt Code-Tuples) | 🔲 | Backlog | @@ -385,7 +389,8 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js` ## 12. Offenes Backlog (priorisiert) -1. **Dev-Regression:** Gewaltschutz + Breitensport + Kinder (ohne Mae Geri) — Katalog-Match verifizieren +1. **H1 Katalog-Prompt-Snippets** — modulare LLM-Anweisungen (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** +2. **Dev-Regression:** Gewaltschutz + Breitensport + Kinder (ohne Mae Geri) — Katalog-Match verifizieren 2. **PathBuilder-Parität** — gleiche `planning_catalog_context`-Dropdowns in `ExerciseProgressionPathBuilder` 3. **QS-UI** — positive LLM-Hinweise als „Highlights“, nicht als „Optimierungspotenziale“ 4. **UI-Wizard** — 4 Schritte (Ziel → Roadmap → Match → Lücken); Backend unverändert diff --git a/frontend/src/components/ProgressionFindingsPanel.jsx b/frontend/src/components/ProgressionFindingsPanel.jsx index 24dad48..b4919cb 100644 --- a/frontend/src/components/ProgressionFindingsPanel.jsx +++ b/frontend/src/components/ProgressionFindingsPanel.jsx @@ -26,7 +26,7 @@ function severityStyle(pathQa) { } } -function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) { +function PathQaPipelineDetails({ pathQa, fairQa = null, draft, title, compact = false }) { const { fixHints: optimizationHints } = useMemo( () => splitPathQaHints(pathQa), [pathQa], @@ -34,7 +34,7 @@ function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) { const rematchLog = Array.isArray(pathQa?.rematch_log) ? pathQa.rematch_log : [] const refineLog = Array.isArray(pathQa?.refine_log) ? pathQa.refine_log : [] const qaTiers = Array.isArray(pathQa?.qa_tiers) ? pathQa.qa_tiers : [] - const qualityPct = pathQaQualityPercent(pathQa) + const qualityPct = pathQaQualityPercent(fairQa || pathQa) const hasContent = qaTiers.length > 0 || (pathQa?.rematch_applied && rematchLog.length > 0) @@ -53,10 +53,17 @@ function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) { background: 'color-mix(in srgb, var(--accent) 6%, var(--surface2))', }} > - + {title} - {qualityPct != null ? ` · ${pathQa.overall_ok ? 'OK' : 'Hinweise'} (${qualityPct} %)` : ''} + {qualityPct != null + ? ` · ${(fairQa || pathQa)?.overall_ok ? 'OK' : 'Hinweise'} (${qualityPct} % fair bewertet)` + : ''} + {fairQa && pathQa && pathQaQualityPercent(pathQa) !== qualityPct ? ( +

+ Rematch-Protokoll (Pipeline-Score {pathQaQualityPercent(pathQa) ?? '—'} %) — nur Prozessinfo, nicht Pfad-QS. +

+ ) : null} {qaTiers.length > 0 ? (