mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:41:57 +00:00
eddeded2a4
- 第一批(安全加固与生命周期止血):隔离测试宿主机密钥与 .env 泄漏;修复 SQLite 取消查询挂死、:memory: 连接池报错与 EngineCache 世代竞态;补齐运行与评测删除时的 Checkpointer 线程、Artifact 磁盘文件与无主附件回收;加固三套沙箱路径越界、进程组清理与 Seatbelt 数据目录隔离,封堵 SSRF IPv4-mapped IPv6 元数据绕过与响应体 OOM;修复推理模型多轮 reasoning_content、结构化输出 Token 漏计与缓存折扣计费 - 第二批(数据库/集群/问数提速与首屏分包):补齐核心表索引与 SQLite WAL 调优,列表接口按需 defer 大列,收敛启动扫描并打通跨进程 slot 槽位唤醒;优化跨源全局选表、哈希桶数字绑定、流式 CSV 导入及多方言分析算子;剥离首屏静态引入的 @xyflow/react 与全局样式(首屏主包降至 461.59 kB),补充流式 rAF 攒批、Store 精细订阅与表单无障碍绑定 - 第三批(架构演进与工程自动化闭环):记录库超挂接阈值历史版本自动增量归档至单库多表(解除 SQLite 10 库 ATTACH 硬上限);重构知识库为词面 BM25 与全集合归一化向量矩阵真·双路独立召回并支持游标分批重建;升级多方言感知 SQL 模板词法分析(E'...' / $$...$$ / q'[...]' / # / 嵌套注释)与 Decimal / bool / NaN 参数校验,消除 ledger 层对 api 路由层的反向依赖;拆分 3101 行 CaliberDrafts 页面为 7 个领域子模块,消除检查脚本本机路径硬编码并新增 GitHub Actions CI 门禁与三批回归测试套件
125 lines
5.4 KiB
Python
125 lines
5.4 KiB
Python
"""LangGraph 的断点库:应用库是 SQLite 时用 AsyncSqliteSaver(数据目录里的 checkpoints.db,和业务表分开免得抢写锁),
|
||
是 PostgreSQL 时用官方的 AsyncPostgresSaver(psycopg 3 + 连接池),放在同一个库里,AGENTLAB_CHECKPOINT_URL 另给就放那里。
|
||
|
||
它建的四张表(checkpoints、checkpoint_blobs、checkpoint_writes、checkpoint_migrations)和应用的表不重名
|
||
(test_db_portable 守着),所以同一个库、同一个 schema 就行。
|
||
|
||
应用里读断点一律走 saver 的公开接口(aget_state / aget_state_history / aget_tuple / alist),不直接查这几张表:
|
||
两种库的表结构完全不一样(SQLite 整份断点是一个 msgpack,PostgreSQL 拆成 JSONB 加 blob)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import contextlib
|
||
from collections.abc import AsyncIterator, Iterable
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||
|
||
from app.core.config import settings
|
||
|
||
#: 建断点表时拿的会话级咨询锁的号:几个进程同时启动,各自的 setup(建表、记迁移版本)一个一个来
|
||
_SETUP_LOCK = 0x4167656E74436B70 # "AgentCkp"
|
||
|
||
|
||
def _pool_size() -> int:
|
||
# 运行里每个超步写一次断点、每个任务写一次 pending writes,都是一来一回的短操作。并发运行多时多给几条
|
||
return max(4, min(settings.max_concurrent_runs, 20))
|
||
|
||
|
||
@contextlib.asynccontextmanager
|
||
async def open_postgres_saver(conninfo: str, *, max_size: int | None = None,
|
||
application_name: str = "agentlab") -> AsyncIterator[BaseCheckpointSaver]:
|
||
"""PostgreSQL 的断点库:开连接池、建表(幂等),退出时关池子。application_name 同应用库(db/base.APPLICATION_NAME)。"""
|
||
from app.db.base import _require_postgres_driver
|
||
|
||
_require_postgres_driver("psycopg", "psycopg_pool", "langgraph.checkpoint.postgres")
|
||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||
from psycopg.rows import dict_row
|
||
from psycopg_pool import AsyncConnectionPool
|
||
|
||
# 连接参数照 AsyncPostgresSaver.from_conn_string:自动提交(它不自己管事务)、语句第一次执行就预备、行按 dict 取
|
||
pool = AsyncConnectionPool(
|
||
conninfo, min_size=1, max_size=max_size or _pool_size(), open=False,
|
||
kwargs={"autocommit": True, "prepare_threshold": 0, "row_factory": dict_row,
|
||
"application_name": application_name},
|
||
check=AsyncConnectionPool.check_connection, name="agentlab-checkpoints")
|
||
await pool.open(wait=True)
|
||
try:
|
||
saver = AsyncPostgresSaver(pool) # type: ignore[arg-type]
|
||
async with pool.connection() as conn:
|
||
await conn.execute("SELECT pg_advisory_lock(%s)", (_SETUP_LOCK,))
|
||
try:
|
||
await saver.setup()
|
||
finally:
|
||
await conn.execute("SELECT pg_advisory_unlock(%s)", (_SETUP_LOCK,))
|
||
yield saver
|
||
finally:
|
||
await pool.close()
|
||
|
||
|
||
@contextlib.asynccontextmanager
|
||
async def open_sqlite_saver(path: Path | str) -> AsyncIterator[BaseCheckpointSaver]:
|
||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||
|
||
async with AsyncSqliteSaver.from_conn_string(str(path)) as saver:
|
||
conn = getattr(saver, "conn", None)
|
||
if conn is not None:
|
||
await conn.execute("PRAGMA journal_mode=WAL")
|
||
await conn.execute("PRAGMA synchronous=NORMAL")
|
||
await conn.execute("PRAGMA journal_size_limit=67108864")
|
||
await conn.execute("PRAGMA busy_timeout=5000")
|
||
yield saver
|
||
|
||
|
||
def open_saver() -> contextlib.AbstractAsyncContextManager[BaseCheckpointSaver]:
|
||
"""按配置开这个进程的断点库。"""
|
||
conninfo = settings.checkpoint_conninfo
|
||
if conninfo:
|
||
return open_postgres_saver(conninfo)
|
||
return open_sqlite_saver(settings.checkpoint_path)
|
||
|
||
|
||
async def _delete_on(saver: Any, ids: list[str]) -> None:
|
||
adelete = getattr(saver, "adelete_thread", None)
|
||
if not callable(adelete):
|
||
return
|
||
setup = getattr(saver, "setup", None)
|
||
if callable(setup):
|
||
with contextlib.suppress(Exception):
|
||
await setup()
|
||
for tid in ids:
|
||
with contextlib.suppress(Exception):
|
||
await adelete(tid)
|
||
|
||
|
||
async def delete_threads(thread_ids: Iterable[str]) -> None:
|
||
"""运行被彻底删除或评测明细被回收时,一并清理断点库里对应的 thread。
|
||
|
||
注意:正常运行结束(succeeded / cancelled)时不能删,因为 GET /api/runs/{id}/state 和 /checkpoints
|
||
还要回看历史断点;只有在运行记录本身从数据库删除之后才调这里。
|
||
"""
|
||
ids = sorted({str(t) for t in thread_ids if t})
|
||
if not ids:
|
||
return
|
||
from app.engine.runner import run_manager
|
||
|
||
saver = run_manager._checkpointer
|
||
if saver is not None:
|
||
await _delete_on(saver, ids)
|
||
return
|
||
if settings.checkpoint_conninfo or settings.checkpoint_path.is_file():
|
||
async with open_saver() as opened:
|
||
await _delete_on(opened, ids)
|
||
|
||
|
||
async def count(saver: Any, thread_id: str | None = None) -> tuple[int, int]:
|
||
"""(断点条数, pending writes 条数)。thread_id 为空数全部。copy-to-postgres 核对用,走公开接口。"""
|
||
config = {"configurable": {"thread_id": thread_id}} if thread_id else None
|
||
checkpoints = writes = 0
|
||
async for tup in saver.alist(config):
|
||
checkpoints += 1
|
||
writes += len(tup.pending_writes or ())
|
||
return checkpoints, writes
|