实现 AI Core 与 Agent Core 基础功能

- 更新 README 描述从后端壳子到 AI Core/Agent Core
- 添加 ToolCall 和 ToolResult 数据结构定义
- 扩展 AgentRun 模型增加输出、错误码、工具调用结果等字段
- 添加 mock 提供商类型支持
- 实现聊天、代理运行、工具调用和提供商管理的核心路由逻辑
- 集成容器化依赖注入和错误处理机制
- 更新 API 接口契约和文档说明
This commit is contained in:
2026-08-27 13:55:00 +08:00
parent ce155b27f4
commit b71984d951
18 changed files with 1369 additions and 40 deletions
+181
View File
@@ -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())
+1 -1
View File
@@ -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"