mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-12 00:30:28 +08:00
105 lines
4.2 KiB
Python
105 lines
4.2 KiB
Python
"""真实 Agent 图中的子代理跨轮复用与上下文压缩回归。"""
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
from langchain.agents import create_agent
|
|
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
|
|
from langchain_core.messages import AIMessage, HumanMessage
|
|
from langchain_core.tools import tool
|
|
|
|
from app.agent.middleware.subagents import (
|
|
SubAgentTaskControlMiddleware,
|
|
_builtin_subagent_profiles,
|
|
)
|
|
from app.agent.middleware.summarization import ContextPreservingSummarizationMiddleware
|
|
|
|
|
|
@pytest.fixture
|
|
def anyio_backend():
|
|
"""固定使用运行时支持的 asyncio 后端。"""
|
|
return "asyncio"
|
|
|
|
|
|
class _TaskModel(FakeMessagesListChatModel):
|
|
"""提供确定的工具调用响应,避免测试访问真实模型。"""
|
|
|
|
def bind_tools(self, tools, **kwargs):
|
|
"""允许 LangChain 绑定工具并复用脚本响应。"""
|
|
return self
|
|
|
|
|
|
def _task_call(call_id: str) -> AIMessage:
|
|
"""生成需要子代理返回结果的委派调用。"""
|
|
return AIMessage(content="", tool_calls=[{
|
|
"name": "subagent_task",
|
|
"args": {"action": "run", "description": "检查下载器状态", "timeout_ms": 1000},
|
|
"id": call_id,
|
|
"type": "tool_call",
|
|
}])
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_cached_graph_can_delegate_on_consecutive_turns(monkeypatch):
|
|
"""正常结束仅回收本轮任务,缓存图下一轮仍能实际调用子代理。"""
|
|
model = _TaskModel(responses=[_task_call("one"), AIMessage(content="检查完成"),
|
|
_task_call("two"), AIMessage(content="再次完成")])
|
|
middleware = SubAgentTaskControlMiddleware(
|
|
model=model, profiles=_builtin_subagent_profiles(), tools=[],
|
|
)
|
|
run_task = AsyncMock(return_value="下载器在线")
|
|
monkeypatch.setattr(middleware._provider, "run_task", run_task)
|
|
graph = create_agent(model=model, middleware=[middleware])
|
|
|
|
for question in ("检查下载器", "再检查一次"):
|
|
result = await graph.ainvoke({"messages": [HumanMessage(content=question)]})
|
|
output = next(message for message in result["messages"] if message.type == "tool")
|
|
assert json.loads(output.content)["tasks"][0]["result"] == "下载器在线"
|
|
assert middleware._tasks == {}
|
|
|
|
assert run_task.await_count == 2
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_turn_cleanup_never_reopens_sealed_controller():
|
|
"""永久关闭的 owner 不会因回合结束被重新开放。"""
|
|
middleware = SubAgentTaskControlMiddleware(
|
|
model=_TaskModel(responses=[AIMessage(content="完成")]),
|
|
profiles=_builtin_subagent_profiles(), tools=[],
|
|
)
|
|
middleware.seal()
|
|
await middleware.aafter_agent({}, None)
|
|
response = json.loads(await middleware._control_task(action="start", description="检查"))
|
|
assert response["success"] is False
|
|
assert "关闭" in response["error"]
|
|
assert await middleware.close() is True
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_subagent_compacts_large_input_before_model_call(monkeypatch):
|
|
"""独立子图也执行最终请求压缩,大量工具输出不会绕过预算边界。"""
|
|
@tool
|
|
def diagnostic() -> str:
|
|
"""读取诊断日志,不访问外部服务。"""
|
|
return "诊断日志中的连接信息。" * 450
|
|
|
|
diagnostic.tags = ["read", "system"]
|
|
model = _TaskModel(
|
|
responses=[*[AIMessage(content="", tool_calls=[{
|
|
"name": "diagnostic", "args": {}, "id": f"log-{index}", "type": "tool_call",
|
|
}]) for index in range(3)], AIMessage(content="诊断完成")],
|
|
profile={"max_input_tokens": 4096, "max_output_tokens": 512},
|
|
)
|
|
middleware = SubAgentTaskControlMiddleware(
|
|
model=model, profiles=_builtin_subagent_profiles(), tools=[diagnostic],
|
|
)
|
|
summarize = AsyncMock(return_value="用户要求检查下载器;已收集大量日志,继续分析。")
|
|
monkeypatch.setattr(ContextPreservingSummarizationMiddleware, "acreate_summary", summarize)
|
|
result = await middleware._provider.run_task(
|
|
description="检查下载器。",
|
|
subagent_type="general-purpose",
|
|
)
|
|
assert result == "诊断完成"
|
|
assert summarize.await_count >= 1
|