Enhance Path Evaluation and Slot Management Features
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
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 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
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 1m13s
- Introduced `_parse_slot_refs_from_text` to extract and convert slot references from text, improving the handling of user input in path evaluations. - Updated `_problematic_slots_from_path_qa` to utilize the new parsing function, enhancing the identification of problematic slots based on various hints and issues. - Enhanced `ProgressionGraphEditor` and `ProgressionOptimizeCompareModal` to better display identified problem slots and their associated reasons, improving user feedback during evaluations. - Added tests for new parsing functionality and its integration with existing slot management processes, ensuring robustness in slot reference handling.
This commit is contained in:
parent
3468b2066e
commit
e9bf5bd1a5
|
|
@ -6,6 +6,7 @@ planning_progression_roadmap.py und PLANNING_PROGRESSION_ROADMAP_SPEC.md.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Set, Tuple
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -149,6 +150,7 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
baseline_quality_score: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
||||
include_incremental_diff_scoring: bool = False
|
||||
unified_slot_review: bool = False
|
||||
baseline_path_qa_snapshot: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
def _resolve_planning_catalog_context(
|
||||
|
|
@ -1165,6 +1167,7 @@ def _match_roadmap_slot(
|
|||
anchor_variant_id: Optional[int],
|
||||
used: Set[int],
|
||||
slot_priority_exercise_id: Optional[int] = None,
|
||||
skip_post_match_gate: bool = False,
|
||||
) -> Tuple[Optional[Dict[str, Any]], Optional[StageSpecArtifact]]:
|
||||
"""Einzelnen Roadmap-Slot matchen (Initial-Build und Auto-Rematch)."""
|
||||
major_by_index: Dict[int, MajorStep] = {}
|
||||
|
|
@ -1331,11 +1334,15 @@ def _match_roadmap_slot(
|
|||
else:
|
||||
step["slot_status"] = "matched"
|
||||
step["roadmap_match_source"] = "stage_spec"
|
||||
if step.get("roadmap_match_source") != "slot_best_match" and not _roadmap_step_passes_post_match_gate(
|
||||
cur,
|
||||
step,
|
||||
goal_query=goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
if (
|
||||
not skip_post_match_gate
|
||||
and step.get("roadmap_match_source") != "slot_best_match"
|
||||
and not _roadmap_step_passes_post_match_gate(
|
||||
cur,
|
||||
step,
|
||||
goal_query=goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
)
|
||||
):
|
||||
return None, stage_spec
|
||||
return step, None
|
||||
|
|
@ -2478,6 +2485,21 @@ def _resolve_hint_major_index(
|
|||
return pos if pos >= 0 else None
|
||||
|
||||
|
||||
def _parse_slot_refs_from_text(text: str) -> Set[int]:
|
||||
"""„Schritt 8“ / „Slot 8“ / „Stufe 8“ → 0-basierter major_step_index (7)."""
|
||||
found: Set[int] = set()
|
||||
if not text:
|
||||
return found
|
||||
for match in re.finditer(r"(?:schritt|slot|stufe)\s*(\d+)", text.lower()):
|
||||
try:
|
||||
n = int(match.group(1))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if n >= 1:
|
||||
found.add(n - 1)
|
||||
return found
|
||||
|
||||
|
||||
def _problematic_slots_from_path_qa(
|
||||
baseline_qa: Optional[Mapping[str, Any]],
|
||||
baseline_steps: Sequence[Mapping[str, Any]],
|
||||
|
|
@ -2504,13 +2526,32 @@ def _problematic_slots_from_path_qa(
|
|||
if not isinstance(hint, dict):
|
||||
continue
|
||||
action = str(hint.get("action") or "").strip().lower()
|
||||
if action in ("review_roadmap", "refine_stage_spec"):
|
||||
if action == "review_roadmap":
|
||||
continue
|
||||
midx = _resolve_hint_major_index(hint, stage_specs)
|
||||
if midx is None:
|
||||
title = str(hint.get("title") or "")
|
||||
for ref in _parse_slot_refs_from_text(
|
||||
" ".join(
|
||||
str(hint.get(k) or "")
|
||||
for k in ("reason", "issue", "title", "roadmap_learning_goal")
|
||||
)
|
||||
):
|
||||
midx = ref
|
||||
break
|
||||
if title:
|
||||
for step in baseline_steps or []:
|
||||
if not isinstance(step, dict):
|
||||
continue
|
||||
st = str(step.get("title") or "").strip()
|
||||
smidx = step.get("roadmap_major_step_index")
|
||||
if st and title.lower() in st.lower() and smidx is not None:
|
||||
midx = int(smidx)
|
||||
break
|
||||
if midx is None:
|
||||
continue
|
||||
_add(
|
||||
midx,
|
||||
int(midx),
|
||||
str(
|
||||
hint.get("reason")
|
||||
or hint.get("issue")
|
||||
|
|
@ -2519,6 +2560,18 @@ def _problematic_slots_from_path_qa(
|
|||
),
|
||||
)
|
||||
|
||||
llm_text_parts: List[str] = []
|
||||
for key in ("topic_coverage",):
|
||||
raw = (baseline_qa or {}).get(key)
|
||||
if raw:
|
||||
llm_text_parts.append(str(raw))
|
||||
for key in ("issues", "recommendations", "sequence_notes"):
|
||||
for raw in (baseline_qa or {}).get(key) or []:
|
||||
llm_text_parts.append(str(raw or ""))
|
||||
combined = "\n".join(llm_text_parts)
|
||||
for midx in _parse_slot_refs_from_text(combined):
|
||||
_add(midx, "In Pfad-Bewertung als Schachstelle genannt")
|
||||
|
||||
for raw in (baseline_qa or {}).get("issues") or []:
|
||||
text = str(raw or "").strip()
|
||||
if not text:
|
||||
|
|
@ -2535,7 +2588,8 @@ def _problematic_slots_from_path_qa(
|
|||
continue
|
||||
title = str(step.get("title") or "").strip()
|
||||
if (
|
||||
f"slot {slot_no}" in text.lower()
|
||||
f"schritt {slot_no}" in text.lower()
|
||||
or f"slot {slot_no}" in text.lower()
|
||||
or f"stufe {slot_no}" in text.lower()
|
||||
or (title and title.lower() in text.lower())
|
||||
):
|
||||
|
|
@ -2896,6 +2950,7 @@ def _roadmap_slot_library_candidates(
|
|||
used: Set[int],
|
||||
exclude_exercise_id: Optional[int] = None,
|
||||
max_candidates: int = 5,
|
||||
skip_post_match_gate: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Mehrere Bibliotheks-Kandidaten je Slot (beste zuerst, aktuelle optional ausgeschlossen)."""
|
||||
pick_used = set(used)
|
||||
|
|
@ -2925,6 +2980,7 @@ def _roadmap_slot_library_candidates(
|
|||
anchor_variant_id=anchor_variant_id,
|
||||
used=pick_used,
|
||||
slot_priority_exercise_id=None,
|
||||
skip_post_match_gate=skip_post_match_gate,
|
||||
)
|
||||
if not step or step.get("exercise_id") is None:
|
||||
break
|
||||
|
|
@ -2993,34 +3049,55 @@ def _run_unified_slot_improvement_review(
|
|||
detail="unified_slot_review erfordert Roadmap (roadmap_override / roadmap_first)",
|
||||
)
|
||||
|
||||
eval_body = body.model_copy(
|
||||
update={
|
||||
"include_llm_path_qa": body.include_llm_path_qa,
|
||||
"include_ai_gap_fill": body.include_ai_gap_fill,
|
||||
"auto_rematch_after_qa": False,
|
||||
}
|
||||
)
|
||||
baseline_steps = _evaluate_steps_from_payload(cur, body.baseline_evaluate_steps)
|
||||
qa_pack = _run_evaluate_only_path_qa(
|
||||
cur,
|
||||
body=eval_body,
|
||||
goal_query=goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
steps=list(baseline_steps),
|
||||
roadmap_ctx=roadmap_ctx,
|
||||
snapshot = (
|
||||
dict(body.baseline_path_qa_snapshot)
|
||||
if isinstance(body.baseline_path_qa_snapshot, dict)
|
||||
else None
|
||||
)
|
||||
baseline_steps = list(qa_pack.get("steps") or baseline_steps)
|
||||
baseline_qa = qa_pack.get("path_qa") if isinstance(qa_pack.get("path_qa"), dict) else {}
|
||||
if baseline_qa.get("quality_score") is None:
|
||||
baseline_qa = dict(baseline_qa)
|
||||
baseline_qa["quality_score"] = compute_deterministic_path_quality_score(
|
||||
gaps=baseline_qa.get("large_gaps") or [],
|
||||
off_topic_steps=baseline_qa.get("off_topic_steps") or [],
|
||||
steps=baseline_steps,
|
||||
multistage_qa=baseline_qa,
|
||||
if snapshot:
|
||||
baseline_qa = snapshot
|
||||
if baseline_qa.get("quality_score") is None:
|
||||
baseline_qa["quality_score"] = compute_deterministic_path_quality_score(
|
||||
gaps=baseline_qa.get("large_gaps") or [],
|
||||
off_topic_steps=baseline_qa.get("off_topic_steps") or [],
|
||||
steps=baseline_steps,
|
||||
multistage_qa=baseline_qa,
|
||||
)
|
||||
baseline_score = (
|
||||
float(body.baseline_quality_score)
|
||||
if body.baseline_quality_score is not None
|
||||
else _path_qa_quality_score(baseline_qa)
|
||||
)
|
||||
baseline_score = _path_qa_quality_score(baseline_qa)
|
||||
gap_fill_offers = list(qa_pack.get("gap_fill_offers") or [])
|
||||
gap_fill_offers: List[Dict[str, Any]] = []
|
||||
else:
|
||||
eval_body = body.model_copy(
|
||||
update={
|
||||
"include_llm_path_qa": body.include_llm_path_qa,
|
||||
"include_ai_gap_fill": body.include_ai_gap_fill,
|
||||
"auto_rematch_after_qa": False,
|
||||
}
|
||||
)
|
||||
qa_pack = _run_evaluate_only_path_qa(
|
||||
cur,
|
||||
body=eval_body,
|
||||
goal_query=goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
steps=list(baseline_steps),
|
||||
roadmap_ctx=roadmap_ctx,
|
||||
)
|
||||
baseline_steps = list(qa_pack.get("steps") or baseline_steps)
|
||||
baseline_qa = qa_pack.get("path_qa") if isinstance(qa_pack.get("path_qa"), dict) else {}
|
||||
if baseline_qa.get("quality_score") is None:
|
||||
baseline_qa = dict(baseline_qa)
|
||||
baseline_qa["quality_score"] = compute_deterministic_path_quality_score(
|
||||
gaps=baseline_qa.get("large_gaps") or [],
|
||||
off_topic_steps=baseline_qa.get("off_topic_steps") or [],
|
||||
steps=baseline_steps,
|
||||
multistage_qa=baseline_qa,
|
||||
)
|
||||
baseline_score = _path_qa_quality_score(baseline_qa)
|
||||
gap_fill_offers = list(qa_pack.get("gap_fill_offers") or [])
|
||||
off_topic_map = _off_topic_reasons_by_slot(baseline_qa.get("off_topic_steps") or [])
|
||||
problem_slots = _problematic_slots_from_path_qa(
|
||||
baseline_qa,
|
||||
|
|
@ -3076,6 +3153,7 @@ def _run_unified_slot_improvement_review(
|
|||
except (TypeError, ValueError):
|
||||
exclude_id = None
|
||||
|
||||
relax_match_gate = bool(off_topic or slot_problem)
|
||||
candidates = _roadmap_slot_library_candidates(
|
||||
cur,
|
||||
tenant=tenant,
|
||||
|
|
@ -3093,8 +3171,9 @@ def _run_unified_slot_improvement_review(
|
|||
anchor_id=anchor_id,
|
||||
anchor_variant_id=anchor_variant_id,
|
||||
used=used_other,
|
||||
exclude_exercise_id=exclude_id if not off_topic else int(current_id) if current_id else None,
|
||||
max_candidates=3,
|
||||
exclude_exercise_id=int(current_id) if current_id is not None else exclude_id,
|
||||
max_candidates=5 if relax_match_gate else 3,
|
||||
skip_post_match_gate=relax_match_gate,
|
||||
)
|
||||
|
||||
accepted_for_slot = False
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Schachstellen-Erkennung für unified Slot-Review."""
|
||||
from planning_exercise_path_builder import (
|
||||
_parse_slot_refs_from_text,
|
||||
_problematic_slots_from_path_qa,
|
||||
_slot_suggestion_accepted,
|
||||
)
|
||||
|
|
@ -51,3 +52,46 @@ def test_slot_suggestion_accepted_for_problem_slot():
|
|||
major_idx=1,
|
||||
slot_problem=True,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_slot_refs_schritt_is_one_based():
|
||||
assert _parse_slot_refs_from_text("Schritt 8 (Ukemi Vorwärts) entfernen") == {7}
|
||||
assert _parse_slot_refs_from_text("slot 3 und Stufe 5") == {2, 4}
|
||||
|
||||
|
||||
def test_problematic_slots_from_refine_stage_spec_hint():
|
||||
qa = {
|
||||
"optimization_hints": [
|
||||
{
|
||||
"action": "refine_stage_spec",
|
||||
"step_index": 7,
|
||||
"issue": "stage_mismatch",
|
||||
"reason": "Stufen-Fit zu schwach (0.00) für „Integration von Täuschung“",
|
||||
}
|
||||
],
|
||||
"off_topic_steps": [],
|
||||
}
|
||||
steps = [
|
||||
{"roadmap_major_step_index": i, "exercise_id": i + 1, "title": f"Übung {i + 1}"}
|
||||
for i in range(8)
|
||||
]
|
||||
steps[7]["title"] = "Ukemi Vorwärts"
|
||||
specs = [_spec(i) for i in range(8)]
|
||||
problems = _problematic_slots_from_path_qa(qa, steps, specs)
|
||||
assert 7 in problems
|
||||
|
||||
|
||||
def test_problematic_slots_from_llm_schritt_text():
|
||||
qa = {
|
||||
"optimization_hints": [],
|
||||
"off_topic_steps": [],
|
||||
"issues": [
|
||||
"Schritt 8 (Ukemi Vorwärts) hat keinen Bezug zur Kumite-Beinarbeit",
|
||||
],
|
||||
}
|
||||
steps = [
|
||||
{"roadmap_major_step_index": 7, "exercise_id": 99, "title": "Ukemi Vorwärts"},
|
||||
]
|
||||
specs = [_spec(7)]
|
||||
problems = _problematic_slots_from_path_qa(qa, steps, specs)
|
||||
assert 7 in problems
|
||||
|
|
|
|||
|
|
@ -507,6 +507,11 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
evaluate_only: false,
|
||||
unified_slot_review: true,
|
||||
baseline_evaluate_steps: slotsToEvaluateSteps(synced),
|
||||
baseline_path_qa_snapshot: baselineRes?.path_qa || null,
|
||||
baseline_quality_score:
|
||||
baselineRes?.path_qa?.quality_score != null
|
||||
? Number(baselineRes.path_qa.quality_score)
|
||||
: null,
|
||||
include_llm_intent: false,
|
||||
auto_rematch_after_qa: false,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -222,10 +222,50 @@ export default function ProgressionOptimizeCompareModal({
|
|||
) : null}
|
||||
|
||||
{dialogDiffs.length === 0 ? (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)' }}>
|
||||
Keine Verbesserung gefunden — dein Pfad ist für alle Slots bereits optimal bewertet
|
||||
oder es fehlen passende Bibliotheks-Treffer (KI-Angebote im Bewertungs-Panel).
|
||||
</p>
|
||||
<>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)' }}>
|
||||
Keine Bibliotheks-Verbesserung mit messbarem Gewinn — Schachstellen siehe unten.
|
||||
KI-Angebote im Bewertungs-Panel oder „Brücke / KI-Angebot“ nutzen.
|
||||
</p>
|
||||
{comparison?.problem_slots && Object.keys(comparison.problem_slots).length > 0 ? (
|
||||
<ul
|
||||
style={{
|
||||
listStyle: 'none',
|
||||
padding: 0,
|
||||
margin: '12px 0 0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
{Object.entries(comparison.problem_slots).map(([midxRaw, reasons]) => {
|
||||
const midx = Number(midxRaw)
|
||||
const reasonList = Array.isArray(reasons) ? reasons : [reasons]
|
||||
return (
|
||||
<li
|
||||
key={`problem-${midxRaw}`}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--danger)',
|
||||
background: 'var(--surface2)',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<strong style={{ color: 'var(--danger)' }}>
|
||||
Schachstelle Slot {midx + 1}
|
||||
</strong>
|
||||
<ul style={{ margin: '6px 0 0', paddingLeft: '16px', color: 'var(--text2)' }}>
|
||||
{reasonList.filter(Boolean).map((text, i) => (
|
||||
<li key={`${midxRaw}-r-${i}`}>{text}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '8px', flexWrap: 'wrap' }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user