166 lines
4.9 KiB
Python
166 lines
4.9 KiB
Python
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
|
|
|
|
|
|
class _IntentArgs(BaseModel):
|
|
query: str
|
|
|
|
|
|
class _SequenceLLM:
|
|
def __init__(self, responses):
|
|
self.responses = list(responses)
|
|
|
|
async def acall(self, *_args, **_kwargs):
|
|
return self.responses.pop(0)
|
|
|
|
|
|
def _response(content="", *, tool_calls=None):
|
|
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"),
|
|
(
|
|
(
|
|
"执行rnaseq",
|
|
"rnaseq_whole_genome",
|
|
"请选择全基因组还是基因家族成员RNA-seq",
|
|
),
|
|
("执行全基因组rnaseq", "rnaseq_whole_genome", None),
|
|
("对已确认家族成员执行rnaseq", "rnaseq_family_member", None),
|
|
("执行motif分析", "gene_family", None),
|
|
),
|
|
)
|
|
def test_new_analysis_hands_original_request_to_intent_without_main_agent_planning(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
query: str,
|
|
workflow_id: str,
|
|
clarification_question: str | None,
|
|
) -> None:
|
|
captured_queries = []
|
|
registry = ConversationToolRegistry()
|
|
registry.register(
|
|
name="recognize_analysis_intent",
|
|
description="Recognize an analysis intent.",
|
|
args_model=_IntentArgs,
|
|
handler=lambda args: captured_queries.append(args.query)
|
|
or {"stages": ["motif"], "primary_stage": "motif"},
|
|
permission="pure",
|
|
)
|
|
runtime = RouterRuntime(
|
|
_SequenceLLM(
|
|
[
|
|
_response(
|
|
tool_calls=[
|
|
LLMToolCall(
|
|
id="recognize-direct-analysis",
|
|
name="recognize_analysis_intent",
|
|
arguments=json.dumps({"query": query}, ensure_ascii=False),
|
|
)
|
|
]
|
|
),
|
|
_response(
|
|
'{"status":"ok","action":"propose_analysis",'
|
|
'"message":"分析请求已交给现有意图系统。"}'
|
|
),
|
|
]
|
|
),
|
|
registry,
|
|
max_iterations=5,
|
|
)
|
|
|
|
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 == "propose_analysis"
|
|
assert outcome.proposal is None
|
|
assert captured_queries == [query]
|
|
assert outcome.intent["stages"] == ["motif"]
|
|
|
|
|
|
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"
|
|
|
|
|
|
def test_router_response_language_does_not_rewrite_business_query() -> None:
|
|
query = "执行接下来的分析"
|
|
messages = RouterRuntime._initial_messages(
|
|
query=query,
|
|
history=[],
|
|
session_id="english-router-session",
|
|
job_id="job-english-router",
|
|
decision=RouteDecision(
|
|
goal="general",
|
|
needs=[],
|
|
subject=query,
|
|
),
|
|
goal_ledger=None,
|
|
language="en",
|
|
)
|
|
|
|
assert messages[-1].content == query
|
|
assert "Respond to the user in English." in messages[0].content
|