实现 AI Core 与 Agent Core 基础功能
- 更新 README 描述从后端壳子到 AI Core/Agent Core - 添加 ToolCall 和 ToolResult 数据结构定义 - 扩展 AgentRun 模型增加输出、错误码、工具调用结果等字段 - 添加 mock 提供商类型支持 - 实现聊天、代理运行、工具调用和提供商管理的核心路由逻辑 - 集成容器化依赖注入和错误处理机制 - 更新 API 接口契约和文档说明
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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())
|
||||
@@ -28,7 +28,7 @@ def test_query_shells_are_empty_and_typed() -> None:
|
||||
assert notes.page.limit == 20
|
||||
assert skills.items == []
|
||||
assert plugins.items == []
|
||||
assert providers.items == []
|
||||
assert [provider.provider_id for provider in providers.items] == ["mock"]
|
||||
assert index.status == "idle"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user