mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-28 17:06:05 +00:00
* feat(checkpoint): production-shaped full/delta benchmark with configurable snapshot frequency - Group benchmark scripts into per-family folders (checkpoint/, sandbox/) - Extract shared benchmark infrastructure into checkpoint_bench_common.py - Add checkpoint_delta_snapshot_frequency config (default 1000, process-frozen); freeze it in make_lead_agent and DeerFlowClient; key the state-schema adaptation cache by resolved frequency - New bench_production.py: per-case child processes run N ainvoke turns through the real lead-agent graph (scripted deterministic model, real AsyncSqliteSaver), then measure GET /state + POST /history through the real Gateway route stack in one event loop (httpx ASGITransport), cold/warm accessor-cache split, cross-mode digest gates - New summarize_production.py: delta/full ratios plus decision metrics (snapshot_write_spike, cache_effect_ms, checkpoint_write_share, auto-discovered history per-limit ratios) * fix(checkpoint): address production benchmark review
162 lines
6.5 KiB
Python
162 lines
6.5 KiB
Python
"""Unified database backend configuration.
|
|
|
|
Controls BOTH the LangGraph checkpointer and the DeerFlow application
|
|
persistence layer (runs, threads metadata, users, etc.). The user
|
|
configures one backend; the system handles physical separation details.
|
|
|
|
SQLite mode: checkpointer and app share a single .db file
|
|
({sqlite_dir}/deerflow.db) with WAL journal mode enabled on every
|
|
connection. WAL allows concurrent readers and a single writer without
|
|
blocking, making a unified file safe for both workloads. Writers
|
|
that contend for the lock wait via the default 5-second sqlite3
|
|
busy timeout rather than failing immediately.
|
|
|
|
Postgres mode: both use the same database URL but maintain independent
|
|
connection pools with different lifecycles.
|
|
|
|
Memory mode: checkpointer uses MemorySaver, app uses in-memory stores.
|
|
No database is initialized.
|
|
|
|
Sensitive values (postgres_url) should use $VAR syntax in config.yaml
|
|
to reference environment variables from .env:
|
|
|
|
database:
|
|
backend: postgres
|
|
postgres_url: $DATABASE_URL
|
|
|
|
The $VAR resolution is handled by AppConfig.resolve_env_variables()
|
|
before this config is instantiated -- DatabaseConfig itself does not
|
|
need to do any environment variable processing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
CheckpointChannelMode = Literal["full", "delta"]
|
|
|
|
|
|
class DatabaseConfig(BaseModel):
|
|
backend: Literal["memory", "sqlite", "postgres"] = Field(
|
|
default="memory",
|
|
description=("Storage backend for both checkpointer and application data. 'memory' for development (no persistence across restarts), 'sqlite' for single-node deployment, 'postgres' for production multi-node deployment."),
|
|
)
|
|
checkpoint_channel_mode: CheckpointChannelMode = Field(
|
|
default="full",
|
|
description=(
|
|
"Checkpoint representation for accumulating channels. "
|
|
"'full' preserves full-value message checkpoints; 'delta' uses "
|
|
"LangGraph DeltaChannel for messages. Restart is required, and all "
|
|
"processes sharing one checkpoint database must use the same value."
|
|
),
|
|
)
|
|
checkpoint_delta_snapshot_frequency: int = Field(
|
|
default=1000,
|
|
ge=1,
|
|
description=(
|
|
"DeltaChannel snapshot frequency used in checkpoint 'delta' mode: "
|
|
"every N message-channel writes the saver stores a full snapshot "
|
|
"instead of a delta sentinel. Restart-required, and all processes "
|
|
"sharing one checkpoint database must use the same value."
|
|
),
|
|
)
|
|
sqlite_dir: str = Field(
|
|
default=".deer-flow/data",
|
|
description=("Directory for the SQLite database file. Both checkpointer and application data share {sqlite_dir}/deerflow.db."),
|
|
)
|
|
postgres_url: str = Field(
|
|
default="",
|
|
description=(
|
|
"PostgreSQL connection URL, shared by checkpointer and app. "
|
|
"Use $DATABASE_URL in config.yaml to reference .env. "
|
|
"Example: postgresql://user:pass@host:5432/deerflow "
|
|
"(the +asyncpg driver suffix is added automatically where needed)."
|
|
),
|
|
)
|
|
echo_sql: bool = Field(
|
|
default=False,
|
|
description="Echo all SQL statements to log (debug only).",
|
|
)
|
|
pool_size: int = Field(
|
|
default=5,
|
|
description="Connection pool size for the app ORM engine (postgres only).",
|
|
)
|
|
pool_recycle: int = Field(
|
|
default=300,
|
|
gt=0,
|
|
description="Seconds before app ORM PostgreSQL connections are recycled.",
|
|
)
|
|
command_timeout: float | None = Field(
|
|
default=30,
|
|
gt=0,
|
|
description="Timeout in seconds for app ORM PostgreSQL commands. Set to null to disable the command timeout.",
|
|
)
|
|
|
|
# -- Derived helpers (not user-configured) --
|
|
|
|
@property
|
|
def _resolved_sqlite_dir(self) -> str:
|
|
"""Resolve sqlite_dir to an absolute path (relative to CWD)."""
|
|
from pathlib import Path
|
|
|
|
return str(Path(self.sqlite_dir).resolve())
|
|
|
|
@property
|
|
def sqlite_path(self) -> str:
|
|
"""Unified SQLite file path shared by checkpointer and app."""
|
|
return os.path.join(self._resolved_sqlite_dir, "deerflow.db")
|
|
|
|
# Backward-compatible aliases
|
|
@property
|
|
def checkpointer_sqlite_path(self) -> str:
|
|
"""SQLite file path for the LangGraph checkpointer (alias for sqlite_path)."""
|
|
return self.sqlite_path
|
|
|
|
@property
|
|
def app_sqlite_path(self) -> str:
|
|
"""SQLite file path for application ORM data (alias for sqlite_path)."""
|
|
return self.sqlite_path
|
|
|
|
@property
|
|
def app_sqlalchemy_url(self) -> str:
|
|
"""SQLAlchemy async URL for the application ORM engine."""
|
|
if self.backend == "sqlite":
|
|
return f"sqlite+aiosqlite:///{self.sqlite_path}"
|
|
if self.backend == "postgres":
|
|
url = self.postgres_url
|
|
if url.startswith("postgresql://"):
|
|
url = url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
|
elif url.startswith("postgres://"):
|
|
# libpq's short alias: accepted by the psycopg checkpointer, but not a SQLAlchemy dialect.
|
|
url = url.replace("postgres://", "postgresql+asyncpg://", 1)
|
|
return url
|
|
raise ValueError(f"No SQLAlchemy URL for backend={self.backend!r}")
|
|
|
|
@property
|
|
def app_sync_sqlalchemy_url(self) -> str:
|
|
"""SQLAlchemy *synchronous* URL for the application ORM data.
|
|
|
|
Used by the ``agent_storage.backend: db`` store, whose consumers (the
|
|
LangGraph graph factory, the setup/update tools) are synchronous and may
|
|
run on the event loop or in a separate process from the gateway, where an
|
|
async engine cannot be driven. Points at the same database file/server as
|
|
:meth:`app_sqlalchemy_url`; only the driver differs (both drivers —
|
|
stdlib sqlite3 and psycopg — ship with the app, so this adds no
|
|
dependency).
|
|
"""
|
|
if self.backend == "sqlite":
|
|
return f"sqlite:///{self.sqlite_path}"
|
|
if self.backend == "postgres":
|
|
url = self.postgres_url
|
|
if url.startswith("postgresql+asyncpg://"):
|
|
url = url.replace("postgresql+asyncpg://", "postgresql+psycopg://", 1)
|
|
elif url.startswith("postgresql://"):
|
|
url = url.replace("postgresql://", "postgresql+psycopg://", 1)
|
|
elif url.startswith("postgres://"):
|
|
url = url.replace("postgres://", "postgresql+psycopg://", 1)
|
|
return url
|
|
raise ValueError(f"No SQLAlchemy URL for backend={self.backend!r}")
|