feat(fase2): feed debate converged output into candidate generation
This commit is contained in:
parent
7573c4439f
commit
1694141bff
2 changed files with 67 additions and 4 deletions
|
|
@ -27,6 +27,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from agent_framework import BaseChatClient, SessionContext
|
from agent_framework import BaseChatClient, SessionContext
|
||||||
|
|
||||||
|
|
@ -56,13 +57,35 @@ from portfolio_optimiser.workflow import fresh_workflow
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RunResult:
|
class RunResult:
|
||||||
"""The outcome of one project run: the validated/rejected proposal, its first-class
|
"""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
|
outcome: ValidatedProposal | Rejection
|
||||||
provenance: ProvenanceStamp
|
provenance: ProvenanceStamp
|
||||||
verdict: Verdict
|
verdict: Verdict
|
||||||
retrieved: list[Verdict]
|
retrieved: list[Verdict]
|
||||||
store: VerdictStore
|
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:
|
def _project_by_id(project_id: str) -> Project:
|
||||||
|
|
@ -136,10 +159,14 @@ async def run_project(
|
||||||
tools=[retrieval_tool],
|
tools=[retrieval_tool],
|
||||||
middleware=[budget_mw],
|
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.
|
# 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
|
proposal = outcome.proposal
|
||||||
|
|
||||||
# 6. First-class provenance stamp (authoritative; independent of MAF Annotation).
|
# 6. First-class provenance stamp (authoritative; independent of MAF Annotation).
|
||||||
|
|
@ -168,7 +195,12 @@ async def run_project(
|
||||||
notify(verdict)
|
notify(verdict)
|
||||||
|
|
||||||
return RunResult(
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,37 @@ async def test_wiring_budget_middleware_and_retrieval_tool(
|
||||||
assert any(isinstance(t, FunctionTool) for t in tools)
|
assert any(isinstance(t, FunctionTool) for t in tools)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_g_validated_proposal_derives_from_debate(
|
||||||
|
docs_dir, make_client_factory, fresh_store, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
"""F1: the candidate fed to generation derives from the DEBATE, not just retrieval. Spy
|
||||||
|
the context passed to generate_via_llm; the proposer's converged output ('Reduce scope',
|
||||||
|
present in _VALID but NOT in the docs_dir fixture) must reach generation. Deleting the
|
||||||
|
debate->generation wiring makes this fail."""
|
||||||
|
from portfolio_optimiser import run as run_mod
|
||||||
|
|
||||||
|
captured: dict[str, str] = {}
|
||||||
|
real_generate = run_mod.generate_via_llm
|
||||||
|
|
||||||
|
async def spy_generate(chat_client, project, context, meter, **kw):
|
||||||
|
captured["context"] = context
|
||||||
|
return await real_generate(chat_client, project, context, meter, **kw)
|
||||||
|
|
||||||
|
monkeypatch.setattr(run_mod, "generate_via_llm", spy_generate)
|
||||||
|
result = await run_project(
|
||||||
|
"FV42-GSV-E1",
|
||||||
|
"local",
|
||||||
|
docs_dir=docs_dir,
|
||||||
|
verdict_input=_VI,
|
||||||
|
client_factory=make_client_factory(_VALID),
|
||||||
|
store=fresh_store,
|
||||||
|
)
|
||||||
|
debate_marker = "Reduce scope" # in _VALID (proposer output); NOT in docs_dir
|
||||||
|
assert result.debate_output and debate_marker in result.debate_output
|
||||||
|
assert debate_marker in captured["context"] # debate output reached generation
|
||||||
|
assert isinstance(result.outcome, ValidatedProposal)
|
||||||
|
|
||||||
|
|
||||||
async def test_f_malformed_contract_raises_before_any_chat(docs_dir, make_client_factory) -> None:
|
async def test_f_malformed_contract_raises_before_any_chat(docs_dir, make_client_factory) -> None:
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
base = make_client_factory(_VALID)
|
base = make_client_factory(_VALID)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue