refactor: 简化 AgentScope 运行时模块与事件处理
- 移除冗余的 user_token 参数传递 - 重构 tool.result 事件使用 ToolAgentOutput 模型 - 重构 text.end 事件使用 WorkerAgentOutput 模型 - 简化 store 模块的 tool result 处理逻辑 - 更新 router/service 适配新事件结构 - 清理废弃的测试文件与设计文档 - 新增 AgentRuns 多模态存储设计文档
This commit is contained in:
@@ -1,608 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agentscope.schemas.user_context import (
|
||||
UserAgentContext,
|
||||
parse_profile_settings,
|
||||
)
|
||||
from core.agentscope.runtime.agent_route_runtime import AgentRouteRuntime
|
||||
from core.agentscope.schemas import ReportOutput, RuntimeOutput
|
||||
from core.agentscope.schemas.agent_runtime import RunCommand
|
||||
from core.agentscope.schemas.execution import ExecutionBatchOutput, ExecutionTaskOutput
|
||||
from core.agentscope.schemas.execution import ExecutionToolCall
|
||||
from core.agentscope.schemas.intent import IntentOutput, IntentTask
|
||||
|
||||
|
||||
def _user_context() -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=uuid4(),
|
||||
username="tester",
|
||||
bio=None,
|
||||
settings=parse_profile_settings(
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"country": "CN",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_emits_started_text_and_finished_events() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FakeOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="TASK_EXECUTION",
|
||||
intent_summary="summary",
|
||||
direct_response=None,
|
||||
tasks=[IntentTask(task_id="t1", title="exec", objective="do")],
|
||||
complexity="complex",
|
||||
response_metadata={"latencyMs": 120},
|
||||
),
|
||||
execution=ExecutionBatchOutput(
|
||||
task_results=[
|
||||
ExecutionTaskOutput(
|
||||
task_id="t1",
|
||||
status="SUCCESS",
|
||||
execution_summary="execution-ok",
|
||||
execution_data={},
|
||||
user_feedback_needs=[],
|
||||
response_metadata={"latencyMs": 300},
|
||||
tool_calls=[
|
||||
ExecutionToolCall(
|
||||
tool_name="calendar_write",
|
||||
args={"title": "A"},
|
||||
result={"event_id": "evt-1"},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
overall_status="SUCCESS",
|
||||
aggregate_summary="ok",
|
||||
),
|
||||
report=ReportOutput(
|
||||
assistant_text="hello world",
|
||||
response_metadata={
|
||||
"model": "qwen3.5-flash",
|
||||
"inputTokens": 10,
|
||||
"outputTokens": 5,
|
||||
"cost": 0.123,
|
||||
"latencyMs": 250,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FakeOrchestrator(), pipeline=_FakePipeline()
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
assert [item["type"] for item in calls] == [
|
||||
"run.started",
|
||||
"step.start",
|
||||
"step.finish",
|
||||
"step.start",
|
||||
"text.start",
|
||||
"text.delta",
|
||||
"text.end",
|
||||
"text.start",
|
||||
"text.delta",
|
||||
"text.end",
|
||||
"tool.result",
|
||||
"step.finish",
|
||||
"step.start",
|
||||
"text.start",
|
||||
"text.delta",
|
||||
"text.end",
|
||||
"step.finish",
|
||||
"run.finished",
|
||||
]
|
||||
assert calls[1]["data"]["stepName"] == "intent"
|
||||
assert calls[2]["data"]["stepName"] == "intent"
|
||||
assert calls[3]["data"]["stepName"] == "execution"
|
||||
assert calls[4]["data"]["stage"] == "intent"
|
||||
assert calls[7]["data"]["stage"] == "execution"
|
||||
assert calls[10]["data"]["toolName"] == "calendar_write"
|
||||
assert calls[10]["data"]["toolCallId"] == "run-1-t1-1"
|
||||
assert calls[10]["data"]["messageId"] == "tool-result-run-1-t1-1"
|
||||
tool_content = calls[10]["data"]["content"]
|
||||
assert tool_content == "calendar_write 执行完成"
|
||||
assert calls[11]["data"]["stepName"] == "execution"
|
||||
assert calls[12]["data"]["stepName"] == "report"
|
||||
assert calls[14]["data"]["delta"] == "hello world"
|
||||
assert calls[13]["data"]["messageId"] == calls[14]["data"]["messageId"]
|
||||
assert calls[14]["data"]["messageId"] == calls[15]["data"]["messageId"]
|
||||
assert calls[15]["data"]["model"] == "qwen3.5-flash"
|
||||
assert calls[15]["data"]["inputTokens"] == 10
|
||||
assert calls[15]["data"]["outputTokens"] == 5
|
||||
assert calls[15]["data"]["cost"] == 0.123
|
||||
assert calls[15]["data"]["latencyMs"] == 250
|
||||
assert calls[16]["data"]["stepName"] == "report"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_emits_run_error_when_orchestrator_fails() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FailOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FailOrchestrator(),
|
||||
pipeline=_FakePipeline(),
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
with pytest.raises(RuntimeError, match="boom"):
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
assert [item["type"] for item in calls] == [
|
||||
"run.started",
|
||||
"step.start",
|
||||
"run.error",
|
||||
]
|
||||
assert calls[1]["data"]["stepName"] == "intent"
|
||||
assert calls[2]["data"]["message"] == "runtime execution failed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_passes_binary_payload_to_orchestrator() -> None:
|
||||
captured_user_input: object | None = None
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
return str(event.get("type", ""))
|
||||
|
||||
class _CaptureOrchestrator:
|
||||
async def run(self, **kwargs: object) -> RuntimeOutput:
|
||||
nonlocal captured_user_input
|
||||
captured_user_input = kwargs.get("user_input")
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="DIRECT_RESPONSE",
|
||||
intent_summary="summary",
|
||||
direct_response="done",
|
||||
tasks=[],
|
||||
complexity="simple",
|
||||
),
|
||||
execution=None,
|
||||
report=ReportOutput(
|
||||
assistant_text="ok",
|
||||
response_metadata={},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_CaptureOrchestrator(),
|
||||
pipeline=_FakePipeline(),
|
||||
)
|
||||
command = RunCommand.model_validate(
|
||||
{
|
||||
"threadId": "thread-1",
|
||||
"runId": "run-1",
|
||||
"messages": [
|
||||
{
|
||||
"id": "u1",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello"},
|
||||
{
|
||||
"type": "binary",
|
||||
"mimeType": "image/png",
|
||||
"data": "aGVsbG8=",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
assert isinstance(captured_user_input, list)
|
||||
first = captured_user_input[0]
|
||||
assert isinstance(first, dict)
|
||||
content = first.get("content")
|
||||
assert isinstance(content, list)
|
||||
binary = content[1]
|
||||
assert isinstance(binary, dict)
|
||||
assert binary.get("data") == "aGVsbG8="
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_direct_response_finishes_without_report_stage() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _DirectOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="DIRECT_RESPONSE",
|
||||
intent_summary="summary",
|
||||
direct_response="direct-answer",
|
||||
tasks=[],
|
||||
complexity="simple",
|
||||
response_metadata={"latencyMs": 88},
|
||||
),
|
||||
execution=None,
|
||||
report=ReportOutput(
|
||||
assistant_text="direct-answer",
|
||||
response_metadata={"latencyMs": 88},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_DirectOrchestrator(),
|
||||
pipeline=_FakePipeline(),
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
assert [item["type"] for item in calls] == [
|
||||
"run.started",
|
||||
"step.start",
|
||||
"step.finish",
|
||||
"text.start",
|
||||
"text.delta",
|
||||
"text.end",
|
||||
"run.finished",
|
||||
]
|
||||
assert calls[3]["data"]["stage"] == "intent"
|
||||
assert calls[4]["data"]["delta"] == "direct-answer"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_tool_result_parses_json_string_ui_payload() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FakeOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="TASK_EXECUTION",
|
||||
intent_summary="summary",
|
||||
direct_response=None,
|
||||
tasks=[IntentTask(task_id="t1", title="exec", objective="do")],
|
||||
complexity="complex",
|
||||
response_metadata={},
|
||||
),
|
||||
execution=ExecutionBatchOutput(
|
||||
task_results=[
|
||||
ExecutionTaskOutput(
|
||||
task_id="t1",
|
||||
status="SUCCESS",
|
||||
execution_summary="execution-ok",
|
||||
execution_data={},
|
||||
user_feedback_needs=[],
|
||||
response_metadata={},
|
||||
tool_calls=[
|
||||
ExecutionToolCall(
|
||||
tool_name="calendar_write",
|
||||
args={"title": "A"},
|
||||
result='{"type":"calendar_card.v1","version":"v1","data":{"ok":true,"title":"A"},"actions":[]}',
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
overall_status="SUCCESS",
|
||||
aggregate_summary="ok",
|
||||
),
|
||||
report=ReportOutput(
|
||||
assistant_text="hello world",
|
||||
response_metadata={},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FakeOrchestrator(), pipeline=_FakePipeline()
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
tool_events = [item for item in calls if item.get("type") == "tool.result"]
|
||||
assert len(tool_events) == 1
|
||||
data = tool_events[0]["data"]
|
||||
assert isinstance(data, dict)
|
||||
assert isinstance(data.get("ui"), dict)
|
||||
assert data["ui"]["type"] == "calendar_card.v1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_tool_result_keeps_plain_text_content() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FakeOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="TASK_EXECUTION",
|
||||
intent_summary="summary",
|
||||
direct_response=None,
|
||||
tasks=[IntentTask(task_id="t1", title="exec", objective="do")],
|
||||
complexity="complex",
|
||||
response_metadata={},
|
||||
),
|
||||
execution=ExecutionBatchOutput(
|
||||
task_results=[
|
||||
ExecutionTaskOutput(
|
||||
task_id="t1",
|
||||
status="SUCCESS",
|
||||
execution_summary="execution-ok",
|
||||
execution_data={},
|
||||
user_feedback_needs=[],
|
||||
response_metadata={},
|
||||
tool_calls=[
|
||||
ExecutionToolCall(
|
||||
tool_name="calendar_write",
|
||||
args={"title": "A"},
|
||||
result="created successfully",
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
overall_status="SUCCESS",
|
||||
aggregate_summary="ok",
|
||||
),
|
||||
report=ReportOutput(
|
||||
assistant_text="hello world",
|
||||
response_metadata={},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FakeOrchestrator(), pipeline=_FakePipeline()
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
tool_events = [item for item in calls if item.get("type") == "tool.result"]
|
||||
assert len(tool_events) == 1
|
||||
data = tool_events[0]["data"]
|
||||
assert isinstance(data, dict)
|
||||
assert data["content"] == "created successfully"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_tool_result_sanitizes_sensitive_payload() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FakeOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="TASK_EXECUTION",
|
||||
intent_summary="summary",
|
||||
direct_response=None,
|
||||
tasks=[IntentTask(task_id="t1", title="exec", objective="do")],
|
||||
complexity="complex",
|
||||
response_metadata={},
|
||||
),
|
||||
execution=ExecutionBatchOutput(
|
||||
task_results=[
|
||||
ExecutionTaskOutput(
|
||||
task_id="t1",
|
||||
status="SUCCESS",
|
||||
execution_summary="execution-ok",
|
||||
execution_data={},
|
||||
user_feedback_needs=[],
|
||||
response_metadata={},
|
||||
tool_calls=[
|
||||
ExecutionToolCall(
|
||||
tool_name="calendar_write",
|
||||
args={
|
||||
"title": "A",
|
||||
"accessToken": "arg-secret",
|
||||
"author": "alice",
|
||||
},
|
||||
result={
|
||||
"ok": True,
|
||||
"accessToken": "secret-token",
|
||||
"message": "Authorization: Bearer inline-token",
|
||||
"nested": [
|
||||
{
|
||||
"authorizationHeader": "Bearer abc",
|
||||
}
|
||||
],
|
||||
},
|
||||
error="failed authorization=Bearer abc123 detail",
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
overall_status="SUCCESS",
|
||||
aggregate_summary="ok",
|
||||
),
|
||||
report=ReportOutput(
|
||||
assistant_text="hello world",
|
||||
response_metadata={},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FakeOrchestrator(), pipeline=_FakePipeline()
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
tool_events = [item for item in calls if item.get("type") == "tool.result"]
|
||||
assert len(tool_events) == 1
|
||||
data = tool_events[0]["data"]
|
||||
assert isinstance(data, dict)
|
||||
assert isinstance(data["result"], dict)
|
||||
assert data["result"]["accessToken"] == "[REDACTED]"
|
||||
assert data["result"]["message"] == "Authorization=[REDACTED]"
|
||||
nested = data["result"]["nested"]
|
||||
assert isinstance(nested, list)
|
||||
assert nested[0]["authorizationHeader"] == "[REDACTED]"
|
||||
assert isinstance(data["args"], dict)
|
||||
assert data["args"]["accessToken"] == "[REDACTED]"
|
||||
assert data["args"]["author"] == "alice"
|
||||
assert data["error"] == "failed authorization=[REDACTED] detail"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_tool_result_keeps_non_object_result() -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class _FakePipeline:
|
||||
async def emit(self, *, session_id: str, event: dict[str, object]) -> str:
|
||||
assert session_id == "thread-1"
|
||||
calls.append(event)
|
||||
return f"{len(calls)}-0"
|
||||
|
||||
class _FakeOrchestrator:
|
||||
async def run(self, **_: object) -> RuntimeOutput:
|
||||
return RuntimeOutput(
|
||||
intent=IntentOutput(
|
||||
route="TASK_EXECUTION",
|
||||
intent_summary="summary",
|
||||
direct_response=None,
|
||||
tasks=[IntentTask(task_id="t1", title="exec", objective="do")],
|
||||
complexity="complex",
|
||||
response_metadata={},
|
||||
),
|
||||
execution=ExecutionBatchOutput(
|
||||
task_results=[
|
||||
ExecutionTaskOutput(
|
||||
task_id="t1",
|
||||
status="SUCCESS",
|
||||
execution_summary="execution-ok",
|
||||
execution_data={},
|
||||
user_feedback_needs=[],
|
||||
response_metadata={},
|
||||
tool_calls=[
|
||||
ExecutionToolCall(
|
||||
tool_name="calendar_write",
|
||||
args={"title": "A"},
|
||||
result=["evt-1", "evt-2"],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
overall_status="SUCCESS",
|
||||
aggregate_summary="ok",
|
||||
),
|
||||
report=ReportOutput(
|
||||
assistant_text="hello world",
|
||||
response_metadata={},
|
||||
),
|
||||
)
|
||||
|
||||
runtime = AgentRouteRuntime(
|
||||
orchestrator=_FakeOrchestrator(), pipeline=_FakePipeline()
|
||||
)
|
||||
command = RunCommand(threadId="thread-1", runId="run-1", messages=[])
|
||||
|
||||
await runtime.run(
|
||||
command=command,
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_user_context(),
|
||||
session=cast(AsyncSession, object()),
|
||||
)
|
||||
|
||||
tool_events = [item for item in calls if item.get("type") == "tool.result"]
|
||||
assert len(tool_events) == 1
|
||||
data = tool_events[0]["data"]
|
||||
assert isinstance(data, dict)
|
||||
assert isinstance(data["result"], dict)
|
||||
assert data["result"]["value"] == ["evt-1", "evt-2"]
|
||||
@@ -1,229 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agentscope.schemas.system_agent_config import SystemAgentLLMConfig
|
||||
from core.agentscope.schemas.user_context import (
|
||||
UserAgentContext,
|
||||
parse_profile_settings,
|
||||
)
|
||||
from core.agentscope.runtime.config_loader import RuntimeStageConfig
|
||||
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
|
||||
from schemas.user import UserContext, parse_profile_settings
|
||||
|
||||
|
||||
def _ctx() -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=uuid4(),
|
||||
username="alice",
|
||||
bio=None,
|
||||
settings=parse_profile_settings(
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"country": "CN",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
class _FakePipeline:
|
||||
def __init__(self) -> None:
|
||||
self.events: list[dict[str, Any]] = []
|
||||
|
||||
|
||||
def _stage_config() -> dict[str, RuntimeStageConfig]:
|
||||
llm = SystemAgentLLMConfig(temperature=0.1, max_tokens=256, timeout_seconds=30)
|
||||
return {
|
||||
"intent": RuntimeStageConfig("intent", "qwen3.5-flash", "dashscope", llm),
|
||||
"execution": RuntimeStageConfig("execution", "deepseek-chat", "deepseek", llm),
|
||||
"report": RuntimeStageConfig("report", "deepseek-chat", "deepseek", llm),
|
||||
}
|
||||
async def emit(self, *, session_id: str, event: dict[str, Any]) -> str:
|
||||
self.events.append({"session_id": session_id, "event": event})
|
||||
return "1-0"
|
||||
|
||||
|
||||
class _FakeRunner:
|
||||
def __init__(self) -> None:
|
||||
self.intent_calls = 0
|
||||
self.execution_calls = 0
|
||||
self.report_calls = 0
|
||||
self.last_user_input: str | list[dict[str, Any]] | None = None
|
||||
|
||||
async def run_json_stage(
|
||||
async def run_router_then_worker(
|
||||
self,
|
||||
*,
|
||||
stage_config: RuntimeStageConfig,
|
||||
agent_name: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
toolkit: Any | None,
|
||||
session,
|
||||
user_context,
|
||||
user_input,
|
||||
router_toolkit,
|
||||
worker_toolkit,
|
||||
extra_context=None,
|
||||
) -> dict[str, Any]:
|
||||
del agent_name, system_prompt, user_prompt, toolkit
|
||||
if stage_config.stage == "intent":
|
||||
self.intent_calls += 1
|
||||
return {
|
||||
"route": "DIRECT_RESPONSE",
|
||||
"intent_summary": "直接问候",
|
||||
"direct_response": "你好",
|
||||
"tasks": [],
|
||||
"complexity": "simple",
|
||||
"response_metadata": {"model": "qwen3.5-flash", "latencyMs": 100},
|
||||
}
|
||||
self.report_calls += 1
|
||||
del session, user_context, router_toolkit, worker_toolkit, extra_context
|
||||
self.last_user_input = user_input
|
||||
return {
|
||||
"assistant_text": "已完成",
|
||||
"response_metadata": {"source": "report-agent"},
|
||||
"worker": {
|
||||
"status": "success",
|
||||
"answer": "done",
|
||||
"key_points": [],
|
||||
"result_type": "summary",
|
||||
"suggested_actions": [],
|
||||
"error": None,
|
||||
"response_metadata": {
|
||||
"model": "qwen3.5-flash",
|
||||
"inputTokens": 10,
|
||||
"outputTokens": 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _ComplexRunner(_FakeRunner):
|
||||
async def run_json_stage(
|
||||
self,
|
||||
*,
|
||||
stage_config: RuntimeStageConfig,
|
||||
agent_name: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
toolkit: Any | None,
|
||||
) -> dict[str, Any]:
|
||||
del agent_name, system_prompt, user_prompt, toolkit
|
||||
if stage_config.stage == "intent":
|
||||
self.intent_calls += 1
|
||||
return {
|
||||
"route": "TASK_EXECUTION",
|
||||
"intent_summary": "需要写入日历",
|
||||
"direct_response": None,
|
||||
"tasks": [
|
||||
{"task_id": "t1", "title": "创建事件", "objective": "写入明天会议"}
|
||||
],
|
||||
"complexity": "complex",
|
||||
}
|
||||
if stage_config.stage == "execution":
|
||||
self.execution_calls += 1
|
||||
return {
|
||||
"task_id": "t1",
|
||||
"status": "SUCCESS",
|
||||
"execution_summary": "done",
|
||||
"execution_data": {},
|
||||
"user_feedback_needs": [],
|
||||
}
|
||||
self.report_calls += 1
|
||||
return {
|
||||
"assistant_text": "任务执行完成",
|
||||
"response_metadata": {"source": "report-agent"},
|
||||
}
|
||||
def _user_context() -> UserContext:
|
||||
return UserContext(
|
||||
id="00000000-0000-0000-0000-000000000001",
|
||||
username="alice",
|
||||
email="alice@example.com",
|
||||
avatar_url=None,
|
||||
bio=None,
|
||||
settings=parse_profile_settings(None),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_direct_response_skips_execution(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_runner = _FakeRunner()
|
||||
def _run_command_with_binary() -> Any:
|
||||
from ag_ui.core import RunAgentInput
|
||||
|
||||
async def _fake_config_loader(
|
||||
_session: AsyncSession,
|
||||
) -> dict[str, RuntimeStageConfig]:
|
||||
return _stage_config()
|
||||
|
||||
class _FakeToolkit:
|
||||
def get_json_schemas(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
return RunAgentInput.model_validate(
|
||||
{
|
||||
"threadId": "00000000-0000-0000-0000-000000000010",
|
||||
"runId": "run-1",
|
||||
"state": {},
|
||||
"messages": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar_read",
|
||||
"description": "read",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
"id": "u1",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "看这张图"},
|
||||
{
|
||||
"type": "binary",
|
||||
"mimeType": "image/png",
|
||||
"url": "https://example.com/signed.png",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
async def call_tool_function(self, tool_call: dict[str, Any]):
|
||||
del tool_call
|
||||
if False:
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.runtime.orchestrator.build_stage_toolkit",
|
||||
lambda **_: _FakeToolkit(),
|
||||
],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {},
|
||||
}
|
||||
)
|
||||
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(
|
||||
runner=fake_runner,
|
||||
config_loader=_fake_config_loader,
|
||||
)
|
||||
result = await orchestrator.run(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_ctx(),
|
||||
user_input="你好",
|
||||
)
|
||||
|
||||
assert result.intent.route == "DIRECT_RESPONSE"
|
||||
assert result.execution is None
|
||||
assert result.report.assistant_text == "你好"
|
||||
assert result.report.response_metadata["model"] == "qwen3.5-flash"
|
||||
assert fake_runner.execution_calls == 0
|
||||
assert fake_runner.report_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_complex_route_runs_execution(
|
||||
async def test_orchestrator_maps_binary_to_model_image_url(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_runner = _ComplexRunner()
|
||||
|
||||
async def _fake_config_loader(
|
||||
_session: AsyncSession,
|
||||
) -> dict[str, RuntimeStageConfig]:
|
||||
return _stage_config()
|
||||
|
||||
class _FakeToolkit:
|
||||
def get_json_schemas(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar_read",
|
||||
"description": "read",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar_write",
|
||||
"description": "write",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
async def call_tool_function(self, tool_call: dict[str, Any]):
|
||||
del tool_call
|
||||
if False:
|
||||
yield None
|
||||
|
||||
pipeline = _FakePipeline()
|
||||
runner = _FakeRunner()
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.runtime.orchestrator.build_stage_toolkit",
|
||||
lambda **_: _FakeToolkit(),
|
||||
lambda **_: None,
|
||||
)
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(pipeline=pipeline, runner=runner)
|
||||
|
||||
await orchestrator.run(
|
||||
command=_run_command_with_binary(),
|
||||
owner_id=UUID("00000000-0000-0000-0000-000000000001"),
|
||||
user_context=_user_context(),
|
||||
session=None,
|
||||
)
|
||||
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(
|
||||
runner=fake_runner,
|
||||
config_loader=_fake_config_loader,
|
||||
)
|
||||
result = await orchestrator.run(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_ctx(),
|
||||
user_input="帮我安排明天会议",
|
||||
assert isinstance(runner.last_user_input, list)
|
||||
assert runner.last_user_input[0]["type"] == "text"
|
||||
assert runner.last_user_input[1]["type"] == "image_url"
|
||||
assert (
|
||||
runner.last_user_input[1]["image_url"]["url"]
|
||||
== "https://example.com/signed.png"
|
||||
)
|
||||
|
||||
assert result.intent.route == "TASK_EXECUTION"
|
||||
assert result.execution is not None
|
||||
assert result.execution.overall_status == "SUCCESS"
|
||||
assert fake_runner.execution_calls == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrator_emits_worker_output_on_text_end(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pipeline = _FakePipeline()
|
||||
runner = _FakeRunner()
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.runtime.orchestrator.build_stage_toolkit",
|
||||
lambda **_: None,
|
||||
)
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(pipeline=pipeline, runner=runner)
|
||||
|
||||
await orchestrator.run(
|
||||
command=_run_command_with_binary(),
|
||||
owner_id=UUID("00000000-0000-0000-0000-000000000001"),
|
||||
user_context=_user_context(),
|
||||
session=None,
|
||||
)
|
||||
|
||||
emitted = [item["event"] for item in pipeline.events]
|
||||
text_end = next(item for item in emitted if item.get("type") == "text.end")
|
||||
assert text_end["data"]["workerAgentOutput"]["answer"] == "done"
|
||||
assert any(item.get("type") == "run.finished" for item in emitted)
|
||||
|
||||
Reference in New Issue
Block a user