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]
|
||||
|
||||
|
||||
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(
|
||||
cur,
|
||||
*,
|
||||
|
|
@ -3107,6 +3157,261 @@ def _slot_auto_select_library(
|
|||
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(
|
||||
cur,
|
||||
*,
|
||||
|
|
@ -3124,7 +3429,7 @@ def _run_unified_slot_improvement_review(
|
|||
roadmap_edited: bool,
|
||||
) -> 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:
|
||||
raise HTTPException(
|
||||
|
|
@ -3137,6 +3442,52 @@ def _run_unified_slot_improvement_review(
|
|||
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)
|
||||
snapshot = (
|
||||
dict(body.baseline_path_qa_snapshot)
|
||||
|
|
@ -3206,256 +3557,49 @@ def _run_unified_slot_improvement_review(
|
|||
|
||||
for step_index, stage_spec in enumerate(roadmap_ctx.stage_specs):
|
||||
major_idx = int(stage_spec.major_step_index)
|
||||
current = dict(steps_by_major.get(major_idx, {}))
|
||||
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, [])) + 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(
|
||||
try:
|
||||
slot_review = _build_unified_slot_review_entry(
|
||||
cur,
|
||||
exercise_id=int(current_id),
|
||||
step=current,
|
||||
stage_spec=stage_spec,
|
||||
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,
|
||||
major_idx=major_idx,
|
||||
current=steps_by_major.get(major_idx, {}),
|
||||
baseline_steps=baseline_steps,
|
||||
baseline_qa=baseline_qa,
|
||||
baseline_score=baseline_score,
|
||||
steps_by_major=steps_by_major,
|
||||
problem_slots=problem_slots,
|
||||
off_topic_map=off_topic_map,
|
||||
off_topic_scores=off_topic_scores,
|
||||
gap_fill_offers=gap_fill_offers,
|
||||
suggestions=suggestions,
|
||||
rejected=rejected,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
projected_qa=projected_qa,
|
||||
quality_delta=quality_delta,
|
||||
off_topic_reasons=off_reasons,
|
||||
candidate_reasons=best_candidate.get("reasons") or [],
|
||||
),
|
||||
}
|
||||
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": 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
|
||||
)
|
||||
)
|
||||
)
|
||||
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(
|
||||
{
|
||||
except Exception as exc:
|
||||
slot_review = {
|
||||
"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,
|
||||
"baseline_exercise_id": None,
|
||||
"baseline_title": None,
|
||||
"baseline_slot_score": None,
|
||||
"baseline_slot_status": None,
|
||||
"slot_problem": major_idx in problem_slots,
|
||||
"off_topic": major_idx in off_topic_map,
|
||||
"problem_reasons": [f"Slot-Review fehlgeschlagen: {exc}"[:300]],
|
||||
"library_alternative": None,
|
||||
"ai_alternative": None,
|
||||
"review_error": str(exc)[:300],
|
||||
}
|
||||
)
|
||||
slot_reviews.append(slot_review)
|
||||
|
||||
improvement_diffs = [_suggestion_as_slot_diff(s) for s in suggestions]
|
||||
problem_slot_payload = {
|
||||
|
|
@ -3470,12 +3614,17 @@ def _run_unified_slot_improvement_review(
|
|||
"rejected_count": len(rejected),
|
||||
}
|
||||
|
||||
try:
|
||||
target_summary = path_target_profile.to_summary_dict(cur)
|
||||
except Exception:
|
||||
target_summary = {}
|
||||
|
||||
return {
|
||||
"goal_query": goal_query,
|
||||
"max_steps_requested": max_steps,
|
||||
"steps": 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_llm_applied": semantic_llm_applied,
|
||||
"query_intent_summary": first_intent_summary,
|
||||
|
|
|
|||
|
|
@ -111,3 +111,20 @@ def test_slot_auto_select_requires_higher_score():
|
|||
baseline_exercise_id=1,
|
||||
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
|
||||
? Number(baselineRes.path_qa.quality_score)
|
||||
: null,
|
||||
include_llm_path_qa: false,
|
||||
include_llm_intent: false,
|
||||
auto_rematch_after_qa: false,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user