From 89c67802944ce0861bcf397bdcd98c7efe16484b Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 13 Jun 2026 08:36:53 +0200 Subject: [PATCH] Enhance AI Gap Fill Logic and Progression Path Handling - Integrated `try_suggest_ai_stage_step` to suggest AI-generated gap fill steps based on user input, improving the automation of the planning process. - Updated `_enrich_roadmap_unfilled_gap_offers` to conditionally include AI gap fill proposals, enhancing the offer generation logic. - Implemented `_merge_gap_fill_offers_from_steps` to consolidate gap fill offers from various steps, ensuring a comprehensive list of available offers. - Modified `ProgressionGraphEditor` to utilize the new merging logic for gap fill offers, improving the user experience in managing offers. - Enhanced utility functions to streamline the collection and filtering of gap fill offers from API responses. - Bumped version to reflect the new features and improvements. --- backend/planning_exercise_path_builder.py | 45 +++++++++++++++++-- .../src/components/ProgressionGraphEditor.jsx | 35 ++++++++------- frontend/src/utils/progressionGraphDraft.js | 19 +++++++- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/backend/planning_exercise_path_builder.py b/backend/planning_exercise_path_builder.py index 44b8638..833b5eb 100644 --- a/backend/planning_exercise_path_builder.py +++ b/backend/planning_exercise_path_builder.py @@ -47,6 +47,7 @@ from planning_exercise_path_ai_fill import ( apply_gap_fill_after_qa, build_gap_fill_offer, collect_gap_fill_specs, + try_suggest_ai_stage_step, ) from planning_exercise_retrieval import run_multistage_planning_retrieval from planning_exercise_semantics import ( @@ -1534,6 +1535,15 @@ def _enrich_roadmap_unfilled_gap_offers( "KI-Entwurf für diese Stufe." ), } + proposal = None + if body.include_ai_gap_fill: + proposal = try_suggest_ai_stage_step( + cur, + goal_query=goal_query, + brief=semantic_brief, + spec=spec, + steps=steps, + ) offer = build_gap_fill_offer( spec=spec, steps=steps, @@ -2117,8 +2127,8 @@ def _run_evaluate_only_path_qa( gap_specs, goal_query=goal_query, brief=semantic_brief, - include_ai_calls=False, - max_ai_proposals=0, + include_ai_calls=bool(body.include_ai_gap_fill), + max_ai_proposals=3, auto_insert_proposals=False, roadmap_snapshot=path_roadmap_snapshot, ) @@ -2394,6 +2404,28 @@ def _evaluate_steps_for_compare_qa( return suggest_progression_path(cur, tenant=tenant, body=eval_body) +def _merge_gap_fill_offers_from_steps( + steps: Sequence[Mapping[str, Any]], + offers: Sequence[Mapping[str, Any]], +) -> List[Dict[str, Any]]: + """Gap-Angebote aus Schritt-gap_offer + Top-Level-Liste vereinigen.""" + merged: List[Dict[str, Any]] = [dict(o) for o in offers or [] if isinstance(o, dict)] + seen = {o.get("offer_id") for o in merged if o.get("offer_id")} + for raw in steps or []: + if not isinstance(raw, dict): + continue + go = raw.get("gap_offer") + if not isinstance(go, dict): + continue + oid = go.get("offer_id") + if oid and oid in seen: + continue + if oid: + seen.add(oid) + merged.append(dict(go)) + return merged + + def _build_progression_compare_response( baseline: Mapping[str, Any], proposed: Mapping[str, Any], @@ -2414,6 +2446,10 @@ def _build_progression_compare_response( ) actionable_diffs = _actionable_slot_diffs(slot_diffs) apply_steps = list(proposed_steps) + gap_fill_offers = _merge_gap_fill_offers_from_steps( + apply_steps, + proposed.get("gap_fill_offers") or [], + ) return { **dict(proposed), "comparison_mode": True, @@ -2423,6 +2459,7 @@ def _build_progression_compare_response( "proposed_steps_pipeline": proposed_steps, "proposed_path_qa": fair_qa, "proposed_path_qa_pipeline": pipeline_qa, + "gap_fill_offers": gap_fill_offers, "slot_diffs": slot_diffs, "slot_diffs_actionable": actionable_diffs, "slot_diff_count": len(actionable_diffs), @@ -2967,8 +3004,8 @@ def suggest_progression_path( gap_specs, goal_query=goal_query, brief=semantic_brief, - include_ai_calls=False, - max_ai_proposals=0, + include_ai_calls=bool(body.include_ai_gap_fill), + max_ai_proposals=3, auto_insert_proposals=False, roadmap_snapshot=path_roadmap_snapshot, ) diff --git a/frontend/src/components/ProgressionGraphEditor.jsx b/frontend/src/components/ProgressionGraphEditor.jsx index f5c9bff..56d41dc 100644 --- a/frontend/src/components/ProgressionGraphEditor.jsx +++ b/frontend/src/components/ProgressionGraphEditor.jsx @@ -32,6 +32,7 @@ import { collectGapOffersFromApiResponse, dedupeGapOffersBySlot, filterGapOffersForUnfilledSlots, + mergeGapOffersForDraft, pathQaQualityPercent, applyResolvedStructuredToDraft, buildPlanningArtifactFromDraft, @@ -500,11 +501,14 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa return res } - const gapOffersFromMatchResponse = (synced, res) => - filterGapOffersForUnfilledSlots( - synced, - dedupeGapOffersBySlot(collectGapOffersFromApiResponse(res), synced), - ) + const runMatchCompareFlow = async (synced, { source = 'match' } = {}) => { + const res = await fetchMatchCompare(synced) + const evalRes = await fetchPathEvaluate(synced) + setGapFillOffers(mergeGapOffersForDraft(synced, res, evalRes)) + presentMatchCompare(res, { source }) + setPathQa(evalRes?.path_qa || res?.baseline_path_qa || null) + return res + } const presentMatchCompare = (res, { source = 'manual' } = {}) => { setSemanticBrief(res?.semantic_brief_summary || null) @@ -523,22 +527,17 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa let notice = diffCount > 0 ? `Match: ${diffCount} Slot-Vorschlag/Vorschläge — bitte im Dialog prüfen und auswählen.` - : 'Match: Keine abweichenden Slot-Vorschläge — Dialog zur Kontrolle geöffnet.' + : 'Match: Keine abweichenden Bibliotheks-Slots — Dialog zur Kontrolle geöffnet.' + const gapCount = collectGapOffersFromApiResponse(res).length + if (gapCount > 0) { + notice += ` ${gapCount} KI-Angebot(e) für leere Slots im Panel „Graph-Bewertung“.` + } if (bPct != null && pPct != null && pPct !== bPct) { notice += ` Pfad-QS Vorschlag fair bewertet: ${bPct} % → ${pPct} %.` } setMatchNotice(notice) } - const runMatchCompareFlow = async (synced, { source = 'match' } = {}) => { - const res = await fetchMatchCompare(synced) - setGapFillOffers(gapOffersFromMatchResponse(synced, res)) - presentMatchCompare(res, { source }) - const evalRes = await fetchPathEvaluate(synced) - setPathQa(evalRes?.path_qa || res?.baseline_path_qa || null) - return res - } - const runMatch = async () => { const q = (draft?.goalQuery || '').trim() if (q.length < 3) { @@ -601,7 +600,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const evalRes = await fetchPathEvaluate(syncedNext) const { draft: evaluated, remainingOffers } = applyEvaluateResult(syncedNext, evalRes) setDraft({ ...evaluated, dirty: true }) - setGapFillOffers(remainingOffers) + const mergedOffers = mergeGapOffersForDraft(evaluated, comparePayload, evalRes) + setGapFillOffers(mergedOffers.length > 0 ? mergedOffers : remainingOffers) setProposedPathQa(null) setCompareOpen(false) setComparePayload(null) @@ -632,7 +632,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const res = await fetchPathEvaluate(synced) const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, res) setDraft(evaluated) - setGapFillOffers(remainingOffers) + const mergedOffers = mergeGapOffersForDraft(evaluated, res) + setGapFillOffers(mergedOffers.length > 0 ? mergedOffers : remainingOffers) } catch (e) { setActionErr(e.message || 'Bewertung fehlgeschlagen') } finally { diff --git a/frontend/src/utils/progressionGraphDraft.js b/frontend/src/utils/progressionGraphDraft.js index e7e46da..747590f 100644 --- a/frontend/src/utils/progressionGraphDraft.js +++ b/frontend/src/utils/progressionGraphDraft.js @@ -387,12 +387,29 @@ export function collectGapOffersFromApiResponse(res) { } 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 || []) { + const stepSources = [ + ...(res?.steps || []), + ...(res?.proposed_steps || []), + ...(res?.proposed_steps_pipeline || []), + ] + for (const step of stepSources) { if (step?.gap_offer) add(step.gap_offer) } return out } +/** KI-Angebote aus einer oder mehreren Planungs-Antworten für leere Slots sammeln. */ +export function mergeGapOffersForDraft(draft, ...responses) { + const collected = [] + for (const res of responses) { + if (res) collected.push(...collectGapOffersFromApiResponse(res)) + } + return filterGapOffersForUnfilledSlots( + draft, + dedupeGapOffersBySlot(collected, draft), + ) +} + /** Maximal ein Angebot pro Slot — Roadmap-Lücken vor Brücken/QS. */ export function dedupeGapOffersBySlot(offers, draft) { const bySlot = new Map()