2026-05-22 21:52:18 +02:00
# Planungs-KI: Übungssuche & Kontext für Neu-Anlage
**Version:** 0.1
**Datum:** 2026-05-22
2026-05-22 22:04:34 +02:00
**Status:** P0.1 — Hybrid-Retrieval + Phase-1-Profil-Score (`profile_v1`); LLM-Rerank P2
2026-05-22 21:52:18 +02:00
**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 |
2026-05-22 22:04:34 +02:00
| **S1b** | Hybrid-Retrieval | Score: Volltext + Graph + Skills + Plan + **Profil** | ✅ |
| **S1b+** | Profil-Vorselektion | `ExerciseMatchProfile` × `PlanningTargetProfile` | ✅ `profile_v1` |
2026-05-22 21:52:18 +02:00
| **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
2026-05-22 22:04:34 +02:00
+ w_profile * profile_match(exercise, target)
2026-05-22 21:52:18 +02:00
+ w_repeat * (candidate in unit_plan ? -1 : 0)
+ w_group_repeat * (candidate in group_recent ? -0.5 : 0)
```
2026-05-22 22:04:34 +02:00
**`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“.
2026-05-22 21:52:18 +02:00
---
## 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"
},
2026-05-22 22:04:34 +02:00
"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",
2026-05-22 21:52:18 +02:00
"intent_resolved": "suggest_next",
"hits": [
{
"id": 99,
"title": "…",
"summary": "…",
"score": 0.78,
2026-05-22 22:04:34 +02:00
"reasons": ["Nachfolger im Progressionsgraph", "Fokusbereich passend zum Planungsziel"],
2026-05-22 21:52:18 +02:00
"focus_area": "…"
}
]
}
```
2026-05-22 22:04:34 +02:00
**Modul:** `backend/planning_exercise_suggest.py` · `backend/planning_exercise_profiles.py` · Router `backend/routers/planning_exercise_suggest.py`
2026-05-22 21:52:18 +02:00
---
## 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 |
2026-05-22 22:04:34 +02:00
| **P0.1** ✅ | `ExerciseMatchProfile` / `PlanningTargetProfile` , `profile_v1` , `target_profile_summary` |
2026-05-22 21:52:18 +02:00
| **P1** | LLM Intent-JSON; Neu-Anlage mit Pack |
| **P2** | LLM-Rerank + Kurzbegründung |
| **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 22:04:34 +02:00
- **2026-05-22:** P0.1 — `planning_exercise_profiles.py` , Profil-Score in Hybrid-Retrieval, `retrieval_phase: profile_v1` , `target_profile_summary` .
2026-05-22 21:52:18 +02:00
---
## 11. Bekannte P0-Lücken
- **Ungespeicherte Plan-Änderungen:** API liest DB-Stand der Einheit — offene Formular-Items folgen in P0.1 (Client übergibt `planned_exercise_ids[]` ).
- **Progressionsgraph-ID:** noch nicht aus UI wählbar (`progression_graph_id` nur per API).
- **LLM-Intent / Rerank:** P1/P2 laut Roadmap §9.
2026-05-22 22:04:34 +02:00
---
## 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):** Top 20– 40 Kandidaten nach Hybrid+Profil → LLM `planning_exercise_search_rank` mit **Titel + summary + goal** ; nur IDs aus Kandidatenliste.