Enhance Path QA with Roadmap-First Features and Gap Detection Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 42s
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 1m15s

- 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.
This commit is contained in:
Lars 2026-06-09 10:17:30 +02:00
parent 779e2477ba
commit 87f258be38
7 changed files with 119 additions and 12 deletions

View File

@ -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:

View File

@ -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",

View File

@ -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]})

View File

@ -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",

View File

@ -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** |
| **F0F2** | 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** |

View File

@ -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)

View File

@ -907,6 +907,11 @@ export default function ExerciseProgressionPathBuilder({
: ''}
</p>
) : null}
{pathQa.roadmap_qa_mode === 'roadmap_first_lite' ? (
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>
QS an Roadmap gekoppelt: keine Brücken/Reihenfolge zwischen Major Steps (didaktisch bereits geplant).
</p>
) : null}
</div>
) : null}