Implement Roadmap-First Retrieval and Enhance Planning AI Features
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 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m26s

- Introduced a roadmap-first approach for retrieval, allowing for structured exercise suggestions based on stage specifications and major steps.
- Added functionality to generate gap-fill offers for unfilled roadmap stages, improving the relevance of exercise recommendations.
- Updated the `ExerciseProgressionPathBuilder` to support the new roadmap-first feature, enhancing user experience with clearer exercise paths.
- Incremented application version to 0.8.206 and updated the database schema version to reflect these changes.
This commit is contained in:
Lars 2026-06-08 12:40:17 +02:00
parent 7411543a97
commit d4e9bded23
10 changed files with 431 additions and 52 deletions

View File

@ -506,7 +506,7 @@ Nach Pfad-Bildung:
| Prompts | Migration **078/079** — Slugs in `ai_prompts` (Admin), **kein** Template im Python-Code |
| UI | `ExerciseProgressionPathBuilder` — Roadmap-Box (Major Steps) |
**Übergang:** `include_roadmap_preview=true` liefert `progression_roadmap` **parallel** zum retrieval-first Pfad. **Ziel F3:** `roadmap_first=true` steuert Retrieval.
**F3 (0.8.206):** `roadmap_first=true` (Default im UI) — Retrieval pro `stage_spec`/Major Step; `roadmap_unfilled` Gap-Angebote. Ohne Flag: retrieval-first wie bisher, Roadmap nur Preview.
**Mitai Workflow-Engine:** bewusst **nicht** jetzt — Pipeline workflow-ready für spätere Anbindung.

View File

@ -187,7 +187,7 @@ Jede Phase: `(ctx) → ctx`, Zwischenergebnisse in API-Response für **Human-in-
| **F0** | Spec + Doku + `planning_progression_roadmap.py` Scaffold | 🔄 0.8.204 |
| **F1** | `include_roadmap_preview` in API + deterministische A/B | 🔄 0.8.204 |
| **F2** | LLM Phase A/B/C über `ai_prompts` (078/079), `include_llm_roadmap` | 🔄 0.8.205 |
| **F3** | Retrieval aus `stage_specs` (roadmap_first) | 🔲 |
| **F3** | Retrieval aus `stage_specs` (roadmap_first) | ✅ 0.8.206 |
| **F4** | UI Roadmap-Review | 🔲 |
| **F5** | Trainingsplanung: eigene Pipeline + ggf. Workflow-Engine | 🔲 |

View File

@ -23,7 +23,11 @@ from planning_exercise_path_qa import (
strip_off_topic_steps_from_path,
try_llm_qa_progression_path,
)
from planning_exercise_path_ai_fill import apply_gap_fill_after_qa, collect_gap_fill_specs
from planning_exercise_path_ai_fill import (
apply_gap_fill_after_qa,
build_gap_fill_offer,
collect_gap_fill_specs,
)
from planning_exercise_retrieval import run_multistage_planning_retrieval
from planning_exercise_semantics import (
PlanningSemanticBrief,
@ -47,8 +51,14 @@ from planning_exercise_suggest import (
resolve_planning_exercise_intent,
)
from planning_progression_roadmap import (
MajorStep,
ProgressionRoadmapContext,
StageSpecArtifact,
build_roadmap_unfilled_gap_specs,
progression_roadmap_to_api_dict,
resolve_step_exercise_kind_filter,
run_progression_roadmap_pipeline,
stage_spec_retrieval_query,
)
from routers.training_planning import _has_planning_role
@ -169,8 +179,12 @@ def _run_path_step_retrieval(
step_b: Optional[Dict[str, Any]] = None,
path_target_profile: Optional[PlanningTargetProfile] = None,
path_intent: Optional[str] = None,
step_query_override: Optional[str] = None,
step_phase_override: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], PlanningTargetProfile, Dict[str, Any], str]:
step_query = step_retrieval_query(semantic_brief, goal_query, step_index, max_steps)
step_query = step_query_override or step_retrieval_query(
semantic_brief, goal_query, step_index, max_steps
)
if bridge_mode and step_a and step_b:
phase = step_phase_for_index(semantic_brief, step_index, max_steps)
parts = [semantic_brief.primary_topic or semantic_brief.retrieval_query or goal_query]
@ -200,7 +214,8 @@ def _run_path_step_retrieval(
"has_planning_reference": bool(planned_ids or anchor_id or bridge_mode),
"semantic_brief": semantic_brief,
"retrieval_query": step_query,
"path_step_phase": step_phase_for_index(semantic_brief, step_index, max_steps),
"path_step_phase": step_phase_override
or step_phase_for_index(semantic_brief, step_index, max_steps),
}
pack = apply_progression_context_to_pack(
cur,
@ -331,6 +346,130 @@ def _make_bridge_search_fn(
return _bridge_search
def _annotate_roadmap_step(
step: Dict[str, Any],
*,
stage_spec: StageSpecArtifact,
major_step: Optional[MajorStep],
) -> Dict[str, Any]:
reasons = list(step.get("reasons") or [])
learning_goal = (stage_spec.learning_goal or "").strip()
if learning_goal:
roadmap_reason = f"Roadmap: {learning_goal[:120]}"
if roadmap_reason not in reasons:
reasons.insert(0, roadmap_reason)
step["reasons"] = reasons[:4]
step["roadmap_major_step_index"] = stage_spec.major_step_index
step["roadmap_phase"] = major_step.phase if major_step else None
step["roadmap_learning_goal"] = learning_goal or None
step["roadmap_match_source"] = "stage_spec"
return step
def _build_steps_roadmap_first(
cur,
*,
tenant: TenantContext,
body: ProgressionPathSuggestRequest,
goal_query: str,
max_steps: int,
semantic_brief: PlanningSemanticBrief,
path_target_profile: PlanningTargetProfile,
path_intent: str,
roadmap_ctx: ProgressionRoadmapContext,
) -> Tuple[List[Dict[str, Any]], List[Tuple[int, StageSpecArtifact]]]:
"""Retrieval pro stage_spec statt iterativem Pfad-Bau (Phase F3)."""
stage_specs = list(roadmap_ctx.stage_specs or [])[:max_steps]
if not stage_specs and roadmap_ctx.roadmap:
stage_specs = [
StageSpecArtifact(
major_step_index=m.index,
learning_goal=m.learning_goal,
)
for m in roadmap_ctx.roadmap.major_steps[:max_steps]
]
major_by_index: Dict[int, MajorStep] = {}
if roadmap_ctx.roadmap:
major_by_index = {m.index: m for m in roadmap_ctx.roadmap.major_steps}
used: Set[int] = set()
steps: List[Dict[str, Any]] = []
planned_ids: List[int] = []
anchor_id: Optional[int] = None
anchor_variant_id: Optional[int] = None
unfilled: List[Tuple[int, StageSpecArtifact]] = []
for step_index, stage_spec in enumerate(stage_specs):
major = major_by_index.get(stage_spec.major_step_index)
step_query = stage_spec_retrieval_query(
semantic_brief=semantic_brief,
goal_query=goal_query,
stage_spec=stage_spec,
major_step=major,
)
step_kind = resolve_step_exercise_kind_filter(stage_spec, body.exercise_kind_any)
hits, _, _, _ = _run_path_step_retrieval(
cur,
tenant=tenant,
goal_query=goal_query,
step_index=step_index,
max_steps=max_steps,
planned_ids=planned_ids,
anchor_id=anchor_id,
anchor_variant_id=anchor_variant_id,
progression_graph_id=body.progression_graph_id,
include_llm_intent=body.include_llm_intent and step_index == 0,
exercise_kind_any=step_kind,
semantic_brief=semantic_brief,
path_target_profile=path_target_profile,
path_intent=path_intent,
step_query_override=step_query,
step_phase_override=major.phase if major else None,
)
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
if not hit and step_query != goal_query:
hits, _, _, _ = _run_path_step_retrieval(
cur,
tenant=tenant,
goal_query=goal_query,
step_index=step_index,
max_steps=max_steps,
planned_ids=planned_ids,
anchor_id=anchor_id,
anchor_variant_id=anchor_variant_id,
progression_graph_id=body.progression_graph_id,
include_llm_intent=False,
exercise_kind_any=step_kind,
semantic_brief=semantic_brief,
path_target_profile=path_target_profile,
path_intent=path_intent,
step_query_override=goal_query,
step_phase_override=major.phase if major else None,
)
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
if not hit:
unfilled.append((step_index, stage_spec))
continue
step = _annotate_roadmap_step(
_hit_to_path_step(hit),
stage_spec=stage_spec,
major_step=major,
)
steps.append(step)
eid = int(step["exercise_id"])
used.add(eid)
planned_ids.append(eid)
anchor_id = eid
anchor_variant_id = step.get("variant_id")
return steps, unfilled
def suggest_progression_path(
cur,
*,
@ -360,41 +499,98 @@ def suggest_progression_path(
include_llm_intent=body.include_llm_intent,
)
roadmap_first = bool(body.roadmap_first)
include_roadmap = roadmap_first or body.include_roadmap_preview
progression_roadmap: Optional[Dict[str, Any]] = None
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]] = []
roadmap_gap_offers: List[Dict[str, Any]] = []
if include_roadmap:
roadmap_ctx = run_progression_roadmap_pipeline(
goal_query,
max_steps=max_steps,
semantic_brief=semantic_brief,
cur=cur,
include_llm_roadmap=body.include_llm_roadmap,
)
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
used: Set[int] = set()
steps: List[Dict[str, Any]] = []
planned_ids: List[int] = []
anchor_id: Optional[int] = None
anchor_variant_id: Optional[int] = None
for step_index in range(max_steps):
hits, _tp, _qis, _intent = _run_path_step_retrieval(
if roadmap_first and roadmap_ctx is not None:
steps, roadmap_unfilled = _build_steps_roadmap_first(
cur,
tenant=tenant,
body=body,
goal_query=goal_query,
step_index=step_index,
max_steps=max_steps,
planned_ids=planned_ids,
anchor_id=anchor_id,
anchor_variant_id=anchor_variant_id,
progression_graph_id=body.progression_graph_id,
include_llm_intent=body.include_llm_intent,
exercise_kind_any=body.exercise_kind_any,
semantic_brief=semantic_brief,
path_target_profile=path_target_profile,
path_intent=path_intent,
roadmap_ctx=roadmap_ctx,
)
planned_ids = [int(s["exercise_id"]) for s in steps if s.get("exercise_id") is not None]
if planned_ids:
anchor_id = planned_ids[-1]
anchor_variant_id = steps[-1].get("variant_id")
if body.include_ai_gap_fill and roadmap_unfilled:
major_by_index = (
{m.index: m for m in roadmap_ctx.roadmap.major_steps}
if roadmap_ctx.roadmap
else {}
)
roadmap_gap_specs = build_roadmap_unfilled_gap_specs(
unfilled_specs=roadmap_unfilled,
major_steps_by_index=major_by_index,
steps=steps,
brief=semantic_brief,
goal_query=goal_query,
)
for spec in roadmap_gap_specs:
roadmap_gap_offers.append(
build_gap_fill_offer(
spec=spec,
steps=steps,
goal_query=goal_query,
brief=semantic_brief,
proposal=None,
)
)
else:
for step_index in range(max_steps):
hits, _tp, _qis, _intent = _run_path_step_retrieval(
cur,
tenant=tenant,
goal_query=goal_query,
step_index=step_index,
max_steps=max_steps,
planned_ids=planned_ids,
anchor_id=anchor_id,
anchor_variant_id=anchor_variant_id,
progression_graph_id=body.progression_graph_id,
include_llm_intent=body.include_llm_intent,
exercise_kind_any=body.exercise_kind_any,
semantic_brief=semantic_brief,
path_target_profile=path_target_profile,
path_intent=path_intent,
)
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
if not hit:
break
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
if not hit:
break
step = _hit_to_path_step(hit)
steps.append(step)
eid = int(step["exercise_id"])
used.add(eid)
planned_ids.append(eid)
anchor_id = eid
anchor_variant_id = step.get("variant_id")
step = _hit_to_path_step(hit)
steps.append(step)
eid = int(step["exercise_id"])
used.add(eid)
planned_ids.append(eid)
anchor_id = eid
anchor_variant_id = step.get("variant_id")
if len(steps) < 2:
raise HTTPException(
@ -490,6 +686,12 @@ def suggest_progression_path(
auto_insert_proposals=False,
)
if roadmap_gap_offers:
seen_offer_ids = {o.get("offer_id") for o in gap_fill_offers}
for offer in roadmap_gap_offers:
if offer.get("offer_id") not in seen_offer_ids:
gap_fill_offers.append(offer)
path_qa = build_path_qa_summary(
gaps=gaps,
bridge_inserts=bridge_inserts,
@ -505,6 +707,8 @@ def suggest_progression_path(
target_profile_summary = path_target_profile.to_summary_dict(cur)
retrieval_parts = ["profile_v1", "full_library", "path_builder", "semantics"]
if roadmap_first:
retrieval_parts.append("roadmap_first")
if body.include_path_qa:
retrieval_parts.append("path_qa")
if llm_qa_applied:
@ -515,20 +719,10 @@ def suggest_progression_path(
retrieval_parts.append("ai_gap_fill")
if gap_fill_offers:
retrieval_parts.append("gap_fill_offers")
progression_roadmap: Optional[Dict[str, Any]] = None
if body.include_roadmap_preview or body.roadmap_first:
roadmap_ctx = run_progression_roadmap_pipeline(
goal_query,
max_steps=max_steps,
semantic_brief=semantic_brief,
cur=cur,
include_llm_roadmap=body.include_llm_roadmap,
)
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
if include_roadmap:
retrieval_parts.append("roadmap_preview")
if body.roadmap_first:
retrieval_parts.append("roadmap_first_pending")
if roadmap_unfilled:
retrieval_parts.append("roadmap_unfilled")
return {
"goal_query": goal_query,
@ -543,7 +737,8 @@ def suggest_progression_path(
"path_qa": path_qa,
"gap_fill_offers": gap_fill_offers,
"progression_roadmap": progression_roadmap,
"roadmap_first": body.roadmap_first,
"roadmap_first": roadmap_first,
"roadmap_unfilled_count": len(roadmap_unfilled),
"retrieval_phase": "+".join(retrieval_parts),
}

View File

@ -13,7 +13,7 @@ from __future__ import annotations
import json
import logging
import re
from typing import Any, Dict, List, Optional, Sequence, Tuple
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
from pydantic import BaseModel, Field, ValidationError
@ -421,6 +421,107 @@ def consolidate_micro_to_major(
return majors, notes
def _normalize_query(query: Optional[str]) -> str:
return re.sub(r"\s+", " ", (query or "").strip())
def stage_spec_retrieval_query(
*,
semantic_brief: PlanningSemanticBrief,
goal_query: str,
stage_spec: StageSpecArtifact,
major_step: Optional[MajorStep] = None,
) -> str:
"""Retrieval-Query für einen Roadmap-Major-Step (Phase F3)."""
parts: List[str] = []
topic = (semantic_brief.primary_topic or semantic_brief.retrieval_query or goal_query).strip()
if topic:
parts.append(topic)
learning_goal = (stage_spec.learning_goal or "").strip()
if learning_goal:
parts.append(learning_goal)
phase = (major_step.phase if major_step else "").strip().lower()
if phase:
parts.append(phase)
if stage_spec.load_profile:
parts.extend(str(x).strip() for x in stage_spec.load_profile[:2] if str(x).strip())
exercise_type = (stage_spec.exercise_type or "").strip().lower()
if exercise_type == "partner_drill":
parts.append("partner")
elif exercise_type == "kombination":
parts.append("kombination")
return _normalize_query(" ".join(parts)) or _normalize_query(goal_query)
def stage_spec_exercise_kind_filter(stage_spec: StageSpecArtifact) -> Optional[List[str]]:
"""Mappt didaktischen exercise_type auf DB exercise_kind (simple/combination)."""
et = (stage_spec.exercise_type or "").strip().lower()
if et == "kombination":
return ["combination"]
if et in ("kihon_einzel", "partner_drill", "grundtechnik"):
return ["simple"]
return None
def resolve_step_exercise_kind_filter(
stage_spec: StageSpecArtifact,
request_filter: Optional[Sequence[str]],
) -> Optional[List[str]]:
"""Schnittmenge aus Roadmap-Stufe und optionalem Request-Filter."""
stage_filter = stage_spec_exercise_kind_filter(stage_spec)
if not request_filter:
return stage_filter
req = [str(x).strip().lower() for x in request_filter if str(x).strip()]
if not stage_filter:
return req or None
merged = [k for k in stage_filter if k in req]
return merged or req
def build_roadmap_unfilled_gap_specs(
*,
unfilled_specs: Sequence[Tuple[int, StageSpecArtifact]],
major_steps_by_index: Mapping[int, MajorStep],
steps: Sequence[Mapping[str, Any]],
brief: PlanningSemanticBrief,
goal_query: str,
) -> List[Dict[str, Any]]:
"""Gap-Fill-Angebote für Roadmap-Stufen ohne Bibliothekstreffer."""
topic = (brief.primary_topic or "Technik").strip()
specs: List[Dict[str, Any]] = []
for roadmap_idx, stage_spec in unfilled_specs:
major = major_steps_by_index.get(stage_spec.major_step_index)
phase = (major.phase if major else "vertiefung").strip().lower()
insert_after = min(max(roadmap_idx - 1, -1), max(len(steps) - 1, -1))
title_hint = (stage_spec.learning_goal or f"{topic}{phase}").strip()[:120]
sketch_parts = [
f"Planungsziel: {goal_query}",
f"Roadmap-Stufe {stage_spec.major_step_index + 1} ({phase}): {stage_spec.learning_goal}",
]
if stage_spec.success_criteria:
sketch_parts.append(f"Erfolgskriterien: {', '.join(stage_spec.success_criteria[:3])}")
specs.append(
{
"source": "roadmap_unfilled",
"insert_after_index": insert_after,
"gap": {
"expected_phase": phase,
"roadmap_major_step_index": stage_spec.major_step_index,
"learning_goal": stage_spec.learning_goal,
},
"phase": phase,
"title_hint": title_hint,
"sketch": "\n".join(sketch_parts),
"rationale": (
f"Keine passende Bibliotheks-Übung für Roadmap-Stufe "
f"{stage_spec.major_step_index + 1} ({phase})."
),
"roadmap_major_step_index": stage_spec.major_step_index,
}
)
return specs[:5]
def build_stage_specs(
major_steps: Sequence[MajorStep],
*,
@ -553,7 +654,11 @@ __all__ = [
"RoadmapArtifact",
"StageSpecArtifact",
"build_goal_analysis",
"build_roadmap_unfilled_gap_specs",
"build_stage_specs",
"resolve_step_exercise_kind_filter",
"stage_spec_exercise_kind_filter",
"stage_spec_retrieval_query",
"consolidate_micro_to_major",
"develop_micro_objectives",
"progression_roadmap_to_api_dict",

View File

@ -1,5 +1,10 @@
"""Tests Planungs-KI Phase C3/E — Pfad-Vorschläge."""
from planning_exercise_path_builder import _pick_best_path_hit, _hit_to_path_step
"""Tests Planungs-KI Phase C3/E/F — Pfad-Vorschläge."""
from planning_exercise_path_builder import (
_annotate_roadmap_step,
_hit_to_path_step,
_pick_best_path_hit,
)
from planning_progression_roadmap import MajorStep, StageSpecArtifact
def test_pick_next_path_hit_skips_used():
@ -23,3 +28,17 @@ def test_hit_to_path_step_maps_variant():
assert step["exercise_id"] == 10
assert step["variant_id"] == 7
assert step["suggested_variant_name"] == "Leicht"
def test_annotate_roadmap_step_adds_metadata():
spec = StageSpecArtifact(major_step_index=1, learning_goal="Grundstellung Mae Geri")
major = MajorStep(index=1, phase="grundlage", learning_goal=spec.learning_goal, consolidates=["m1"])
step = _annotate_roadmap_step(
{"exercise_id": 5, "title": "Test", "reasons": ["Bibliothek"]},
stage_spec=spec,
major_step=major,
)
assert step["roadmap_major_step_index"] == 1
assert step["roadmap_phase"] == "grundlage"
assert step["roadmap_match_source"] == "stage_spec"
assert any("Roadmap:" in r for r in step["reasons"])

View File

@ -3,11 +3,17 @@ from planning_progression_roadmap import (
PROMPT_SLUG_GOAL_ANALYSIS,
PROMPT_SLUG_ROADMAP,
PROMPT_SLUG_STAGE_SPEC,
MajorStep,
StageSpecArtifact,
build_goal_analysis,
build_roadmap_unfilled_gap_specs,
consolidate_micro_to_major,
develop_micro_objectives,
progression_roadmap_to_api_dict,
resolve_step_exercise_kind_filter,
run_progression_roadmap_pipeline,
stage_spec_exercise_kind_filter,
stage_spec_retrieval_query,
)
from planning_exercise_semantics import build_semantic_brief
@ -43,6 +49,47 @@ def test_major_steps_have_learning_goals():
assert step.consolidates
def test_stage_spec_retrieval_query_includes_learning_goal():
brief = build_semantic_brief("Mae Geri Perfektion")
spec = StageSpecArtifact(
major_step_index=1,
learning_goal="Koordination und Präzision vertiefen",
load_profile=["präzision"],
exercise_type="kihon_einzel",
)
major = MajorStep(index=1, phase="vertiefung", learning_goal=spec.learning_goal, consolidates=["m3"])
q = stage_spec_retrieval_query(
semantic_brief=brief,
goal_query="Mae Geri Perfektion",
stage_spec=spec,
major_step=major,
)
assert "vertiefung" in q.lower()
assert "Koordination" in q or "Präzision" in q
def test_stage_spec_exercise_kind_filter_maps_combination():
spec = StageSpecArtifact(major_step_index=0, exercise_type="kombination")
assert stage_spec_exercise_kind_filter(spec) == ["combination"]
assert resolve_step_exercise_kind_filter(spec, ["simple"]) == ["simple"]
def test_build_roadmap_unfilled_gap_specs():
brief = build_semantic_brief("Mae Geri")
spec = StageSpecArtifact(major_step_index=2, learning_goal="Anwendung im Partnerdrill")
major = MajorStep(index=2, phase="anwendung", learning_goal=spec.learning_goal, consolidates=["m5"])
specs = build_roadmap_unfilled_gap_specs(
unfilled_specs=[(2, spec)],
major_steps_by_index={2: major},
steps=[{"exercise_id": 1, "title": "A"}, {"exercise_id": 2, "title": "B"}],
brief=brief,
goal_query="Mae Geri",
)
assert len(specs) == 1
assert specs[0]["source"] == "roadmap_unfilled"
assert specs[0]["phase"] == "anwendung"
def test_api_dict_exposes_prompt_slug_catalog():
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
api = progression_roadmap_to_api_dict(ctx)

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.205"
APP_VERSION = "0.8.206"
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.17.1", # F2: Roadmap-LLM via ai_prompts-Slugs, kein Hardcoding
"planning_exercise_suggest": "0.18.0", # F3: roadmap_first Retrieval pro stage_spec
"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.206",
"date": "2026-06-07",
"changes": [
"Phase F3: roadmap_first — Bibliotheks-Match pro stage_spec/Major Step statt iterativem Pfad.",
"Gap-Angebote für unbesetzte Roadmap-Stufen (roadmap_unfilled).",
"UI: Pfad-Builder sendet roadmap_first; Übungen an Roadmap gekoppelt.",
],
},
{
"version": "0.8.205",
"date": "2026-06-07",

View File

@ -1,7 +1,7 @@
# Shinkan Jinkendo Entwicklungsstand & Handover
**Stand:** 2026-06-07
**App-Version / DB-Schema:** App **`0.8.204`** (Planungs-KI Phase F0); DB **`20260606085`** — maßgeblich **`backend/version.py`**.
**App-Version / DB-Schema:** App **`0.8.206`** (Planungs-KI Phase F3); 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**.
@ -107,18 +107,19 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
| **E** | Semantik-Schicht (Brief, Phrasen-Score) + Pfad-QA (Lücken, Brücken, LLM-QS) | ✅ **0.8.186** |
| **E2** | Pfad-Neuordnung (LLM) + KI-Neuanlage bei unüberbrückbaren Lücken | ✅ **0.8.187** |
| **E3** | `gap_fill_offers`, Off-Topic-Strip, voller KI-Call bei Lücken | ✅ **0.8.203** |
| **F0F1** | **Roadmap-first** Progressionsgraph (A→B→C), Workflow-lite, API-Preview | 🔄 **0.8.204** |
| **F0F2** | Roadmap-Pipeline + LLM-Prompts (078/079) | ✅ **0.8.205** |
| **F3** | `roadmap_first` — Retrieval pro `stage_spec` | ✅ **0.8.206** |
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | 🔲 |
**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`**
**Backend:** `planning_exercise_suggest.py`, `planning_exercise_retrieval.py`, `planning_exercise_path_builder.py`, **`planning_progression_roadmap.py`** · Router `POST /api/planning/exercise-suggest`, `POST /api/planning/progression-path-suggest` (`include_roadmap_preview`)
**Backend:** `planning_exercise_suggest.py`, `planning_exercise_retrieval.py`, `planning_exercise_path_builder.py`, **`planning_progression_roadmap.py`** · Router `POST /api/planning/exercise-suggest`, `POST /api/planning/progression-path-suggest` (`roadmap_first`, `include_roadmap_preview`)
**Frontend:** `ExerciseProgressionPathBuilder` — Roadmap-Box (Major Steps) + bestehender Pfad-Review · `ExercisePickerModal` (Planung)
**Frontend:** `ExerciseProgressionPathBuilder` — Roadmap-Box + Pfad je Major Step (`roadmap_first`) · `ExercisePickerModal` (Planung)
**Superadmin:** Übungs-Anreicherung (Skills) — `exercise_enrichment_admin` (**0.8.178+**), separater Admin-Flow
**Offen (F2+):** LLM Roadmap (Prompts **078**), `roadmap_first` Retrieval, Roadmap-UI editierbar; Trainingsplanung eigene Pipeline (Gruppenkontext); Enrichment
**Offen (F4+):** Roadmap-UI editierbar; Trainingsplanung eigene Pipeline (Gruppenkontext); Enrichment
#### Übungs-KI Formular / Schnellanlage (Stand **0.8.171**)

View File

@ -58,10 +58,11 @@ Diese Roadmap ergänzt die **Architektur-Refaktor-Roadmap** (`UMSETZUNGSPLAN_ROA
- [x] Deterministischer Fallback wenn Prompt/OpenRouter fehlt
- [ ] Response/UI: genutzte `prompt_slugs` sichtbar machen (Admin-Hinweis)
### F3 — roadmap-first
### F3 — roadmap-first (0.8.206)
- [ ] Retrieval pro `major_step` + `stage_spec` statt iterativem Pfad-Bau
- [ ] QA/Lücken an Roadmap koppeln
- [x] Retrieval pro `major_step` + `stage_spec` statt iterativem Pfad-Bau
- [x] Gap-Angebote für unbesetzte Roadmap-Stufen (`roadmap_unfilled`)
- [ ] QA/Lücken vollständig an Roadmap koppeln (Brücken optional reduzieren)
### F4 — UI

View File

@ -58,6 +58,7 @@ const OFFER_SOURCE_LABELS = {
unfilled_gap: 'Lücke',
off_topic: 'Themenfremd',
llm_suggested: 'QS-Empfehlung',
roadmap_unfilled: 'Roadmap-Stufe',
}
function resolveDefaultFocusAreaId(targetSummary, focusAreas) {
@ -349,6 +350,7 @@ export default function ExerciseProgressionPathBuilder({
include_ai_gap_fill: true,
include_roadmap_preview: true,
include_llm_roadmap: true,
roadmap_first: true,
progression_graph_id: Number(graphId),
})
const qa = res?.path_qa || null
@ -531,7 +533,7 @@ export default function ExerciseProgressionPathBuilder({
{progressionRoadmap.llm_roadmap_applied
? ' (KI-Prompts aus Admin-Konfiguration)'
: ' (heuristischer Fallback — KI-Prompts in ai_prompts)'}
. Übungen unten: Bibliothekssuche (Übergangsphase).
. Übungen unten: je Major Step aus der Bibliothek (roadmap-first).
</p>
<ol style={{ margin: 0, paddingLeft: '18px', fontSize: '13px', lineHeight: 1.5 }}>
{progressionRoadmap.roadmap.major_steps.map((step) => (