Enhance Gap Fill Offer Handling and Progression Path Logic
All checks were successful
Deploy Development / deploy (push) Successful in 41s
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 1m24s
All checks were successful
Deploy Development / deploy (push) Successful in 41s
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 1m24s
- Updated `suggest_progression_path` to ensure unique gap fill offers are collected and added based on their IDs, improving the relevance of suggestions. - Refined the logic for setting `slot_status` and handling `gap_offer` and `proposal_key` in steps, enhancing clarity in progression path management. - Improved the `collectGapOffersFromApiResponse` function to consolidate gap offers from various sources, ensuring comprehensive offer retrieval. - Enhanced the handling of unfilled slots in `applyMatchStepsToSlots`, ensuring proper assignment of proposals and gap offers. - Added tests to validate the new logic for gap fill offers and slot assignments, ensuring robustness in path suggestion features.
This commit is contained in:
parent
6d130a7e09
commit
de939481ba
|
|
@ -2107,7 +2107,7 @@ def suggest_progression_path(
|
|||
max_steps=max_steps,
|
||||
)
|
||||
if body.include_ai_gap_fill:
|
||||
seen_offer_ids = {o.get("offer_id") for o in gap_fill_offers}
|
||||
seen_offer_ids = {o.get("offer_id") for o in gap_fill_offers if o.get("offer_id")}
|
||||
for step in steps:
|
||||
if step.get("exercise_id") is not None:
|
||||
continue
|
||||
|
|
@ -2115,6 +2115,12 @@ def suggest_progression_path(
|
|||
major_idx = int(step["roadmap_major_step_index"])
|
||||
except (TypeError, ValueError, KeyError):
|
||||
continue
|
||||
if step.get("gap_offer") and step.get("proposal_key"):
|
||||
oid = step["gap_offer"].get("offer_id")
|
||||
if oid and oid not in seen_offer_ids:
|
||||
gap_fill_offers.append(dict(step["gap_offer"]))
|
||||
seen_offer_ids.add(oid)
|
||||
continue
|
||||
stage_spec = next(
|
||||
(
|
||||
s
|
||||
|
|
@ -2123,15 +2129,19 @@ def suggest_progression_path(
|
|||
),
|
||||
None,
|
||||
)
|
||||
if stage_spec is None:
|
||||
continue
|
||||
learning_goal = (
|
||||
(stage_spec.learning_goal if stage_spec else None)
|
||||
or step.get("roadmap_learning_goal")
|
||||
or step.get("title")
|
||||
or ""
|
||||
).strip()
|
||||
spec = {
|
||||
"source": "roadmap_unfilled",
|
||||
"insert_after_index": max(major_idx - 1, -1),
|
||||
"roadmap_major_step_index": major_idx,
|
||||
"phase": (step.get("roadmap_phase") or "vertiefung").strip().lower(),
|
||||
"title_hint": (stage_spec.learning_goal or step.get("title") or f"Slot {major_idx + 1}")[:120],
|
||||
"sketch": (stage_spec.learning_goal or "").strip(),
|
||||
"title_hint": (learning_goal or f"Slot {major_idx + 1}")[:120],
|
||||
"sketch": learning_goal,
|
||||
"rationale": f"Slot {major_idx + 1} — keine passende Bibliotheks-Übung; KI-Entwurf für diese Stufe.",
|
||||
}
|
||||
offer = build_gap_fill_offer(
|
||||
|
|
@ -2148,7 +2158,10 @@ def suggest_progression_path(
|
|||
semantic_brief=semantic_brief,
|
||||
),
|
||||
)
|
||||
if offer.get("offer_id") not in seen_offer_ids:
|
||||
step["gap_offer"] = offer
|
||||
step["proposal_key"] = offer.get("offer_id")
|
||||
step["slot_status"] = "unfilled"
|
||||
if offer.get("offer_id") and offer.get("offer_id") not in seen_offer_ids:
|
||||
gap_fill_offers.append(offer)
|
||||
seen_offer_ids.add(offer.get("offer_id"))
|
||||
|
||||
|
|
|
|||
|
|
@ -214,10 +214,21 @@ const GAP_OFFER_SOURCE_PRIORITY = {
|
|||
}
|
||||
|
||||
export function collectGapOffersFromApiResponse(res) {
|
||||
const top = Array.isArray(res?.gap_fill_offers) ? res.gap_fill_offers : []
|
||||
if (top.length) return top
|
||||
const qa = res?.path_qa || {}
|
||||
return Array.isArray(qa?.gap_fill_offers) ? qa.gap_fill_offers : []
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
const add = (offer) => {
|
||||
if (!offer || typeof offer !== 'object') return
|
||||
const id = offer.offer_id || `${offer.source}-${offer.roadmap_major_step_index}`
|
||||
if (seen.has(id)) return
|
||||
seen.add(id)
|
||||
out.push(offer)
|
||||
}
|
||||
for (const offer of res?.gap_fill_offers || []) add(offer)
|
||||
for (const offer of res?.path_qa?.gap_fill_offers || []) add(offer)
|
||||
for (const step of res?.steps || []) {
|
||||
if (step?.gap_offer) add(step.gap_offer)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Maximal ein Angebot pro Slot — Roadmap-Lücken vor Brücken/QS. */
|
||||
|
|
@ -805,13 +816,25 @@ export function applyMatchStepsToSlots(draft, apiSteps) {
|
|||
|
||||
const isProposal = Boolean(step.is_ai_proposal) || step.exercise_id == null
|
||||
const hasAiPayload = Boolean(step.ai_suggestion) || Boolean(step.proposal_key)
|
||||
if (isProposal && !hasAiPayload) {
|
||||
const wasLibrary =
|
||||
nextSlots[idx].primary?.kind === 'library' && nextSlots[idx].primary.exerciseId != null
|
||||
const mustClear = step.slot_status === 'unfilled' || step.slot_status === 'stripped'
|
||||
if (!wasLibrary || mustClear) {
|
||||
const isUnfilledSlot =
|
||||
step.slot_status === 'unfilled' ||
|
||||
step.slot_status === 'stripped' ||
|
||||
step.roadmap_match_source === 'unfilled' ||
|
||||
Boolean(step.gap_offer)
|
||||
if (isProposal && !hasAiPayload && isUnfilledSlot) {
|
||||
const offer = step.gap_offer || {}
|
||||
nextSlots[idx].primary = proposalSlotExercise({
|
||||
title:
|
||||
offer.title_hint ||
|
||||
step.roadmap_learning_goal ||
|
||||
step.title ||
|
||||
nextSlots[idx].learning_goal ||
|
||||
`Slot ${idx + 1}`,
|
||||
proposalKey: offer.offer_id || step.proposal_key || `roadmap-unfilled-${idx}`,
|
||||
aiSuggestion: offer.ai_suggestion || null,
|
||||
})
|
||||
} else if (isProposal && !hasAiPayload) {
|
||||
nextSlots[idx].primary = emptySlotExercise()
|
||||
}
|
||||
} else if (isProposal) {
|
||||
nextSlots[idx].primary = proposalSlotExercise({
|
||||
title: step.title || nextSlots[idx].learning_goal,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user