# Planungs-KI: Übungssuche & Kontext für Neu-Anlage **Version:** 0.1 **Datum:** 2026-05-22 **Status:** P0.1 — Hybrid-Retrieval + Phase-1-Profil-Score (`profile_v1`); LLM-Rerank P2 **Bezüge:** `AI_PLANNING_KI_MULTISTAGE_FORECAST.md` · `AI_PROMPT_TARGET_ARCHITECTURE.md` · `SKILL_SCORING_SPEC.md` · `TRAINING_FRAMEWORK_SPEC.md` §3 (Progressionsgraph) --- ## 1. Ziel Trainer in der **Trainingsplanung** sollen Übungen finden oder anlegen können mit natürlichen Anfragen wie: - „Vertiefung zu Übung XY“ - „Nächste sinnvolle Übung im Progressionsgraph Z“ - „Baut auf der bisherigen Planung auf — Reaktionsschnelligkeit mit Partnern“ - **Preset:** „Schlage mir die nächste Übung vor“ **Suche** (Bibliothek) und **Neu mit KI-Assistent** (Anlage) nutzen dasselbe **`PlanningExerciseContextPack`** — unterschiedliches Ergebnis (Treffer vs. Entwurf). --- ## 2. Architektur (Mehrstufig) | Stufe | Name | Technik | P0 | |-------|------|---------|-----| | **S0** | Kontext-Pack | SQL/API, deterministisch | ✅ | | **S1a** | Intent strukturieren | Optional LLM `planning_exercise_search_intent` | Heuristik | | **S1b** | Hybrid-Retrieval | Score: Volltext + Graph + Skills + Plan + **Profil** | ✅ | | **S1b+** | Profil-Vorselektion | `ExerciseMatchProfile` × `PlanningTargetProfile` | ✅ `profile_v1` | | **S1c** | Rerank + Begründung | Optional LLM `planning_exercise_search_rank` | Regelbasierte `reasons[]` | | **S2** | Neu-Anlage | Bestehende `suggestExerciseAi` + Pack als Zusatzkontext | Später | Zwischen jeder Stufe: **nur erlaubte `exercise_id`s** (Governance / Sichtbarkeit). --- ## 3. Intent-Typen | `intent_hint` | Bedeutung | Retrieval-Gewichtung (P0) | |---------------|-----------|---------------------------| | `suggest_next` | Nächste Übung (Default bei leerer/kurzer Query) | Progression + Skill-Overlap + Plan-Kontinuität | | `progression_next` | Explizit Graph-Folge | Progression hoch | | `deepen_exercise` | Vertiefung zu Anker-Übung | Skill-Overlap hoch, ähnlicher Fokus | | `continue_plan_goal` | Auf bisherigen Plan aufbauen | Plan-Kontinuität, Wiederholungsstrafe | | `free_search` | Freitext / Stichwort | Volltext hoch | **S1a (später):** Freitext → JSON `{ intent, skill_hints[], requires_partner, level_hint, … }` validiert per Pydantic. **P0:** `intent_hint` vom Client oder Keyword-Heuristik auf `query`. --- ## 4. PlanningExerciseContextPack (S0) Serverseitig aus Request + DB (tokenbewusst für spätere LLM-Stufen): | Feld | Quelle | UI-Chip | |------|--------|---------| | `unit_id`, Titel, `group_id`, Gruppenname | `training_units` + `training_groups` | Gruppe · Einheit | | `section_order_index`, Abschnittstitel | `training_unit_sections` | Abschnitt | | `planned_exercise_ids[]` | Items der Einheit (Reihenfolge) | „N Übungen im Plan“ | | `anchor_exercise_id`, Titel | Request oder letzte Übung vor Einfügepunkt | Anker | | `anchor_skill_ids[]` | `exercise_skills` | (intern) | | `progression_graph_id` | Request (optional) | Graph | | `progression_successor_ids[]` | `exercise_progression_edges` ab Anker | (intern) | | `group_recent_exercise_ids[]` | Letzte Einheiten derselben Gruppe | Wiederholungsstrafe | | `framework_slot_notes` | Rahmen-Slot falls `framework_slot_id` | (später) | **Berechtigung:** `get_tenant_context` + `_assert_training_unit_permission` wie `GET /training-units/{id}`. --- ## 5. Hybrid-Retrieval (S1b, P0) Kandidaten: sichtbare Übungen (`library_content_visibility_sql`), ohne `archived`, max. ~400 (recent). **Score** (0–1, gewichtet nach Intent): ``` score = w_ft * fulltext_rank + w_prog * progression_hit + w_skill * skill_jaccard(anchor, candidate) + w_plan * plan_affinity + w_profile * profile_match(exercise, target) + w_repeat * (candidate in unit_plan ? -1 : 0) + w_group_repeat * (candidate in group_recent ? -0.5 : 0) ``` **`profile_match`** (0–1): siehe §12–§13 — Katalog-Dimensionen + Skill-Gewichte + Skill-Gap. **`reasons[]`** (regelbasiert, Deutsch): z. B. „Nachfolger im Progressionsgraph“, „Fähigkeiten passen zur Anker-Übung“, „Fokusbereich passend zum Planungsziel“, „Deckt Skill-Lücke im bisherigen Plan“, „Volltext-Treffer“. --- ## 6. API ### `POST /api/planning/exercise-suggest` **Body:** ```json { "unit_id": 123, "section_order_index": 0, "phase_order_index": null, "parallel_stream_order_index": null, "anchor_exercise_id": 456, "progression_graph_id": 7, "query": "Schlage mir die nächste Übung vor", "intent_hint": "suggest_next", "limit": 20, "exercise_kind_any": ["simple"] } ``` **Response:** ```json { "context_summary": { "unit_title": "…", "group_name": "…", "section_title": "Hauptteil", "planned_count": 4, "anchor_title": "Partner-Fangspiel" }, "target_profile_summary": { "sources": ["framework_catalog", "current_unit_plan", "anchor_exercise"], "focus_areas": ["Reaktion & Abwehr"], "top_skills": [{ "skill_id": 12, "name": "Reaktionsgeschwindigkeit", "weight": 1.0 }], "has_skill_gap": true }, "retrieval_phase": "profile_v1", "intent_resolved": "suggest_next", "hits": [ { "id": 99, "title": "…", "summary": "…", "score": 0.78, "reasons": ["Nachfolger im Progressionsgraph", "Fokusbereich passend zum Planungsziel"], "focus_area": "…" } ] } ``` **Modul:** `backend/planning_exercise_suggest.py` · `backend/planning_exercise_profiles.py` · Router `backend/routers/planning_exercise_suggest.py` --- ## 7. Frontend | Ort | Verhalten | |-----|-----------| | `ExercisePickerModal` | Prop `planningContext` → Planungs-API statt reiner `listExercises`; Kontext-Chips; `reasons` unter Treffer | | `TrainingUnitEditPage` | `planningContext` aus Einheit + Picker-Ziel (Anker = letzte Übung im Abschnitt) | | Rahmen / Kombi-Formular | analog, sobald `unit_id` / Slot-Blueprint bekannt | | Übungsliste | weiter Volltext; Schalter „Neu mit KI-Assistent“ ohne Planungs-Pack | **Zweites Suchfeld** im Picker: Query = Volltext + ergänzender Begriff (ODER in P0 als Konkatenation an Backend). --- ## 8. Neu-Anlage (Anbindung, Phase P1) Wenn `hits` leer oder Trainer wählt „Mit KI anlegen“: - Gleiches `context_summary` an `suggestExerciseAi` anhängen (Felder `planning_context_json` o. ä. — noch offen) - Kurzbeschreibung optional leer (freier Vorschlag) oder aus Intent/Skizze --- ## 9. Phasen-Roadmap | Phase | Inhalt | |-------|--------| | **P0** ✅ | Context-Pack, Hybrid-Score, API, Picker in Planung | | **P0.1** ✅ | `ExerciseMatchProfile` / `PlanningTargetProfile`, `profile_v1`, `target_profile_summary` | | **P2** ✅ (optional) | LLM-Rerank `planning_exercise_search_rank`, `include_llm_rank`, `llm_rank_applied` | | **P1** | LLM Intent-JSON; Neu-Anlage mit Pack | | **P3** | Skill-Discovery / Framework-Ziele im Pack | --- ## 10. Changelog - **2026-05-22:** Erstfassung; P0 API + Planungs-Picker. - **2026-05-22:** P0 implementiert (`planning_exercise_suggest.py`, Router, Picker); unsaved Formular-Plan noch nicht an API (nur persistierte Einheit). - **2026-05-22:** P0.1 — `planning_exercise_profiles.py`, Profil-Score in Hybrid-Retrieval, `retrieval_phase: profile_v1`, `target_profile_summary`. - **2026-05-22:** P2 — LLM-Rerank optional (`include_llm_rank`); Client `planned_exercise_ids[]`; Prompt Migration 072. --- ## 11. Bekannte P0-Lücken - **Ungespeicherte Plan-Änderungen:** ✅ Client übergibt `planned_exercise_ids[]` aus Formular (TrainingUnitEditPage). - **Progressionsgraph-ID:** noch nicht aus UI wählbar (`progression_graph_id` nur per API). - **LLM-Intent:** P1 laut Roadmap §9. --- ## 15. LLM-Rerank (P2) **Request:** | Feld | Typ | Default | Bedeutung | |------|-----|---------|-----------| | `planned_exercise_ids` | `int[]` | — | Optional: Reihenfolge aus Formular (überschreibt DB-Plan) | | `include_llm_rank` | `bool` | `false` | Top-32 Hybrid-Kandidaten → OpenRouter Prompt `planning_exercise_search_rank` | **Response:** | Feld | Wert | |------|------| | `retrieval_phase` | `profile_v1` oder `profile_v1+llm_rank` | | `llm_rank_applied` | `true` wenn LLM erfolgreich sortiert hat | | `hits[].llm_rank` | optional: Position nach LLM (1…n) | **Fallback:** Kein API-Key, inaktiver Prompt oder Parse-Fehler → Hybrid-Reihenfolge unverändert, `llm_rank_applied: false`. **Prompt:** Migration **072**, Slug `planning_exercise_search_rank` — Kandidaten als JSON mit Titel, summary, goal (Plaintext), skills; Ausgabe `{ ranked_ids, reasons }`. --- ## 12. ExerciseMatchProfile & PlanningTargetProfile (Phase 1) Ziel: deterministische Vorselektion über **Profil-Dimensionen** statt nur Titel/Jaccard. ### 12.1 ExerciseMatchProfile (pro Übung) | Feld | Quelle | |------|--------| | `focus_area_ids` | `exercise_focus_areas` (Primary = 1.0, sonst 0.85) | | `style_direction_ids` | `exercise_style_directions` | | `training_type_ids` | `exercise_training_types` | | `target_group_ids` | `exercise_target_groups` | | `skill_weights` | `exercise_skills` × Intensitäts-Multiplikator (`skill_scoring._skill_link_multiplier`) | Bulk-Lader: `load_exercise_match_profiles_bulk(cur, exercise_ids)`. ### 12.2 PlanningTargetProfile (Planungsziel) Zusammensetzung aus mehreren Quellen (`sources[]`): | Quelle | Inhalt | |--------|--------| | `framework_catalog` | Fokus/Stil/Trainingsstil/Zielgruppe aus `training_framework_program_*` | | `framework_slot_skill_profile` | Skill-Profil des Slot-Blueprints (`profile_for_occurrences`) | | `framework_overall_skill_profile` | Fallback: alle Blueprint-Einheiten des Rahmens | | `current_unit_plan` | Skill-Profil der bereits eingeplanten Übungen dieser Einheit | | `anchor_exercise` | Katalog + Skills der Anker-Übung (Intent-abhängig) | | `skill_gap_vs_plan` | `target_skills − plan_skills` (normalisiert, Schwelle > 0.08) | Builder: `build_planning_target_profile(cur, unit=…, planned_exercise_ids=…, anchor_exercise_id=…, intent=…)`. Rahmen-Anbindung über `unit.framework_slot_id` oder `origin_framework_slot_id`. --- ## 13. Profil-Score (Formeln) **Gewichtete Überlappung** (Katalog + Skills): ``` overlap(a, b) = Σ min(a[k], b[k]) / Σ max(a[k], b[k]) ``` **Skill-Gap-Abdeckung:** ``` gap_coverage(gap, candidate) = Σ min(gap[k], candidate[k]) / Σ gap[k] ``` **Profil-Score** (intent-gewichtet, Summe Dimensionen = 1.0): ``` profile_score = w_focus * overlap(focus) + w_style * overlap(style) + w_tt * overlap(training_type) + w_tg * overlap(target_group) + w_skill * overlap(skill_weights) + w_gap * gap_coverage(skill_gap) ``` Intent-Gewichte (Auszug): `deepen_exercise` → Skill hoch; `continue_plan_goal` → Gap hoch; `free_search` → Gap + Skill moderat. Scorer: `score_exercise_against_target(exercise_profile, target_profile, intent=…) → (score, reasons[])`. --- ## 14. Hybrid + Profil (P0.1) Im Hybrid-Score kommt **`w_profile * profile_score`** hinzu (Intent-abhängig ~0.15–0.35). Jaccard auf Anker-Skills bleibt parallel (schneller Anker-Fokus). **Response-Felder:** | Feld | Bedeutung | |------|-----------| | `retrieval_phase` | `"profile_v1"` — Phase-1 aktiv, kein LLM-Rerank | | `target_profile_summary` | Lesbare Kurzinfo für UI-Chips (Fokus, Top-Skills, Quellen) | **Phase 2 (P2):** siehe §15 — optional per `include_llm_rank`.