From 87f258be38df8dd42f7c5220a93773b1c2cf1c93 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Jun 2026 10:17:30 +0200 Subject: [PATCH] Enhance Path QA with Roadmap-First Features and Gap Detection Improvements - Introduced `roadmap_qa_mode` to manage QA behavior based on roadmap-first logic, improving gap detection between major steps. - Updated `detect_path_gaps` to skip gaps for roadmap-planned neighbor pairs, enhancing the accuracy of path assessments. - Added new helper function `is_roadmap_planned_neighbor_pair` to facilitate roadmap neighbor checks. - Updated relevant tests to validate new functionality and ensure robustness. - Incremented application version to 0.8.209 to reflect these changes. --- backend/planning_exercise_path_builder.py | 31 ++++++++++-- backend/planning_exercise_path_qa.py | 31 +++++++++++- .../tests/test_planning_exercise_path_qa.py | 48 ++++++++++++++++++- backend/version.py | 12 ++++- docs/HANDOVER.md | 2 +- docs/architecture/PLANNING_KI_ROADMAP.md | 2 +- .../ExerciseProgressionPathBuilder.jsx | 5 ++ 7 files changed, 119 insertions(+), 12 deletions(-) diff --git a/backend/planning_exercise_path_builder.py b/backend/planning_exercise_path_builder.py index 2aaad49..d2ca3f8 100644 --- a/backend/planning_exercise_path_builder.py +++ b/backend/planning_exercise_path_builder.py @@ -652,10 +652,18 @@ def suggest_progression_path( reorder_applied = False reorder_notes: List[str] = [] + roadmap_qa_mode: Optional[str] = None if body.include_path_qa: - gaps = detect_path_gaps(cur, steps, brief=semantic_brief) + if roadmap_first: + roadmap_qa_mode = "roadmap_first_lite" + gaps = detect_path_gaps( + cur, + steps, + brief=semantic_brief, + roadmap_first=roadmap_first, + ) unfilled_gaps: List[Dict[str, Any]] = [] - if gaps: + if gaps and not roadmap_first: bridge_fn = _make_bridge_search_fn( cur, tenant=tenant, @@ -676,6 +684,8 @@ def suggest_progression_path( brief=semantic_brief, bridge_search_fn=bridge_fn, ) + elif gaps and roadmap_first: + unfilled_gaps = list(gaps) if body.include_llm_path_qa: llm_qa, llm_qa_applied = try_llm_qa_progression_path( @@ -687,7 +697,12 @@ def suggest_progression_path( bridge_inserts=bridge_inserts, ) - if body.include_path_reorder and llm_qa_applied and llm_qa: + if ( + body.include_path_reorder + and not roadmap_first + and llm_qa_applied + and llm_qa + ): q_score = llm_qa.get("quality_score") try: q_val = float(q_score) if q_score is not None else None @@ -700,7 +715,12 @@ def suggest_progression_path( steps, stripped_off_topic = strip_off_topic_steps_from_path(steps, off_topic_steps) if stripped_off_topic: off_topic_steps = [] - gaps = detect_path_gaps(cur, steps, brief=semantic_brief) + gaps = detect_path_gaps( + cur, + steps, + brief=semantic_brief, + roadmap_first=roadmap_first, + ) llm_gap_specs = parse_llm_suggested_new_exercises( llm_qa, @@ -746,12 +766,15 @@ def suggest_progression_path( llm_applied=llm_qa_applied, reorder_applied=reorder_applied, reorder_notes=reorder_notes, + roadmap_qa_mode=roadmap_qa_mode, ) 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 roadmap_qa_mode: + retrieval_parts.append(roadmap_qa_mode) if body.include_path_qa: retrieval_parts.append("path_qa") if llm_qa_applied: diff --git a/backend/planning_exercise_path_qa.py b/backend/planning_exercise_path_qa.py index aad489e..2f41247 100644 --- a/backend/planning_exercise_path_qa.py +++ b/backend/planning_exercise_path_qa.py @@ -141,21 +141,45 @@ def measure_step_transition_gap( } +def is_roadmap_planned_neighbor_pair( + step_a: Mapping[str, Any], + step_b: Mapping[str, Any], +) -> bool: + """Aufeinanderfolgende Major Steps aus roadmap_first — kein Skill-Übergangs-Lücke.""" + if step_a.get("roadmap_match_source") != "stage_spec": + return False + if step_b.get("roadmap_match_source") != "stage_spec": + return False + idx_a = step_a.get("roadmap_major_step_index") + idx_b = step_b.get("roadmap_major_step_index") + if idx_a is None or idx_b is None: + return False + try: + return int(idx_b) == int(idx_a) + 1 + except (TypeError, ValueError): + return False + + def detect_path_gaps( cur, steps: Sequence[Mapping[str, Any]], *, brief: PlanningSemanticBrief, + roadmap_first: bool = False, ) -> List[Dict[str, Any]]: if len(steps) < 2: return [] gaps: List[Dict[str, Any]] = [] total_segments = len(steps) - 1 for i in range(total_segments): + step_a = steps[i] + step_b = steps[i + 1] + if roadmap_first and is_roadmap_planned_neighbor_pair(step_a, step_b): + continue gap = measure_step_transition_gap( cur, - steps[i], - steps[i + 1], + step_a, + step_b, brief=brief, segment_index=i, total_segments=total_segments, @@ -516,6 +540,7 @@ def build_path_qa_summary( llm_applied: bool, reorder_applied: bool = False, reorder_notes: Optional[Sequence[str]] = None, + roadmap_qa_mode: Optional[str] = None, ) -> Dict[str, Any]: offers = list(gap_fill_offers or []) off_topic = list(off_topic_steps or []) @@ -534,6 +559,7 @@ def build_path_qa_summary( "llm_qa_applied": llm_applied, "reorder_applied": reorder_applied, "reorder_notes": list(reorder_notes or []), + "roadmap_qa_mode": roadmap_qa_mode, } if llm_qa: summary["overall_ok"] = bool(llm_qa.get("overall_ok", True)) @@ -562,6 +588,7 @@ __all__ = [ "build_path_qa_summary", "detect_off_topic_steps", "detect_path_gaps", + "is_roadmap_planned_neighbor_pair", "strip_off_topic_steps_from_path", "find_step_pair_index", "insert_bridge_exercises", diff --git a/backend/tests/test_planning_exercise_path_qa.py b/backend/tests/test_planning_exercise_path_qa.py index f781cbf..929e187 100644 --- a/backend/tests/test_planning_exercise_path_qa.py +++ b/backend/tests/test_planning_exercise_path_qa.py @@ -1,7 +1,11 @@ -"""Tests Planungs-KI Phase E — Pfad-QA.""" +"""Tests Planungs-KI Phase E/F — Pfad-QA.""" from planning_exercise_path_builder import _pick_best_path_hit from planning_exercise_semantics import build_semantic_brief -from planning_exercise_path_qa import apply_llm_path_reorder +from planning_exercise_path_qa import ( + apply_llm_path_reorder, + detect_path_gaps, + is_roadmap_planned_neighbor_pair, +) def test_pick_best_path_hit_prefers_semantic_score(): @@ -62,6 +66,46 @@ def test_apply_llm_path_reorder_permutation(): assert notes +def test_is_roadmap_planned_neighbor_pair(): + a = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 1} + b = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 2} + c = {"roadmap_match_source": "stage_spec", "roadmap_major_step_index": 4} + assert is_roadmap_planned_neighbor_pair(a, b) is True + assert is_roadmap_planned_neighbor_pair(a, c) is False + assert is_roadmap_planned_neighbor_pair({"exercise_id": 1}, b) is False + + +def test_detect_path_gaps_skips_roadmap_neighbors(): + brief = build_semantic_brief("Mae Geri") + steps = [ + { + "exercise_id": 1, + "title": "A", + "roadmap_match_source": "stage_spec", + "roadmap_major_step_index": 0, + }, + { + "exercise_id": 2, + "title": "B", + "roadmap_match_source": "stage_spec", + "roadmap_major_step_index": 1, + }, + ] + + class _FakeCur: + def execute(self, *args, **kwargs): + return None + + def fetchall(self): + return [] + + def fetchone(self): + return {"title": "X", "summary": "", "goal": ""} + + gaps = detect_path_gaps(_FakeCur(), steps, brief=brief, roadmap_first=True) + assert gaps == [] + + def test_apply_llm_path_reorder_invalid_ignored(): steps = [{"exercise_id": 1}, {"exercise_id": 2}] reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]}) diff --git a/backend/version.py b/backend/version.py index 919529e..f0d45b9 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.208" +APP_VERSION = "0.8.209" 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.20.0", # Phase D: planning_context an suggestExerciseAi + "planning_exercise_suggest": "0.20.1", # F3-Polish: roadmap_first QA lite (keine Brücken/Reorder) "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,14 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.209", + "date": "2026-06-07", + "changes": [ + "F3-Polish: roadmap_first — keine Brücken zwischen Major Steps, kein LLM-Reorder.", + "Pfad-QS: Lücken nur noch bei Nicht-Roadmap-Übergängen; roadmap_qa_mode in Response.", + ], + }, { "version": "0.8.208", "date": "2026-06-07", diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 35c6322..869ba42 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -108,7 +108,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl | **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–F2** | Roadmap-Pipeline + LLM-Prompts (078/079) | ✅ **0.8.205** | -| **F3** | `roadmap_first` — Retrieval pro `stage_spec` | ✅ **0.8.206** | +| **F3** | `roadmap_first` — Retrieval + QA lite (keine Brücken/Reorder) | ✅ **0.8.209** | | **F4** | Roadmap-Review UI + `roadmap_override` | ✅ **0.8.207** | | **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | ✅ **0.8.208** | diff --git a/docs/architecture/PLANNING_KI_ROADMAP.md b/docs/architecture/PLANNING_KI_ROADMAP.md index dd19042..8f0cda3 100644 --- a/docs/architecture/PLANNING_KI_ROADMAP.md +++ b/docs/architecture/PLANNING_KI_ROADMAP.md @@ -62,7 +62,7 @@ Diese Roadmap ergänzt die **Architektur-Refaktor-Roadmap** (`UMSETZUNGSPLAN_ROA - [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) +- [x] QA/Lücken an Roadmap gekoppelt (`roadmap_first_lite`: keine Brücken/Reorder zwischen Major Steps) ### F4 — UI (0.8.207) diff --git a/frontend/src/components/ExerciseProgressionPathBuilder.jsx b/frontend/src/components/ExerciseProgressionPathBuilder.jsx index 4082596..57c5bae 100644 --- a/frontend/src/components/ExerciseProgressionPathBuilder.jsx +++ b/frontend/src/components/ExerciseProgressionPathBuilder.jsx @@ -907,6 +907,11 @@ export default function ExerciseProgressionPathBuilder({ : ''}

) : null} + {pathQa.roadmap_qa_mode === 'roadmap_first_lite' ? ( +

+ QS an Roadmap gekoppelt: keine Brücken/Reihenfolge zwischen Major Steps (didaktisch bereits geplant). +

+ ) : null} ) : null}