diff --git a/.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md b/.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md index e4a383e..7d3dbbe 100644 --- a/.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md +++ b/.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md @@ -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. diff --git a/.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md b/.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md index 5961298..785373e 100644 --- a/.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md +++ b/.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md @@ -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 | 🔲 | diff --git a/backend/planning_exercise_path_builder.py b/backend/planning_exercise_path_builder.py index c0f1022..799c25a 100644 --- a/backend/planning_exercise_path_builder.py +++ b/backend/planning_exercise_path_builder.py @@ -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), } diff --git a/backend/planning_progression_roadmap.py b/backend/planning_progression_roadmap.py index c8c90a8..2db9735 100644 --- a/backend/planning_progression_roadmap.py +++ b/backend/planning_progression_roadmap.py @@ -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", diff --git a/backend/tests/test_planning_exercise_path_builder.py b/backend/tests/test_planning_exercise_path_builder.py index 12a2a3d..dc9d598 100644 --- a/backend/tests/test_planning_exercise_path_builder.py +++ b/backend/tests/test_planning_exercise_path_builder.py @@ -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"]) diff --git a/backend/tests/test_planning_progression_roadmap.py b/backend/tests/test_planning_progression_roadmap.py index 645f605..d2ea1d5 100644 --- a/backend/tests/test_planning_progression_roadmap.py +++ b/backend/tests/test_planning_progression_roadmap.py @@ -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) diff --git a/backend/version.py b/backend/version.py index 30c02de..68803c1 100644 --- a/backend/version.py +++ b/backend/version.py @@ -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", diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 9fb25b6..1e1e110 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -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** | -| **F0–F1** | **Roadmap-first** Progressionsgraph (A→B→C), Workflow-lite, API-Preview | 🔄 **0.8.204** | +| **F0–F2** | 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**) diff --git a/docs/architecture/PLANNING_KI_ROADMAP.md b/docs/architecture/PLANNING_KI_ROADMAP.md index d8979b1..64c2fd3 100644 --- a/docs/architecture/PLANNING_KI_ROADMAP.md +++ b/docs/architecture/PLANNING_KI_ROADMAP.md @@ -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 diff --git a/frontend/src/components/ExerciseProgressionPathBuilder.jsx b/frontend/src/components/ExerciseProgressionPathBuilder.jsx index 2808018..9a082d6 100644 --- a/frontend/src/components/ExerciseProgressionPathBuilder.jsx +++ b/frontend/src/components/ExerciseProgressionPathBuilder.jsx @@ -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).

    {progressionRoadmap.roadmap.major_steps.map((step) => (