mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 04:14:46 +08:00
Support langgraph checkpointer (#1005)
* Add checkpointer configuration to config.example.yaml - Introduced a new section for checkpointer configuration to enable state persistence for the embedded DeerFlowClient. - Documented supported types: memory, sqlite, and postgres, along with examples for each. - Clarified that the LangGraph Server manages its own state persistence separately. * refactor(checkpointer): streamline checkpointer initialization and logging * fix(uv.lock): update revision and add new wheel URLs for brotlicffi package * feat: add langchain-anthropic dependency and update related configurations * Fix checkpointer lifecycle, docstring, and path resolution bugs from PR #1005 review (#4) * Initial plan * Address all review suggestions from PR #1005 Co-authored-by: foreleven <4785594+foreleven@users.noreply.github.com> * Fix resolve_path to always return real Path; move SQLite special-string handling to callers Co-authored-by: foreleven <4785594+foreleven@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: foreleven <4785594+foreleven@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: foreleven <4785594+foreleven@users.noreply.github.com>
This commit is contained in:
@@ -6,5 +6,8 @@
|
|||||||
"env": ".env",
|
"env": ".env",
|
||||||
"graphs": {
|
"graphs": {
|
||||||
"lead_agent": "src.agents:make_lead_agent"
|
"lead_agent": "src.agents:make_lead_agent"
|
||||||
|
},
|
||||||
|
"checkpointer": {
|
||||||
|
"path": "./src/agents/checkpointer/async_provider.py:make_checkpointer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ dependencies = [
|
|||||||
"httpx>=0.28.0",
|
"httpx>=0.28.0",
|
||||||
"kubernetes>=30.0.0",
|
"kubernetes>=30.0.0",
|
||||||
"langchain>=1.2.3",
|
"langchain>=1.2.3",
|
||||||
|
"langchain-anthropic>=1.3.4",
|
||||||
"langchain-deepseek>=1.0.1",
|
"langchain-deepseek>=1.0.1",
|
||||||
"langchain-mcp-adapters>=0.1.0",
|
"langchain-mcp-adapters>=0.1.0",
|
||||||
"langchain-openai>=1.1.7",
|
"langchain-openai>=1.1.7",
|
||||||
@@ -32,6 +33,7 @@ dependencies = [
|
|||||||
"ddgs>=9.10.0",
|
"ddgs>=9.10.0",
|
||||||
"duckdb>=1.4.4",
|
"duckdb>=1.4.4",
|
||||||
"langchain-google-genai>=4.2.1",
|
"langchain-google-genai>=4.2.1",
|
||||||
|
"langgraph-checkpoint-sqlite>=3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
|
from .checkpointer import get_checkpointer, make_checkpointer, reset_checkpointer
|
||||||
from .lead_agent import make_lead_agent
|
from .lead_agent import make_lead_agent
|
||||||
from .thread_state import SandboxState, ThreadState
|
from .thread_state import SandboxState, ThreadState
|
||||||
|
|
||||||
__all__ = ["make_lead_agent", "SandboxState", "ThreadState"]
|
__all__ = ["make_lead_agent", "SandboxState", "ThreadState", "get_checkpointer", "reset_checkpointer", "make_checkpointer"]
|
||||||
|
|||||||
9
backend/src/agents/checkpointer/__init__.py
Normal file
9
backend/src/agents/checkpointer/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .async_provider import make_checkpointer
|
||||||
|
from .provider import checkpointer_context, get_checkpointer, reset_checkpointer
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_checkpointer",
|
||||||
|
"reset_checkpointer",
|
||||||
|
"checkpointer_context",
|
||||||
|
"make_checkpointer",
|
||||||
|
]
|
||||||
107
backend/src/agents/checkpointer/async_provider.py
Normal file
107
backend/src/agents/checkpointer/async_provider.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""Async checkpointer factory.
|
||||||
|
|
||||||
|
Provides an **async context manager** for long-running async servers that need
|
||||||
|
proper resource cleanup.
|
||||||
|
|
||||||
|
Supported backends: memory, sqlite, postgres.
|
||||||
|
|
||||||
|
Usage (e.g. FastAPI lifespan)::
|
||||||
|
|
||||||
|
from src.agents.checkpointer.async_provider import make_checkpointer
|
||||||
|
|
||||||
|
async with make_checkpointer() as checkpointer:
|
||||||
|
app.state.checkpointer = checkpointer # None if not configured
|
||||||
|
|
||||||
|
For sync usage see :mod:`src.agents.checkpointer.provider`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
from langgraph.types import Checkpointer
|
||||||
|
|
||||||
|
from src.agents.checkpointer.provider import (
|
||||||
|
POSTGRES_CONN_REQUIRED,
|
||||||
|
POSTGRES_INSTALL,
|
||||||
|
SQLITE_INSTALL,
|
||||||
|
_resolve_sqlite_conn_str,
|
||||||
|
)
|
||||||
|
from src.config.app_config import get_app_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Async factory
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def _async_checkpointer(config) -> AsyncIterator[Checkpointer]:
|
||||||
|
"""Async context manager that constructs and tears down a checkpointer."""
|
||||||
|
if config.type == "memory":
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
|
||||||
|
yield InMemorySaver()
|
||||||
|
return
|
||||||
|
|
||||||
|
if config.type == "sqlite":
|
||||||
|
try:
|
||||||
|
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(SQLITE_INSTALL) from exc
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
conn_str = _resolve_sqlite_conn_str(config.connection_string or "store.db")
|
||||||
|
# Only create parent directories for real filesystem paths
|
||||||
|
if conn_str != ":memory:" and not conn_str.startswith("file:"):
|
||||||
|
pathlib.Path(conn_str).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
async with AsyncSqliteSaver.from_conn_string(conn_str) as saver:
|
||||||
|
await saver.setup()
|
||||||
|
yield saver
|
||||||
|
return
|
||||||
|
|
||||||
|
if config.type == "postgres":
|
||||||
|
try:
|
||||||
|
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(POSTGRES_INSTALL) from exc
|
||||||
|
|
||||||
|
if not config.connection_string:
|
||||||
|
raise ValueError(POSTGRES_CONN_REQUIRED)
|
||||||
|
|
||||||
|
async with AsyncPostgresSaver.from_conn_string(config.connection_string) as saver:
|
||||||
|
await saver.setup()
|
||||||
|
yield saver
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown checkpointer type: {config.type!r}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public async context manager
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def make_checkpointer() -> AsyncIterator[Checkpointer | None]:
|
||||||
|
"""Async context manager that yields a checkpointer for the caller's lifetime.
|
||||||
|
Resources are opened on enter and closed on exit — no global state::
|
||||||
|
|
||||||
|
async with make_checkpointer() as checkpointer:
|
||||||
|
app.state.checkpointer = checkpointer
|
||||||
|
|
||||||
|
Yields ``None`` when no checkpointer is configured in *config.yaml*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = get_app_config()
|
||||||
|
|
||||||
|
if config.checkpointer is None:
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
|
||||||
|
async with _async_checkpointer(config.checkpointer) as saver:
|
||||||
|
yield saver
|
||||||
179
backend/src/agents/checkpointer/provider.py
Normal file
179
backend/src/agents/checkpointer/provider.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
"""Sync checkpointer factory.
|
||||||
|
|
||||||
|
Provides a **sync singleton** and a **sync context manager** for LangGraph
|
||||||
|
graph compilation and CLI tools.
|
||||||
|
|
||||||
|
Supported backends: memory, sqlite, postgres.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
from src.agents.checkpointer.provider import get_checkpointer, checkpointer_context
|
||||||
|
|
||||||
|
# Singleton — reused across calls, closed on process exit
|
||||||
|
cp = get_checkpointer()
|
||||||
|
|
||||||
|
# One-shot — fresh connection, closed on block exit
|
||||||
|
with checkpointer_context() as cp:
|
||||||
|
graph.invoke(input, config={"configurable": {"thread_id": "1"}})
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from langgraph.types import Checkpointer
|
||||||
|
|
||||||
|
from src.config.app_config import get_app_config
|
||||||
|
from src.config.checkpointer_config import CheckpointerConfig
|
||||||
|
from src.config.paths import resolve_path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Error message constants — imported by aio.provider too
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SQLITE_INSTALL = "langgraph-checkpoint-sqlite is required for the SQLite checkpointer. Install it with: uv add langgraph-checkpoint-sqlite"
|
||||||
|
POSTGRES_INSTALL = "langgraph-checkpoint-postgres is required for the PostgreSQL checkpointer. Install it with: uv add langgraph-checkpoint-postgres psycopg[binary] psycopg-pool"
|
||||||
|
POSTGRES_CONN_REQUIRED = "checkpointer.connection_string is required for the postgres backend"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sync factory
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_sqlite_conn_str(raw: str) -> str:
|
||||||
|
"""Return a SQLite connection string ready for use with ``SqliteSaver``.
|
||||||
|
|
||||||
|
SQLite special strings (``":memory:"`` and ``file:`` URIs) are returned
|
||||||
|
unchanged. Plain filesystem paths — relative or absolute — are resolved
|
||||||
|
to an absolute string via :func:`resolve_path`.
|
||||||
|
"""
|
||||||
|
if raw == ":memory:" or raw.startswith("file:"):
|
||||||
|
return raw
|
||||||
|
return str(resolve_path(raw))
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _sync_checkpointer_cm(config: CheckpointerConfig) -> Iterator[Checkpointer]:
|
||||||
|
"""Context manager that creates and tears down a sync checkpointer.
|
||||||
|
|
||||||
|
Returns a configured ``Checkpointer`` instance. Resource cleanup for any
|
||||||
|
underlying connections or pools is handled by higher-level helpers in
|
||||||
|
this module (such as the singleton factory or context manager); this
|
||||||
|
function does not return a separate cleanup callback.
|
||||||
|
"""
|
||||||
|
if config.type == "memory":
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
|
||||||
|
logger.info("Checkpointer: using InMemorySaver (in-process, not persistent)")
|
||||||
|
yield InMemorySaver()
|
||||||
|
return
|
||||||
|
|
||||||
|
if config.type == "sqlite":
|
||||||
|
try:
|
||||||
|
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(SQLITE_INSTALL) from exc
|
||||||
|
|
||||||
|
conn_str = _resolve_sqlite_conn_str(config.connection_string or "store.db")
|
||||||
|
with SqliteSaver.from_conn_string(conn_str) as saver:
|
||||||
|
saver.setup()
|
||||||
|
logger.info("Checkpointer: using SqliteSaver (%s)", conn_str)
|
||||||
|
yield saver
|
||||||
|
return
|
||||||
|
|
||||||
|
if config.type == "postgres":
|
||||||
|
try:
|
||||||
|
from langgraph.checkpoint.postgres import PostgresSaver
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(POSTGRES_INSTALL) from exc
|
||||||
|
|
||||||
|
if not config.connection_string:
|
||||||
|
raise ValueError(POSTGRES_CONN_REQUIRED)
|
||||||
|
|
||||||
|
with PostgresSaver.from_conn_string(config.connection_string) as saver:
|
||||||
|
saver.setup()
|
||||||
|
logger.info("Checkpointer: using PostgresSaver")
|
||||||
|
yield saver
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown checkpointer type: {config.type!r}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sync singleton
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_checkpointer: Checkpointer = None
|
||||||
|
_checkpointer_ctx = None # open context manager keeping the connection alive
|
||||||
|
|
||||||
|
|
||||||
|
def get_checkpointer() -> Checkpointer | None:
|
||||||
|
"""Return the global sync checkpointer singleton, creating it on first call.
|
||||||
|
|
||||||
|
Returns ``None`` when no checkpointer is configured in *config.yaml*.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ImportError: If the required package for the configured backend is not installed.
|
||||||
|
ValueError: If ``connection_string`` is missing for a backend that requires it.
|
||||||
|
"""
|
||||||
|
global _checkpointer, _checkpointer_ctx
|
||||||
|
|
||||||
|
if _checkpointer is not None:
|
||||||
|
return _checkpointer
|
||||||
|
|
||||||
|
from src.config.checkpointer_config import get_checkpointer_config
|
||||||
|
|
||||||
|
config = get_checkpointer_config()
|
||||||
|
if config is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
_checkpointer_ctx = _sync_checkpointer_cm(config)
|
||||||
|
_checkpointer = _checkpointer_ctx.__enter__()
|
||||||
|
|
||||||
|
return _checkpointer
|
||||||
|
|
||||||
|
|
||||||
|
def reset_checkpointer() -> None:
|
||||||
|
"""Reset the sync singleton, forcing recreation on the next call.
|
||||||
|
|
||||||
|
Closes any open backend connections and clears the cached instance.
|
||||||
|
Useful in tests or after a configuration change.
|
||||||
|
"""
|
||||||
|
global _checkpointer, _checkpointer_ctx
|
||||||
|
if _checkpointer_ctx is not None:
|
||||||
|
try:
|
||||||
|
_checkpointer_ctx.__exit__(None, None, None)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Error during checkpointer cleanup", exc_info=True)
|
||||||
|
_checkpointer_ctx = None
|
||||||
|
_checkpointer = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sync context manager
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def checkpointer_context() -> Iterator[Checkpointer | None]:
|
||||||
|
"""Sync context manager that yields a checkpointer and cleans up on exit.
|
||||||
|
|
||||||
|
Unlike :func:`get_checkpointer`, this does **not** cache the instance —
|
||||||
|
each ``with`` block creates and destroys its own connection. Use it in
|
||||||
|
CLI scripts or tests where you want deterministic cleanup::
|
||||||
|
|
||||||
|
with checkpointer_context() as cp:
|
||||||
|
graph.invoke(input, config={"configurable": {"thread_id": "1"}})
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = get_app_config()
|
||||||
|
if config.checkpointer is None:
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
|
||||||
|
with _sync_checkpointer_cm(config.checkpointer) as saver:
|
||||||
|
yield saver
|
||||||
@@ -152,7 +152,10 @@ class DeerFlowClient:
|
|||||||
def _atomic_write_json(path: Path, data: dict) -> None:
|
def _atomic_write_json(path: Path, data: dict) -> None:
|
||||||
"""Write JSON to *path* atomically (temp file + replace)."""
|
"""Write JSON to *path* atomically (temp file + replace)."""
|
||||||
fd = tempfile.NamedTemporaryFile(
|
fd = tempfile.NamedTemporaryFile(
|
||||||
mode="w", dir=path.parent, suffix=".tmp", delete=False,
|
mode="w",
|
||||||
|
dir=path.parent,
|
||||||
|
suffix=".tmp",
|
||||||
|
delete=False,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
json.dump(data, fd, indent=2)
|
json.dump(data, fd, indent=2)
|
||||||
@@ -205,8 +208,13 @@ class DeerFlowClient:
|
|||||||
),
|
),
|
||||||
"state_schema": ThreadState,
|
"state_schema": ThreadState,
|
||||||
}
|
}
|
||||||
if self._checkpointer is not None:
|
checkpointer = self._checkpointer
|
||||||
kwargs["checkpointer"] = self._checkpointer
|
if checkpointer is None:
|
||||||
|
from src.agents.checkpointer import get_checkpointer
|
||||||
|
|
||||||
|
checkpointer = get_checkpointer()
|
||||||
|
if checkpointer is not None:
|
||||||
|
kwargs["checkpointer"] = checkpointer
|
||||||
|
|
||||||
self._agent = create_agent(**kwargs)
|
self._agent = create_agent(**kwargs)
|
||||||
self._agent_config_key = key
|
self._agent_config_key = key
|
||||||
@@ -320,10 +328,7 @@ class DeerFlowClient:
|
|||||||
"type": "ai",
|
"type": "ai",
|
||||||
"content": "",
|
"content": "",
|
||||||
"id": msg_id,
|
"id": msg_id,
|
||||||
"tool_calls": [
|
"tool_calls": [{"name": tc["name"], "args": tc["args"], "id": tc.get("id")} for tc in msg.tool_calls],
|
||||||
{"name": tc["name"], "args": tc["args"], "id": tc.get("id")}
|
|
||||||
for tc in msg.tool_calls
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -494,10 +499,7 @@ class DeerFlowClient:
|
|||||||
"""
|
"""
|
||||||
config_path = ExtensionsConfig.resolve_config_path()
|
config_path = ExtensionsConfig.resolve_config_path()
|
||||||
if config_path is None:
|
if config_path is None:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError("Cannot locate extensions_config.json. Set DEER_FLOW_EXTENSIONS_CONFIG_PATH or ensure it exists in the project root.")
|
||||||
"Cannot locate extensions_config.json. "
|
|
||||||
"Set DEER_FLOW_EXTENSIONS_CONFIG_PATH or ensure it exists in the project root."
|
|
||||||
)
|
|
||||||
|
|
||||||
current_config = get_extensions_config()
|
current_config = get_extensions_config()
|
||||||
|
|
||||||
@@ -561,10 +563,7 @@ class DeerFlowClient:
|
|||||||
|
|
||||||
config_path = ExtensionsConfig.resolve_config_path()
|
config_path = ExtensionsConfig.resolve_config_path()
|
||||||
if config_path is None:
|
if config_path is None:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError("Cannot locate extensions_config.json. Set DEER_FLOW_EXTENSIONS_CONFIG_PATH or ensure it exists in the project root.")
|
||||||
"Cannot locate extensions_config.json. "
|
|
||||||
"Set DEER_FLOW_EXTENSIONS_CONFIG_PATH or ensure it exists in the project root."
|
|
||||||
)
|
|
||||||
|
|
||||||
extensions_config = get_extensions_config()
|
extensions_config = get_extensions_config()
|
||||||
extensions_config.skills[name] = SkillStateConfig(enabled=enabled)
|
extensions_config.skills[name] = SkillStateConfig(enabled=enabled)
|
||||||
@@ -739,7 +738,6 @@ class DeerFlowClient:
|
|||||||
uploaded_files: list[dict] = []
|
uploaded_files: list[dict] = []
|
||||||
|
|
||||||
for src_path in resolved_files:
|
for src_path in resolved_files:
|
||||||
|
|
||||||
dest = uploads_dir / src_path.name
|
dest = uploads_dir / src_path.name
|
||||||
shutil.copy2(src_path, dest)
|
shutil.copy2(src_path, dest)
|
||||||
|
|
||||||
@@ -756,6 +754,7 @@ class DeerFlowClient:
|
|||||||
try:
|
try:
|
||||||
asyncio.get_running_loop()
|
asyncio.get_running_loop()
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||||
md_path = pool.submit(lambda: asyncio.run(convert_file_to_markdown(dest))).result()
|
md_path = pool.submit(lambda: asyncio.run(convert_file_to_markdown(dest))).result()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
@@ -795,15 +794,17 @@ class DeerFlowClient:
|
|||||||
for fp in sorted(uploads_dir.iterdir()):
|
for fp in sorted(uploads_dir.iterdir()):
|
||||||
if fp.is_file():
|
if fp.is_file():
|
||||||
stat = fp.stat()
|
stat = fp.stat()
|
||||||
files.append({
|
files.append(
|
||||||
"filename": fp.name,
|
{
|
||||||
"size": str(stat.st_size),
|
"filename": fp.name,
|
||||||
"path": str(fp),
|
"size": str(stat.st_size),
|
||||||
"virtual_path": f"/mnt/user-data/uploads/{fp.name}",
|
"path": str(fp),
|
||||||
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{fp.name}",
|
"virtual_path": f"/mnt/user-data/uploads/{fp.name}",
|
||||||
"extension": fp.suffix,
|
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{fp.name}",
|
||||||
"modified": stat.st_mtime,
|
"extension": fp.suffix,
|
||||||
})
|
"modified": stat.st_mtime,
|
||||||
|
}
|
||||||
|
)
|
||||||
return {"files": files, "count": len(files)}
|
return {"files": files, "count": len(files)}
|
||||||
|
|
||||||
def delete_upload(self, thread_id: str, filename: str) -> dict:
|
def delete_upload(self, thread_id: str, filename: str) -> dict:
|
||||||
@@ -858,7 +859,7 @@ class DeerFlowClient:
|
|||||||
if not clean_path.startswith(virtual_prefix):
|
if not clean_path.startswith(virtual_prefix):
|
||||||
raise ValueError(f"Path must start with /{virtual_prefix}")
|
raise ValueError(f"Path must start with /{virtual_prefix}")
|
||||||
|
|
||||||
relative = clean_path[len(virtual_prefix):].lstrip("/")
|
relative = clean_path[len(virtual_prefix) :].lstrip("/")
|
||||||
base_dir = get_paths().sandbox_user_data_dir(thread_id)
|
base_dir = get_paths().sandbox_user_data_dir(thread_id)
|
||||||
actual = (base_dir / relative).resolve()
|
actual = (base_dir / relative).resolve()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import yaml
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
from src.config.checkpointer_config import CheckpointerConfig, load_checkpointer_config_from_dict
|
||||||
from src.config.extensions_config import ExtensionsConfig
|
from src.config.extensions_config import ExtensionsConfig
|
||||||
from src.config.memory_config import load_memory_config_from_dict
|
from src.config.memory_config import load_memory_config_from_dict
|
||||||
from src.config.model_config import ModelConfig
|
from src.config.model_config import ModelConfig
|
||||||
@@ -29,6 +30,7 @@ class AppConfig(BaseModel):
|
|||||||
skills: SkillsConfig = Field(default_factory=SkillsConfig, description="Skills configuration")
|
skills: SkillsConfig = Field(default_factory=SkillsConfig, description="Skills configuration")
|
||||||
extensions: ExtensionsConfig = Field(default_factory=ExtensionsConfig, description="Extensions configuration (MCP servers and skills state)")
|
extensions: ExtensionsConfig = Field(default_factory=ExtensionsConfig, description="Extensions configuration (MCP servers and skills state)")
|
||||||
model_config = ConfigDict(extra="allow", frozen=False)
|
model_config = ConfigDict(extra="allow", frozen=False)
|
||||||
|
checkpointer: CheckpointerConfig | None = Field(default=None, description="Checkpointer configuration")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_config_path(cls, config_path: str | None = None) -> Path:
|
def resolve_config_path(cls, config_path: str | None = None) -> Path:
|
||||||
@@ -92,6 +94,10 @@ class AppConfig(BaseModel):
|
|||||||
if "subagents" in config_data:
|
if "subagents" in config_data:
|
||||||
load_subagents_config_from_dict(config_data["subagents"])
|
load_subagents_config_from_dict(config_data["subagents"])
|
||||||
|
|
||||||
|
# Load checkpointer config if present
|
||||||
|
if "checkpointer" in config_data:
|
||||||
|
load_checkpointer_config_from_dict(config_data["checkpointer"])
|
||||||
|
|
||||||
# Load extensions config separately (it's in a different file)
|
# Load extensions config separately (it's in a different file)
|
||||||
extensions_config = ExtensionsConfig.from_file()
|
extensions_config = ExtensionsConfig.from_file()
|
||||||
config_data["extensions"] = extensions_config.model_dump()
|
config_data["extensions"] = extensions_config.model_dump()
|
||||||
|
|||||||
46
backend/src/config/checkpointer_config.py
Normal file
46
backend/src/config/checkpointer_config.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Configuration for LangGraph checkpointer."""
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
CheckpointerType = Literal["memory", "sqlite", "postgres"]
|
||||||
|
|
||||||
|
|
||||||
|
class CheckpointerConfig(BaseModel):
|
||||||
|
"""Configuration for LangGraph state persistence checkpointer."""
|
||||||
|
|
||||||
|
type: CheckpointerType = Field(
|
||||||
|
description="Checkpointer backend type. "
|
||||||
|
"'memory' is in-process only (lost on restart). "
|
||||||
|
"'sqlite' persists to a local file (requires langgraph-checkpoint-sqlite). "
|
||||||
|
"'postgres' persists to PostgreSQL (requires langgraph-checkpoint-postgres)."
|
||||||
|
)
|
||||||
|
connection_string: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Connection string for sqlite (file path) or postgres (DSN). "
|
||||||
|
"Required for sqlite and postgres types. "
|
||||||
|
"For sqlite, use a file path like '.deer-flow/checkpoints.db' or ':memory:' for in-memory. "
|
||||||
|
"For postgres, use a DSN like 'postgresql://user:pass@localhost:5432/db'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Global configuration instance — None means no checkpointer is configured.
|
||||||
|
_checkpointer_config: CheckpointerConfig | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_checkpointer_config() -> CheckpointerConfig | None:
|
||||||
|
"""Get the current checkpointer configuration, or None if not configured."""
|
||||||
|
return _checkpointer_config
|
||||||
|
|
||||||
|
|
||||||
|
def set_checkpointer_config(config: CheckpointerConfig | None) -> None:
|
||||||
|
"""Set the checkpointer configuration."""
|
||||||
|
global _checkpointer_config
|
||||||
|
_checkpointer_config = config
|
||||||
|
|
||||||
|
|
||||||
|
def load_checkpointer_config_from_dict(config_dict: dict) -> None:
|
||||||
|
"""Load checkpointer configuration from a dictionary."""
|
||||||
|
global _checkpointer_config
|
||||||
|
_checkpointer_config = CheckpointerConfig(**config_dict)
|
||||||
@@ -176,3 +176,15 @@ def get_paths() -> Paths:
|
|||||||
if _paths is None:
|
if _paths is None:
|
||||||
_paths = Paths()
|
_paths = Paths()
|
||||||
return _paths
|
return _paths
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_path(path: str) -> Path:
|
||||||
|
"""Resolve *path* to an absolute ``Path``.
|
||||||
|
|
||||||
|
Relative paths are resolved relative to the application base directory.
|
||||||
|
Absolute paths are returned as-is (after normalisation).
|
||||||
|
"""
|
||||||
|
p = Path(path)
|
||||||
|
if not p.is_absolute():
|
||||||
|
p = get_paths().base_dir / path
|
||||||
|
return p.resolve()
|
||||||
|
|||||||
255
backend/tests/test_checkpointer.py
Normal file
255
backend/tests/test_checkpointer.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
"""Unit tests for checkpointer config and singleton factory."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.agents.checkpointer import get_checkpointer, reset_checkpointer
|
||||||
|
from src.config.checkpointer_config import (
|
||||||
|
CheckpointerConfig,
|
||||||
|
get_checkpointer_config,
|
||||||
|
load_checkpointer_config_from_dict,
|
||||||
|
set_checkpointer_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_state():
|
||||||
|
"""Reset singleton state before each test."""
|
||||||
|
set_checkpointer_config(None)
|
||||||
|
reset_checkpointer()
|
||||||
|
yield
|
||||||
|
set_checkpointer_config(None)
|
||||||
|
reset_checkpointer()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Config tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckpointerConfig:
|
||||||
|
def test_load_memory_config(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
config = get_checkpointer_config()
|
||||||
|
assert config is not None
|
||||||
|
assert config.type == "memory"
|
||||||
|
assert config.connection_string is None
|
||||||
|
|
||||||
|
def test_load_sqlite_config(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "sqlite", "connection_string": "/tmp/test.db"})
|
||||||
|
config = get_checkpointer_config()
|
||||||
|
assert config is not None
|
||||||
|
assert config.type == "sqlite"
|
||||||
|
assert config.connection_string == "/tmp/test.db"
|
||||||
|
|
||||||
|
def test_load_postgres_config(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "postgres", "connection_string": "postgresql://localhost/db"})
|
||||||
|
config = get_checkpointer_config()
|
||||||
|
assert config is not None
|
||||||
|
assert config.type == "postgres"
|
||||||
|
assert config.connection_string == "postgresql://localhost/db"
|
||||||
|
|
||||||
|
def test_default_connection_string_is_none(self):
|
||||||
|
config = CheckpointerConfig(type="memory")
|
||||||
|
assert config.connection_string is None
|
||||||
|
|
||||||
|
def test_set_config_to_none(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
set_checkpointer_config(None)
|
||||||
|
assert get_checkpointer_config() is None
|
||||||
|
|
||||||
|
def test_invalid_type_raises(self):
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
load_checkpointer_config_from_dict({"type": "unknown"})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Factory tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetCheckpointer:
|
||||||
|
def test_returns_none_when_not_configured(self):
|
||||||
|
assert get_checkpointer() is None
|
||||||
|
|
||||||
|
def test_memory_returns_in_memory_saver(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
|
||||||
|
cp = get_checkpointer()
|
||||||
|
assert isinstance(cp, InMemorySaver)
|
||||||
|
|
||||||
|
def test_memory_singleton(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
cp1 = get_checkpointer()
|
||||||
|
cp2 = get_checkpointer()
|
||||||
|
assert cp1 is cp2
|
||||||
|
|
||||||
|
def test_reset_clears_singleton(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
cp1 = get_checkpointer()
|
||||||
|
reset_checkpointer()
|
||||||
|
cp2 = get_checkpointer()
|
||||||
|
assert cp1 is not cp2
|
||||||
|
|
||||||
|
def test_sqlite_raises_when_package_missing(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "sqlite", "connection_string": "/tmp/test.db"})
|
||||||
|
with patch.dict(sys.modules, {"langgraph.checkpoint.sqlite": None}):
|
||||||
|
reset_checkpointer()
|
||||||
|
with pytest.raises(ImportError, match="langgraph-checkpoint-sqlite"):
|
||||||
|
get_checkpointer()
|
||||||
|
|
||||||
|
def test_postgres_raises_when_package_missing(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "postgres", "connection_string": "postgresql://localhost/db"})
|
||||||
|
with patch.dict(sys.modules, {"langgraph.checkpoint.postgres": None}):
|
||||||
|
reset_checkpointer()
|
||||||
|
with pytest.raises(ImportError, match="langgraph-checkpoint-postgres"):
|
||||||
|
get_checkpointer()
|
||||||
|
|
||||||
|
def test_postgres_raises_when_connection_string_missing(self):
|
||||||
|
load_checkpointer_config_from_dict({"type": "postgres"})
|
||||||
|
mock_saver = MagicMock()
|
||||||
|
mock_module = MagicMock()
|
||||||
|
mock_module.PostgresSaver = mock_saver
|
||||||
|
with patch.dict(sys.modules, {"langgraph.checkpoint.postgres": mock_module}):
|
||||||
|
reset_checkpointer()
|
||||||
|
with pytest.raises(ValueError, match="connection_string is required"):
|
||||||
|
get_checkpointer()
|
||||||
|
|
||||||
|
def test_sqlite_creates_saver(self):
|
||||||
|
"""SQLite checkpointer is created when package is available."""
|
||||||
|
load_checkpointer_config_from_dict({"type": "sqlite", "connection_string": "/tmp/test.db"})
|
||||||
|
|
||||||
|
mock_saver_instance = MagicMock()
|
||||||
|
mock_cm = MagicMock()
|
||||||
|
mock_cm.__enter__ = MagicMock(return_value=mock_saver_instance)
|
||||||
|
mock_cm.__exit__ = MagicMock(return_value=False)
|
||||||
|
|
||||||
|
mock_saver_cls = MagicMock()
|
||||||
|
mock_saver_cls.from_conn_string = MagicMock(return_value=mock_cm)
|
||||||
|
|
||||||
|
mock_module = MagicMock()
|
||||||
|
mock_module.SqliteSaver = mock_saver_cls
|
||||||
|
|
||||||
|
with patch.dict(sys.modules, {"langgraph.checkpoint.sqlite": mock_module}):
|
||||||
|
reset_checkpointer()
|
||||||
|
cp = get_checkpointer()
|
||||||
|
|
||||||
|
assert cp is mock_saver_instance
|
||||||
|
mock_saver_cls.from_conn_string.assert_called_once()
|
||||||
|
mock_saver_instance.setup.assert_called_once()
|
||||||
|
|
||||||
|
def test_postgres_creates_saver(self):
|
||||||
|
"""Postgres checkpointer is created when packages are available."""
|
||||||
|
load_checkpointer_config_from_dict({"type": "postgres", "connection_string": "postgresql://localhost/db"})
|
||||||
|
|
||||||
|
mock_saver_instance = MagicMock()
|
||||||
|
mock_cm = MagicMock()
|
||||||
|
mock_cm.__enter__ = MagicMock(return_value=mock_saver_instance)
|
||||||
|
mock_cm.__exit__ = MagicMock(return_value=False)
|
||||||
|
|
||||||
|
mock_saver_cls = MagicMock()
|
||||||
|
mock_saver_cls.from_conn_string = MagicMock(return_value=mock_cm)
|
||||||
|
|
||||||
|
mock_pg_module = MagicMock()
|
||||||
|
mock_pg_module.PostgresSaver = mock_saver_cls
|
||||||
|
|
||||||
|
with patch.dict(sys.modules, {"langgraph.checkpoint.postgres": mock_pg_module}):
|
||||||
|
reset_checkpointer()
|
||||||
|
cp = get_checkpointer()
|
||||||
|
|
||||||
|
assert cp is mock_saver_instance
|
||||||
|
mock_saver_cls.from_conn_string.assert_called_once_with("postgresql://localhost/db")
|
||||||
|
mock_saver_instance.setup.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# app_config.py integration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestAppConfigLoadsCheckpointer:
|
||||||
|
def test_load_checkpointer_section(self):
|
||||||
|
"""load_checkpointer_config_from_dict populates the global config."""
|
||||||
|
set_checkpointer_config(None)
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
cfg = get_checkpointer_config()
|
||||||
|
assert cfg is not None
|
||||||
|
assert cfg.type == "memory"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# DeerFlowClient falls back to config checkpointer
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestClientCheckpointerFallback:
|
||||||
|
def test_client_uses_config_checkpointer_when_none_provided(self):
|
||||||
|
"""DeerFlowClient._ensure_agent falls back to get_checkpointer() when checkpointer=None."""
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
|
||||||
|
from src.client import DeerFlowClient
|
||||||
|
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
|
||||||
|
captured_kwargs = {}
|
||||||
|
|
||||||
|
def fake_create_agent(**kwargs):
|
||||||
|
captured_kwargs.update(kwargs)
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
model_mock = MagicMock()
|
||||||
|
config_mock = MagicMock()
|
||||||
|
config_mock.models = [model_mock]
|
||||||
|
config_mock.get_model_config.return_value = MagicMock(supports_vision=False)
|
||||||
|
config_mock.checkpointer = None
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("src.client.get_app_config", return_value=config_mock),
|
||||||
|
patch("src.client.create_agent", side_effect=fake_create_agent),
|
||||||
|
patch("src.client.create_chat_model", return_value=MagicMock()),
|
||||||
|
patch("src.client._build_middlewares", return_value=[]),
|
||||||
|
patch("src.client.apply_prompt_template", return_value=""),
|
||||||
|
patch("src.client.DeerFlowClient._get_tools", return_value=[]),
|
||||||
|
):
|
||||||
|
client = DeerFlowClient(checkpointer=None)
|
||||||
|
config = client._get_runnable_config("test-thread")
|
||||||
|
client._ensure_agent(config)
|
||||||
|
|
||||||
|
assert "checkpointer" in captured_kwargs
|
||||||
|
assert isinstance(captured_kwargs["checkpointer"], InMemorySaver)
|
||||||
|
|
||||||
|
def test_client_explicit_checkpointer_takes_precedence(self):
|
||||||
|
"""An explicitly provided checkpointer is used even when config checkpointer is set."""
|
||||||
|
from src.client import DeerFlowClient
|
||||||
|
|
||||||
|
load_checkpointer_config_from_dict({"type": "memory"})
|
||||||
|
|
||||||
|
explicit_cp = MagicMock()
|
||||||
|
captured_kwargs = {}
|
||||||
|
|
||||||
|
def fake_create_agent(**kwargs):
|
||||||
|
captured_kwargs.update(kwargs)
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
model_mock = MagicMock()
|
||||||
|
config_mock = MagicMock()
|
||||||
|
config_mock.models = [model_mock]
|
||||||
|
config_mock.get_model_config.return_value = MagicMock(supports_vision=False)
|
||||||
|
config_mock.checkpointer = None
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("src.client.get_app_config", return_value=config_mock),
|
||||||
|
patch("src.client.create_agent", side_effect=fake_create_agent),
|
||||||
|
patch("src.client.create_chat_model", return_value=MagicMock()),
|
||||||
|
patch("src.client._build_middlewares", return_value=[]),
|
||||||
|
patch("src.client.apply_prompt_template", return_value=""),
|
||||||
|
patch("src.client.DeerFlowClient._get_tools", return_value=[]),
|
||||||
|
):
|
||||||
|
client = DeerFlowClient(checkpointer=explicit_cp)
|
||||||
|
config = client._get_runnable_config("test-thread")
|
||||||
|
client._ensure_agent(config)
|
||||||
|
|
||||||
|
assert captured_kwargs["checkpointer"] is explicit_cp
|
||||||
@@ -28,6 +28,7 @@ if _skip_reason:
|
|||||||
# Fixtures
|
# Fixtures
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def client():
|
def client():
|
||||||
"""Create a real DeerFlowClient (no mocks)."""
|
"""Create a real DeerFlowClient (no mocks)."""
|
||||||
@@ -38,6 +39,7 @@ def client():
|
|||||||
def thread_tmp(tmp_path):
|
def thread_tmp(tmp_path):
|
||||||
"""Provide a unique thread_id + tmp directory for file operations."""
|
"""Provide a unique thread_id + tmp directory for file operations."""
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
tid = f"live-test-{uuid.uuid4().hex[:8]}"
|
tid = f"live-test-{uuid.uuid4().hex[:8]}"
|
||||||
return tid, tmp_path
|
return tid, tmp_path
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ def thread_tmp(tmp_path):
|
|||||||
# Scenario 1: Basic chat — model responds coherently
|
# Scenario 1: Basic chat — model responds coherently
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveBasicChat:
|
class TestLiveBasicChat:
|
||||||
def test_chat_returns_nonempty_string(self, client):
|
def test_chat_returns_nonempty_string(self, client):
|
||||||
"""chat() returns a non-empty response from the real model."""
|
"""chat() returns a non-empty response from the real model."""
|
||||||
@@ -65,6 +68,7 @@ class TestLiveBasicChat:
|
|||||||
# Scenario 2: Streaming — events arrive in correct order
|
# Scenario 2: Streaming — events arrive in correct order
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveStreaming:
|
class TestLiveStreaming:
|
||||||
def test_stream_yields_messages_tuple_and_end(self, client):
|
def test_stream_yields_messages_tuple_and_end(self, client):
|
||||||
"""stream() produces at least one messages-tuple event and ends with end."""
|
"""stream() produces at least one messages-tuple event and ends with end."""
|
||||||
@@ -81,10 +85,7 @@ class TestLiveStreaming:
|
|||||||
|
|
||||||
def test_stream_ai_content_nonempty(self, client):
|
def test_stream_ai_content_nonempty(self, client):
|
||||||
"""Streamed messages-tuple AI events contain non-empty content."""
|
"""Streamed messages-tuple AI events contain non-empty content."""
|
||||||
ai_messages = [
|
ai_messages = [e for e in client.stream("What color is the sky? One word.") if e.type == "messages-tuple" and e.data.get("type") == "ai" and e.data.get("content")]
|
||||||
e for e in client.stream("What color is the sky? One word.")
|
|
||||||
if e.type == "messages-tuple" and e.data.get("type") == "ai" and e.data.get("content")
|
|
||||||
]
|
|
||||||
assert len(ai_messages) >= 1
|
assert len(ai_messages) >= 1
|
||||||
for m in ai_messages:
|
for m in ai_messages:
|
||||||
assert len(m.data.get("content", "")) > 0
|
assert len(m.data.get("content", "")) > 0
|
||||||
@@ -94,13 +95,11 @@ class TestLiveStreaming:
|
|||||||
# Scenario 3: Tool use — agent calls a tool and returns result
|
# Scenario 3: Tool use — agent calls a tool and returns result
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveToolUse:
|
class TestLiveToolUse:
|
||||||
def test_agent_uses_bash_tool(self, client):
|
def test_agent_uses_bash_tool(self, client):
|
||||||
"""Agent uses bash tool when asked to run a command."""
|
"""Agent uses bash tool when asked to run a command."""
|
||||||
events = list(client.stream(
|
events = list(client.stream("Use the bash tool to run: echo 'LIVE_TEST_OK'. Then tell me the output."))
|
||||||
"Use the bash tool to run: echo 'LIVE_TEST_OK'. "
|
|
||||||
"Then tell me the output."
|
|
||||||
))
|
|
||||||
|
|
||||||
types = [e.type for e in events]
|
types = [e.type for e in events]
|
||||||
print(f" event types: {types}")
|
print(f" event types: {types}")
|
||||||
@@ -122,10 +121,7 @@ class TestLiveToolUse:
|
|||||||
|
|
||||||
def test_agent_uses_ls_tool(self, client):
|
def test_agent_uses_ls_tool(self, client):
|
||||||
"""Agent uses ls tool to list a directory."""
|
"""Agent uses ls tool to list a directory."""
|
||||||
events = list(client.stream(
|
events = list(client.stream("Use the ls tool to list the contents of /mnt/user-data/workspace. Just report what you see."))
|
||||||
"Use the ls tool to list the contents of /mnt/user-data/workspace. "
|
|
||||||
"Just report what you see."
|
|
||||||
))
|
|
||||||
|
|
||||||
types = [e.type for e in events]
|
types = [e.type for e in events]
|
||||||
print(f" event types: {types}")
|
print(f" event types: {types}")
|
||||||
@@ -139,15 +135,11 @@ class TestLiveToolUse:
|
|||||||
# Scenario 4: Multi-tool chain — agent chains tools in sequence
|
# Scenario 4: Multi-tool chain — agent chains tools in sequence
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveMultiToolChain:
|
class TestLiveMultiToolChain:
|
||||||
def test_write_then_read(self, client):
|
def test_write_then_read(self, client):
|
||||||
"""Agent writes a file, then reads it back."""
|
"""Agent writes a file, then reads it back."""
|
||||||
events = list(client.stream(
|
events = list(client.stream("Step 1: Use write_file to write 'integration_test_content' to /mnt/user-data/outputs/live_test.txt. Step 2: Use read_file to read that file back. Step 3: Tell me the content you read."))
|
||||||
"Step 1: Use write_file to write 'integration_test_content' to "
|
|
||||||
"/mnt/user-data/outputs/live_test.txt. "
|
|
||||||
"Step 2: Use read_file to read that file back. "
|
|
||||||
"Step 3: Tell me the content you read."
|
|
||||||
))
|
|
||||||
|
|
||||||
types = [e.type for e in events]
|
types = [e.type for e in events]
|
||||||
print(f" event types: {types}")
|
print(f" event types: {types}")
|
||||||
@@ -164,16 +156,14 @@ class TestLiveMultiToolChain:
|
|||||||
ai_events = [e for e in events if e.type == "messages-tuple" and e.data.get("type") == "ai" and e.data.get("content")]
|
ai_events = [e for e in events if e.type == "messages-tuple" and e.data.get("type") == "ai" and e.data.get("content")]
|
||||||
tr_events = [e for e in events if e.type == "messages-tuple" and e.data.get("type") == "tool"]
|
tr_events = [e for e in events if e.type == "messages-tuple" and e.data.get("type") == "tool"]
|
||||||
final_text = ai_events[-1].data["content"] if ai_events else ""
|
final_text = ai_events[-1].data["content"] if ai_events else ""
|
||||||
assert "integration_test_content" in final_text.lower() or any(
|
assert "integration_test_content" in final_text.lower() or any("integration_test_content" in e.data.get("content", "") for e in tr_events)
|
||||||
"integration_test_content" in e.data.get("content", "")
|
|
||||||
for e in tr_events
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Scenario 5: File upload lifecycle with real filesystem
|
# Scenario 5: File upload lifecycle with real filesystem
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveFileUpload:
|
class TestLiveFileUpload:
|
||||||
def test_upload_list_delete(self, client, thread_tmp):
|
def test_upload_list_delete(self, client, thread_tmp):
|
||||||
"""Upload → list → delete → verify deletion."""
|
"""Upload → list → delete → verify deletion."""
|
||||||
@@ -225,6 +215,7 @@ class TestLiveFileUpload:
|
|||||||
# Scenario 6: Configuration query — real config loading
|
# Scenario 6: Configuration query — real config loading
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveConfigQueries:
|
class TestLiveConfigQueries:
|
||||||
def test_list_models_returns_configured_model(self, client):
|
def test_list_models_returns_configured_model(self, client):
|
||||||
"""list_models() returns at least one configured model with Gateway-aligned fields."""
|
"""list_models() returns at least one configured model with Gateway-aligned fields."""
|
||||||
@@ -266,25 +257,25 @@ class TestLiveConfigQueries:
|
|||||||
# Scenario 7: Artifact read after agent writes
|
# Scenario 7: Artifact read after agent writes
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveArtifact:
|
class TestLiveArtifact:
|
||||||
def test_get_artifact_after_write(self, client):
|
def test_get_artifact_after_write(self, client):
|
||||||
"""Agent writes a file → client reads it back via get_artifact()."""
|
"""Agent writes a file → client reads it back via get_artifact()."""
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
thread_id = f"live-artifact-{uuid.uuid4().hex[:8]}"
|
thread_id = f"live-artifact-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
# Ask agent to write a file
|
# Ask agent to write a file
|
||||||
events = list(client.stream(
|
events = list(
|
||||||
"Use write_file to create /mnt/user-data/outputs/artifact_test.json "
|
client.stream(
|
||||||
"with content: {\"status\": \"ok\", \"source\": \"live_test\"}",
|
'Use write_file to create /mnt/user-data/outputs/artifact_test.json with content: {"status": "ok", "source": "live_test"}',
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Verify write happened
|
# Verify write happened
|
||||||
tc_events = [e for e in events if e.type == "messages-tuple" and e.data.get("type") == "ai" and "tool_calls" in e.data]
|
tc_events = [e for e in events if e.type == "messages-tuple" and e.data.get("type") == "ai" and "tool_calls" in e.data]
|
||||||
assert any(
|
assert any(any(tc["name"] == "write_file" for tc in e.data["tool_calls"]) for e in tc_events)
|
||||||
any(tc["name"] == "write_file" for tc in e.data["tool_calls"])
|
|
||||||
for e in tc_events
|
|
||||||
)
|
|
||||||
|
|
||||||
# Read artifact
|
# Read artifact
|
||||||
content, mime = client.get_artifact(thread_id, "mnt/user-data/outputs/artifact_test.json")
|
content, mime = client.get_artifact(thread_id, "mnt/user-data/outputs/artifact_test.json")
|
||||||
@@ -303,11 +294,13 @@ class TestLiveArtifact:
|
|||||||
# Scenario 8: Per-call overrides
|
# Scenario 8: Per-call overrides
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveOverrides:
|
class TestLiveOverrides:
|
||||||
def test_thinking_disabled_still_works(self, client):
|
def test_thinking_disabled_still_works(self, client):
|
||||||
"""Explicit thinking_enabled=False override produces a response."""
|
"""Explicit thinking_enabled=False override produces a response."""
|
||||||
response = client.chat(
|
response = client.chat(
|
||||||
"Say OK.", thinking_enabled=False,
|
"Say OK.",
|
||||||
|
thinking_enabled=False,
|
||||||
)
|
)
|
||||||
assert len(response) > 0
|
assert len(response) > 0
|
||||||
print(f" response: {response}")
|
print(f" response: {response}")
|
||||||
@@ -317,6 +310,7 @@ class TestLiveOverrides:
|
|||||||
# Scenario 9: Error resilience
|
# Scenario 9: Error resilience
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestLiveErrorResilience:
|
class TestLiveErrorResilience:
|
||||||
def test_delete_nonexistent_upload(self, client):
|
def test_delete_nonexistent_upload(self, client):
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
|
|||||||
136
backend/uv.lock
generated
136
backend/uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||||
@@ -131,6 +131,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosqlite"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-doc"
|
name = "annotated-doc"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
@@ -149,6 +158,25 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anthropic"
|
||||||
|
version = "0.84.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "distro" },
|
||||||
|
{ name = "docstring-parser" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "jiter" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/04/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "4.12.1"
|
version = "4.12.1"
|
||||||
@@ -342,6 +370,11 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/87/ba6298c3d7f8d66ce80d7a487f2a487ebae74a79c6049c7c2990178ce529/brotlicffi-1.2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b13fb476a96f02e477a506423cb5e7bc21e0e3ac4c060c20ba31c44056e38c68", size = 433038, upload-time = "2026-03-05T17:57:37.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/49/16c7a77d1cae0519953ef0389a11a9c2e2e62e87d04f8e7afbae40124255/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17db36fb581f7b951635cd6849553a95c6f2f53c1a707817d06eae5aeff5f6af", size = 1541124, upload-time = "2026-03-05T17:57:39.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/17/fab2c36ea820e2288f8c1bf562de1b6cd9f30e28d66f1ce2929a4baff6de/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40190192790489a7b054312163d0ce82b07d1b6e706251036898ce1684ef12e9", size = 1541983, upload-time = "2026-03-05T17:57:41.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/c9/849a669b3b3bb8ac96005cdef04df4db658c33443a7fc704a6d4a2f07a56/brotlicffi-1.2.0.0-cp314-cp314t-win32.whl", hash = "sha256:a8079e8ecc32ecef728036a1d9b7105991ce6a5385cf51ee8c02297c90fb08c2", size = 349046, upload-time = "2026-03-05T17:57:42.76Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/25/09c0fd21cfc451fa38ad538f4d18d8be566746531f7f27143f63f8c45a9f/brotlicffi-1.2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ca90c4266704ca0a94de8f101b4ec029624273380574e4cf19301acfa46c61a0", size = 385653, upload-time = "2026-03-05T17:57:44.224Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" },
|
||||||
@@ -619,12 +652,14 @@ dependencies = [
|
|||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "kubernetes" },
|
{ name = "kubernetes" },
|
||||||
{ name = "langchain" },
|
{ name = "langchain" },
|
||||||
|
{ name = "langchain-anthropic" },
|
||||||
{ name = "langchain-deepseek" },
|
{ name = "langchain-deepseek" },
|
||||||
{ name = "langchain-google-genai" },
|
{ name = "langchain-google-genai" },
|
||||||
{ name = "langchain-mcp-adapters" },
|
{ name = "langchain-mcp-adapters" },
|
||||||
{ name = "langchain-openai" },
|
{ name = "langchain-openai" },
|
||||||
{ name = "langgraph" },
|
{ name = "langgraph" },
|
||||||
{ name = "langgraph-api" },
|
{ name = "langgraph-api" },
|
||||||
|
{ name = "langgraph-checkpoint-sqlite" },
|
||||||
{ name = "langgraph-cli" },
|
{ name = "langgraph-cli" },
|
||||||
{ name = "langgraph-runtime-inmem" },
|
{ name = "langgraph-runtime-inmem" },
|
||||||
{ name = "markdownify" },
|
{ name = "markdownify" },
|
||||||
@@ -656,12 +691,14 @@ requires-dist = [
|
|||||||
{ name = "httpx", specifier = ">=0.28.0" },
|
{ name = "httpx", specifier = ">=0.28.0" },
|
||||||
{ name = "kubernetes", specifier = ">=30.0.0" },
|
{ name = "kubernetes", specifier = ">=30.0.0" },
|
||||||
{ name = "langchain", specifier = ">=1.2.3" },
|
{ name = "langchain", specifier = ">=1.2.3" },
|
||||||
|
{ name = "langchain-anthropic", specifier = ">=1.3.4" },
|
||||||
{ name = "langchain-deepseek", specifier = ">=1.0.1" },
|
{ name = "langchain-deepseek", specifier = ">=1.0.1" },
|
||||||
{ name = "langchain-google-genai", specifier = ">=4.2.1" },
|
{ name = "langchain-google-genai", specifier = ">=4.2.1" },
|
||||||
{ name = "langchain-mcp-adapters", specifier = ">=0.1.0" },
|
{ name = "langchain-mcp-adapters", specifier = ">=0.1.0" },
|
||||||
{ name = "langchain-openai", specifier = ">=1.1.7" },
|
{ name = "langchain-openai", specifier = ">=1.1.7" },
|
||||||
{ name = "langgraph", specifier = ">=1.0.6" },
|
{ name = "langgraph", specifier = ">=1.0.6" },
|
||||||
{ name = "langgraph-api", specifier = ">=0.7.0,<0.8.0" },
|
{ name = "langgraph-api", specifier = ">=0.7.0,<0.8.0" },
|
||||||
|
{ name = "langgraph-checkpoint-sqlite", specifier = ">=3.0.3" },
|
||||||
{ name = "langgraph-cli", specifier = ">=0.4.14" },
|
{ name = "langgraph-cli", specifier = ">=0.4.14" },
|
||||||
{ name = "langgraph-runtime-inmem", specifier = ">=0.22.1" },
|
{ name = "langgraph-runtime-inmem", specifier = ">=0.22.1" },
|
||||||
{ name = "markdownify", specifier = ">=1.2.2" },
|
{ name = "markdownify", specifier = ">=1.2.2" },
|
||||||
@@ -700,6 +737,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docstring-parser"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenv"
|
name = "dotenv"
|
||||||
version = "0.9.9"
|
version = "0.9.9"
|
||||||
@@ -1415,9 +1461,23 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/de/e5/9b4f58533f8ce3013b1a993289eb11e8607d9c9d9d14699b29c6ac3b4132/langchain-1.2.3-py3-none-any.whl", hash = "sha256:5cdc7c80f672962b030c4b0d16d0d8f26d849c0ada63a4b8653a20d7505512ae", size = 106428, upload-time = "2026-01-08T20:26:29.162Z" },
|
{ url = "https://files.pythonhosted.org/packages/de/e5/9b4f58533f8ce3013b1a993289eb11e8607d9c9d9d14699b29c6ac3b4132/langchain-1.2.3-py3-none-any.whl", hash = "sha256:5cdc7c80f672962b030c4b0d16d0d8f26d849c0ada63a4b8653a20d7505512ae", size = 106428, upload-time = "2026-01-08T20:26:29.162Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langchain-anthropic"
|
||||||
|
version = "1.3.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anthropic" },
|
||||||
|
{ name = "langchain-core" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/30/4e/7c1ffac126f5e62b0b9066f331f91ae69361e73476fd3ca1b19f8d8a3cc3/langchain_anthropic-1.3.4.tar.gz", hash = "sha256:000ed4c2d6fb8842b4ffeed22a74a3e84f9e9bcb63638e4abbb4a1d8ffa07211", size = 671858, upload-time = "2026-02-24T13:54:01.738Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/cf/b7c7b7270efbb3db2edbf14b09ba9110a41628f3a85a11cae9527a35641c/langchain_anthropic-1.3.4-py3-none-any.whl", hash = "sha256:cd112dcc8049aef09f58b3c4338b2c9db5ee98105e08664954a4e40d8bf120b9", size = 47454, upload-time = "2026-02-24T13:54:00.53Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langchain-core"
|
name = "langchain-core"
|
||||||
version = "1.2.7"
|
version = "1.2.17"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jsonpatch" },
|
{ name = "jsonpatch" },
|
||||||
@@ -1429,9 +1489,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "uuid-utils" },
|
{ name = "uuid-utils" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039, upload-time = "2026-01-09T17:44:25.505Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/1d/93/36226f593df52b871fc24d494c274f3a6b2ac76763a2806e7d35611634a1/langchain_core-1.2.17.tar.gz", hash = "sha256:54aa267f3311e347fb2e50951fe08e53761cebfb999ab80e6748d70525bbe872", size = 836130, upload-time = "2026-03-02T22:47:55.846Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232, upload-time = "2026-01-09T17:44:24.236Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/90/073f33ab383a62908eca7ea699586dfea280e77182176e33199c80ddf22a/langchain_core-1.2.17-py3-none-any.whl", hash = "sha256:bf6bd6ce503874e9c2da1669a69383e967c3de1ea808921d19a9a6bff1a9fbbe", size = 502727, upload-time = "2026-03-02T22:47:54.537Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1558,6 +1618,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" },
|
{ url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langgraph-checkpoint-sqlite"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiosqlite" },
|
||||||
|
{ name = "langgraph-checkpoint" },
|
||||||
|
{ name = "sqlite-vec" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/04/61/40b7f8f29d6de92406e668c35265f409f57064907e31eae84ab3f2a3e3e1/langgraph_checkpoint_sqlite-3.0.3.tar.gz", hash = "sha256:438c234d37dabda979218954c9c6eb1db73bee6492c2f1d3a00552fe23fa34ed", size = 123876, upload-time = "2026-01-19T00:38:44.473Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/d8/84ef22ee1cc485c4910df450108fd5e246497379522b3c6cfba896f71bf6/langgraph_checkpoint_sqlite-3.0.3-py3-none-any.whl", hash = "sha256:02eb683a79aa6fcda7cd4de43861062a5d160dbbb990ef8a9fd76c979998a952", size = 33593, upload-time = "2026-01-19T00:38:43.288Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langgraph-cli"
|
name = "langgraph-cli"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -2110,32 +2184,32 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-api"
|
name = "opentelemetry-api"
|
||||||
version = "1.39.1"
|
version = "1.40.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "importlib-metadata" },
|
{ name = "importlib-metadata" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-exporter-otlp-proto-common"
|
name = "opentelemetry-exporter-otlp-proto-common"
|
||||||
version = "1.39.1"
|
version = "1.40.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "opentelemetry-proto" },
|
{ name = "opentelemetry-proto" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-exporter-otlp-proto-http"
|
name = "opentelemetry-exporter-otlp-proto-http"
|
||||||
version = "1.39.1"
|
version = "1.40.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "googleapis-common-protos" },
|
{ name = "googleapis-common-protos" },
|
||||||
@@ -2146,48 +2220,48 @@ dependencies = [
|
|||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-proto"
|
name = "opentelemetry-proto"
|
||||||
version = "1.39.1"
|
version = "1.40.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "protobuf" },
|
{ name = "protobuf" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" },
|
{ url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-sdk"
|
name = "opentelemetry-sdk"
|
||||||
version = "1.39.1"
|
version = "1.40.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "opentelemetry-api" },
|
{ name = "opentelemetry-api" },
|
||||||
{ name = "opentelemetry-semantic-conventions" },
|
{ name = "opentelemetry-semantic-conventions" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-semantic-conventions"
|
name = "opentelemetry-semantic-conventions"
|
||||||
version = "0.60b1"
|
version = "0.61b0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "opentelemetry-api" },
|
{ name = "opentelemetry-api" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3162,11 +3236,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "80.9.0"
|
version = "82.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3219,6 +3293,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b8/a7/903429719d39ac2c42aa37086c90e816d883560f13c87d51f09a2962e021/speechrecognition-3.14.5-py3-none-any.whl", hash = "sha256:0c496d74e9f29b1daadb0d96f5660f47563e42bf09316dacdd57094c5095977e", size = 32856308, upload-time = "2025-12-31T11:25:41.161Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/a7/903429719d39ac2c42aa37086c90e816d883560f13c87d51f09a2962e021/speechrecognition-3.14.5-py3-none-any.whl", hash = "sha256:0c496d74e9f29b1daadb0d96f5660f47563e42bf09316dacdd57094c5095977e", size = 32856308, upload-time = "2025-12-31T11:25:41.161Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlite-vec"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/ed/aabc328f29ee6814033d008ec43e44f2c595447d9cccd5f2aabe60df2933/sqlite_vec-0.1.6-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:77491bcaa6d496f2acb5cc0d0ff0b8964434f141523c121e313f9a7d8088dee3", size = 164075, upload-time = "2024-11-20T16:40:29.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/57/05604e509a129b22e303758bfa062c19afb020557d5e19b008c64016704e/sqlite_vec-0.1.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fdca35f7ee3243668a055255d4dee4dea7eed5a06da8cad409f89facf4595361", size = 165242, upload-time = "2024-11-20T16:40:31.206Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/48/dbb2cc4e5bad88c89c7bb296e2d0a8df58aab9edc75853728c361eefc24f/sqlite_vec-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b0519d9cd96164cd2e08e8eed225197f9cd2f0be82cb04567692a0a4be02da3", size = 103704, upload-time = "2024-11-20T16:40:33.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/76/97f33b1a2446f6ae55e59b33869bed4eafaf59b7f4c662c8d9491b6a714a/sqlite_vec-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:823b0493add80d7fe82ab0fe25df7c0703f4752941aee1c7b2b02cec9656cb24", size = 151556, upload-time = "2024-11-20T16:40:35.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/98/e8bc58b178266eae2fcf4c9c7a8303a8d41164d781b32d71097924a6bebe/sqlite_vec-0.1.6-py3-none-win_amd64.whl", hash = "sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c", size = 281540, upload-time = "2024-11-20T16:40:37.296Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sse-starlette"
|
name = "sse-starlette"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
|
|||||||
@@ -344,3 +344,37 @@ memory:
|
|||||||
fact_confidence_threshold: 0.7 # Minimum confidence for storing facts
|
fact_confidence_threshold: 0.7 # Minimum confidence for storing facts
|
||||||
injection_enabled: true # Whether to inject memory into system prompt
|
injection_enabled: true # Whether to inject memory into system prompt
|
||||||
max_injection_tokens: 2000 # Maximum tokens for memory injection
|
max_injection_tokens: 2000 # Maximum tokens for memory injection
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Checkpointer Configuration
|
||||||
|
# ============================================================================
|
||||||
|
# Configure state persistence for the embedded DeerFlowClient.
|
||||||
|
# The LangGraph Server manages its own state persistence separately
|
||||||
|
# via the server infrastructure (this setting does not affect it).
|
||||||
|
#
|
||||||
|
# When configured, DeerFlowClient will automatically use this checkpointer,
|
||||||
|
# enabling multi-turn conversations to persist across process restarts.
|
||||||
|
#
|
||||||
|
# Supported types:
|
||||||
|
# memory - In-process only. State is lost when the process exits. (default)
|
||||||
|
# sqlite - File-based SQLite persistence. Survives restarts.
|
||||||
|
# Requires: uv add langgraph-checkpoint-sqlite
|
||||||
|
# postgres - PostgreSQL persistence. Suitable for multi-process deployments.
|
||||||
|
# Requires: uv add langgraph-checkpoint-postgres psycopg[binary] psycopg-pool
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
#
|
||||||
|
# In-memory (default when omitted — no persistence):
|
||||||
|
# checkpointer:
|
||||||
|
# type: memory
|
||||||
|
#
|
||||||
|
# SQLite (file-based, single-process):
|
||||||
|
# checkpointer:
|
||||||
|
# type: sqlite
|
||||||
|
# connection_string: checkpoints.db
|
||||||
|
#
|
||||||
|
# PostgreSQL (multi-process, production):
|
||||||
|
# checkpointer:
|
||||||
|
# type: postgres
|
||||||
|
# connection_string: postgresql://user:password@localhost:5432/deerflow
|
||||||
|
|||||||
Reference in New Issue
Block a user