- 更新 README 描述从后端壳子到 AI Core/Agent Core - 添加 ToolCall 和 ToolResult 数据结构定义 - 扩展 AgentRun 模型增加输出、错误码、工具调用结果等字段 - 添加 mock 提供商类型支持 - 实现聊天、代理运行、工具调用和提供商管理的核心路由逻辑 - 集成容器化依赖注入和错误处理机制 - 更新 API 接口契约和文档说明
182 lines
5.9 KiB
Python
182 lines
5.9 KiB
Python
import asyncio
|
|
|
|
from app.agent.permissions import PermissionMode
|
|
from app.agent.tools import ToolExecutionContext
|
|
from app.container import build_container
|
|
from app.contracts import (
|
|
AgentEventType,
|
|
AgentRunCreateRequest,
|
|
AgentRunStatus,
|
|
ToolCall,
|
|
)
|
|
|
|
|
|
def run(coroutine):
|
|
return asyncio.run(coroutine)
|
|
|
|
|
|
def test_mock_provider_completes_agent_run() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input="hello",
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
)
|
|
)
|
|
|
|
completed = await container.agent.wait(created.run_id)
|
|
events = [event async for event in container.agent.events(created.run_id)]
|
|
|
|
assert completed.status == AgentRunStatus.completed
|
|
assert completed.output == "Mock response: hello"
|
|
assert events[0].event == AgentEventType.run_started
|
|
assert events[-1].event == AgentEventType.run_completed
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_agent_calls_registered_tool_and_records_result() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input='/tool system.echo {"text":"hello tool"}',
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
allowed_tools=["system.echo"],
|
|
)
|
|
)
|
|
|
|
completed = await container.agent.wait(created.run_id)
|
|
|
|
assert completed.status == AgentRunStatus.completed
|
|
assert completed.current_step == 2
|
|
assert completed.tool_results[0].success is True
|
|
assert completed.tool_results[0].output == {"text": "hello tool"}
|
|
assert completed.output is not None
|
|
assert "Tool result received" in completed.output
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_tool_arguments_are_validated() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
result = await container.tools.execute(
|
|
ToolCall(
|
|
tool_call_id="call_invalid",
|
|
name="math.add",
|
|
arguments={"left": 1},
|
|
),
|
|
ToolExecutionContext(run_id="run_test"),
|
|
)
|
|
|
|
assert result.success is False
|
|
assert result.error_code == "TOOL_ARGUMENT_INVALID"
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_permission_confirmation_resumes_agent() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
protected_tool = container.tools.get("system.echo")
|
|
protected_tool.definition.permission = "notes.write"
|
|
container.permissions.policy.set_rule("notes.write", PermissionMode.confirm)
|
|
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input='/tool system.echo {"text":"approved"}',
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
allowed_tools=["system.echo"],
|
|
tool_timeout_seconds=2,
|
|
)
|
|
)
|
|
|
|
request_id = None
|
|
async with asyncio.timeout(2):
|
|
async for event in container.agent.events(created.run_id):
|
|
if event.event == AgentEventType.permission_required:
|
|
request_id = str(event.data["request_id"])
|
|
break
|
|
|
|
assert request_id is not None
|
|
assert container.agent.resolve_permission(
|
|
created.run_id, request_id, "allow_once"
|
|
)
|
|
completed = await container.agent.wait(created.run_id)
|
|
assert completed.status == AgentRunStatus.completed
|
|
assert completed.tool_results[0].success is True
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_step_limit_stops_repeated_agent_loop() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input='/tool system.echo {"text":"one step"}',
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
allowed_tools=["system.echo"],
|
|
max_steps=1,
|
|
)
|
|
)
|
|
|
|
completed = await container.agent.wait(created.run_id)
|
|
assert completed.status == AgentRunStatus.failed
|
|
assert completed.error_code == "MAX_STEPS_EXCEEDED"
|
|
assert len(completed.tool_results) == 1
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_token_budget_stops_agent_run() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input="hello budget",
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
token_budget=1,
|
|
)
|
|
)
|
|
|
|
completed = await container.agent.wait(created.run_id)
|
|
assert completed.status == AgentRunStatus.failed
|
|
assert completed.error_code == "TOKEN_BUDGET_EXCEEDED"
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_cancelling_permission_wait_cancels_run() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
protected_tool = container.tools.get("system.echo")
|
|
protected_tool.definition.permission = "notes.write"
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input='/tool system.echo {"text":"cancel"}',
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
allowed_tools=["system.echo"],
|
|
tool_timeout_seconds=10,
|
|
)
|
|
)
|
|
|
|
async with asyncio.timeout(2):
|
|
while container.agent.get_run(created.run_id).status != AgentRunStatus.waiting_permission:
|
|
await asyncio.sleep(0)
|
|
|
|
cancelled = await container.agent.cancel(created.run_id)
|
|
await container.agent.wait(created.run_id)
|
|
assert cancelled.status == AgentRunStatus.cancelled
|
|
assert container.agent.get_run(created.run_id).cancelled is True
|
|
|
|
run(scenario())
|