fix: delegate analysis planning from main agent
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
# Main Agent Analysis Handoff Boundary Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Ensure every `new_analysis` request is passed to the existing IntentAgent without Main Agent workflow, mode, or stage selection.
|
||||
|
||||
**Architecture:** Keep the existing RouterRuntime and proposal confirmation flow. Add one deterministic normalization boundary for `RouteDecision(goal="new_analysis")`, apply it after all route/confirmation branches, and remove the RNA-specific workflow override. The downstream IntentAgent, planner, frontend, contracts, and execution stages remain untouched.
|
||||
|
||||
**Tech Stack:** Python 3.11, Pydantic v2, pytest/pytest-asyncio, FastAPI backend API.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Modify production code only in `gfa_core/mas/agents/router_runtime.py`.
|
||||
- Do not modify frontend, IntentAgent, PlanningAgentV2, stage dependencies, RNA-seq YAML, CLI, or backend stage execution.
|
||||
- Preserve the generic proposal confirmation mechanism.
|
||||
- Preserve the exact user analysis request passed into the proposal and downstream recognizer.
|
||||
- Main Agent must not set `workflow_id`, `clarification_question`, or stages for a `new_analysis` decision.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock the Main Agent handoff boundary with regression tests
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/unit/test_router_analysis_handoff_boundary.py`
|
||||
- Test: `tests/unit/test_router_analysis_handoff_boundary.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `RouterRuntime.run(...)`, `RouteDecision`, and `GoalLedger`.
|
||||
- Produces: regression coverage proving that route-model workflow/mode clarification is discarded for new analyses.
|
||||
|
||||
- [ ] **Step 1: Write a failing generic RNA-seq handoff test**
|
||||
|
||||
Create a small fake tool registry whose `resolve_analysis_handoff_request()` returns its input unchanged. Monkeypatch `runtime._decide_route()` to return:
|
||||
|
||||
```python
|
||||
RouteDecision(
|
||||
goal="new_analysis",
|
||||
needs=["analysis_intent"],
|
||||
subject="执行rnaseq",
|
||||
clarification_question="请选择全基因组还是基因家族成员RNA-seq",
|
||||
workflow_id="rnaseq_whole_genome",
|
||||
analysis_request_kind="execution",
|
||||
)
|
||||
```
|
||||
|
||||
Call `RouterRuntime.run(query="执行rnaseq", ..., goal_ledger=GoalLedger(...))` and assert:
|
||||
|
||||
```python
|
||||
assert outcome.action == "clarify"
|
||||
assert outcome.proposal is not None
|
||||
assert outcome.proposal["request"] == "执行rnaseq"
|
||||
assert "workflow_id" not in outcome.proposal
|
||||
assert "全基因组还是" not in outcome.message
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the generic test and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pytest -q tests/unit/test_router_analysis_handoff_boundary.py
|
||||
```
|
||||
|
||||
Expected: FAIL because current `RouterRuntime.run()` returns the route model's mode clarification before creating a proposal.
|
||||
|
||||
- [ ] **Step 3: Add parameterized explicit-mode and non-RNA analysis cases**
|
||||
|
||||
Use the same runtime-level test for:
|
||||
|
||||
```python
|
||||
(
|
||||
("执行全基因组rnaseq", "rnaseq_whole_genome"),
|
||||
("对已确认家族成员执行rnaseq", "rnaseq_family_member"),
|
||||
("执行motif分析", "gene_family"),
|
||||
)
|
||||
```
|
||||
|
||||
For every case assert that the proposal request equals the original query and has no `workflow_id`.
|
||||
|
||||
- [ ] **Step 4: Run all boundary tests and confirm the expected failures**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pytest -q tests/unit/test_router_analysis_handoff_boundary.py
|
||||
```
|
||||
|
||||
Expected: the new-analysis cases fail only because the Main Agent still retains clarification/workflow selection.
|
||||
|
||||
### Task 2: Implement the deterministic pass-through boundary
|
||||
|
||||
**Files:**
|
||||
- Modify: `gfa_core/mas/agents/router_runtime.py:218-230`
|
||||
- Modify: `gfa_core/mas/agents/router_runtime.py:630-820`
|
||||
- Test: `tests/unit/test_router_analysis_handoff_boundary.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `RouteDecision` produced by the route model or pending-proposal confirmation branch.
|
||||
- Produces: `_analysis_handoff_decision(decision: RouteDecision) -> RouteDecision`.
|
||||
|
||||
- [ ] **Step 1: Add the minimal normalization helper**
|
||||
|
||||
Add one static method on `RouterRuntime`:
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def _analysis_handoff_decision(decision: RouteDecision) -> RouteDecision:
|
||||
if decision.goal != "new_analysis":
|
||||
return decision
|
||||
return RouteDecision(
|
||||
goal="new_analysis",
|
||||
needs=["analysis_intent"],
|
||||
subject=decision.subject,
|
||||
referent=decision.referent,
|
||||
interaction_kind=decision.interaction_kind,
|
||||
analysis_request_kind=decision.analysis_request_kind,
|
||||
confirms_pending_proposal=decision.confirms_pending_proposal,
|
||||
)
|
||||
```
|
||||
|
||||
Apply it once after route decision, pending-proposal confirmation, and next-analysis normalization have converged, before any `clarification_question` early return or proposal construction.
|
||||
|
||||
- [ ] **Step 2: Remove RNA-specific Main Agent workflow selection**
|
||||
|
||||
Delete the `has_rnaseq_marker`, `explicit_family_member`, `explicit_whole_genome`, and replacement `RouteDecision` block in `RouterRuntime.run()`. Explicit mode words remain in `query` and therefore continue downstream unchanged.
|
||||
|
||||
- [ ] **Step 3: Tighten the route prompt without adding new behavior**
|
||||
|
||||
Replace the unspecified-RNA mode instruction with the general boundary:
|
||||
|
||||
```text
|
||||
For goal=new_analysis, do not ask for an analysis mode, select a workflow, or select stages.
|
||||
Leave workflow_id and clarification_question null and request analysis_intent; the existing
|
||||
IntentAgent and PlanningAgent own missing analysis choices and executable stage planning.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run boundary tests and verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pytest -q tests/unit/test_router_analysis_handoff_boundary.py
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 5: Run focused existing conversation tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pytest -q tests/unit/test_agent_chat_actions.py tests/unit/test_autofill_entrypoints.py
|
||||
```
|
||||
|
||||
Expected: all tests pass; generic proposal persistence, config dialog routing, and existing chat actions remain unchanged.
|
||||
|
||||
### Task 3: Verify syntax, diff scope, and live backend behavior
|
||||
|
||||
**Files:**
|
||||
- Verify: `gfa_core/mas/agents/router_runtime.py`
|
||||
- Verify: `tests/unit/test_router_analysis_handoff_boundary.py`
|
||||
- Verify: `docs/superpowers/specs/2026-08-07-main-agent-analysis-handoff-boundary-design.md`
|
||||
- Verify: `docs/superpowers/plans/2026-08-07-main-agent-analysis-handoff-boundary.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: local implementation and the running `/api/agent/chat` and `/api/agent/plan` endpoints.
|
||||
- Produces: fresh evidence for regression safety and end-to-end handoff behavior.
|
||||
|
||||
- [ ] **Step 1: Compile the modified module**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
/home/xia-lab/miniconda3/envs/test4py311/bin/python -m py_compile gfa_core/mas/agents/router_runtime.py
|
||||
```
|
||||
|
||||
Expected: exit code 0 and no output.
|
||||
|
||||
- [ ] **Step 2: Audit changed-file scope**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --name-only
|
||||
git diff --cached --name-only
|
||||
```
|
||||
|
||||
Expected production change: only `gfa_core/mas/agents/router_runtime.py`. No frontend or downstream analysis file appears.
|
||||
|
||||
- [ ] **Step 3: Exercise the real chat API with a fresh session**
|
||||
|
||||
POST `执行rnaseq` to `/api/agent/chat` for the existing RNA-seq test job. Assert that the response does not ask whole-genome versus family-member mode and that its proposal request preserves `执行rnaseq`.
|
||||
|
||||
- [ ] **Step 4: Confirm the proposal and inspect the recognizer trace**
|
||||
|
||||
POST `是` in the same session. Assert that `recognize_analysis_intent` receives a query beginning with `执行rnaseq`, and that returned intent stages come from the downstream IntentAgent.
|
||||
|
||||
- [ ] **Step 5: Verify plan fidelity**
|
||||
|
||||
POST the returned intent to `/api/agent/plan`. Assert the plan stages equal the dependency expansion of the downstream intent rather than a Main Agent-authored list.
|
||||
|
||||
- [ ] **Step 6: Report service state honestly**
|
||||
|
||||
If the live service still runs old code, report that API verification is pending restart; do not claim the runtime behavior is fixed based only on unit tests.
|
||||
@@ -223,9 +223,10 @@ goal=new_analysis. Asking only for status or progress without requesting a new p
|
||||
goal=project_status. The Router must not choose the next stages itself.
|
||||
Do not select a workflow for ordinary
|
||||
chat, product help, external knowledge, existing results, job files, or utilities.
|
||||
For an unspecified RNA-seq mode, leave workflow_id null and ask whether the request is
|
||||
whole-genome or confirmed-family-member RNA-seq. Workflow contracts guide conversation
|
||||
and handoff only; PlanningAgentV2 remains responsible for executable stage planning.
|
||||
For goal=new_analysis, do not ask for an analysis mode, select a workflow, or select
|
||||
stages. Leave workflow_id and clarification_question null and request analysis_intent;
|
||||
the existing IntentAgent and PlanningAgent own missing analysis choices and executable
|
||||
stage planning.
|
||||
|
||||
For goal=new_analysis, classify analysis_request_kind as guidance when the user asks
|
||||
what should be done now, how to continue, or which analysis comes next. Use execution
|
||||
@@ -428,6 +429,20 @@ class RouterRuntime:
|
||||
"utility": "propose_utility",
|
||||
}[decision.goal]
|
||||
|
||||
@staticmethod
|
||||
def _analysis_handoff_decision(decision: RouteDecision) -> RouteDecision:
|
||||
if decision.goal != "new_analysis":
|
||||
return decision
|
||||
return RouteDecision(
|
||||
goal="new_analysis",
|
||||
needs=["analysis_intent"],
|
||||
subject=decision.subject,
|
||||
referent=decision.referent,
|
||||
interaction_kind=decision.interaction_kind,
|
||||
analysis_request_kind=decision.analysis_request_kind,
|
||||
confirms_pending_proposal=decision.confirms_pending_proposal,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _pending_analysis_proposal(
|
||||
goal_ledger: GoalLedger | None,
|
||||
@@ -730,43 +745,7 @@ class RouterRuntime:
|
||||
message="The Router returned two invalid route decisions.",
|
||||
tool_trace=decision_trace,
|
||||
)
|
||||
query_lower = str(query or "").lower()
|
||||
has_rnaseq_marker = any(
|
||||
token in query_lower
|
||||
for token in ("rna-seq", "rnaseq", "rna seq", "转录组")
|
||||
)
|
||||
explicit_family_member = has_rnaseq_marker and any(
|
||||
token in query_lower
|
||||
for token in (
|
||||
"基因家族成员",
|
||||
"家族成员",
|
||||
"family member",
|
||||
"family-member",
|
||||
)
|
||||
)
|
||||
explicit_whole_genome = has_rnaseq_marker and any(
|
||||
token in query_lower
|
||||
for token in ("全基因组", "whole genome", "whole-genome")
|
||||
)
|
||||
if (
|
||||
confirmed_proposal is None
|
||||
and (explicit_family_member or explicit_whole_genome)
|
||||
and (
|
||||
decision.goal != "new_analysis"
|
||||
or decision.clarification_question is not None
|
||||
)
|
||||
):
|
||||
decision = RouteDecision(
|
||||
goal="new_analysis",
|
||||
needs=["analysis_intent"],
|
||||
subject=query,
|
||||
interaction_kind=decision.interaction_kind,
|
||||
workflow_id=(
|
||||
"rnaseq_family_member"
|
||||
if explicit_family_member
|
||||
else "rnaseq_whole_genome"
|
||||
),
|
||||
)
|
||||
decision = self._analysis_handoff_decision(decision)
|
||||
authorized_input_paths = _extract_authorized_input_paths(query)
|
||||
if (
|
||||
decision.goal == "new_analysis"
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from gfa_core.mas.agents.router_runtime import (
|
||||
RouteDecision,
|
||||
RouterRuntime,
|
||||
)
|
||||
from gfa_core.mas.workflow_models import GoalLedger
|
||||
|
||||
|
||||
class _PassThroughTools:
|
||||
@staticmethod
|
||||
def resolve_analysis_handoff_request(
|
||||
query: str,
|
||||
*,
|
||||
workflow_id: str | None = None,
|
||||
guidance: bool = False,
|
||||
) -> str:
|
||||
return query
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("query", "workflow_id", "clarification_question"),
|
||||
(
|
||||
(
|
||||
"执行rnaseq",
|
||||
"rnaseq_whole_genome",
|
||||
"请选择全基因组还是基因家族成员RNA-seq",
|
||||
),
|
||||
("执行全基因组rnaseq", "rnaseq_whole_genome", None),
|
||||
("对已确认家族成员执行rnaseq", "rnaseq_family_member", None),
|
||||
("执行motif分析", "gene_family", None),
|
||||
),
|
||||
)
|
||||
def test_new_analysis_proposal_preserves_original_request_without_main_agent_planning(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
query: str,
|
||||
workflow_id: str,
|
||||
clarification_question: str | None,
|
||||
) -> None:
|
||||
runtime = RouterRuntime(object(), _PassThroughTools(), max_iterations=5) # type: ignore[arg-type]
|
||||
|
||||
async def decide_route(**_: Any) -> tuple[RouteDecision, int, list[dict[str, Any]]]:
|
||||
return (
|
||||
RouteDecision(
|
||||
goal="new_analysis",
|
||||
needs=["analysis_intent"],
|
||||
subject=query,
|
||||
clarification_question=clarification_question,
|
||||
workflow_id=workflow_id, # type: ignore[arg-type]
|
||||
analysis_request_kind="execution",
|
||||
),
|
||||
1,
|
||||
[],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(runtime, "_decide_route", decide_route)
|
||||
|
||||
outcome = asyncio.run(
|
||||
runtime.run(
|
||||
query=query,
|
||||
history=[],
|
||||
session_id="analysis-handoff-session",
|
||||
job_id="job-analysis-handoff",
|
||||
goal_ledger=GoalLedger(
|
||||
conversation_id="analysis-handoff-session",
|
||||
revision=0,
|
||||
user_terms={"active_job_id": "job-analysis-handoff"},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert outcome.action == "clarify"
|
||||
assert outcome.proposal is not None
|
||||
assert outcome.proposal["request"] == query
|
||||
assert "workflow_id" not in outcome.proposal
|
||||
assert "全基因组还是" not in outcome.message
|
||||
|
||||
|
||||
def test_new_analysis_decision_discards_main_agent_workflow_and_clarification() -> None:
|
||||
decision = RouteDecision(
|
||||
goal="new_analysis",
|
||||
needs=["analysis_intent"],
|
||||
subject="执行rnaseq",
|
||||
clarification_question="请选择RNA-seq模式",
|
||||
workflow_id="rnaseq_whole_genome",
|
||||
analysis_request_kind="execution",
|
||||
)
|
||||
|
||||
normalized = RouterRuntime._analysis_handoff_decision(decision)
|
||||
|
||||
assert normalized.goal == "new_analysis"
|
||||
assert normalized.needs == ["analysis_intent"]
|
||||
assert normalized.subject == decision.subject
|
||||
assert normalized.workflow_id is None
|
||||
assert normalized.clarification_question is None
|
||||
assert normalized.analysis_request_kind == "execution"
|
||||
Reference in New Issue
Block a user