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
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:
parent
779e2477ba
commit
87f258be38
|
|
@ -652,10 +652,18 @@ def suggest_progression_path(
|
||||||
reorder_applied = False
|
reorder_applied = False
|
||||||
reorder_notes: List[str] = []
|
reorder_notes: List[str] = []
|
||||||
|
|
||||||
|
roadmap_qa_mode: Optional[str] = None
|
||||||
if body.include_path_qa:
|
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]] = []
|
unfilled_gaps: List[Dict[str, Any]] = []
|
||||||
if gaps:
|
if gaps and not roadmap_first:
|
||||||
bridge_fn = _make_bridge_search_fn(
|
bridge_fn = _make_bridge_search_fn(
|
||||||
cur,
|
cur,
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
|
|
@ -676,6 +684,8 @@ def suggest_progression_path(
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
bridge_search_fn=bridge_fn,
|
bridge_search_fn=bridge_fn,
|
||||||
)
|
)
|
||||||
|
elif gaps and roadmap_first:
|
||||||
|
unfilled_gaps = list(gaps)
|
||||||
|
|
||||||
if body.include_llm_path_qa:
|
if body.include_llm_path_qa:
|
||||||
llm_qa, llm_qa_applied = try_llm_qa_progression_path(
|
llm_qa, llm_qa_applied = try_llm_qa_progression_path(
|
||||||
|
|
@ -687,7 +697,12 @@ def suggest_progression_path(
|
||||||
bridge_inserts=bridge_inserts,
|
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")
|
q_score = llm_qa.get("quality_score")
|
||||||
try:
|
try:
|
||||||
q_val = float(q_score) if q_score is not None else None
|
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)
|
steps, stripped_off_topic = strip_off_topic_steps_from_path(steps, off_topic_steps)
|
||||||
if stripped_off_topic:
|
if stripped_off_topic:
|
||||||
off_topic_steps = []
|
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_gap_specs = parse_llm_suggested_new_exercises(
|
||||||
llm_qa,
|
llm_qa,
|
||||||
|
|
@ -746,12 +766,15 @@ def suggest_progression_path(
|
||||||
llm_applied=llm_qa_applied,
|
llm_applied=llm_qa_applied,
|
||||||
reorder_applied=reorder_applied,
|
reorder_applied=reorder_applied,
|
||||||
reorder_notes=reorder_notes,
|
reorder_notes=reorder_notes,
|
||||||
|
roadmap_qa_mode=roadmap_qa_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
target_profile_summary = path_target_profile.to_summary_dict(cur)
|
target_profile_summary = path_target_profile.to_summary_dict(cur)
|
||||||
retrieval_parts = ["profile_v1", "full_library", "path_builder", "semantics"]
|
retrieval_parts = ["profile_v1", "full_library", "path_builder", "semantics"]
|
||||||
if roadmap_first:
|
if roadmap_first:
|
||||||
retrieval_parts.append("roadmap_first")
|
retrieval_parts.append("roadmap_first")
|
||||||
|
if roadmap_qa_mode:
|
||||||
|
retrieval_parts.append(roadmap_qa_mode)
|
||||||
if body.include_path_qa:
|
if body.include_path_qa:
|
||||||
retrieval_parts.append("path_qa")
|
retrieval_parts.append("path_qa")
|
||||||
if llm_qa_applied:
|
if llm_qa_applied:
|
||||||
|
|
|
||||||
|
|
@ -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(
|
def detect_path_gaps(
|
||||||
cur,
|
cur,
|
||||||
steps: Sequence[Mapping[str, Any]],
|
steps: Sequence[Mapping[str, Any]],
|
||||||
*,
|
*,
|
||||||
brief: PlanningSemanticBrief,
|
brief: PlanningSemanticBrief,
|
||||||
|
roadmap_first: bool = False,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
if len(steps) < 2:
|
if len(steps) < 2:
|
||||||
return []
|
return []
|
||||||
gaps: List[Dict[str, Any]] = []
|
gaps: List[Dict[str, Any]] = []
|
||||||
total_segments = len(steps) - 1
|
total_segments = len(steps) - 1
|
||||||
for i in range(total_segments):
|
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(
|
gap = measure_step_transition_gap(
|
||||||
cur,
|
cur,
|
||||||
steps[i],
|
step_a,
|
||||||
steps[i + 1],
|
step_b,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
segment_index=i,
|
segment_index=i,
|
||||||
total_segments=total_segments,
|
total_segments=total_segments,
|
||||||
|
|
@ -516,6 +540,7 @@ def build_path_qa_summary(
|
||||||
llm_applied: bool,
|
llm_applied: bool,
|
||||||
reorder_applied: bool = False,
|
reorder_applied: bool = False,
|
||||||
reorder_notes: Optional[Sequence[str]] = None,
|
reorder_notes: Optional[Sequence[str]] = None,
|
||||||
|
roadmap_qa_mode: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
offers = list(gap_fill_offers or [])
|
offers = list(gap_fill_offers or [])
|
||||||
off_topic = list(off_topic_steps or [])
|
off_topic = list(off_topic_steps or [])
|
||||||
|
|
@ -534,6 +559,7 @@ def build_path_qa_summary(
|
||||||
"llm_qa_applied": llm_applied,
|
"llm_qa_applied": llm_applied,
|
||||||
"reorder_applied": reorder_applied,
|
"reorder_applied": reorder_applied,
|
||||||
"reorder_notes": list(reorder_notes or []),
|
"reorder_notes": list(reorder_notes or []),
|
||||||
|
"roadmap_qa_mode": roadmap_qa_mode,
|
||||||
}
|
}
|
||||||
if llm_qa:
|
if llm_qa:
|
||||||
summary["overall_ok"] = bool(llm_qa.get("overall_ok", True))
|
summary["overall_ok"] = bool(llm_qa.get("overall_ok", True))
|
||||||
|
|
@ -562,6 +588,7 @@ __all__ = [
|
||||||
"build_path_qa_summary",
|
"build_path_qa_summary",
|
||||||
"detect_off_topic_steps",
|
"detect_off_topic_steps",
|
||||||
"detect_path_gaps",
|
"detect_path_gaps",
|
||||||
|
"is_roadmap_planned_neighbor_pair",
|
||||||
"strip_off_topic_steps_from_path",
|
"strip_off_topic_steps_from_path",
|
||||||
"find_step_pair_index",
|
"find_step_pair_index",
|
||||||
"insert_bridge_exercises",
|
"insert_bridge_exercises",
|
||||||
|
|
|
||||||
|
|
@ -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_path_builder import _pick_best_path_hit
|
||||||
from planning_exercise_semantics import build_semantic_brief
|
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():
|
def test_pick_best_path_hit_prefers_semantic_score():
|
||||||
|
|
@ -62,6 +66,46 @@ def test_apply_llm_path_reorder_permutation():
|
||||||
assert notes
|
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():
|
def test_apply_llm_path_reorder_invalid_ignored():
|
||||||
steps = [{"exercise_id": 1}, {"exercise_id": 2}]
|
steps = [{"exercise_id": 1}, {"exercise_id": 2}]
|
||||||
reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]})
|
reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.208"
|
APP_VERSION = "0.8.209"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-06-07"
|
||||||
DB_SCHEMA_VERSION = "20260606086"
|
DB_SCHEMA_VERSION = "20260606086"
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
||||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
"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_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||||
|
|
@ -53,6 +53,14 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
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",
|
"version": "0.8.208",
|
||||||
"date": "2026-06-07",
|
"date": "2026-06-07",
|
||||||
|
|
|
||||||
|
|
@ -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** |
|
| **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** |
|
| **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** |
|
| **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** |
|
| **F4** | Roadmap-Review UI + `roadmap_override` | ✅ **0.8.207** |
|
||||||
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | ✅ **0.8.208** |
|
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | ✅ **0.8.208** |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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] Retrieval pro `major_step` + `stage_spec` statt iterativem Pfad-Bau
|
||||||
- [x] Gap-Angebote für unbesetzte Roadmap-Stufen (`roadmap_unfilled`)
|
- [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)
|
### F4 — UI (0.8.207)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -907,6 +907,11 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user