- Implemented optional LLM-Rerank functionality in the planning exercise suggestion process, allowing for improved exercise ranking based on user-defined criteria. - Updated the `suggestPlanningExercises` API to accept `planned_exercise_ids` for client-side overrides, enhancing flexibility in exercise selection. - Enhanced the `ExercisePickerModal` to reflect LLM ranking status and support new planning context features. - Incremented application version to 0.8.170 and updated changelog to document the new features and improvements in the planning AI capabilities.
11 KiB
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_ids (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:
{
"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:
{
"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_summaryansuggestExerciseAianhängen (Felderplanning_context_jsono. ä. — 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); Clientplanned_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_idnur 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.