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
|
start_target_only: bool = False
|
||||||
evaluate_only: bool = False
|
evaluate_only: bool = False
|
||||||
evaluate_steps: Optional[List[EvaluateStepPayload]] = None
|
evaluate_steps: Optional[List[EvaluateStepPayload]] = None
|
||||||
|
slot_assignments: Optional[List[EvaluateStepPayload]] = None
|
||||||
roadmap_override: Optional[RoadmapOverridePayload] = None
|
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||||
start_situation: Optional[str] = Field(default=None, max_length=2000)
|
start_situation: Optional[str] = Field(default=None, max_length=2000)
|
||||||
target_state: 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
|
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]:
|
def _hit_to_path_step(hit: Dict[str, Any], *, is_bridge: bool = False) -> Dict[str, Any]:
|
||||||
raw_vid = hit.get("suggested_variant_id")
|
raw_vid = hit.get("suggested_variant_id")
|
||||||
variant_id: Optional[int] = None
|
variant_id: Optional[int] = None
|
||||||
|
|
@ -873,8 +920,29 @@ def _build_steps_roadmap_first(
|
||||||
anchor_variant_id: Optional[int] = None
|
anchor_variant_id: Optional[int] = None
|
||||||
unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
||||||
stage_count = len(stage_specs)
|
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):
|
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(
|
step, unfilled_spec = _match_roadmap_slot(
|
||||||
cur,
|
cur,
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,8 @@ def detect_path_gaps(
|
||||||
for i in range(total_segments):
|
for i in range(total_segments):
|
||||||
step_a = steps[i]
|
step_a = steps[i]
|
||||||
step_b = steps[i + 1]
|
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):
|
if roadmap_first and is_roadmap_planned_neighbor_pair(step_a, step_b):
|
||||||
continue
|
continue
|
||||||
gap = measure_step_transition_gap(
|
gap = measure_step_transition_gap(
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,42 @@ def test_detect_path_gaps_skips_roadmap_neighbors():
|
||||||
assert gaps == []
|
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():
|
def test_apply_llm_path_reorder_invalid_ignored():
|
||||||
steps = [{"exercise_id": 1}, {"exercise_id": 2}]
|
steps = [{"exercise_id": 1}, {"exercise_id": 2}]
|
||||||
reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]})
|
reordered, applied, _ = apply_llm_path_reorder(steps, {"ordered_step_indices": [0, 0]})
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import {
|
||||||
SLOT_MAX,
|
SLOT_MAX,
|
||||||
slotsAsPathStepRows,
|
slotsAsPathStepRows,
|
||||||
slotsToEvaluateSteps,
|
slotsToEvaluateSteps,
|
||||||
|
slotsToSlotAssignments,
|
||||||
syncProgressionRoadmapFromSlots,
|
syncProgressionRoadmapFromSlots,
|
||||||
syncSlotPhasesFromRoadmap,
|
syncSlotPhasesFromRoadmap,
|
||||||
} from '../utils/progressionGraphDraft'
|
} from '../utils/progressionGraphDraft'
|
||||||
|
|
@ -414,6 +415,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
include_llm_roadmap: false,
|
include_llm_roadmap: false,
|
||||||
roadmap_first: true,
|
roadmap_first: true,
|
||||||
roadmap_override: override,
|
roadmap_override: override,
|
||||||
|
slot_assignments: slotsToSlotAssignments(synced),
|
||||||
progression_graph_id: Number(graphId),
|
progression_graph_id: Number(graphId),
|
||||||
...roadmapStructuredPayload(draft.startSituation, draft.targetState, draft.roadmapNotes),
|
...roadmapStructuredPayload(draft.startSituation, draft.targetState, draft.roadmapNotes),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -712,6 +712,21 @@ export function draftSiblingEdgePairs(draft) {
|
||||||
return pairs
|
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) {
|
export function slotsToEvaluateSteps(draft) {
|
||||||
return (draft.slots || []).map((slot) => {
|
return (draft.slots || []).map((slot) => {
|
||||||
const p = slot.primary
|
const p = slot.primary
|
||||||
|
|
@ -789,7 +804,11 @@ export function applyMatchStepsToSlots(draft, apiSteps) {
|
||||||
|
|
||||||
for (let i = 0; i < nextSlots.length; i += 1) {
|
for (let i = 0; i < nextSlots.length; i += 1) {
|
||||||
if (!touchedMajors.has(i)) {
|
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