feat(fase2): feed debate converged output into candidate generation

This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 00:40:44 +02:00
commit 1694141bff
2 changed files with 67 additions and 4 deletions

View file

@ -27,6 +27,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from agent_framework import BaseChatClient, SessionContext
@ -56,13 +57,35 @@ from portfolio_optimiser.workflow import fresh_workflow
@dataclass(frozen=True)
class RunResult:
"""The outcome of one project run: the validated/rejected proposal, its first-class
provenance, the captured (Layer-2) verdict, the ExpeL hits surfaced for it, and the store."""
provenance, the captured (Layer-2) verdict, the ExpeL hits surfaced for it, the store, and
the debate's converged output that the candidate was generated from (F1 traceability)."""
outcome: ValidatedProposal | Rejection
provenance: ProvenanceStamp
verdict: Verdict
retrieved: list[Verdict]
store: VerdictStore
debate_output: str
# The GroupChat orchestrator emits this terminal notice as a participant output; it is NOT the
# debate's substantive result, so the F1 extractor filters it out (verified against MAF 1.9.0).
_ORCH_NOTICE = "reached the maximum number of rounds"
def _debate_text(result: Any) -> str:
"""Extract the proposer's converged output from a ``WorkflowRunResult`` (surfaced by
``output_from=[proposer]``), excluding the orchestrator's termination notice. Returns ``""``
when no substantive participant text is present."""
texts: list[str] = []
for out in result.get_outputs():
text = out if isinstance(out, str) else getattr(out, "text", None)
if not text:
continue
if getattr(out, "author_name", None) == "group_chat_orchestrator" or _ORCH_NOTICE in text:
continue
texts.append(text)
return texts[-1] if texts else ""
def _project_by_id(project_id: str) -> Project:
@ -136,10 +159,14 @@ async def run_project(
tools=[retrieval_tool],
middleware=[budget_mw],
)
await debate.run(f"Find a cost-saving measure for {project.id}.\nContext:\n{context}")
result = await debate.run(f"Find a cost-saving measure for {project.id}.\nContext:\n{context}")
# F1: the candidate must derive from the DEBATE. Feed the proposer's converged output into
# generation (retrieval context is the last-resort fallback only).
debate_output = _debate_text(result)
gen_context = debate_output or context
# 5. Structured candidate -> blocking validation; token bound = the meter in this loop.
outcome = await generate_via_llm(factory("proposer"), project, context, meter)
outcome = await generate_via_llm(factory("proposer"), project, gen_context, meter)
proposal = outcome.proposal
# 6. First-class provenance stamp (authoritative; independent of MAF Annotation).
@ -168,7 +195,12 @@ async def run_project(
notify(verdict)
return RunResult(
outcome=outcome, provenance=stamp, verdict=verdict, retrieved=retrieved, store=store
outcome=outcome,
provenance=stamp,
verdict=verdict,
retrieved=retrieved,
store=store,
debate_output=debate_output,
)