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,
|
apply_gap_fill_after_qa,
|
||||||
build_gap_fill_offer,
|
build_gap_fill_offer,
|
||||||
collect_gap_fill_specs,
|
collect_gap_fill_specs,
|
||||||
|
try_suggest_ai_stage_step,
|
||||||
)
|
)
|
||||||
from planning_exercise_retrieval import run_multistage_planning_retrieval
|
from planning_exercise_retrieval import run_multistage_planning_retrieval
|
||||||
from planning_exercise_semantics import (
|
from planning_exercise_semantics import (
|
||||||
|
|
@ -1534,6 +1535,15 @@ def _enrich_roadmap_unfilled_gap_offers(
|
||||||
"KI-Entwurf für diese Stufe."
|
"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(
|
offer = build_gap_fill_offer(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
steps=steps,
|
steps=steps,
|
||||||
|
|
@ -2117,8 +2127,8 @@ def _run_evaluate_only_path_qa(
|
||||||
gap_specs,
|
gap_specs,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
include_ai_calls=False,
|
include_ai_calls=bool(body.include_ai_gap_fill),
|
||||||
max_ai_proposals=0,
|
max_ai_proposals=3,
|
||||||
auto_insert_proposals=False,
|
auto_insert_proposals=False,
|
||||||
roadmap_snapshot=path_roadmap_snapshot,
|
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)
|
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(
|
def _build_progression_compare_response(
|
||||||
baseline: Mapping[str, Any],
|
baseline: Mapping[str, Any],
|
||||||
proposed: Mapping[str, Any],
|
proposed: Mapping[str, Any],
|
||||||
|
|
@ -2414,6 +2446,10 @@ def _build_progression_compare_response(
|
||||||
)
|
)
|
||||||
actionable_diffs = _actionable_slot_diffs(slot_diffs)
|
actionable_diffs = _actionable_slot_diffs(slot_diffs)
|
||||||
apply_steps = list(proposed_steps)
|
apply_steps = list(proposed_steps)
|
||||||
|
gap_fill_offers = _merge_gap_fill_offers_from_steps(
|
||||||
|
apply_steps,
|
||||||
|
proposed.get("gap_fill_offers") or [],
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
**dict(proposed),
|
**dict(proposed),
|
||||||
"comparison_mode": True,
|
"comparison_mode": True,
|
||||||
|
|
@ -2423,6 +2459,7 @@ def _build_progression_compare_response(
|
||||||
"proposed_steps_pipeline": proposed_steps,
|
"proposed_steps_pipeline": proposed_steps,
|
||||||
"proposed_path_qa": fair_qa,
|
"proposed_path_qa": fair_qa,
|
||||||
"proposed_path_qa_pipeline": pipeline_qa,
|
"proposed_path_qa_pipeline": pipeline_qa,
|
||||||
|
"gap_fill_offers": gap_fill_offers,
|
||||||
"slot_diffs": slot_diffs,
|
"slot_diffs": slot_diffs,
|
||||||
"slot_diffs_actionable": actionable_diffs,
|
"slot_diffs_actionable": actionable_diffs,
|
||||||
"slot_diff_count": len(actionable_diffs),
|
"slot_diff_count": len(actionable_diffs),
|
||||||
|
|
@ -2967,8 +3004,8 @@ def suggest_progression_path(
|
||||||
gap_specs,
|
gap_specs,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
include_ai_calls=False,
|
include_ai_calls=bool(body.include_ai_gap_fill),
|
||||||
max_ai_proposals=0,
|
max_ai_proposals=3,
|
||||||
auto_insert_proposals=False,
|
auto_insert_proposals=False,
|
||||||
roadmap_snapshot=path_roadmap_snapshot,
|
roadmap_snapshot=path_roadmap_snapshot,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
collectGapOffersFromApiResponse,
|
collectGapOffersFromApiResponse,
|
||||||
dedupeGapOffersBySlot,
|
dedupeGapOffersBySlot,
|
||||||
filterGapOffersForUnfilledSlots,
|
filterGapOffersForUnfilledSlots,
|
||||||
|
mergeGapOffersForDraft,
|
||||||
pathQaQualityPercent,
|
pathQaQualityPercent,
|
||||||
applyResolvedStructuredToDraft,
|
applyResolvedStructuredToDraft,
|
||||||
buildPlanningArtifactFromDraft,
|
buildPlanningArtifactFromDraft,
|
||||||
|
|
@ -500,11 +501,14 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const gapOffersFromMatchResponse = (synced, res) =>
|
const runMatchCompareFlow = async (synced, { source = 'match' } = {}) => {
|
||||||
filterGapOffersForUnfilledSlots(
|
const res = await fetchMatchCompare(synced)
|
||||||
synced,
|
const evalRes = await fetchPathEvaluate(synced)
|
||||||
dedupeGapOffersBySlot(collectGapOffersFromApiResponse(res), 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' } = {}) => {
|
const presentMatchCompare = (res, { source = 'manual' } = {}) => {
|
||||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||||
|
|
@ -523,22 +527,17 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
let notice =
|
let notice =
|
||||||
diffCount > 0
|
diffCount > 0
|
||||||
? `Match: ${diffCount} Slot-Vorschlag/Vorschläge — bitte im Dialog prüfen und auswählen.`
|
? `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) {
|
if (bPct != null && pPct != null && pPct !== bPct) {
|
||||||
notice += ` Pfad-QS Vorschlag fair bewertet: ${bPct} % → ${pPct} %.`
|
notice += ` Pfad-QS Vorschlag fair bewertet: ${bPct} % → ${pPct} %.`
|
||||||
}
|
}
|
||||||
setMatchNotice(notice)
|
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 runMatch = async () => {
|
||||||
const q = (draft?.goalQuery || '').trim()
|
const q = (draft?.goalQuery || '').trim()
|
||||||
if (q.length < 3) {
|
if (q.length < 3) {
|
||||||
|
|
@ -601,7 +600,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
const evalRes = await fetchPathEvaluate(syncedNext)
|
const evalRes = await fetchPathEvaluate(syncedNext)
|
||||||
const { draft: evaluated, remainingOffers } = applyEvaluateResult(syncedNext, evalRes)
|
const { draft: evaluated, remainingOffers } = applyEvaluateResult(syncedNext, evalRes)
|
||||||
setDraft({ ...evaluated, dirty: true })
|
setDraft({ ...evaluated, dirty: true })
|
||||||
setGapFillOffers(remainingOffers)
|
const mergedOffers = mergeGapOffersForDraft(evaluated, comparePayload, evalRes)
|
||||||
|
setGapFillOffers(mergedOffers.length > 0 ? mergedOffers : remainingOffers)
|
||||||
setProposedPathQa(null)
|
setProposedPathQa(null)
|
||||||
setCompareOpen(false)
|
setCompareOpen(false)
|
||||||
setComparePayload(null)
|
setComparePayload(null)
|
||||||
|
|
@ -632,7 +632,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
const res = await fetchPathEvaluate(synced)
|
const res = await fetchPathEvaluate(synced)
|
||||||
const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, res)
|
const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, res)
|
||||||
setDraft(evaluated)
|
setDraft(evaluated)
|
||||||
setGapFillOffers(remainingOffers)
|
const mergedOffers = mergeGapOffersForDraft(evaluated, res)
|
||||||
|
setGapFillOffers(mergedOffers.length > 0 ? mergedOffers : remainingOffers)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setActionErr(e.message || 'Bewertung fehlgeschlagen')
|
setActionErr(e.message || 'Bewertung fehlgeschlagen')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -387,12 +387,29 @@ export function collectGapOffersFromApiResponse(res) {
|
||||||
}
|
}
|
||||||
for (const offer of res?.gap_fill_offers || []) add(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 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)
|
if (step?.gap_offer) add(step.gap_offer)
|
||||||
}
|
}
|
||||||
return out
|
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. */
|
/** Maximal ein Angebot pro Slot — Roadmap-Lücken vor Brücken/QS. */
|
||||||
export function dedupeGapOffersBySlot(offers, draft) {
|
export function dedupeGapOffersBySlot(offers, draft) {
|
||||||
const bySlot = new Map()
|
const bySlot = new Map()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user