Add Slot Assignments and Enhance Path Handling Logic
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 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
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 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced `slot_assignments` to `ProgressionPathSuggestRequest` for improved handling of existing slot assignments in path building. - Implemented `_slot_assignments_by_major_index` and `_path_step_from_slot_assignment` functions to facilitate the integration of slot assignments into the path generation process. - Updated `_build_steps_roadmap_first` to utilize slot assignments, enhancing the accuracy of path steps based on existing exercise slots. - Enhanced `detect_path_gaps` to skip empty slots, preventing unnecessary errors during gap detection. - Added tests to validate the new slot assignment handling and ensure robustness in path generation logic.
This commit is contained in:
parent
480890d0c6
commit
7203c871fc
|
|
@ -115,6 +115,7 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
start_target_only: bool = False
|
||||
evaluate_only: bool = False
|
||||
evaluate_steps: Optional[List[EvaluateStepPayload]] = None
|
||||
slot_assignments: Optional[List[EvaluateStepPayload]] = None
|
||||
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||
start_situation: Optional[str] = Field(default=None, max_length=2000)
|
||||
target_state: Optional[str] = Field(default=None, max_length=2000)
|
||||
|
|
@ -262,6 +263,52 @@ def _build_path_target_profile(
|
|||
return target, query_intent_summary, intent
|
||||
|
||||
|
||||
def _slot_assignments_by_major_index(
|
||||
assignments: Optional[List[EvaluateStepPayload]],
|
||||
) -> Dict[int, EvaluateStepPayload]:
|
||||
out: Dict[int, EvaluateStepPayload] = {}
|
||||
for raw in assignments or []:
|
||||
if raw.exercise_id is None or raw.roadmap_major_step_index is None:
|
||||
continue
|
||||
out[int(raw.roadmap_major_step_index)] = raw
|
||||
return out
|
||||
|
||||
|
||||
def _path_step_from_slot_assignment(
|
||||
cur,
|
||||
*,
|
||||
assignment: EvaluateStepPayload,
|
||||
stage_spec: StageSpecArtifact,
|
||||
major_step: Optional[MajorStep],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Bestehende Slot-Zuordnung aus dem Graph-Editor (nach KI-Anlage) übernehmen."""
|
||||
eid = int(assignment.exercise_id)
|
||||
cur.execute(
|
||||
"SELECT id, title, summary FROM exercises WHERE id = %s",
|
||||
(eid,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
title = (assignment.title or row.get("title") or "").strip() or str(row.get("title") or "")
|
||||
step = {
|
||||
"exercise_id": eid,
|
||||
"variant_id": assignment.variant_id,
|
||||
"title": title,
|
||||
"summary": row.get("summary"),
|
||||
"score": None,
|
||||
"semantic_score": None,
|
||||
"reasons": ["Bestehende Slot-Zuordnung (Graph-Editor)"],
|
||||
"variants": [],
|
||||
"slot_assignment": True,
|
||||
}
|
||||
return _annotate_roadmap_step(
|
||||
step,
|
||||
stage_spec=stage_spec,
|
||||
major_step=major_step,
|
||||
)
|
||||
|
||||
|
||||
def _hit_to_path_step(hit: Dict[str, Any], *, is_bridge: bool = False) -> Dict[str, Any]:
|
||||
raw_vid = hit.get("suggested_variant_id")
|
||||
variant_id: Optional[int] = None
|
||||
|
|
@ -873,8 +920,29 @@ def _build_steps_roadmap_first(
|
|||
anchor_variant_id: Optional[int] = None
|
||||
unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
||||
stage_count = len(stage_specs)
|
||||
assignments = _slot_assignments_by_major_index(body.slot_assignments)
|
||||
majors_by_index: Dict[int, MajorStep] = {}
|
||||
if roadmap_ctx.roadmap:
|
||||
majors_by_index = {m.index: m for m in roadmap_ctx.roadmap.major_steps}
|
||||
|
||||
for step_index, stage_spec in enumerate(stage_specs):
|
||||
major_idx = stage_spec.major_step_index
|
||||
if major_idx in assignments:
|
||||
pinned = _path_step_from_slot_assignment(
|
||||
cur,
|
||||
assignment=assignments[major_idx],
|
||||
stage_spec=stage_spec,
|
||||
major_step=majors_by_index.get(major_idx),
|
||||
)
|
||||
if pinned:
|
||||
steps.append(pinned)
|
||||
eid = int(pinned["exercise_id"])
|
||||
used.add(eid)
|
||||
planned_ids.append(eid)
|
||||
anchor_id = eid
|
||||
anchor_variant_id = pinned.get("variant_id")
|
||||
continue
|
||||
|
||||
step, unfilled_spec = _match_roadmap_slot(
|
||||
cur,
|
||||
tenant=tenant,
|
||||
|
|
|
|||
|
|
@ -182,6 +182,8 @@ def detect_path_gaps(
|
|||
for i in range(total_segments):
|
||||
step_a = steps[i]
|
||||
step_b = steps[i + 1]
|
||||
if step_a.get("exercise_id") is None or step_b.get("exercise_id") is None:
|
||||
continue
|
||||
if roadmap_first and is_roadmap_planned_neighbor_pair(step_a, step_b):
|
||||
continue
|
||||
gap = measure_step_transition_gap(
|
||||
|
|
|
|||
|
|
@ -106,6 +106,42 @@ def test_detect_path_gaps_skips_roadmap_neighbors():
|
|||
assert gaps == []
|
||||
|
||||
|
||||
def test_detect_path_gaps_skips_empty_slots():
|
||||
"""Graph-Bewertung: leere Slots dürfen keinen 500er durch Übergangs-Lücken auslösen."""
|
||||
brief = build_semantic_brief("Mawashi Geri Kumite")
|
||||
steps = [
|
||||
{
|
||||
"exercise_id": 10,
|
||||
"title": "Stand",
|
||||
"roadmap_major_step_index": 0,
|
||||
},
|
||||
{
|
||||
"exercise_id": None,
|
||||
"title": "(leer: Slot 2)",
|
||||
"is_ai_proposal": True,
|
||||
"roadmap_major_step_index": 1,
|
||||
},
|
||||
{
|
||||
"exercise_id": 11,
|
||||
"title": "Anwendung",
|
||||
"roadmap_major_step_index": 2,
|
||||
},
|
||||
]
|
||||
|
||||
class _FakeCur:
|
||||
def execute(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def fetchall(self):
|
||||
return []
|
||||
|
||||
def fetchone(self):
|
||||
return {"title": "X", "summary": "", "goal": ""}
|
||||
|
||||
gaps = detect_path_gaps(_FakeCur(), steps, brief=brief, roadmap_first=True)
|
||||
assert isinstance(gaps, list)
|
||||
|
||||
|
||||
def test_apply_llm_path_reorder_invalid_ignored():
|
||||
steps = [{"exercise_id": 1}, {"exercise_id": 2}]
|
||||
reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]})
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
SLOT_MAX,
|
||||
slotsAsPathStepRows,
|
||||
slotsToEvaluateSteps,
|
||||
slotsToSlotAssignments,
|
||||
syncProgressionRoadmapFromSlots,
|
||||
syncSlotPhasesFromRoadmap,
|
||||
} from '../utils/progressionGraphDraft'
|
||||
|
|
@ -414,6 +415,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
include_llm_roadmap: false,
|
||||
roadmap_first: true,
|
||||
roadmap_override: override,
|
||||
slot_assignments: slotsToSlotAssignments(synced),
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(draft.startSituation, draft.targetState, draft.roadmapNotes),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -712,6 +712,21 @@ export function draftSiblingEdgePairs(draft) {
|
|||
return pairs
|
||||
}
|
||||
|
||||
/** Bereits zugeordnete Bibliotheks-Übungen für Re-Match (Pins). */
|
||||
export function slotsToSlotAssignments(draft) {
|
||||
return (draft.slots || [])
|
||||
.filter((slot) => slot.primary?.kind === 'library' && slot.primary.exerciseId != null)
|
||||
.map((slot) => ({
|
||||
exercise_id: slot.primary.exerciseId,
|
||||
variant_id: slot.primary.variantId || null,
|
||||
title: slot.primary.exerciseTitle || null,
|
||||
is_ai_proposal: false,
|
||||
roadmap_major_step_index: slot.majorStepIndex,
|
||||
roadmap_phase: slot.phase || null,
|
||||
roadmap_learning_goal: slot.learning_goal || null,
|
||||
}))
|
||||
}
|
||||
|
||||
export function slotsToEvaluateSteps(draft) {
|
||||
return (draft.slots || []).map((slot) => {
|
||||
const p = slot.primary
|
||||
|
|
@ -789,7 +804,11 @@ export function applyMatchStepsToSlots(draft, apiSteps) {
|
|||
|
||||
for (let i = 0; i < nextSlots.length; i += 1) {
|
||||
if (!touchedMajors.has(i)) {
|
||||
nextSlots[i].primary = emptySlotExercise()
|
||||
const keep =
|
||||
nextSlots[i].primary?.kind === 'library' && nextSlots[i].primary.exerciseId != null
|
||||
if (!keep) {
|
||||
nextSlots[i].primary = emptySlotExercise()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user