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 |
|
| Prompts | Migration **078/079** — Slugs in `ai_prompts` (Admin), **kein** Template im Python-Code |
|
||||||
| UI | `ExerciseProgressionPathBuilder` — Roadmap-Box (Major Steps) |
|
| 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.
|
**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 |
|
| **F0** | Spec + Doku + `planning_progression_roadmap.py` Scaffold | 🔄 0.8.204 |
|
||||||
| **F1** | `include_roadmap_preview` in API + deterministische A/B | 🔄 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 |
|
| **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 | 🔲 |
|
| **F4** | UI Roadmap-Review | 🔲 |
|
||||||
| **F5** | Trainingsplanung: eigene Pipeline + ggf. Workflow-Engine | 🔲 |
|
| **F5** | Trainingsplanung: eigene Pipeline + ggf. Workflow-Engine | 🔲 |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ from planning_exercise_path_qa import (
|
||||||
strip_off_topic_steps_from_path,
|
strip_off_topic_steps_from_path,
|
||||||
try_llm_qa_progression_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_retrieval import run_multistage_planning_retrieval
|
||||||
from planning_exercise_semantics import (
|
from planning_exercise_semantics import (
|
||||||
PlanningSemanticBrief,
|
PlanningSemanticBrief,
|
||||||
|
|
@ -47,8 +51,14 @@ from planning_exercise_suggest import (
|
||||||
resolve_planning_exercise_intent,
|
resolve_planning_exercise_intent,
|
||||||
)
|
)
|
||||||
from planning_progression_roadmap import (
|
from planning_progression_roadmap import (
|
||||||
|
MajorStep,
|
||||||
|
ProgressionRoadmapContext,
|
||||||
|
StageSpecArtifact,
|
||||||
|
build_roadmap_unfilled_gap_specs,
|
||||||
progression_roadmap_to_api_dict,
|
progression_roadmap_to_api_dict,
|
||||||
|
resolve_step_exercise_kind_filter,
|
||||||
run_progression_roadmap_pipeline,
|
run_progression_roadmap_pipeline,
|
||||||
|
stage_spec_retrieval_query,
|
||||||
)
|
)
|
||||||
from routers.training_planning import _has_planning_role
|
from routers.training_planning import _has_planning_role
|
||||||
|
|
||||||
|
|
@ -169,8 +179,12 @@ def _run_path_step_retrieval(
|
||||||
step_b: Optional[Dict[str, Any]] = None,
|
step_b: Optional[Dict[str, Any]] = None,
|
||||||
path_target_profile: Optional[PlanningTargetProfile] = None,
|
path_target_profile: Optional[PlanningTargetProfile] = None,
|
||||||
path_intent: Optional[str] = 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]:
|
) -> 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:
|
if bridge_mode and step_a and step_b:
|
||||||
phase = step_phase_for_index(semantic_brief, step_index, max_steps)
|
phase = step_phase_for_index(semantic_brief, step_index, max_steps)
|
||||||
parts = [semantic_brief.primary_topic or semantic_brief.retrieval_query or goal_query]
|
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),
|
"has_planning_reference": bool(planned_ids or anchor_id or bridge_mode),
|
||||||
"semantic_brief": semantic_brief,
|
"semantic_brief": semantic_brief,
|
||||||
"retrieval_query": step_query,
|
"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(
|
pack = apply_progression_context_to_pack(
|
||||||
cur,
|
cur,
|
||||||
|
|
@ -331,6 +346,130 @@ def _make_bridge_search_fn(
|
||||||
return _bridge_search
|
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(
|
def suggest_progression_path(
|
||||||
cur,
|
cur,
|
||||||
*,
|
*,
|
||||||
|
|
@ -360,41 +499,98 @@ def suggest_progression_path(
|
||||||
include_llm_intent=body.include_llm_intent,
|
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()
|
used: Set[int] = set()
|
||||||
steps: List[Dict[str, Any]] = []
|
steps: List[Dict[str, Any]] = []
|
||||||
planned_ids: List[int] = []
|
planned_ids: List[int] = []
|
||||||
anchor_id: Optional[int] = None
|
anchor_id: Optional[int] = None
|
||||||
anchor_variant_id: Optional[int] = None
|
anchor_variant_id: Optional[int] = None
|
||||||
|
|
||||||
for step_index in range(max_steps):
|
if roadmap_first and roadmap_ctx is not None:
|
||||||
hits, _tp, _qis, _intent = _run_path_step_retrieval(
|
steps, roadmap_unfilled = _build_steps_roadmap_first(
|
||||||
cur,
|
cur,
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
|
body=body,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
step_index=step_index,
|
|
||||||
max_steps=max_steps,
|
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,
|
semantic_brief=semantic_brief,
|
||||||
path_target_profile=path_target_profile,
|
path_target_profile=path_target_profile,
|
||||||
path_intent=path_intent,
|
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)
|
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
|
||||||
if not hit:
|
if not hit:
|
||||||
break
|
break
|
||||||
|
|
||||||
step = _hit_to_path_step(hit)
|
step = _hit_to_path_step(hit)
|
||||||
steps.append(step)
|
steps.append(step)
|
||||||
eid = int(step["exercise_id"])
|
eid = int(step["exercise_id"])
|
||||||
used.add(eid)
|
used.add(eid)
|
||||||
planned_ids.append(eid)
|
planned_ids.append(eid)
|
||||||
anchor_id = eid
|
anchor_id = eid
|
||||||
anchor_variant_id = step.get("variant_id")
|
anchor_variant_id = step.get("variant_id")
|
||||||
|
|
||||||
if len(steps) < 2:
|
if len(steps) < 2:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -490,6 +686,12 @@ def suggest_progression_path(
|
||||||
auto_insert_proposals=False,
|
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(
|
path_qa = build_path_qa_summary(
|
||||||
gaps=gaps,
|
gaps=gaps,
|
||||||
bridge_inserts=bridge_inserts,
|
bridge_inserts=bridge_inserts,
|
||||||
|
|
@ -505,6 +707,8 @@ def suggest_progression_path(
|
||||||
|
|
||||||
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:
|
||||||
|
retrieval_parts.append("roadmap_first")
|
||||||
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:
|
||||||
|
|
@ -515,20 +719,10 @@ def suggest_progression_path(
|
||||||
retrieval_parts.append("ai_gap_fill")
|
retrieval_parts.append("ai_gap_fill")
|
||||||
if gap_fill_offers:
|
if gap_fill_offers:
|
||||||
retrieval_parts.append("gap_fill_offers")
|
retrieval_parts.append("gap_fill_offers")
|
||||||
|
if include_roadmap:
|
||||||
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)
|
|
||||||
retrieval_parts.append("roadmap_preview")
|
retrieval_parts.append("roadmap_preview")
|
||||||
if body.roadmap_first:
|
if roadmap_unfilled:
|
||||||
retrieval_parts.append("roadmap_first_pending")
|
retrieval_parts.append("roadmap_unfilled")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"goal_query": goal_query,
|
"goal_query": goal_query,
|
||||||
|
|
@ -543,7 +737,8 @@ def suggest_progression_path(
|
||||||
"path_qa": path_qa,
|
"path_qa": path_qa,
|
||||||
"gap_fill_offers": gap_fill_offers,
|
"gap_fill_offers": gap_fill_offers,
|
||||||
"progression_roadmap": progression_roadmap,
|
"progression_roadmap": progression_roadmap,
|
||||||
"roadmap_first": body.roadmap_first,
|
"roadmap_first": roadmap_first,
|
||||||
|
"roadmap_unfilled_count": len(roadmap_unfilled),
|
||||||
"retrieval_phase": "+".join(retrieval_parts),
|
"retrieval_phase": "+".join(retrieval_parts),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
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
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
|
|
@ -421,6 +421,107 @@ def consolidate_micro_to_major(
|
||||||
return majors, notes
|
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(
|
def build_stage_specs(
|
||||||
major_steps: Sequence[MajorStep],
|
major_steps: Sequence[MajorStep],
|
||||||
*,
|
*,
|
||||||
|
|
@ -553,7 +654,11 @@ __all__ = [
|
||||||
"RoadmapArtifact",
|
"RoadmapArtifact",
|
||||||
"StageSpecArtifact",
|
"StageSpecArtifact",
|
||||||
"build_goal_analysis",
|
"build_goal_analysis",
|
||||||
|
"build_roadmap_unfilled_gap_specs",
|
||||||
"build_stage_specs",
|
"build_stage_specs",
|
||||||
|
"resolve_step_exercise_kind_filter",
|
||||||
|
"stage_spec_exercise_kind_filter",
|
||||||
|
"stage_spec_retrieval_query",
|
||||||
"consolidate_micro_to_major",
|
"consolidate_micro_to_major",
|
||||||
"develop_micro_objectives",
|
"develop_micro_objectives",
|
||||||
"progression_roadmap_to_api_dict",
|
"progression_roadmap_to_api_dict",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
"""Tests Planungs-KI Phase C3/E — Pfad-Vorschläge."""
|
"""Tests Planungs-KI Phase C3/E/F — Pfad-Vorschläge."""
|
||||||
from planning_exercise_path_builder import _pick_best_path_hit, _hit_to_path_step
|
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():
|
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["exercise_id"] == 10
|
||||||
assert step["variant_id"] == 7
|
assert step["variant_id"] == 7
|
||||||
assert step["suggested_variant_name"] == "Leicht"
|
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_GOAL_ANALYSIS,
|
||||||
PROMPT_SLUG_ROADMAP,
|
PROMPT_SLUG_ROADMAP,
|
||||||
PROMPT_SLUG_STAGE_SPEC,
|
PROMPT_SLUG_STAGE_SPEC,
|
||||||
|
MajorStep,
|
||||||
|
StageSpecArtifact,
|
||||||
build_goal_analysis,
|
build_goal_analysis,
|
||||||
|
build_roadmap_unfilled_gap_specs,
|
||||||
consolidate_micro_to_major,
|
consolidate_micro_to_major,
|
||||||
develop_micro_objectives,
|
develop_micro_objectives,
|
||||||
progression_roadmap_to_api_dict,
|
progression_roadmap_to_api_dict,
|
||||||
|
resolve_step_exercise_kind_filter,
|
||||||
run_progression_roadmap_pipeline,
|
run_progression_roadmap_pipeline,
|
||||||
|
stage_spec_exercise_kind_filter,
|
||||||
|
stage_spec_retrieval_query,
|
||||||
)
|
)
|
||||||
from planning_exercise_semantics import build_semantic_brief
|
from planning_exercise_semantics import build_semantic_brief
|
||||||
|
|
||||||
|
|
@ -43,6 +49,47 @@ def test_major_steps_have_learning_goals():
|
||||||
assert step.consolidates
|
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():
|
def test_api_dict_exposes_prompt_slug_catalog():
|
||||||
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
||||||
api = progression_roadmap_to_api_dict(ctx)
|
api = progression_roadmap_to_api_dict(ctx)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.205"
|
APP_VERSION = "0.8.206"
|
||||||
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.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_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,15 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
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",
|
"version": "0.8.205",
|
||||||
"date": "2026-06-07",
|
"date": "2026-06-07",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||||
|
|
||||||
**Stand:** 2026-06-07
|
**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**.
|
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** |
|
| **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** |
|
| **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–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) | 🔲 |
|
| **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`**
|
**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
|
**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**)
|
#### Ü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
|
- [x] Deterministischer Fallback wenn Prompt/OpenRouter fehlt
|
||||||
- [ ] Response/UI: genutzte `prompt_slugs` sichtbar machen (Admin-Hinweis)
|
- [ ] 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
|
- [x] Retrieval pro `major_step` + `stage_spec` statt iterativem Pfad-Bau
|
||||||
- [ ] QA/Lücken an Roadmap koppeln
|
- [x] Gap-Angebote für unbesetzte Roadmap-Stufen (`roadmap_unfilled`)
|
||||||
|
- [ ] QA/Lücken vollständig an Roadmap koppeln (Brücken optional reduzieren)
|
||||||
|
|
||||||
### F4 — UI
|
### F4 — UI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ const OFFER_SOURCE_LABELS = {
|
||||||
unfilled_gap: 'Lücke',
|
unfilled_gap: 'Lücke',
|
||||||
off_topic: 'Themenfremd',
|
off_topic: 'Themenfremd',
|
||||||
llm_suggested: 'QS-Empfehlung',
|
llm_suggested: 'QS-Empfehlung',
|
||||||
|
roadmap_unfilled: 'Roadmap-Stufe',
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDefaultFocusAreaId(targetSummary, focusAreas) {
|
function resolveDefaultFocusAreaId(targetSummary, focusAreas) {
|
||||||
|
|
@ -349,6 +350,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
include_ai_gap_fill: true,
|
include_ai_gap_fill: true,
|
||||||
include_roadmap_preview: true,
|
include_roadmap_preview: true,
|
||||||
include_llm_roadmap: true,
|
include_llm_roadmap: true,
|
||||||
|
roadmap_first: true,
|
||||||
progression_graph_id: Number(graphId),
|
progression_graph_id: Number(graphId),
|
||||||
})
|
})
|
||||||
const qa = res?.path_qa || null
|
const qa = res?.path_qa || null
|
||||||
|
|
@ -531,7 +533,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
{progressionRoadmap.llm_roadmap_applied
|
{progressionRoadmap.llm_roadmap_applied
|
||||||
? ' (KI-Prompts aus Admin-Konfiguration)'
|
? ' (KI-Prompts aus Admin-Konfiguration)'
|
||||||
: ' (heuristischer Fallback — KI-Prompts in ai_prompts)'}
|
: ' (heuristischer Fallback — KI-Prompts in ai_prompts)'}
|
||||||
. Übungen unten: Bibliothekssuche (Übergangsphase).
|
. Übungen unten: je Major Step aus der Bibliothek (roadmap-first).
|
||||||
</p>
|
</p>
|
||||||
<ol style={{ margin: 0, paddingLeft: '18px', fontSize: '13px', lineHeight: 1.5 }}>
|
<ol style={{ margin: 0, paddingLeft: '18px', fontSize: '13px', lineHeight: 1.5 }}>
|
||||||
{progressionRoadmap.roadmap.major_steps.map((step) => (
|
{progressionRoadmap.roadmap.major_steps.map((step) => (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user