初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,252 @@
|
||||
"""工具调用 API(api-spec §15)。
|
||||
|
||||
- GET /tools:列出已注册工具策略元数据(从 tool_policies 表,含 executable 派生)。
|
||||
- POST /tools:注册自定义工具策略(百工枢器用治理声明,记审计)。
|
||||
- GET /tools/calls/{tool_call_id}:单次工具调用事件详情。
|
||||
- PATCH /tools/{tool_name}:编辑工具策略元数据(spec §15: risk_level 等)。
|
||||
- DELETE /tools/{tool_name}:注销自定义工具(builtin 不可删)。
|
||||
- POST /tools/{tool_name}/invoke:调用工具;高风险返回 requires_confirmation + confirmation_id,
|
||||
交由 /safety/confirmations/{id}/approve 消费。
|
||||
- POST /tools/{tool_name}/invoke/stream:流式调用(SSE),实时推送 tool.stdout/tool.stderr
|
||||
增量与 tool.completed;司命 deny/ask-未确认/沙箱错误分别以 tool.failed / tool.requires_confirmation 结束流。
|
||||
|
||||
策略写操作(POST/PATCH/DELETE)后刷新 ToolRuntime 的司命决策器,使变更即时生效;
|
||||
tool_runtime 未就绪时仅写库(重启后从 db 加载)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from pydantic import BaseModel
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from app.api.v1.deps import get_confirmation_store, get_optional_tool_runtime, get_tool_runtime
|
||||
from app.application.tool_runtime.runtime import ToolRuntime
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.integrations.sandbox.exceptions import SandboxError
|
||||
from app.models.safety import PendingConfirmationRecord
|
||||
from app.modules.safety.confirmation import (
|
||||
ConfirmationStore,
|
||||
PendingConfirmation,
|
||||
new_confirmation_id,
|
||||
)
|
||||
from app.modules.safety.exceptions import PermissionDenied, RequiresConfirmation
|
||||
from app.schemas.safety import (
|
||||
CreateToolPolicyRequest,
|
||||
ToolCallDetail,
|
||||
ToolPolicyResponse,
|
||||
UpdateToolPolicyRequest,
|
||||
)
|
||||
from app.schemas.skills import ToolCapability
|
||||
from app.services import governance as governance_service
|
||||
from app.services import skills as skills_service
|
||||
|
||||
router = APIRouter(prefix="/tools", tags=["tools"])
|
||||
|
||||
|
||||
class InvokeRequest(BaseModel):
|
||||
input: dict[str, Any]
|
||||
run_id: str | None = None
|
||||
character_id: str | None = None
|
||||
trace_id: str | None = None
|
||||
confirmed: bool = False
|
||||
|
||||
|
||||
class ToolResultPayload(BaseModel):
|
||||
ok: bool
|
||||
exit_code: int | None = None
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
duration_ms: int = 0
|
||||
timeout: bool = False
|
||||
truncated: bool = False
|
||||
trace_id: str = ""
|
||||
|
||||
|
||||
class CompletedResponse(BaseModel):
|
||||
status: str = "completed"
|
||||
result: ToolResultPayload
|
||||
|
||||
|
||||
class RequiresConfirmationResponse(BaseModel):
|
||||
status: str = "requires_confirmation"
|
||||
confirmation_id: str
|
||||
risk_level: str
|
||||
reason: str
|
||||
|
||||
|
||||
async def _sync_confirmation_record(pending: PendingConfirmation) -> None:
|
||||
"""把 InMemoryConfirmationStore 的待确认记录同步持久化到 DB。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
record = PendingConfirmationRecord(
|
||||
id=pending.confirmation_id,
|
||||
tool=pending.tool,
|
||||
input_json=json.dumps(pending.input) if pending.input else "{}",
|
||||
risk_level=pending.risk_level,
|
||||
reason=pending.reason or "",
|
||||
run_id=pending.run_id,
|
||||
character_id=pending.character_id,
|
||||
trace_id=pending.trace_id,
|
||||
status="pending",
|
||||
)
|
||||
session.add(record)
|
||||
await session.commit()
|
||||
|
||||
|
||||
# —— §15 工具策略治理 ——
|
||||
|
||||
|
||||
@router.get("", response_model=list[ToolPolicyResponse])
|
||||
async def list_tools() -> list[ToolPolicyResponse]:
|
||||
return await governance_service.list_tool_policies()
|
||||
|
||||
|
||||
@router.get("/capabilities", response_model=list[ToolCapability])
|
||||
async def list_capabilities(
|
||||
runtime: ToolRuntime | None = Depends(get_optional_tool_runtime),
|
||||
) -> list[ToolCapability]:
|
||||
"""统一列举所有能力(builtin / mcp / skill,含来源标记,skills-design §6)。"""
|
||||
return await skills_service.list_all_capabilities(runtime)
|
||||
|
||||
|
||||
@router.post("", response_model=ToolPolicyResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def register_tool(
|
||||
payload: CreateToolPolicyRequest,
|
||||
runtime: ToolRuntime | None = Depends(get_optional_tool_runtime),
|
||||
) -> ToolPolicyResponse:
|
||||
response = await governance_service.create_tool_policy(payload)
|
||||
if runtime is not None:
|
||||
await runtime.reload_governor()
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/calls/{tool_call_id}", response_model=ToolCallDetail)
|
||||
async def get_tool_call(tool_call_id: str) -> ToolCallDetail:
|
||||
return await governance_service.get_tool_call(tool_call_id)
|
||||
|
||||
|
||||
@router.patch("/{tool_name}", response_model=ToolPolicyResponse)
|
||||
async def update_tool(
|
||||
tool_name: str,
|
||||
payload: UpdateToolPolicyRequest,
|
||||
runtime: ToolRuntime | None = Depends(get_optional_tool_runtime),
|
||||
) -> ToolPolicyResponse:
|
||||
response = await governance_service.update_tool_policy(tool_name, payload)
|
||||
if runtime is not None:
|
||||
await runtime.reload_governor()
|
||||
return response
|
||||
|
||||
|
||||
@router.delete("/{tool_name}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_tool(
|
||||
tool_name: str,
|
||||
runtime: ToolRuntime | None = Depends(get_optional_tool_runtime),
|
||||
) -> Response:
|
||||
await governance_service.delete_tool_policy(tool_name)
|
||||
if runtime is not None:
|
||||
await runtime.reload_governor()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
# —— §15 工具调用 ——
|
||||
|
||||
|
||||
@router.post("/{tool_name}/invoke", response_model=None)
|
||||
async def invoke_tool(
|
||||
tool_name: str,
|
||||
body: InvokeRequest,
|
||||
runtime: ToolRuntime = Depends(get_tool_runtime),
|
||||
confirmations: ConfirmationStore = Depends(get_confirmation_store),
|
||||
) -> CompletedResponse | RequiresConfirmationResponse:
|
||||
try:
|
||||
result = await runtime.execute(
|
||||
tool_name,
|
||||
body.input,
|
||||
run_id=body.run_id,
|
||||
character_id=body.character_id,
|
||||
trace_id=body.trace_id,
|
||||
confirmed=body.confirmed,
|
||||
)
|
||||
except RequiresConfirmation as exc:
|
||||
pending = await confirmations.create(
|
||||
PendingConfirmation(
|
||||
confirmation_id=new_confirmation_id(),
|
||||
tool=tool_name,
|
||||
input=body.input,
|
||||
risk_level=exc.risk_level or "L2",
|
||||
reason=exc.message,
|
||||
run_id=body.run_id,
|
||||
character_id=body.character_id,
|
||||
trace_id=body.trace_id,
|
||||
)
|
||||
)
|
||||
# 同步持久化 PendingConfirmationRecord(供 safety/confirmations 新端点查找)
|
||||
await _sync_confirmation_record(pending)
|
||||
return RequiresConfirmationResponse(
|
||||
confirmation_id=pending.confirmation_id,
|
||||
risk_level=pending.risk_level,
|
||||
reason=pending.reason,
|
||||
)
|
||||
|
||||
return CompletedResponse(
|
||||
result=ToolResultPayload(
|
||||
ok=result.ok,
|
||||
exit_code=result.exit_code,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
duration_ms=result.duration_ms,
|
||||
timeout=result.timeout,
|
||||
truncated=result.truncated,
|
||||
trace_id=result.trace_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{tool_name}/invoke/stream")
|
||||
async def invoke_tool_stream(
|
||||
tool_name: str,
|
||||
body: InvokeRequest,
|
||||
runtime: ToolRuntime = Depends(get_tool_runtime),
|
||||
) -> EventSourceResponse:
|
||||
"""流式调用工具(SSE)。
|
||||
|
||||
事件:``tool.stdout`` / ``tool.stderr``(data 为 ``{"data":"<base64>"}``,前端解码)、
|
||||
``tool.completed``(data 为结束汇总 JSON)。司命 deny → ``tool.failed``;
|
||||
ask 且未确认 → ``tool.requires_confirmation``(前端应先走非流式 invoke 完成 confirmation,
|
||||
再以 confirmed=True 调用本端点);沙箱错误 → ``tool.failed``。
|
||||
"""
|
||||
|
||||
async def event_gen():
|
||||
try:
|
||||
async for ev in runtime.execute_stream(
|
||||
tool_name,
|
||||
body.input,
|
||||
run_id=body.run_id,
|
||||
character_id=body.character_id,
|
||||
trace_id=body.trace_id,
|
||||
confirmed=body.confirmed,
|
||||
):
|
||||
if ev.kind in ("stdout", "stderr"):
|
||||
yield {"event": f"tool.{ev.kind}", "data": json.dumps({"data": ev.payload})}
|
||||
elif ev.kind == "done":
|
||||
yield {"event": "tool.completed", "data": json.dumps(ev.payload)}
|
||||
except RequiresConfirmation as exc:
|
||||
yield {
|
||||
"event": "tool.requires_confirmation",
|
||||
"data": json.dumps({"risk_level": exc.risk_level, "reason": exc.message}),
|
||||
}
|
||||
except PermissionDenied as exc:
|
||||
yield {
|
||||
"event": "tool.failed",
|
||||
"data": json.dumps({"error_code": "permission_denied", "error_message": str(exc)}),
|
||||
}
|
||||
except SandboxError as exc:
|
||||
yield {
|
||||
"event": "tool.failed",
|
||||
"data": json.dumps({"error_code": exc.code, "error_message": str(exc)}),
|
||||
}
|
||||
|
||||
return EventSourceResponse(event_gen())
|
||||
Reference in new issue
Block a user