初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,178 @@
|
||||
"""待确认动作的存储(高风险工具调用的确认中转)。
|
||||
|
||||
司命决策为 ask 时不直接执行,而是登记一个 PendingConfirmation 并返回 confirmation_id;
|
||||
前端展示确认卡片,用户 approve/deny 后由安全 API 消费。
|
||||
|
||||
提供两种实现:
|
||||
- ``InMemoryConfirmationStore``:进程内 dict,默认开发模式,不依赖 DB
|
||||
- ``DbConfirmationStore``:数据库持久化,生产多 worker 模式,确认项跨进程可见
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Protocol
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from app.models.safety import PendingConfirmationRecord
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(UTC).isoformat()
|
||||
|
||||
|
||||
def new_confirmation_id() -> str:
|
||||
return f"confirm_{uuid.uuid4().hex}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PendingConfirmation:
|
||||
"""一次待确认的工具调用。"""
|
||||
|
||||
confirmation_id: str
|
||||
tool: str
|
||||
input: dict[str, Any]
|
||||
risk_level: str
|
||||
reason: str
|
||||
run_id: str | None = None
|
||||
character_id: str | None = None
|
||||
trace_id: str | None = None
|
||||
created_at: str = field(default_factory=_now_iso)
|
||||
|
||||
|
||||
class ConfirmationStore(Protocol):
|
||||
"""确认存储协议(全异步接口)。"""
|
||||
|
||||
async def create(self, pending: PendingConfirmation) -> PendingConfirmation: ...
|
||||
|
||||
async def get(self, confirmation_id: str) -> PendingConfirmation | None: ...
|
||||
|
||||
async def pop(self, confirmation_id: str) -> PendingConfirmation | None: ...
|
||||
|
||||
async def list_pending(self) -> list[PendingConfirmation]: ...
|
||||
|
||||
async def mark_timed_out(self, confirmation_id: str) -> None: ...
|
||||
|
||||
|
||||
class InMemoryConfirmationStore:
|
||||
"""内存确认存储,供本地与测试使用。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._items: dict[str, PendingConfirmation] = {}
|
||||
|
||||
async def create(self, pending: PendingConfirmation) -> PendingConfirmation:
|
||||
self._items[pending.confirmation_id] = pending
|
||||
return pending
|
||||
|
||||
async def get(self, confirmation_id: str) -> PendingConfirmation | None:
|
||||
return self._items.get(confirmation_id)
|
||||
|
||||
async def pop(self, confirmation_id: str) -> PendingConfirmation | None:
|
||||
return self._items.pop(confirmation_id, None)
|
||||
|
||||
async def list_pending(self) -> list[PendingConfirmation]:
|
||||
return sorted(self._items.values(), key=lambda item: item.created_at)
|
||||
|
||||
async def mark_timed_out(self, confirmation_id: str) -> None:
|
||||
self._items.pop(confirmation_id, None)
|
||||
|
||||
|
||||
class DbConfirmationStore:
|
||||
"""数据库确认存储,供多 worker 生产环境使用。
|
||||
|
||||
待确认项持久化到 ``pending_confirmations`` 表,跨进程可见。
|
||||
需要从 ``app.state.session_factory`` 或 ``async_sessionmaker`` 注入。
|
||||
"""
|
||||
|
||||
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
|
||||
self._session_factory = session_factory
|
||||
|
||||
async def create(self, pending: PendingConfirmation) -> PendingConfirmation:
|
||||
async with self._session_factory() as session:
|
||||
record = PendingConfirmationRecord(
|
||||
id=pending.confirmation_id,
|
||||
tool=pending.tool,
|
||||
input_json=json.dumps(pending.input, ensure_ascii=False),
|
||||
risk_level=pending.risk_level,
|
||||
reason=pending.reason,
|
||||
run_id=pending.run_id,
|
||||
character_id=pending.character_id,
|
||||
trace_id=pending.trace_id,
|
||||
)
|
||||
session.add(record)
|
||||
await session.commit()
|
||||
return pending
|
||||
|
||||
async def get(self, confirmation_id: str) -> PendingConfirmation | None:
|
||||
async with self._session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(PendingConfirmationRecord).where(
|
||||
PendingConfirmationRecord.id == confirmation_id,
|
||||
PendingConfirmationRecord.status == "pending",
|
||||
)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if record is None:
|
||||
return None
|
||||
return self._record_to_pending(record)
|
||||
|
||||
async def pop(self, confirmation_id: str) -> PendingConfirmation | None:
|
||||
async with self._session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(PendingConfirmationRecord).where(
|
||||
PendingConfirmationRecord.id == confirmation_id,
|
||||
PendingConfirmationRecord.status == "pending",
|
||||
)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if record is None:
|
||||
return None
|
||||
record.status = "consumed"
|
||||
await session.commit()
|
||||
return self._record_to_pending(record)
|
||||
|
||||
async def list_pending(self) -> list[PendingConfirmation]:
|
||||
async with self._session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(PendingConfirmationRecord)
|
||||
.where(PendingConfirmationRecord.status == "pending")
|
||||
.order_by(PendingConfirmationRecord.created_at.asc())
|
||||
)
|
||||
records: Sequence[PendingConfirmationRecord] = result.scalars().all()
|
||||
return [self._record_to_pending(r) for r in records]
|
||||
|
||||
async def mark_timed_out(self, confirmation_id: str) -> None:
|
||||
"""将待确认记录标记为超时(Agent 循环确认等待超时)。"""
|
||||
async with self._session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(PendingConfirmationRecord).where(
|
||||
PendingConfirmationRecord.id == confirmation_id,
|
||||
PendingConfirmationRecord.status == "pending",
|
||||
)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if record is None:
|
||||
return
|
||||
record.status = "timed_out"
|
||||
record.resolved_at = datetime.now(UTC)
|
||||
await session.commit()
|
||||
|
||||
@staticmethod
|
||||
def _record_to_pending(record: PendingConfirmationRecord) -> PendingConfirmation:
|
||||
return PendingConfirmation(
|
||||
confirmation_id=record.id,
|
||||
tool=record.tool,
|
||||
input=json.loads(record.input_json) if record.input_json else {},
|
||||
risk_level=record.risk_level,
|
||||
reason=record.reason,
|
||||
run_id=record.run_id,
|
||||
character_id=record.character_id,
|
||||
trace_id=record.trace_id,
|
||||
created_at=record.created_at.isoformat() if record.created_at else _now_iso(),
|
||||
)
|
||||
Reference in new issue
Block a user