Implement Off-Topic Slot Gap Specification and Unified Slot Review Enhancements
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 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m34s
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 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m34s
- Introduced `_build_off_topic_slot_gap_spec` to generate specifications for off-topic slots, improving the handling of filled but thematically inappropriate slots. - Added `_build_unified_slot_review_entry` to streamline the review process for slots, incorporating various parameters for better evaluation and suggestions. - Enhanced existing logic in slot management to improve the robustness of path evaluations and user feedback. - Added tests for the new off-topic slot gap specification to ensure functionality and correctness.
This commit is contained in:
parent
cd457e3ea0
commit
f0e581a9f5
|
|
@ -2024,6 +2024,56 @@ def _build_evaluate_empty_slot_gap_specs(
|
||||||
return specs[:8]
|
return specs[:8]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_off_topic_slot_gap_spec(
|
||||||
|
step: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
goal_query: str = "",
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""KI-Angebot für belegten, aber themenfremden Slot (Ersatz statt Leerstelle)."""
|
||||||
|
del goal_query
|
||||||
|
major_idx = step.get("roadmap_major_step_index")
|
||||||
|
if major_idx is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
roadmap_idx = int(major_idx)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
phase = (step.get("roadmap_phase") or "vertiefung").strip().lower()
|
||||||
|
learning_goal = (step.get("roadmap_learning_goal") or step.get("title") or "").strip()
|
||||||
|
rejected_title = (step.get("title") or "").strip()
|
||||||
|
title_hint = learning_goal[:120] if learning_goal else f"Slot {roadmap_idx + 1}"
|
||||||
|
rationale = (
|
||||||
|
f"Slot {roadmap_idx + 1}: Ersatz für „{rejected_title}“ — passende Übung per KI."
|
||||||
|
if rejected_title
|
||||||
|
else f"Slot {roadmap_idx + 1} — KI-Entwurf für diese Roadmap-Stufe."
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"source": "off_topic",
|
||||||
|
"insert_after_index": max(roadmap_idx - 1, -1),
|
||||||
|
"replace_step_index": roadmap_idx,
|
||||||
|
"gap": {
|
||||||
|
"expected_phase": phase,
|
||||||
|
"roadmap_major_step_index": roadmap_idx,
|
||||||
|
"learning_goal": learning_goal,
|
||||||
|
},
|
||||||
|
"phase": phase,
|
||||||
|
"title_hint": title_hint,
|
||||||
|
"sketch": learning_goal or title_hint,
|
||||||
|
"rationale": rationale[:400],
|
||||||
|
"roadmap_major_step_index": roadmap_idx,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _gap_offer_major_index(offer: Mapping[str, Any]) -> Optional[int]:
|
||||||
|
raw = offer.get("roadmap_major_step_index")
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _run_evaluate_only_path_qa(
|
def _run_evaluate_only_path_qa(
|
||||||
cur,
|
cur,
|
||||||
*,
|
*,
|
||||||
|
|
@ -3107,6 +3157,261 @@ def _slot_auto_select_library(
|
||||||
return float(proposed_slot_score) > float(baseline_slot_score) + 0.001
|
return float(proposed_slot_score) > float(baseline_slot_score) + 0.001
|
||||||
|
|
||||||
|
|
||||||
|
def _build_unified_slot_review_entry(
|
||||||
|
cur,
|
||||||
|
*,
|
||||||
|
tenant: TenantContext,
|
||||||
|
body: ProgressionPathSuggestRequest,
|
||||||
|
goal_query: str,
|
||||||
|
max_steps: int,
|
||||||
|
semantic_brief: PlanningSemanticBrief,
|
||||||
|
path_target_profile: PlanningTargetProfile,
|
||||||
|
path_intent: str,
|
||||||
|
roadmap_ctx: ProgressionRoadmapContext,
|
||||||
|
stage_spec: StageSpecArtifact,
|
||||||
|
step_index: int,
|
||||||
|
stage_count: int,
|
||||||
|
major_idx: int,
|
||||||
|
current: Mapping[str, Any],
|
||||||
|
baseline_steps: Sequence[Mapping[str, Any]],
|
||||||
|
baseline_qa: Mapping[str, Any],
|
||||||
|
baseline_score: Optional[float],
|
||||||
|
steps_by_major: Mapping[int, Mapping[str, Any]],
|
||||||
|
problem_slots: Mapping[int, Sequence[str]],
|
||||||
|
off_topic_map: Mapping[int, Sequence[str]],
|
||||||
|
off_topic_scores: Mapping[int, float],
|
||||||
|
gap_fill_offers: List[Dict[str, Any]],
|
||||||
|
suggestions: List[Dict[str, Any]],
|
||||||
|
rejected: List[Dict[str, Any]],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
current = dict(current or {})
|
||||||
|
current.setdefault("roadmap_major_step_index", major_idx)
|
||||||
|
current.setdefault("roadmap_learning_goal", stage_spec.learning_goal)
|
||||||
|
current_id = current.get("exercise_id")
|
||||||
|
slot_problem = major_idx in problem_slots
|
||||||
|
off_topic = slot_problem or major_idx in off_topic_map or bool(
|
||||||
|
current.get("slot_status") in {"off_topic", "stripped"}
|
||||||
|
)
|
||||||
|
off_reasons = list(problem_slots.get(major_idx, [])) + list(off_topic_map.get(major_idx, []))
|
||||||
|
|
||||||
|
baseline_slot_score: Optional[float] = off_topic_scores.get(major_idx)
|
||||||
|
if baseline_slot_score is None and current_id is not None and not current.get("is_ai_proposal"):
|
||||||
|
baseline_slot_score = _score_exercise_stage_fit_for_spec(
|
||||||
|
cur,
|
||||||
|
exercise_id=int(current_id),
|
||||||
|
step=current,
|
||||||
|
stage_spec=stage_spec,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
step_index=step_index,
|
||||||
|
stage_count=stage_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
planned_ids = [
|
||||||
|
int(s["exercise_id"])
|
||||||
|
for midx, s in sorted(steps_by_major.items())
|
||||||
|
if midx != major_idx and s.get("exercise_id") is not None
|
||||||
|
]
|
||||||
|
anchor_id: Optional[int] = None
|
||||||
|
anchor_variant_id: Optional[int] = None
|
||||||
|
used_other: Set[int] = set(planned_ids)
|
||||||
|
for midx in sorted(steps_by_major):
|
||||||
|
if midx >= major_idx:
|
||||||
|
break
|
||||||
|
step = steps_by_major[midx]
|
||||||
|
eid = step.get("exercise_id")
|
||||||
|
if eid is not None:
|
||||||
|
anchor_id = int(eid)
|
||||||
|
vid = step.get("variant_id")
|
||||||
|
anchor_variant_id = int(vid) if vid is not None else None
|
||||||
|
|
||||||
|
candidates = _roadmap_slot_library_candidates(
|
||||||
|
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,
|
||||||
|
stage_spec=stage_spec,
|
||||||
|
step_index=step_index,
|
||||||
|
stage_count=stage_count,
|
||||||
|
planned_ids=planned_ids,
|
||||||
|
anchor_id=anchor_id,
|
||||||
|
anchor_variant_id=anchor_variant_id,
|
||||||
|
used=used_other,
|
||||||
|
exclude_exercise_id=int(current_id) if current_id is not None else None,
|
||||||
|
max_candidates=5,
|
||||||
|
skip_post_match_gate=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
best_candidate: Optional[Dict[str, Any]] = None
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
cand_id = int(candidate.get("exercise_id"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if current_id is not None and int(current_id) == cand_id:
|
||||||
|
continue
|
||||||
|
best_candidate = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
library_alt: Optional[Dict[str, Any]] = None
|
||||||
|
if best_candidate is not None:
|
||||||
|
try:
|
||||||
|
cand_id = int(best_candidate.get("exercise_id"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
cand_id = None
|
||||||
|
if cand_id is not None:
|
||||||
|
proposed_slot_score = _score_exercise_stage_fit_for_spec(
|
||||||
|
cur,
|
||||||
|
exercise_id=cand_id,
|
||||||
|
step={**current, **best_candidate, "roadmap_major_step_index": major_idx},
|
||||||
|
stage_spec=stage_spec,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
step_index=step_index,
|
||||||
|
stage_count=stage_count,
|
||||||
|
)
|
||||||
|
suggestion_type = (
|
||||||
|
"remove_and_replace"
|
||||||
|
if (off_topic or slot_problem) and current_id is not None
|
||||||
|
else ("library_fill" if current_id is None else "library_improvement")
|
||||||
|
)
|
||||||
|
auto_select = _slot_auto_select_library(
|
||||||
|
baseline_slot_score=baseline_slot_score,
|
||||||
|
proposed_slot_score=proposed_slot_score,
|
||||||
|
baseline_exercise_id=int(current_id) if current_id is not None else None,
|
||||||
|
proposed_exercise_id=cand_id,
|
||||||
|
)
|
||||||
|
slot_score_delta = (
|
||||||
|
round(float(proposed_slot_score) - float(baseline_slot_score), 4)
|
||||||
|
if proposed_slot_score is not None and baseline_slot_score is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
pro_contra = _build_slot_pro_contra(
|
||||||
|
current_step=current,
|
||||||
|
proposed_step=best_candidate,
|
||||||
|
suggestion_type=suggestion_type,
|
||||||
|
baseline_qa=baseline_qa,
|
||||||
|
projected_qa=None,
|
||||||
|
quality_delta=None,
|
||||||
|
off_topic_reasons=off_reasons,
|
||||||
|
candidate_reasons=best_candidate.get("reasons") or [],
|
||||||
|
)
|
||||||
|
if slot_score_delta is not None and slot_score_delta > 0:
|
||||||
|
fit_msg = f"Stufen-Fit +{round(slot_score_delta * 100)} Prozentpunkte"
|
||||||
|
if fit_msg not in pro_contra["proposed_pro"]:
|
||||||
|
pro_contra["proposed_pro"].insert(0, fit_msg)
|
||||||
|
library_alt = {
|
||||||
|
"exercise_id": cand_id,
|
||||||
|
"title": (best_candidate.get("title") or "").strip() or None,
|
||||||
|
"slot_score": proposed_slot_score,
|
||||||
|
"slot_score_delta": slot_score_delta,
|
||||||
|
"quality_delta": None,
|
||||||
|
"auto_select": auto_select,
|
||||||
|
"suggestion_type": suggestion_type,
|
||||||
|
"reasons": list(best_candidate.get("reasons") or [])[:4],
|
||||||
|
"pro_contra": pro_contra,
|
||||||
|
}
|
||||||
|
lib_entry = {
|
||||||
|
"roadmap_major_step_index": major_idx,
|
||||||
|
"baseline_exercise_id": int(current_id) if current_id is not None else None,
|
||||||
|
"baseline_title": (current.get("title") or "").strip() or None,
|
||||||
|
"proposed_exercise_id": cand_id,
|
||||||
|
"proposed_title": library_alt["title"],
|
||||||
|
"baseline_slot_status": current.get("slot_status"),
|
||||||
|
"proposed_slot_status": best_candidate.get("slot_status") or "matched",
|
||||||
|
"suggestion_type": suggestion_type,
|
||||||
|
"quality_delta": None,
|
||||||
|
"baseline_slot_score": baseline_slot_score,
|
||||||
|
"proposed_slot_score": proposed_slot_score,
|
||||||
|
"slot_score_delta": slot_score_delta,
|
||||||
|
"auto_select": auto_select,
|
||||||
|
"baseline_quality_score": baseline_score,
|
||||||
|
"improves_path": auto_select,
|
||||||
|
"off_topic": off_topic,
|
||||||
|
"slot_problem": slot_problem,
|
||||||
|
"problem_reasons": off_reasons[:6],
|
||||||
|
"proposed_is_ai_proposal": False,
|
||||||
|
"pro_contra": pro_contra,
|
||||||
|
}
|
||||||
|
if auto_select:
|
||||||
|
suggestions.append(lib_entry)
|
||||||
|
else:
|
||||||
|
rejected.append(lib_entry)
|
||||||
|
|
||||||
|
show_ai_option = bool(
|
||||||
|
body.include_ai_gap_fill
|
||||||
|
and (
|
||||||
|
current_id is None
|
||||||
|
or off_topic
|
||||||
|
or slot_problem
|
||||||
|
or bool(current.get("is_ai_proposal"))
|
||||||
|
or (
|
||||||
|
baseline_slot_score is not None
|
||||||
|
and baseline_slot_score < _SLOT_FIT_POOR_THRESHOLD
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ai_alt: Optional[Dict[str, Any]] = None
|
||||||
|
if show_ai_option:
|
||||||
|
slot_offer = next(
|
||||||
|
(
|
||||||
|
o
|
||||||
|
for o in gap_fill_offers
|
||||||
|
if isinstance(o, dict) and _gap_offer_major_index(o) == major_idx
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not slot_offer:
|
||||||
|
gap_spec: Optional[Dict[str, Any]] = None
|
||||||
|
if current_id is None:
|
||||||
|
empty_specs = _build_evaluate_empty_slot_gap_specs(
|
||||||
|
[current],
|
||||||
|
goal_query=goal_query,
|
||||||
|
)
|
||||||
|
gap_spec = empty_specs[0] if empty_specs else None
|
||||||
|
elif off_topic or slot_problem:
|
||||||
|
gap_spec = _build_off_topic_slot_gap_spec(current, goal_query=goal_query)
|
||||||
|
if gap_spec:
|
||||||
|
slot_offer = build_gap_fill_offer(
|
||||||
|
spec=gap_spec,
|
||||||
|
steps=baseline_steps,
|
||||||
|
goal_query=goal_query,
|
||||||
|
brief=semantic_brief,
|
||||||
|
proposal=None,
|
||||||
|
roadmap_snapshot=_roadmap_gap_snapshot_for_spec(
|
||||||
|
cur,
|
||||||
|
roadmap_ctx,
|
||||||
|
gap_spec,
|
||||||
|
goal_query=goal_query,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
gap_fill_offers.append(slot_offer)
|
||||||
|
if slot_offer:
|
||||||
|
ai_alt = {
|
||||||
|
"title_hint": slot_offer.get("title_hint") or f"Slot {major_idx + 1}",
|
||||||
|
"gap_offer": slot_offer,
|
||||||
|
"auto_select": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"roadmap_major_step_index": major_idx,
|
||||||
|
"roadmap_learning_goal": (stage_spec.learning_goal or "").strip() or None,
|
||||||
|
"baseline_exercise_id": int(current_id) if current_id is not None else None,
|
||||||
|
"baseline_title": (current.get("title") or "").strip() or None,
|
||||||
|
"baseline_slot_score": baseline_slot_score,
|
||||||
|
"baseline_slot_status": current.get("slot_status"),
|
||||||
|
"slot_problem": slot_problem,
|
||||||
|
"off_topic": off_topic,
|
||||||
|
"problem_reasons": off_reasons[:6],
|
||||||
|
"library_alternative": library_alt,
|
||||||
|
"ai_alternative": ai_alt,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _run_unified_slot_improvement_review(
|
def _run_unified_slot_improvement_review(
|
||||||
cur,
|
cur,
|
||||||
*,
|
*,
|
||||||
|
|
@ -3124,7 +3429,7 @@ def _run_unified_slot_improvement_review(
|
||||||
roadmap_edited: bool,
|
roadmap_edited: bool,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Ein Workflow: Pfad bewerten → je Slot Alternativen suchen → Einzel-QS → nur Verbesserungen.
|
Ein Workflow: Pfad bewerten → je Slot Alternativen suchen → Stufen-Fit vergleichen.
|
||||||
"""
|
"""
|
||||||
if not body.baseline_evaluate_steps:
|
if not body.baseline_evaluate_steps:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -3137,6 +3442,52 @@ def _run_unified_slot_improvement_review(
|
||||||
detail="unified_slot_review erfordert Roadmap (roadmap_override / roadmap_first)",
|
detail="unified_slot_review erfordert Roadmap (roadmap_override / roadmap_first)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return _run_unified_slot_improvement_review_core(
|
||||||
|
cur,
|
||||||
|
tenant=tenant,
|
||||||
|
body=body,
|
||||||
|
goal_query=goal_query,
|
||||||
|
max_steps=max_steps,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
semantic_llm_applied=semantic_llm_applied,
|
||||||
|
path_target_profile=path_target_profile,
|
||||||
|
path_intent=path_intent,
|
||||||
|
first_intent_summary=first_intent_summary,
|
||||||
|
roadmap_ctx=roadmap_ctx,
|
||||||
|
progression_roadmap=progression_roadmap,
|
||||||
|
roadmap_edited=roadmap_edited,
|
||||||
|
)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"unified_slot_review fehlgeschlagen: {exc}",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _run_unified_slot_improvement_review_core(
|
||||||
|
cur,
|
||||||
|
*,
|
||||||
|
tenant: TenantContext,
|
||||||
|
body: ProgressionPathSuggestRequest,
|
||||||
|
goal_query: str,
|
||||||
|
max_steps: int,
|
||||||
|
semantic_brief: PlanningSemanticBrief,
|
||||||
|
semantic_llm_applied: bool,
|
||||||
|
path_target_profile: PlanningTargetProfile,
|
||||||
|
path_intent: str,
|
||||||
|
first_intent_summary: Mapping[str, Any],
|
||||||
|
roadmap_ctx: ProgressionRoadmapContext,
|
||||||
|
progression_roadmap: Optional[Dict[str, Any]],
|
||||||
|
roadmap_edited: bool,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
if not body.baseline_evaluate_steps:
|
||||||
|
raise HTTPException(status_code=400, detail="baseline_evaluate_steps fehlt")
|
||||||
|
if not roadmap_ctx.stage_specs:
|
||||||
|
raise HTTPException(status_code=400, detail="roadmap stage_specs fehlt")
|
||||||
|
|
||||||
baseline_steps = _evaluate_steps_from_payload(cur, body.baseline_evaluate_steps)
|
baseline_steps = _evaluate_steps_from_payload(cur, body.baseline_evaluate_steps)
|
||||||
snapshot = (
|
snapshot = (
|
||||||
dict(body.baseline_path_qa_snapshot)
|
dict(body.baseline_path_qa_snapshot)
|
||||||
|
|
@ -3206,47 +3557,8 @@ def _run_unified_slot_improvement_review(
|
||||||
|
|
||||||
for step_index, stage_spec in enumerate(roadmap_ctx.stage_specs):
|
for step_index, stage_spec in enumerate(roadmap_ctx.stage_specs):
|
||||||
major_idx = int(stage_spec.major_step_index)
|
major_idx = int(stage_spec.major_step_index)
|
||||||
current = dict(steps_by_major.get(major_idx, {}))
|
try:
|
||||||
current.setdefault("roadmap_major_step_index", major_idx)
|
slot_review = _build_unified_slot_review_entry(
|
||||||
current.setdefault("roadmap_learning_goal", stage_spec.learning_goal)
|
|
||||||
current_id = current.get("exercise_id")
|
|
||||||
slot_problem = major_idx in problem_slots
|
|
||||||
off_topic = slot_problem or major_idx in off_topic_map or bool(
|
|
||||||
current.get("slot_status") in {"off_topic", "stripped"}
|
|
||||||
)
|
|
||||||
off_reasons = list(problem_slots.get(major_idx, [])) + off_topic_map.get(major_idx, [])
|
|
||||||
|
|
||||||
baseline_slot_score: Optional[float] = off_topic_scores.get(major_idx)
|
|
||||||
if baseline_slot_score is None and current_id is not None and not current.get("is_ai_proposal"):
|
|
||||||
baseline_slot_score = _score_exercise_stage_fit_for_spec(
|
|
||||||
cur,
|
|
||||||
exercise_id=int(current_id),
|
|
||||||
step=current,
|
|
||||||
stage_spec=stage_spec,
|
|
||||||
semantic_brief=semantic_brief,
|
|
||||||
step_index=step_index,
|
|
||||||
stage_count=stage_count,
|
|
||||||
)
|
|
||||||
|
|
||||||
planned_ids = [
|
|
||||||
int(s["exercise_id"])
|
|
||||||
for midx, s in sorted(steps_by_major.items())
|
|
||||||
if midx != major_idx and s.get("exercise_id") is not None
|
|
||||||
]
|
|
||||||
anchor_id: Optional[int] = None
|
|
||||||
anchor_variant_id: Optional[int] = None
|
|
||||||
used_other: Set[int] = set(planned_ids)
|
|
||||||
for midx in sorted(steps_by_major):
|
|
||||||
if midx >= major_idx:
|
|
||||||
break
|
|
||||||
step = steps_by_major[midx]
|
|
||||||
eid = step.get("exercise_id")
|
|
||||||
if eid is not None:
|
|
||||||
anchor_id = int(eid)
|
|
||||||
vid = step.get("variant_id")
|
|
||||||
anchor_variant_id = int(vid) if vid is not None else None
|
|
||||||
|
|
||||||
candidates = _roadmap_slot_library_candidates(
|
|
||||||
cur,
|
cur,
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
body=body,
|
body=body,
|
||||||
|
|
@ -3259,203 +3571,35 @@ def _run_unified_slot_improvement_review(
|
||||||
stage_spec=stage_spec,
|
stage_spec=stage_spec,
|
||||||
step_index=step_index,
|
step_index=step_index,
|
||||||
stage_count=stage_count,
|
stage_count=stage_count,
|
||||||
planned_ids=planned_ids,
|
major_idx=major_idx,
|
||||||
anchor_id=anchor_id,
|
current=steps_by_major.get(major_idx, {}),
|
||||||
anchor_variant_id=anchor_variant_id,
|
baseline_steps=baseline_steps,
|
||||||
used=used_other,
|
|
||||||
exclude_exercise_id=int(current_id) if current_id is not None else None,
|
|
||||||
max_candidates=5,
|
|
||||||
skip_post_match_gate=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
best_candidate: Optional[Dict[str, Any]] = None
|
|
||||||
for candidate in candidates:
|
|
||||||
try:
|
|
||||||
cand_id = int(candidate.get("exercise_id"))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
if current_id is not None and int(current_id) == cand_id:
|
|
||||||
continue
|
|
||||||
best_candidate = candidate
|
|
||||||
break
|
|
||||||
|
|
||||||
proposed_slot_score: Optional[float] = None
|
|
||||||
quality_delta: Optional[float] = None
|
|
||||||
projected_qa: Optional[Dict[str, Any]] = None
|
|
||||||
library_alt: Optional[Dict[str, Any]] = None
|
|
||||||
if best_candidate is not None:
|
|
||||||
try:
|
|
||||||
cand_id = int(best_candidate.get("exercise_id"))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
cand_id = None
|
|
||||||
if cand_id is not None:
|
|
||||||
proposed_slot_score = _score_exercise_stage_fit_for_spec(
|
|
||||||
cur,
|
|
||||||
exercise_id=cand_id,
|
|
||||||
step={**current, **best_candidate, "roadmap_major_step_index": major_idx},
|
|
||||||
stage_spec=stage_spec,
|
|
||||||
semantic_brief=semantic_brief,
|
|
||||||
step_index=step_index,
|
|
||||||
stage_count=stage_count,
|
|
||||||
)
|
|
||||||
diff_stub = {
|
|
||||||
"roadmap_major_step_index": major_idx,
|
|
||||||
"baseline_exercise_id": int(current_id) if current_id is not None else None,
|
|
||||||
"baseline_title": (current.get("title") or "").strip() or None,
|
|
||||||
"proposed_exercise_id": cand_id,
|
|
||||||
"proposed_title": (best_candidate.get("title") or "").strip() or None,
|
|
||||||
}
|
|
||||||
merged_steps = _apply_slot_diff_to_steps(baseline_steps, diff_stub, baseline_steps)
|
|
||||||
for i, raw in enumerate(merged_steps):
|
|
||||||
if int(raw.get("roadmap_major_step_index", -1)) == major_idx:
|
|
||||||
merged_steps[i] = {
|
|
||||||
**raw,
|
|
||||||
**best_candidate,
|
|
||||||
"roadmap_major_step_index": major_idx,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
projected_qa = _quick_evaluate_steps_qa(
|
|
||||||
cur,
|
|
||||||
goal_query=goal_query,
|
|
||||||
semantic_brief=semantic_brief,
|
|
||||||
steps=merged_steps,
|
|
||||||
roadmap_ctx=roadmap_ctx,
|
|
||||||
)
|
|
||||||
quality_delta = _quality_delta(
|
|
||||||
baseline_score,
|
|
||||||
_path_qa_quality_score(projected_qa),
|
|
||||||
)
|
|
||||||
suggestion_type = (
|
|
||||||
"remove_and_replace"
|
|
||||||
if (off_topic or slot_problem) and current_id is not None
|
|
||||||
else ("library_fill" if current_id is None else "library_improvement")
|
|
||||||
)
|
|
||||||
auto_select = _slot_auto_select_library(
|
|
||||||
baseline_slot_score=baseline_slot_score,
|
|
||||||
proposed_slot_score=proposed_slot_score,
|
|
||||||
baseline_exercise_id=int(current_id) if current_id is not None else None,
|
|
||||||
proposed_exercise_id=cand_id,
|
|
||||||
)
|
|
||||||
library_alt = {
|
|
||||||
"exercise_id": cand_id,
|
|
||||||
"title": (best_candidate.get("title") or "").strip() or None,
|
|
||||||
"slot_score": proposed_slot_score,
|
|
||||||
"slot_score_delta": (
|
|
||||||
round(float(proposed_slot_score) - float(baseline_slot_score), 4)
|
|
||||||
if proposed_slot_score is not None and baseline_slot_score is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"quality_delta": quality_delta,
|
|
||||||
"auto_select": auto_select,
|
|
||||||
"suggestion_type": suggestion_type,
|
|
||||||
"reasons": list(best_candidate.get("reasons") or [])[:4],
|
|
||||||
"pro_contra": _build_slot_pro_contra(
|
|
||||||
current_step=current,
|
|
||||||
proposed_step=best_candidate,
|
|
||||||
suggestion_type=suggestion_type,
|
|
||||||
baseline_qa=baseline_qa,
|
baseline_qa=baseline_qa,
|
||||||
projected_qa=projected_qa,
|
baseline_score=baseline_score,
|
||||||
quality_delta=quality_delta,
|
steps_by_major=steps_by_major,
|
||||||
off_topic_reasons=off_reasons,
|
problem_slots=problem_slots,
|
||||||
candidate_reasons=best_candidate.get("reasons") or [],
|
off_topic_map=off_topic_map,
|
||||||
),
|
off_topic_scores=off_topic_scores,
|
||||||
}
|
gap_fill_offers=gap_fill_offers,
|
||||||
lib_entry = {
|
suggestions=suggestions,
|
||||||
"roadmap_major_step_index": major_idx,
|
rejected=rejected,
|
||||||
"baseline_exercise_id": int(current_id) if current_id is not None else None,
|
|
||||||
"baseline_title": (current.get("title") or "").strip() or None,
|
|
||||||
"proposed_exercise_id": cand_id,
|
|
||||||
"proposed_title": library_alt["title"],
|
|
||||||
"baseline_slot_status": current.get("slot_status"),
|
|
||||||
"proposed_slot_status": best_candidate.get("slot_status") or "matched",
|
|
||||||
"suggestion_type": suggestion_type,
|
|
||||||
"quality_delta": quality_delta,
|
|
||||||
"baseline_slot_score": baseline_slot_score,
|
|
||||||
"proposed_slot_score": proposed_slot_score,
|
|
||||||
"slot_score_delta": library_alt["slot_score_delta"],
|
|
||||||
"auto_select": auto_select,
|
|
||||||
"baseline_quality_score": baseline_score,
|
|
||||||
"projected_quality_score": _path_qa_quality_score(projected_qa),
|
|
||||||
"projected_path_qa": projected_qa,
|
|
||||||
"improves_path": auto_select,
|
|
||||||
"off_topic": off_topic,
|
|
||||||
"slot_problem": slot_problem,
|
|
||||||
"problem_reasons": off_reasons[:6],
|
|
||||||
"proposed_is_ai_proposal": False,
|
|
||||||
"pro_contra": library_alt["pro_contra"],
|
|
||||||
}
|
|
||||||
if auto_select:
|
|
||||||
suggestions.append(lib_entry)
|
|
||||||
elif cand_id is not None:
|
|
||||||
rejected.append(lib_entry)
|
|
||||||
|
|
||||||
show_ai_option = bool(
|
|
||||||
body.include_ai_gap_fill
|
|
||||||
and (
|
|
||||||
current_id is None
|
|
||||||
or off_topic
|
|
||||||
or slot_problem
|
|
||||||
or bool(current.get("is_ai_proposal"))
|
|
||||||
or (
|
|
||||||
baseline_slot_score is not None
|
|
||||||
and baseline_slot_score < _SLOT_FIT_POOR_THRESHOLD
|
|
||||||
)
|
)
|
||||||
)
|
except Exception as exc:
|
||||||
)
|
slot_review = {
|
||||||
ai_alt: Optional[Dict[str, Any]] = None
|
|
||||||
if show_ai_option:
|
|
||||||
slot_offer = next(
|
|
||||||
(
|
|
||||||
o
|
|
||||||
for o in gap_fill_offers
|
|
||||||
if isinstance(o, dict)
|
|
||||||
and int(o.get("roadmap_major_step_index", -1)) == major_idx
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if not slot_offer:
|
|
||||||
empty_specs = _build_evaluate_empty_slot_gap_specs(
|
|
||||||
[current],
|
|
||||||
goal_query=goal_query,
|
|
||||||
)
|
|
||||||
if empty_specs:
|
|
||||||
slot_offer = build_gap_fill_offer(
|
|
||||||
spec=empty_specs[0],
|
|
||||||
steps=baseline_steps,
|
|
||||||
goal_query=goal_query,
|
|
||||||
brief=semantic_brief,
|
|
||||||
proposal=None,
|
|
||||||
roadmap_snapshot=_roadmap_gap_snapshot_for_spec(
|
|
||||||
cur,
|
|
||||||
roadmap_ctx,
|
|
||||||
empty_specs[0],
|
|
||||||
goal_query=goal_query,
|
|
||||||
semantic_brief=semantic_brief,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
gap_fill_offers.append(slot_offer)
|
|
||||||
if slot_offer:
|
|
||||||
ai_alt = {
|
|
||||||
"title_hint": slot_offer.get("title_hint") or f"Slot {major_idx + 1}",
|
|
||||||
"gap_offer": slot_offer,
|
|
||||||
"auto_select": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
slot_reviews.append(
|
|
||||||
{
|
|
||||||
"roadmap_major_step_index": major_idx,
|
"roadmap_major_step_index": major_idx,
|
||||||
"roadmap_learning_goal": (stage_spec.learning_goal or "").strip() or None,
|
"roadmap_learning_goal": (stage_spec.learning_goal or "").strip() or None,
|
||||||
"baseline_exercise_id": int(current_id) if current_id is not None else None,
|
"baseline_exercise_id": None,
|
||||||
"baseline_title": (current.get("title") or "").strip() or None,
|
"baseline_title": None,
|
||||||
"baseline_slot_score": baseline_slot_score,
|
"baseline_slot_score": None,
|
||||||
"baseline_slot_status": current.get("slot_status"),
|
"baseline_slot_status": None,
|
||||||
"slot_problem": slot_problem,
|
"slot_problem": major_idx in problem_slots,
|
||||||
"off_topic": off_topic,
|
"off_topic": major_idx in off_topic_map,
|
||||||
"problem_reasons": off_reasons[:6],
|
"problem_reasons": [f"Slot-Review fehlgeschlagen: {exc}"[:300]],
|
||||||
"library_alternative": library_alt,
|
"library_alternative": None,
|
||||||
"ai_alternative": ai_alt,
|
"ai_alternative": None,
|
||||||
|
"review_error": str(exc)[:300],
|
||||||
}
|
}
|
||||||
)
|
slot_reviews.append(slot_review)
|
||||||
|
|
||||||
improvement_diffs = [_suggestion_as_slot_diff(s) for s in suggestions]
|
improvement_diffs = [_suggestion_as_slot_diff(s) for s in suggestions]
|
||||||
problem_slot_payload = {
|
problem_slot_payload = {
|
||||||
|
|
@ -3470,12 +3614,17 @@ def _run_unified_slot_improvement_review(
|
||||||
"rejected_count": len(rejected),
|
"rejected_count": len(rejected),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
target_summary = path_target_profile.to_summary_dict(cur)
|
||||||
|
except Exception:
|
||||||
|
target_summary = {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"goal_query": goal_query,
|
"goal_query": goal_query,
|
||||||
"max_steps_requested": max_steps,
|
"max_steps_requested": max_steps,
|
||||||
"steps": baseline_steps,
|
"steps": baseline_steps,
|
||||||
"step_count": len(baseline_steps),
|
"step_count": len(baseline_steps),
|
||||||
"target_profile_summary": path_target_profile.to_summary_dict(cur),
|
"target_profile_summary": target_summary,
|
||||||
"semantic_brief_summary": brief_to_summary_dict(semantic_brief),
|
"semantic_brief_summary": brief_to_summary_dict(semantic_brief),
|
||||||
"semantic_llm_applied": semantic_llm_applied,
|
"semantic_llm_applied": semantic_llm_applied,
|
||||||
"query_intent_summary": first_intent_summary,
|
"query_intent_summary": first_intent_summary,
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,20 @@ def test_slot_auto_select_requires_higher_score():
|
||||||
baseline_exercise_id=1,
|
baseline_exercise_id=1,
|
||||||
proposed_exercise_id=2,
|
proposed_exercise_id=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_off_topic_slot_gap_spec_for_filled_slot():
|
||||||
|
from planning_exercise_path_builder import _build_off_topic_slot_gap_spec
|
||||||
|
|
||||||
|
spec = _build_off_topic_slot_gap_spec(
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": 7,
|
||||||
|
"exercise_id": 99,
|
||||||
|
"title": "Ukemi Vorwärts",
|
||||||
|
"roadmap_learning_goal": "Integration Täuschung",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert spec is not None
|
||||||
|
assert spec["source"] == "off_topic"
|
||||||
|
assert spec["roadmap_major_step_index"] == 7
|
||||||
|
assert "Ukemi" in spec["rationale"]
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
baselineRes?.path_qa?.quality_score != null
|
baselineRes?.path_qa?.quality_score != null
|
||||||
? Number(baselineRes.path_qa.quality_score)
|
? Number(baselineRes.path_qa.quality_score)
|
||||||
: null,
|
: null,
|
||||||
|
include_llm_path_qa: false,
|
||||||
include_llm_intent: false,
|
include_llm_intent: false,
|
||||||
auto_rematch_after_qa: false,
|
auto_rematch_after_qa: false,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user