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
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:
parent
7411543a97
commit
d4e9bded23
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 | 🔲 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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,12 +499,69 @@ 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
|
||||
|
||||
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,
|
||||
max_steps=max_steps,
|
||||
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,
|
||||
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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**)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user