From 77011a21b738ca7aa163bf9baa73032138fd6176 Mon Sep 17 00:00:00 2001 From: potaser Date: Tue, 11 Aug 2026 06:37:24 +0800 Subject: [PATCH] fix: resume confirmed analysis handoff --- gfa_core/mas/agents/conversation_tools.py | 1 + gfa_core/mas/agents/router_runtime.py | 2 +- ...est_main_agent_data_preparation_handoff.py | 31 ++++++ .../test_router_analysis_handoff_boundary.py | 97 +++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/gfa_core/mas/agents/conversation_tools.py b/gfa_core/mas/agents/conversation_tools.py index 42bd2c9b5..2ba8b8df8 100644 --- a/gfa_core/mas/agents/conversation_tools.py +++ b/gfa_core/mas/agents/conversation_tools.py @@ -299,6 +299,7 @@ class BoundRecognizeAnalysisIntentArgs(BaseModel): model_config = ConfigDict(extra="forbid") query: str prefix: str = "" + language: str = "zh" _prefix_is_literal = field_validator("prefix")(_validate_prefix) diff --git a/gfa_core/mas/agents/router_runtime.py b/gfa_core/mas/agents/router_runtime.py index 33e2137ee..692351ee7 100644 --- a/gfa_core/mas/agents/router_runtime.py +++ b/gfa_core/mas/agents/router_runtime.py @@ -1405,7 +1405,7 @@ class RouterRuntime: tool_trace=decision_trace, interaction_kind=decision.interaction_kind, ) - if decision.goal == "new_analysis": + if decision.goal == "new_analysis" and confirmed_proposal is None: proposal_query = self.tools.resolve_analysis_handoff_request( effective_query, workflow_id=decision.workflow_id, diff --git a/tests/unit/test_main_agent_data_preparation_handoff.py b/tests/unit/test_main_agent_data_preparation_handoff.py index 482263857..bb6ce4ade 100644 --- a/tests/unit/test_main_agent_data_preparation_handoff.py +++ b/tests/unit/test_main_agent_data_preparation_handoff.py @@ -1,5 +1,7 @@ +import asyncio import json +from gfa_core.mas.agents.intent_v2 import IntentAgentV2 from gfa_core.mas.agents.conversation_tools import build_plantai_conversation_tools from gfa_core.mas.llm import LLMManager from gfa_core.mas.workflow_models import GoalLedger @@ -83,3 +85,32 @@ def test_next_step_question_is_not_rewritten_to_genome_prepare(tmp_path): assert resolved.startswith("接下来需要做什么?\n") assert "执行数据准备(genome_prepare)" not in resolved + + +def test_bound_analysis_intent_uses_default_language(monkeypatch, tmp_path): + captured_queries = [] + + async def fake_execute(_self, state, message): + captured_queries.append(message.payload["query"]) + state.results["results"] = { + "intent": { + "gene_family": "SPL", + "stages": ["ppi", "kaks"], + "primary_stage": "ppi", + } + } + return state + + monkeypatch.setattr(IntentAgentV2, "execute", fake_execute) + result = json.loads( + asyncio.run( + _registry(tmp_path).dispatch( + "recognize_analysis_intent", + json.dumps({"query": "执行Ft的PPI和kaks分析"}, ensure_ascii=False), + ) + ) + ) + + assert result["ok"] is True + assert result["data"]["stages"] == ["ppi", "kaks"] + assert captured_queries[0].startswith("执行Ft的PPI和kaks分析\n") diff --git a/tests/unit/test_router_analysis_handoff_boundary.py b/tests/unit/test_router_analysis_handoff_boundary.py index 1a3b6d55c..74aaaf3a1 100644 --- a/tests/unit/test_router_analysis_handoff_boundary.py +++ b/tests/unit/test_router_analysis_handoff_boundary.py @@ -1,14 +1,18 @@ from __future__ import annotations import asyncio +import json from typing import Any import pytest +from pydantic import BaseModel +from gfa_core.mas.agents.conversation_tools import ConversationToolRegistry from gfa_core.mas.agents.router_runtime import ( RouteDecision, RouterRuntime, ) +from gfa_core.mas.llm import LLMResponse, LLMToolCall from gfa_core.mas.workflow_models import GoalLedger @@ -39,6 +43,28 @@ class _ContextEnrichingTools: ) +class _IntentArgs(BaseModel): + query: str + + +class _SequenceLLM: + def __init__(self, responses: list[LLMResponse]) -> None: + self.responses = list(responses) + + async def acall(self, *_args: Any, **_kwargs: Any) -> LLMResponse: + return self.responses.pop(0) + + +def _response(content: str = "", *, tool_calls: list[LLMToolCall] | None = None) -> LLMResponse: + return LLMResponse( + content=content, + model="test-model", + provider="test-provider", + usage={}, + tool_calls=list(tool_calls or []), + ) + + @pytest.mark.parametrize( ("query", "workflow_id", "clarification_question"), ( @@ -134,6 +160,77 @@ def test_context_enrichment_does_not_replace_original_analysis_handoff() -> None assert outcome.proposal["original_request"] == "执行kaks分析" +def test_confirmed_analysis_proposal_skips_second_confirmation() -> None: + job_id = "job-confirmed-analysis-handoff" + enriched_request = ( + "执行Ft的PPI和kaks分析\n" + "已确认上下文:基因家族 SPL;目标物种 Ft;参考物种 At。" + ) + recognized_queries: list[str] = [] + registry = ConversationToolRegistry() + registry.register( + name="recognize_analysis_intent", + description="Recognize an analysis intent.", + args_model=_IntentArgs, + handler=lambda args: recognized_queries.append(args.query) + or { + "gene_family": "SPL", + "stages": ["ppi", "kaks"], + "primary_stage": "ppi", + "params": {"targetSpecies": ["Ft"], "referenceSpecies": ["At"]}, + }, + permission="pure", + ) + llm = _SequenceLLM( + [ + _response( + tool_calls=[ + LLMToolCall( + id="recognize-confirmed-analysis-1", + name="recognize_analysis_intent", + arguments=json.dumps( + {"query": enriched_request}, ensure_ascii=False + ), + ) + ] + ), + _response( + '{"status":"ok","action":"propose_analysis",' + '"message":"已交给现有分析系统。"}' + ), + ] + ) + ledger = GoalLedger( + conversation_id="session-confirmed-analysis-handoff", + revision=1, + user_terms={ + "active_job_id": job_id, + "pending_proposal": { + "proposal_id": "proposal-confirmed-analysis", + "request": enriched_request, + "original_request": "执行Ft的PPI和kaks分析", + "subject": "执行Ft的PPI和kaks分析", + "job_id": job_id, + }, + }, + ) + + outcome = asyncio.run( + RouterRuntime(llm, registry, max_iterations=5).run( + query="继续", + history=[], + session_id="session-confirmed-analysis-handoff", + job_id=job_id, + goal_ledger=ledger, + ) + ) + + assert outcome.action == "propose_analysis" + assert outcome.intent is not None + assert outcome.intent["stages"] == ["ppi", "kaks"] + assert recognized_queries == [enriched_request] + + def test_new_analysis_decision_discards_main_agent_workflow_and_clarification() -> None: decision = RouteDecision( goal="new_analysis",