Enhance AI Gap Fill Logic and Progression Path Handling
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- 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.
This commit is contained in:
parent
3f130aa8ad
commit
89c6780294
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user