Implement Planning Context Integration for Exercise AI Suggestions
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added `planning_context` to the `suggestExerciseAi` endpoint, enabling structured planning context for new exercise creation. - Updated relevant components and backend logic to handle the new planning context, enhancing the AI's exercise suggestion capabilities. - Incremented application version to 0.8.208 to reflect these changes.
This commit is contained in:
parent
f074a8bef0
commit
779e2477ba
|
|
@ -172,7 +172,7 @@ score = w_ft * fulltext_rank
|
|||
|
||||
Wenn `hits` leer oder Trainer wählt „Mit KI anlegen“:
|
||||
|
||||
- Gleiches `context_summary` an `suggestExerciseAi` anhängen (Felder `planning_context_json` o. ä. — noch offen)
|
||||
- `planning_context` im Request-Body → `planning_context_json` in Übungs-Prompts (Migration **085**); Pfad-Builder + Picker ✅ **0.8.208**
|
||||
- Kurzbeschreibung optional leer (freier Vorschlag) oder aus Intent/Skizze
|
||||
|
||||
---
|
||||
|
|
@ -193,7 +193,7 @@ Wenn `hits` leer oder Trainer wählt „Mit KI anlegen“:
|
|||
| **C3** | Graph-Builder (Ziel → Pfad → speichern) | ✅ **0.8.185** |
|
||||
| **E** | Semantik-Schicht + Pfad-QA (Lücken/Brücken/LLM-QS) | ✅ **0.8.186** |
|
||||
| **E2** | Pfad-Neuordnung + KI-Lückenfüller | ✅ **0.8.187** |
|
||||
| **D** | Neu-Anlage: Pack an `suggestExerciseAi` | 🔲 |
|
||||
| **D** | Neu-Anlage: `planning_context` an `suggestExerciseAi` (Migration **085**) | ✅ **0.8.208** |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Keine Imports aus exercise_ai — vermeidet Zirkelimporte mit ai_prompt_job / ex
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Sequence, Tuple
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
|
@ -31,6 +31,7 @@ class ExerciseFormAiPromptContext(BaseModel):
|
|||
trainer_notes: Optional[str] = None
|
||||
focus_hint: Optional[str] = None
|
||||
focus_areas_context: Optional[List[ExerciseFormAiFocusRow]] = None
|
||||
planning_context: Optional[Dict[str, Any]] = None
|
||||
|
||||
def focus_area_tuples(self) -> Optional[List[Tuple[int, bool]]]:
|
||||
if not self.focus_areas_context:
|
||||
|
|
@ -57,6 +58,7 @@ class ExerciseFormAiPromptContext(BaseModel):
|
|||
trainer_notes: Optional[str] = None,
|
||||
focus_area_hint: Optional[str] = None,
|
||||
focus_areas_context: Optional[Sequence[ExerciseFormAiFocusRow]] = None,
|
||||
planning_context: Optional[Dict[str, Any]] = None,
|
||||
) -> ExerciseFormAiPromptContext:
|
||||
"""Mappt Felder aus POST /exercises/ai/suggest (focus_area_hint → focus_hint)."""
|
||||
hint = (focus_area_hint or "").strip() or None
|
||||
|
|
@ -68,6 +70,7 @@ class ExerciseFormAiPromptContext(BaseModel):
|
|||
trainer_notes=trainer_notes,
|
||||
focus_hint=hint,
|
||||
focus_areas_context=list(focus_areas_context) if focus_areas_context else None,
|
||||
planning_context=dict(planning_context) if planning_context else None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ def resolve_exercise_form_variables(cur, slug: str, ctx: ExerciseFormAiPromptCon
|
|||
focus_areas_context=ctx.focus_area_tuples(),
|
||||
preparation=ctx.preparation,
|
||||
trainer_notes=ctx.trainer_notes,
|
||||
planning_context=ctx.planning_context,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -650,10 +650,13 @@ def build_exercise_placeholder_variables(
|
|||
focus_areas_context: Optional[Sequence[Tuple[int, bool]]],
|
||||
preparation: Optional[str] = None,
|
||||
trainer_notes: Optional[str] = None,
|
||||
planning_context: Optional[Mapping[str, Any]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Baut die Variable-Map fuer {{platzhalter}} passend zur Slug fuer Uebungs-KI.
|
||||
"""
|
||||
from planning_exercise_form_context import planning_context_prompt_variables
|
||||
|
||||
s = (slug or "").strip().lower()
|
||||
if s == "pipeline":
|
||||
return {}
|
||||
|
|
@ -671,8 +674,19 @@ def build_exercise_placeholder_variables(
|
|||
"exercise_preparation": p_plain or "-",
|
||||
"exercise_trainer_notes": n_plain or "-",
|
||||
}
|
||||
ctx.update(planning_context_prompt_variables(planning_context))
|
||||
if s == "exercise_summary":
|
||||
return {k: ctx[k] for k in ("exercise_title", "exercise_focus_area", "exercise_goal", "exercise_execution")}
|
||||
return {
|
||||
k: ctx[k]
|
||||
for k in (
|
||||
"exercise_title",
|
||||
"exercise_focus_area",
|
||||
"exercise_goal",
|
||||
"exercise_execution",
|
||||
"planning_context_json",
|
||||
"has_planning_context",
|
||||
)
|
||||
}
|
||||
if s == "exercise_instruction_rewrite":
|
||||
return ctx
|
||||
if s == "exercise_skill_suggestions":
|
||||
|
|
@ -893,6 +907,7 @@ def run_exercise_ai_suggestion(
|
|||
execution=execution,
|
||||
focus_area_hint=focus_area_hint,
|
||||
focus_areas_context=focus_areas_context,
|
||||
planning_context=form_ctx.planning_context,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
|
@ -938,6 +953,7 @@ def run_exercise_ai_suggestion(
|
|||
execution=execution,
|
||||
focus_area_hint=focus_area_hint,
|
||||
focus_areas_context=focus_areas_context,
|
||||
planning_context=form_ctx.planning_context,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
|
@ -1015,6 +1031,7 @@ def run_exercise_ai_suggestion(
|
|||
trainer_notes=trainer_notes,
|
||||
focus_area_hint=focus_area_hint,
|
||||
focus_areas_context=focus_areas_context,
|
||||
planning_context=form_ctx.planning_context,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
|
|
|||
181
backend/migrations/085_ai_prompt_exercise_planning_context.sql
Normal file
181
backend/migrations/085_ai_prompt_exercise_planning_context.sql
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
-- Migration 085: Planungskontext in Übungs-KI-Prompts (Phase D)
|
||||
-- Platzhalter: {{planning_context_json}}, {{#has_planning_context}} … {{/has_planning_context}}
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET template = $s$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Erstelle eine kurze Kurzbeschreibung fuer Listen und Trainingsplaene.
|
||||
|
||||
Anforderungen:
|
||||
- Hochstens etwa 200 Zeichen (bei Bedarf gekuerzt fuer Mobile)
|
||||
- Kern: Welche Trainingsqualitaeten? Wie fuehrt man die Uebung kurz aus?
|
||||
- Sachlich, auf Deutsch
|
||||
|
||||
Uebung: {{exercise_title}}
|
||||
Fokuskontext: {{exercise_focus_area}}
|
||||
Ziel (Fliesstext, kann HTML sein): {{exercise_goal}}
|
||||
Durchfuehrung (Fliesstext, kann HTML sein): {{exercise_execution}}
|
||||
{{#has_planning_context}}
|
||||
Planungskontext (JSON — Einordnung in Trainingsplan oder Progressionspfad):
|
||||
{{planning_context_json}}
|
||||
{{/has_planning_context}}
|
||||
|
||||
Antworte NUR mit der Kurzbeschreibung als einfachen Text (keine Markdown-Codeblocks, keine Anfuehrungszeichen um den ganzen Text).$s$,
|
||||
default_template = $s$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Erstelle eine kurze Kurzbeschreibung fuer Listen und Trainingsplaene.
|
||||
|
||||
Anforderungen:
|
||||
- Hochstens etwa 200 Zeichen (bei Bedarf gekuerzt fuer Mobile)
|
||||
- Kern: Welche Trainingsqualitaeten? Wie fuehrt man die Uebung kurz aus?
|
||||
- Sachlich, auf Deutsch
|
||||
|
||||
Uebung: {{exercise_title}}
|
||||
Fokuskontext: {{exercise_focus_area}}
|
||||
Ziel (Fliesstext, kann HTML sein): {{exercise_goal}}
|
||||
Durchfuehrung (Fliesstext, kann HTML sein): {{exercise_execution}}
|
||||
{{#has_planning_context}}
|
||||
Planungskontext (JSON — Einordnung in Trainingsplan oder Progressionspfad):
|
||||
{{planning_context_json}}
|
||||
{{/has_planning_context}}
|
||||
|
||||
Antworte NUR mit der Kurzbeschreibung als einfachen Text (keine Markdown-Codeblocks, keine Anfuehrungszeichen um den ganzen Text).$s$
|
||||
WHERE slug = 'exercise_summary';
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET template = $j$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Ordne diese Uebung dem globalen Skill-Katalog zu.
|
||||
|
||||
Daten zur Uebung:
|
||||
Titel: {{exercise_title}}
|
||||
Fokuskontext (optional): {{exercise_focus_area}}
|
||||
Ziel (gekuerzt_plain): {{exercise_goal}}
|
||||
Durchfuehrung (gekuerzt_plain): {{exercise_execution}}
|
||||
{{#has_planning_context}}
|
||||
Planungskontext (JSON):
|
||||
{{planning_context_json}}
|
||||
{{/has_planning_context}}
|
||||
|
||||
Verfuegbare Faehigkeiten (Auswahl NUR ueber diese IDs — keine anderen IDs verwenden):
|
||||
{{skills_catalog}}
|
||||
|
||||
Waehle hoechstens 5 passende Skills. Für jede Faehigkeit:
|
||||
- skill_id: ganze Zahl aus der Liste
|
||||
- required_level: eines von basis, grundlagen, aufbau, fortgeschritten, optimierung
|
||||
- target_level: derselbe Wertvorrat
|
||||
- intensity: eines von niedrig, mittel, hoch
|
||||
- is_primary (optional): true fuer die Hauptfaehigkeit der Uebung, sondern false/weglassen
|
||||
|
||||
Antworte NUR mit einem JSON-Array ohne Erklaertext, keine Markdown-Fences.
|
||||
|
||||
Beispielformat:
|
||||
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
|
||||
|
||||
Wenn nichts gut passt, antworte mit [].$j$,
|
||||
default_template = $j$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Ordne diese Uebung dem globalen Skill-Katalog zu.
|
||||
|
||||
Daten zur Uebung:
|
||||
Titel: {{exercise_title}}
|
||||
Fokuskontext (optional): {{exercise_focus_area}}
|
||||
Ziel (gekuerzt_plain): {{exercise_goal}}
|
||||
Durchfuehrung (gekuerzt_plain): {{exercise_execution}}
|
||||
{{#has_planning_context}}
|
||||
Planungskontext (JSON):
|
||||
{{planning_context_json}}
|
||||
{{/has_planning_context}}
|
||||
|
||||
Verfuegbare Faehigkeiten (Auswahl NUR ueber diese IDs — keine anderen IDs verwenden):
|
||||
{{skills_catalog}}
|
||||
|
||||
Waehle hoechstens 5 passende Skills. Für jede Faehigkeit:
|
||||
- skill_id: ganze Zahl aus der Liste
|
||||
- required_level: eines von basis, grundlagen, aufbau, fortgeschritten, optimierung
|
||||
- target_level: derselbe Wertvorrat
|
||||
- intensity: eines von niedrig, mittel, hoch
|
||||
- is_primary (optional): true fuer die Hauptfaehigkeit der Uebung, sondern false/weglassen
|
||||
|
||||
Antworte NUR mit einem JSON-Array ohne Erklaertext, keine Markdown-Fences.
|
||||
|
||||
Beispielformat:
|
||||
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
|
||||
|
||||
Wenn nichts gut passt, antworte mit [].$j$
|
||||
WHERE slug = 'exercise_skill_suggestions';
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET template = $t$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Ueberarbeite die Anleitung dieser Uebung: verbessere Formulierung, ergaenze fehlende Kernpunkte, kuerze ueberfluessige Passagen.
|
||||
Wichtig: Texte sollen praezise und nachvollziehbar bleiben — keine Fuellsaetze, keine Wiederholungen, kein Marketing.
|
||||
|
||||
Stil:
|
||||
- Deutsch, sachlich, direkt an Trainer gerichtet (Durchfuehrung: Imperativ oder klare Schritte)
|
||||
- Ziel: 1–3 kurze Absaetze (Kern des Trainingsziels)
|
||||
- Durchfuehrung: klare Schritte (nummerierte Liste oder kurze Absaetze)
|
||||
- Vorbereitung/Aufbau: nur wenn noetig (Raum, Material, Aufbau) — sonst leerer String
|
||||
- Trainer-Hinweise: Sicherheit, typische Fehler, Coaching-Tipps — knapp, Stichpunkte oder kurze Absaetze
|
||||
|
||||
Format (HTML fuer Rich-Text-Editor):
|
||||
- Erlaubt: <p>, <ul>, <ol>, <li>, <strong>, <em>, <br>
|
||||
- Keine Ueberschriften (h1–h6), keine Tabellen, kein Markdown, keine Code-Fences
|
||||
- Medienverweise {{exerciseMedia:ID}} aus den Eingabetexten UNVERAENDERT an passender Stelle uebernehmen
|
||||
|
||||
Eingabe:
|
||||
Titel: {{exercise_title}}
|
||||
Fokuskontext: {{exercise_focus_area}}
|
||||
|
||||
Ziel (Plaintext, Ausgang): {{exercise_goal}}
|
||||
Durchfuehrung (Plaintext, Ausgang): {{exercise_execution}}
|
||||
Vorbereitung/Aufbau (Plaintext, Ausgang): {{exercise_preparation}}
|
||||
Trainer-Hinweise (Plaintext, Ausgang): {{exercise_trainer_notes}}
|
||||
{{#has_planning_context}}
|
||||
Planungskontext (JSON):
|
||||
{{planning_context_json}}
|
||||
{{/has_planning_context}}
|
||||
|
||||
Antworte NUR mit einem JSON-Objekt (kein Text davor/danach):
|
||||
{
|
||||
"goal": "<p>…</p>",
|
||||
"execution": "<ol><li>…</li></ol>",
|
||||
"preparation": "<p>…</p> oder \"\"",
|
||||
"trainer_notes": "<ul><li>…</li></ul> oder \"\""
|
||||
}
|
||||
|
||||
Leere Felder als leerer String "" wenn nichts Sinnvolles ergibt.$t$,
|
||||
default_template = $t$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Ueberarbeite die Anleitung dieser Uebung: verbessere Formulierung, ergaenze fehlende Kernpunkte, kuerze ueberfluessige Passagen.
|
||||
Wichtig: Texte sollen praezise und nachvollziehbar bleiben — keine Fuellsaetze, keine Wiederholungen, kein Marketing.
|
||||
|
||||
Stil:
|
||||
- Deutsch, sachlich, direkt an Trainer gerichtet (Durchfuehrung: Imperativ oder klare Schritte)
|
||||
- Ziel: 1–3 kurze Absaetze (Kern des Trainingsziels)
|
||||
- Durchfuehrung: klare Schritte (nummerierte Liste oder kurze Absaetze)
|
||||
- Vorbereitung/Aufbau: nur wenn noetig (Raum, Material, Aufbau) — sonst leerer String
|
||||
- Trainer-Hinweise: Sicherheit, typische Fehler, Coaching-Tipps — knapp, Stichpunkte oder kurze Absaetze
|
||||
|
||||
Format (HTML fuer Rich-Text-Editor):
|
||||
- Erlaubt: <p>, <ul>, <ol>, <li>, <strong>, <em>, <br>
|
||||
- Keine Ueberschriften (h1–h6), keine Tabellen, kein Markdown, keine Code-Fences
|
||||
- Medienverweise {{exerciseMedia:ID}} aus den Eingabetexten UNVERAENDERT an passender Stelle uebernehmen
|
||||
|
||||
Eingabe:
|
||||
Titel: {{exercise_title}}
|
||||
Fokuskontext: {{exercise_focus_area}}
|
||||
|
||||
Ziel (Plaintext, Ausgang): {{exercise_goal}}
|
||||
Durchfuehrung (Plaintext, Ausgang): {{exercise_execution}}
|
||||
Vorbereitung/Aufbau (Plaintext, Ausgang): {{exercise_preparation}}
|
||||
Trainer-Hinweise (Plaintext, Ausgang): {{exercise_trainer_notes}}
|
||||
{{#has_planning_context}}
|
||||
Planungskontext (JSON):
|
||||
{{planning_context_json}}
|
||||
{{/has_planning_context}}
|
||||
|
||||
Antworte NUR mit einem JSON-Objekt (kein Text davor/danach):
|
||||
{
|
||||
"goal": "<p>…</p>",
|
||||
"execution": "<ol><li>…</li></ol>",
|
||||
"preparation": "<p>…</p> oder \"\"",
|
||||
"trainer_notes": "<ul><li>…</li></ul> oder \"\""
|
||||
}
|
||||
|
||||
Leere Felder als leerer String "" wenn nichts Sinnvolles ergibt.$t$
|
||||
WHERE slug = 'exercise_instruction_rewrite';
|
||||
138
backend/planning_exercise_form_context.py
Normal file
138
backend/planning_exercise_form_context.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
Planungs-KI Phase D: strukturierter Planungskontext für POST /exercises/ai/suggest.
|
||||
|
||||
Wird als ``planning_context_json`` in Übungs-Prompts (summary, skills, instructions) injiziert.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
|
||||
_MAX_JSON_CHARS = 6000
|
||||
_MAX_STRING = 800
|
||||
|
||||
|
||||
def compact_planning_context_json(obj: Any) -> str:
|
||||
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def _trim_str(val: Any, *, limit: int = _MAX_STRING) -> Optional[str]:
|
||||
if val is None:
|
||||
return None
|
||||
s = str(val).strip()
|
||||
if not s:
|
||||
return None
|
||||
if len(s) > limit:
|
||||
return s[: limit - 1] + "…"
|
||||
return s
|
||||
|
||||
|
||||
def sanitize_planning_context_for_ai(ctx: Optional[Mapping[str, Any]]) -> Dict[str, Any]:
|
||||
"""Reduziert Client-Payload auf prompt-taugliche, begrenzte Felder."""
|
||||
if not ctx:
|
||||
return {}
|
||||
out: Dict[str, Any] = {}
|
||||
for key, val in dict(ctx).items():
|
||||
if val is None:
|
||||
continue
|
||||
k = str(key).strip()
|
||||
if not k:
|
||||
continue
|
||||
if isinstance(val, str):
|
||||
t = _trim_str(val)
|
||||
if t:
|
||||
out[k] = t
|
||||
elif isinstance(val, (int, float, bool)):
|
||||
out[k] = val
|
||||
elif isinstance(val, list):
|
||||
items = []
|
||||
for item in val[:12]:
|
||||
if isinstance(item, str):
|
||||
t = _trim_str(item, limit=200)
|
||||
if t:
|
||||
items.append(t)
|
||||
elif isinstance(item, (int, float, bool)):
|
||||
items.append(item)
|
||||
elif isinstance(item, dict):
|
||||
sub = sanitize_planning_context_for_ai(item)
|
||||
if sub:
|
||||
items.append(sub)
|
||||
if items:
|
||||
out[k] = items
|
||||
elif isinstance(val, dict):
|
||||
sub = sanitize_planning_context_for_ai(val)
|
||||
if sub:
|
||||
out[k] = sub
|
||||
raw = compact_planning_context_json(out)
|
||||
if len(raw) > _MAX_JSON_CHARS:
|
||||
out["truncated"] = True
|
||||
out.pop("path_steps_preview", None)
|
||||
raw = compact_planning_context_json(out)
|
||||
if len(raw) > _MAX_JSON_CHARS:
|
||||
return {"source": out.get("source"), "truncated": True, "goal_query": out.get("goal_query")}
|
||||
return out
|
||||
|
||||
|
||||
def planning_context_prompt_variables(
|
||||
planning_context: Optional[Mapping[str, Any]],
|
||||
) -> Dict[str, str]:
|
||||
cleaned = sanitize_planning_context_for_ai(planning_context)
|
||||
if not cleaned:
|
||||
return {"planning_context_json": "-", "has_planning_context": ""}
|
||||
return {
|
||||
"planning_context_json": compact_planning_context_json(cleaned),
|
||||
"has_planning_context": "true",
|
||||
}
|
||||
|
||||
|
||||
def build_progression_path_gap_planning_context(
|
||||
*,
|
||||
goal_query: str,
|
||||
primary_topic: Optional[str] = None,
|
||||
progression_graph_id: Optional[int] = None,
|
||||
offer: Optional[Mapping[str, Any]] = None,
|
||||
neighbor_before: Optional[Mapping[str, Any]] = None,
|
||||
neighbor_after: Optional[Mapping[str, Any]] = None,
|
||||
path_step_count: int = 0,
|
||||
major_step_count: Optional[int] = None,
|
||||
roadmap_phase: Optional[str] = None,
|
||||
roadmap_learning_goal: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
|
||||
offer = offer or {}
|
||||
gap = offer.get("gap") if isinstance(offer.get("gap"), dict) else {}
|
||||
major_idx = offer.get("roadmap_major_step_index")
|
||||
if major_idx is None and isinstance(gap, dict):
|
||||
major_idx = gap.get("roadmap_major_step_index")
|
||||
|
||||
ctx: Dict[str, Any] = {
|
||||
"source": "progression_path_gap_fill",
|
||||
"goal_query": _trim_str(goal_query, limit=2000),
|
||||
"primary_topic": _trim_str(primary_topic),
|
||||
"progression_graph_id": progression_graph_id,
|
||||
"gap_source": _trim_str(offer.get("source")),
|
||||
"gap_phase": _trim_str(offer.get("phase") or gap.get("expected_phase")),
|
||||
"roadmap_major_step_index": major_idx,
|
||||
"roadmap_phase": _trim_str(roadmap_phase or offer.get("phase")),
|
||||
"roadmap_learning_goal": _trim_str(
|
||||
roadmap_learning_goal or offer.get("title_hint") or gap.get("learning_goal"),
|
||||
limit=1200,
|
||||
),
|
||||
"neighbor_before_title": _trim_str(
|
||||
(neighbor_before or {}).get("title") or offer.get("from_title")
|
||||
),
|
||||
"neighbor_after_title": _trim_str(
|
||||
(neighbor_after or {}).get("title") or offer.get("to_title")
|
||||
),
|
||||
"path_step_count": path_step_count,
|
||||
"major_step_count": major_step_count,
|
||||
}
|
||||
return sanitize_planning_context_for_ai(ctx)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"build_progression_path_gap_planning_context",
|
||||
"compact_planning_context_json",
|
||||
"planning_context_prompt_variables",
|
||||
"sanitize_planning_context_for_ai",
|
||||
]
|
||||
|
|
@ -385,6 +385,10 @@ class ExerciseAiSuggestBody(BaseModel):
|
|||
include_summary: bool = True
|
||||
include_skills: bool = True
|
||||
include_instructions: bool = False
|
||||
planning_context: Optional[dict] = Field(
|
||||
default=None,
|
||||
description="Optionaler Planungskontext (Einheit, Pfad, Roadmap-Stufe) für KI-Neuanlage",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_include_any(self):
|
||||
|
|
@ -403,6 +407,7 @@ class ExerciseAiSuggestBody(BaseModel):
|
|||
trainer_notes=self.trainer_notes,
|
||||
focus_area_hint=self.focus_area_hint,
|
||||
focus_areas_context=self.focus_areas_context,
|
||||
planning_context=self.planning_context,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
46
backend/tests/test_planning_exercise_form_context.py
Normal file
46
backend/tests/test_planning_exercise_form_context.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""Tests Planungs-KI Phase D — planning_context für suggestExerciseAi."""
|
||||
from planning_exercise_form_context import (
|
||||
build_progression_path_gap_planning_context,
|
||||
planning_context_prompt_variables,
|
||||
sanitize_planning_context_for_ai,
|
||||
)
|
||||
|
||||
|
||||
def test_planning_context_prompt_variables_empty():
|
||||
vars_ = planning_context_prompt_variables(None)
|
||||
assert vars_["planning_context_json"] == "-"
|
||||
assert vars_["has_planning_context"] == ""
|
||||
|
||||
|
||||
def test_planning_context_prompt_variables_with_data():
|
||||
vars_ = planning_context_prompt_variables({"source": "test", "goal_query": "Mae Geri"})
|
||||
assert vars_["has_planning_context"] == "true"
|
||||
assert "Mae Geri" in vars_["planning_context_json"]
|
||||
|
||||
|
||||
def test_build_progression_path_gap_context():
|
||||
ctx = build_progression_path_gap_planning_context(
|
||||
goal_query="Mae Geri Perfektion",
|
||||
primary_topic="Mae Geri",
|
||||
progression_graph_id=3,
|
||||
offer={
|
||||
"source": "roadmap_unfilled",
|
||||
"phase": "vertiefung",
|
||||
"title_hint": "Koordination Mae Geri",
|
||||
"roadmap_major_step_index": 2,
|
||||
"from_title": "Schritt A",
|
||||
"to_title": "Schritt B",
|
||||
},
|
||||
neighbor_before={"title": "Schritt A"},
|
||||
neighbor_after={"title": "Schritt B"},
|
||||
path_step_count=4,
|
||||
major_step_count=5,
|
||||
)
|
||||
assert ctx["source"] == "progression_path_gap_fill"
|
||||
assert ctx["roadmap_major_step_index"] == 2
|
||||
assert ctx["neighbor_before_title"] == "Schritt A"
|
||||
|
||||
|
||||
def test_sanitize_truncates_long_strings():
|
||||
ctx = sanitize_planning_context_for_ai({"goal_query": "x" * 900})
|
||||
assert len(ctx["goal_query"]) <= 800
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.207"
|
||||
APP_VERSION = "0.8.208"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260606086"
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
|||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||
"planning_exercise_suggest": "0.19.0", # F4: roadmap_only + roadmap_override, editierbare Roadmap-UI
|
||||
"planning_exercise_suggest": "0.20.0", # Phase D: planning_context an suggestExerciseAi
|
||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||
|
|
@ -53,6 +53,15 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.208",
|
||||
"date": "2026-06-07",
|
||||
"changes": [
|
||||
"Phase D: planning_context an POST /exercises/ai/suggest — Prompts Migration 085.",
|
||||
"Pfad-Builder + Planungs-Picker senden strukturierten Planungskontext bei KI-Neuanlage.",
|
||||
"Modul planning_exercise_form_context.py; Platzhalter planning_context_json in Übungs-Prompts.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.207",
|
||||
"date": "2026-06-07",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||
|
||||
**Stand:** 2026-06-07
|
||||
**App-Version / DB-Schema:** App **`0.8.207`** (Planungs-KI Phase F4); DB **`20260606086`** — maßgeblich **`backend/version.py`**.
|
||||
**App-Version / DB-Schema:** App **`0.8.208`** (Planungs-KI Phase D); DB **`20260606086`** — maßgeblich **`backend/version.py`**.
|
||||
|
||||
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
|||
| **F0–F2** | Roadmap-Pipeline + LLM-Prompts (078/079) | ✅ **0.8.205** |
|
||||
| **F3** | `roadmap_first` — Retrieval pro `stage_spec` | ✅ **0.8.206** |
|
||||
| **F4** | Roadmap-Review UI + `roadmap_override` | ✅ **0.8.207** |
|
||||
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | 🔲 |
|
||||
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | ✅ **0.8.208** |
|
||||
|
||||
**Architektur-Entscheidung (2026-06-07):** Progressionsgraph = **Roadmap-first** (Ziel → Major Steps → Übungs-Match). **Keine Gruppenanalyse** im Graphen. Mitai Workflow-Engine **später** — jetzt `planning_progression_roadmap.py`. Spec: **`.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md`** · Roadmap: **`docs/architecture/PLANNING_KI_ROADMAP.md`**
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Diese Roadmap ergänzt die **Architektur-Refaktor-Roadmap** (`UMSETZUNGSPLAN_ROA
|
|||
| E–E3 | Progressionsgraph | Semantik, QA, Lücken-Angebote | ✅ |
|
||||
| **F0–F1** | Progressionsgraph | Roadmap-Pipeline Scaffold + API-Preview | 🔄 **0.8.204** |
|
||||
| **F2–F4** | Progressionsgraph | LLM Roadmap, roadmap-first Retrieval, UI Review | 🔲 |
|
||||
| D | Übungs-Neuanlage | `planning_context` an `suggestExerciseAi` | 🔲 |
|
||||
| D | Übungs-Neuanlage | `planning_context` an `suggestExerciseAi` | ✅ **0.8.208** |
|
||||
| G | Trainingsplanung | Kontext-Pack Gruppe/Historie, S0–S4 | 🔲 |
|
||||
| H | Plattform | Mitai-Workflow-Engine (optional) | 🔲 Backlog |
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
aiPreviewToQuickCreateDraft,
|
||||
} from '../utils/exerciseAiQuickCreate'
|
||||
import { resolveExercisePickVariantId } from '../utils/exercisePlanningPick'
|
||||
import { buildPickerPlanningContextForAi } from '../utils/planningContextForExerciseAi'
|
||||
|
||||
const PAGE_SIZE = 100
|
||||
/** Backend POST /api/planning/exercise-suggest erlaubt max. 50 */
|
||||
|
|
@ -707,6 +708,12 @@ export default function ExercisePickerModal({
|
|||
|
||||
setQuickAiError('')
|
||||
setQuickCreateDraft(null)
|
||||
const planningContextPayload = buildPickerPlanningContextForAi({
|
||||
planningContextSummary,
|
||||
planningContext,
|
||||
searchQuery: planningSubmittedQuery || searchInput || aiSearchInput,
|
||||
})
|
||||
|
||||
setQuickSaving(true)
|
||||
try {
|
||||
const aiRes = await api.suggestExerciseAi({
|
||||
|
|
@ -717,6 +724,7 @@ export default function ExercisePickerModal({
|
|||
trainer_notes: '',
|
||||
focus_area_hint: focusHint || undefined,
|
||||
focus_areas_context: [{ focus_area_id: focusId, is_primary: true }],
|
||||
planning_context: planningContextPayload || undefined,
|
||||
include_summary: true,
|
||||
include_skills: true,
|
||||
include_instructions: true,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
buildQuickCreateAiPreview,
|
||||
buildQuickCreateExercisePayloadFromDraft,
|
||||
} from '../utils/exerciseAiQuickCreate'
|
||||
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi'
|
||||
|
||||
function emptyPathStep() {
|
||||
return { exerciseId: null, exerciseTitle: '', variantId: null, variants: [], reasons: [] }
|
||||
|
|
@ -354,6 +355,15 @@ export default function ExerciseProgressionPathBuilder({
|
|||
setQuickCreateDraft(null)
|
||||
setQuickSaving(true)
|
||||
setGeneratingOfferId(offer?.offer_id || null)
|
||||
const planningContext = buildPathGapPlanningContextForAi({
|
||||
goalQuery,
|
||||
semanticBrief,
|
||||
offer,
|
||||
graphId,
|
||||
pathSteps,
|
||||
editableMajorSteps,
|
||||
progressionRoadmap,
|
||||
})
|
||||
try {
|
||||
const aiRes = await api.suggestExerciseAi({
|
||||
title,
|
||||
|
|
@ -363,6 +373,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
trainer_notes: '',
|
||||
focus_area_hint: focusHint || undefined,
|
||||
focus_areas_context: [{ focus_area_id: focusId, is_primary: true }],
|
||||
planning_context: planningContext || undefined,
|
||||
include_summary: true,
|
||||
include_skills: true,
|
||||
include_instructions: true,
|
||||
|
|
|
|||
75
frontend/src/utils/planningContextForExerciseAi.js
Normal file
75
frontend/src/utils/planningContextForExerciseAi.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Planungs-KI Phase D: strukturierter Kontext für suggestExerciseAi.
|
||||
*/
|
||||
|
||||
export function buildPickerPlanningContextForAi({
|
||||
planningContextSummary = null,
|
||||
planningContext = null,
|
||||
searchQuery = '',
|
||||
} = {}) {
|
||||
if (!planningContextSummary && !planningContext) return null
|
||||
const ctx = {
|
||||
source: 'planning_picker',
|
||||
search_query: (searchQuery || '').trim() || null,
|
||||
unit_title: planningContextSummary?.unit_title || null,
|
||||
group_name: planningContextSummary?.group_name || null,
|
||||
section_title:
|
||||
planningContextSummary?.section_title || planningContext?.sectionTitle || null,
|
||||
section_guidance_notes: planningContext?.sectionGuidanceNotes || null,
|
||||
section_exercise_count: planningContextSummary?.section_exercise_count ?? null,
|
||||
last_section_exercise_title:
|
||||
planningContextSummary?.last_section_exercise_title ||
|
||||
planningContext?.lastExerciseTitle ||
|
||||
null,
|
||||
anchor_title: planningContextSummary?.anchor_title || null,
|
||||
progression_graph_name: planningContextSummary?.progression_graph_name || null,
|
||||
planned_count: planningContextSummary?.planned_count ?? null,
|
||||
intent_resolved:
|
||||
planningContextSummary?.intent_resolved || planningContext?.intentHint || null,
|
||||
}
|
||||
return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== ''))
|
||||
}
|
||||
|
||||
export function buildPathGapPlanningContextForAi({
|
||||
goalQuery = '',
|
||||
semanticBrief = null,
|
||||
offer = null,
|
||||
graphId = null,
|
||||
pathSteps = [],
|
||||
editableMajorSteps = [],
|
||||
progressionRoadmap = null,
|
||||
} = {}) {
|
||||
const afterIdx = Number(offer?.insert_after_index)
|
||||
const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null
|
||||
const stepB =
|
||||
Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx + 1] : null
|
||||
const majorIdxRaw =
|
||||
offer?.roadmap_major_step_index ?? offer?.gap?.roadmap_major_step_index
|
||||
const majorIdx =
|
||||
majorIdxRaw != null && Number.isFinite(Number(majorIdxRaw)) ? Number(majorIdxRaw) : null
|
||||
const majorStep =
|
||||
majorIdx != null && editableMajorSteps[majorIdx] ? editableMajorSteps[majorIdx] : null
|
||||
|
||||
const ctx = {
|
||||
source: 'progression_path_gap_fill',
|
||||
goal_query: (goalQuery || '').trim() || null,
|
||||
primary_topic: semanticBrief?.primary_topic || null,
|
||||
progression_graph_id: graphId != null ? Number(graphId) : null,
|
||||
gap_source: offer?.source || null,
|
||||
gap_phase: offer?.phase || offer?.gap?.expected_phase || null,
|
||||
roadmap_major_step_index: majorIdx,
|
||||
roadmap_phase: majorStep?.phase || offer?.phase || null,
|
||||
roadmap_learning_goal:
|
||||
(majorStep?.learning_goal || offer?.title_hint || offer?.gap?.learning_goal || '').trim() ||
|
||||
null,
|
||||
neighbor_before_title: stepA?.exerciseTitle || offer?.from_title || null,
|
||||
neighbor_after_title: stepB?.exerciseTitle || offer?.to_title || null,
|
||||
path_step_count: Array.isArray(pathSteps) ? pathSteps.length : 0,
|
||||
major_step_count:
|
||||
editableMajorSteps?.length ||
|
||||
progressionRoadmap?.major_step_count ||
|
||||
progressionRoadmap?.roadmap?.major_steps?.length ||
|
||||
null,
|
||||
}
|
||||
return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== ''))
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user