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,
|
max_steps=max_steps,
|
||||||
)
|
)
|
||||||
if body.include_ai_gap_fill:
|
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:
|
for step in steps:
|
||||||
if step.get("exercise_id") is not None:
|
if step.get("exercise_id") is not None:
|
||||||
continue
|
continue
|
||||||
|
|
@ -2115,6 +2115,12 @@ def suggest_progression_path(
|
||||||
major_idx = int(step["roadmap_major_step_index"])
|
major_idx = int(step["roadmap_major_step_index"])
|
||||||
except (TypeError, ValueError, KeyError):
|
except (TypeError, ValueError, KeyError):
|
||||||
continue
|
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(
|
stage_spec = next(
|
||||||
(
|
(
|
||||||
s
|
s
|
||||||
|
|
@ -2123,15 +2129,19 @@ def suggest_progression_path(
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if stage_spec is None:
|
learning_goal = (
|
||||||
continue
|
(stage_spec.learning_goal if stage_spec else None)
|
||||||
|
or step.get("roadmap_learning_goal")
|
||||||
|
or step.get("title")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
spec = {
|
spec = {
|
||||||
"source": "roadmap_unfilled",
|
"source": "roadmap_unfilled",
|
||||||
"insert_after_index": max(major_idx - 1, -1),
|
"insert_after_index": max(major_idx - 1, -1),
|
||||||
"roadmap_major_step_index": major_idx,
|
"roadmap_major_step_index": major_idx,
|
||||||
"phase": (step.get("roadmap_phase") or "vertiefung").strip().lower(),
|
"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],
|
"title_hint": (learning_goal or f"Slot {major_idx + 1}")[:120],
|
||||||
"sketch": (stage_spec.learning_goal or "").strip(),
|
"sketch": learning_goal,
|
||||||
"rationale": f"Slot {major_idx + 1} — keine passende Bibliotheks-Übung; KI-Entwurf für diese Stufe.",
|
"rationale": f"Slot {major_idx + 1} — keine passende Bibliotheks-Übung; KI-Entwurf für diese Stufe.",
|
||||||
}
|
}
|
||||||
offer = build_gap_fill_offer(
|
offer = build_gap_fill_offer(
|
||||||
|
|
@ -2148,7 +2158,10 @@ def suggest_progression_path(
|
||||||
semantic_brief=semantic_brief,
|
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)
|
gap_fill_offers.append(offer)
|
||||||
seen_offer_ids.add(offer.get("offer_id"))
|
seen_offer_ids.add(offer.get("offer_id"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,10 +214,21 @@ const GAP_OFFER_SOURCE_PRIORITY = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectGapOffersFromApiResponse(res) {
|
export function collectGapOffersFromApiResponse(res) {
|
||||||
const top = Array.isArray(res?.gap_fill_offers) ? res.gap_fill_offers : []
|
const out = []
|
||||||
if (top.length) return top
|
const seen = new Set()
|
||||||
const qa = res?.path_qa || {}
|
const add = (offer) => {
|
||||||
return Array.isArray(qa?.gap_fill_offers) ? qa.gap_fill_offers : []
|
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. */
|
/** 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 isProposal = Boolean(step.is_ai_proposal) || step.exercise_id == null
|
||||||
const hasAiPayload = Boolean(step.ai_suggestion) || Boolean(step.proposal_key)
|
const hasAiPayload = Boolean(step.ai_suggestion) || Boolean(step.proposal_key)
|
||||||
if (isProposal && !hasAiPayload) {
|
const isUnfilledSlot =
|
||||||
const wasLibrary =
|
step.slot_status === 'unfilled' ||
|
||||||
nextSlots[idx].primary?.kind === 'library' && nextSlots[idx].primary.exerciseId != null
|
step.slot_status === 'stripped' ||
|
||||||
const mustClear = step.slot_status === 'unfilled' || step.slot_status === 'stripped'
|
step.roadmap_match_source === 'unfilled' ||
|
||||||
if (!wasLibrary || mustClear) {
|
Boolean(step.gap_offer)
|
||||||
nextSlots[idx].primary = emptySlotExercise()
|
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) {
|
} else if (isProposal) {
|
||||||
nextSlots[idx].primary = proposalSlotExercise({
|
nextSlots[idx].primary = proposalSlotExercise({
|
||||||
title: step.title || nextSlots[idx].learning_goal,
|
title: step.title || nextSlots[idx].learning_goal,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user