mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
refactor(agents): consolidate chat runtime infra under chat/runtime
Move the lower-level runtime/infra modules out of multi_agent_chat/shared/ (they were never used by subagents, so they failed the shared-by-all-siblings rule) and unify them with the already-relocated checkpointer: agents/runtime/ -> agents/chat/runtime/ mac/shared/errors.py -> chat/runtime/errors.py mac/shared/llm_config.py -> chat/runtime/llm_config.py mac/shared/prompt_caching.py -> chat/runtime/prompt_caching.py mac/shared/mention_resolver.py -> chat/runtime/mention_resolver.py mac/shared/path_resolver.py -> chat/runtime/path_resolver.py These sit below the agent packages: the boundary + agent factory + shared middleware depend on them, and they import no agent code (acyclic).
This commit is contained in:
parent
7d866a2279
commit
f2a61bc0ef
52 changed files with 97 additions and 87 deletions
|
|
@ -24,13 +24,13 @@ from typing import Any
|
|||
from langchain.agents.middleware import AgentMiddleware, AgentState
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import (
|
||||
DOCUMENTS_ROOT,
|
||||
safe_filename,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.runtime.path_resolver import (
|
||||
DOCUMENTS_ROOT,
|
||||
safe_filename,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,15 +34,15 @@ from langgraph.runtime import Runtime
|
|||
from sqlalchemy import select
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import (
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.runtime.path_resolver import (
|
||||
DOCUMENTS_ROOT,
|
||||
PathIndex,
|
||||
build_path_index,
|
||||
doc_to_virtual_path,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.db import Document, shielded_async_session
|
||||
from app.utils.perf import get_perf_logger
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,9 @@ from app.agents.chat.multi_agent_chat.shared.filesystem_selection import (
|
|||
FilesystemMode,
|
||||
FilesystemSelection,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.llm_config import AgentConfig
|
||||
from app.agents.chat.multi_agent_chat.shared.middleware.filesystem.backends.resolver import (
|
||||
build_backend_resolver,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.prompt_caching import (
|
||||
apply_litellm_prompt_caching,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.subagents import (
|
||||
get_subagents_to_exclude,
|
||||
main_prompt_registry_subagent_lines,
|
||||
|
|
@ -34,6 +30,10 @@ from app.agents.chat.multi_agent_chat.subagents import (
|
|||
from app.agents.chat.multi_agent_chat.subagents.mcp_tools.index import (
|
||||
load_mcp_tools_by_connector,
|
||||
)
|
||||
from app.agents.chat.runtime.llm_config import AgentConfig
|
||||
from app.agents.chat.runtime.prompt_caching import (
|
||||
apply_litellm_prompt_caching,
|
||||
)
|
||||
from app.db import ChatVisibility
|
||||
from app.services.connector_service import ConnectorService
|
||||
from app.services.user_tool_allowlist import (
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ What this provides:
|
|||
tools can poll to abort cooperatively. The event is reset between
|
||||
turns. Tools should check ``runtime.context.cancel_event.is_set()``
|
||||
in tight inner loops.
|
||||
- A typed :class:`~app.agents.chat.multi_agent_chat.shared.errors.BusyError` raised when a
|
||||
- A typed :class:`~app.agents.chat.runtime.errors.BusyError` raised when a
|
||||
second turn arrives while the lock is held.
|
||||
|
||||
Note: SurfSense's ``stream_new_chat`` is the call site that should
|
||||
|
|
@ -46,7 +46,7 @@ from langchain.agents.middleware.types import (
|
|||
from langgraph.config import get_config
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.errors import BusyError
|
||||
from app.agents.chat.runtime.errors import BusyError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from app.agents.chat.multi_agent_chat.shared.middleware.filesystem.backends.document_xml import (
|
||||
build_document_xml,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import (
|
||||
from app.agents.chat.runtime.path_resolver import (
|
||||
DOCUMENTS_ROOT,
|
||||
build_path_index,
|
||||
doc_to_virtual_path,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import DOCUMENTS_ROOT
|
||||
from app.agents.chat.runtime.path_resolver import DOCUMENTS_ROOT
|
||||
|
||||
|
||||
def is_cloud(mode: FilesystemMode) -> bool:
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from langchain.tools import ToolRuntime
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import DOCUMENTS_ROOT
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.runtime.path_resolver import DOCUMENTS_ROOT
|
||||
|
||||
from ..shared.paths import TEMP_PREFIX, basename
|
||||
from .mode import is_cloud
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ from langchain_core.messages import ToolMessage
|
|||
from langchain_core.tools import BaseTool, StructuredTool
|
||||
from langgraph.types import Command
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import DOCUMENTS_ROOT
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.runtime.path_resolver import DOCUMENTS_ROOT
|
||||
|
||||
from ...middleware.async_dispatch import run_async_blocking
|
||||
from ...middleware.path_resolution import resolve_relative
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ from langchain_core.messages import ToolMessage
|
|||
from langchain_core.tools import BaseTool, StructuredTool
|
||||
from langgraph.types import Command
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import DOCUMENTS_ROOT
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.runtime.path_resolver import DOCUMENTS_ROOT
|
||||
|
||||
from ...middleware.async_dispatch import run_async_blocking
|
||||
from ...middleware.mode import is_cloud
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ from langgraph.types import Command
|
|||
from app.agents.chat.multi_agent_chat.shared.middleware.filesystem.backends.kb_postgres import (
|
||||
KBPostgresBackend,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import DOCUMENTS_ROOT
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.state.reducers import _CLEAR
|
||||
from app.agents.chat.runtime.path_resolver import DOCUMENTS_ROOT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...middleware import SurfSenseFilesystemMiddleware
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ from langgraph.types import Command
|
|||
from app.agents.chat.multi_agent_chat.shared.middleware.filesystem.backends.kb_postgres import (
|
||||
KBPostgresBackend,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import DOCUMENTS_ROOT
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.state.reducers import _CLEAR
|
||||
from app.agents.chat.runtime.path_resolver import DOCUMENTS_ROOT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...middleware import SurfSenseFilesystemMiddleware
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ from langgraph.types import Command
|
|||
from app.agents.chat.multi_agent_chat.shared.middleware.filesystem.backends.kb_postgres import (
|
||||
KBPostgresBackend,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import DOCUMENTS_ROOT
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.state.reducers import _CLEAR
|
||||
from app.agents.chat.runtime.path_resolver import DOCUMENTS_ROOT
|
||||
|
||||
from ...middleware.path_resolution import current_cwd
|
||||
from ...shared.paths import is_ancestor_of
|
||||
|
|
|
|||
|
|
@ -47,12 +47,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from app.agents.chat.multi_agent_chat.shared.feature_flags import get_flags
|
||||
from app.agents.chat.multi_agent_chat.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import (
|
||||
DOCUMENTS_ROOT,
|
||||
parse_documents_path,
|
||||
safe_folder_segment,
|
||||
virtual_path_to_doc,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.receipts.receipt import (
|
||||
Receipt,
|
||||
make_receipt,
|
||||
|
|
@ -61,6 +55,12 @@ from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
|||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.state.reducers import _CLEAR
|
||||
from app.agents.chat.runtime.path_resolver import (
|
||||
DOCUMENTS_ROOT,
|
||||
parse_documents_path,
|
||||
safe_folder_segment,
|
||||
virtual_path_to_doc,
|
||||
)
|
||||
from app.db import (
|
||||
AgentActionLog,
|
||||
Chunk,
|
||||
|
|
|
|||
|
|
@ -47,14 +47,14 @@ from app.agents.chat.multi_agent_chat.shared.date_filters import (
|
|||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.feature_flags import get_flags
|
||||
from app.agents.chat.multi_agent_chat.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import (
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.agents.chat.runtime.path_resolver import (
|
||||
PathIndex,
|
||||
build_path_index,
|
||||
doc_to_virtual_path,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.shared.state.filesystem_state import (
|
||||
SurfSenseFilesystemState,
|
||||
)
|
||||
from app.db import (
|
||||
NATIVE_TO_LEGACY_DOCTYPE,
|
||||
Chunk,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ from typing import Any
|
|||
|
||||
from langchain_core.messages import ToolMessage
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.errors import StreamingError
|
||||
from app.agents.chat.multi_agent_chat.shared.permissions import Rule
|
||||
from app.agents.chat.runtime.errors import StreamingError
|
||||
|
||||
|
||||
def build_deny_message(tool_call: dict[str, Any], rule: Rule) -> ToolMessage:
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ from langchain_core.messages import AIMessage, ToolMessage
|
|||
from langchain_core.tools import BaseTool
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.errors import CorrectedError, RejectedError
|
||||
from app.agents.chat.multi_agent_chat.shared.permissions import Ruleset
|
||||
from app.agents.chat.runtime.errors import CorrectedError, RejectedError
|
||||
from app.services.user_tool_allowlist import TrustedToolSaver
|
||||
|
||||
from ..ask.edit import merge_edited_args
|
||||
|
|
|
|||
16
surfsense_backend/app/agents/chat/runtime/__init__.py
Normal file
16
surfsense_backend/app/agents/chat/runtime/__init__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Lower-level runtime infrastructure for the chat agents.
|
||||
|
||||
Modules here are the foundation layer used to *run* chat agents: wired by the
|
||||
boundary (routes/tasks) and/or imported by the agent factory + shared
|
||||
middleware, but never part of any single agent's domain logic. Because they sit
|
||||
below the agent packages, both the boundary and the agents may depend on them
|
||||
(forward dependency), while they never import agent code.
|
||||
|
||||
Contents:
|
||||
- ``checkpointer`` LangGraph Postgres checkpoint saver (boundary lifespan)
|
||||
- ``llm_config`` LLM provider/model configuration resolution
|
||||
- ``prompt_caching`` LiteLLM prompt-caching configuration
|
||||
- ``errors`` agent-runtime error contracts (raised by MW, caught at boundary)
|
||||
- ``path_resolver`` filesystem path resolution helpers
|
||||
- ``mention_resolver`` @-mention resolution helpers
|
||||
"""
|
||||
144
surfsense_backend/app/agents/chat/runtime/checkpointer.py
Normal file
144
surfsense_backend/app/agents/chat/runtime/checkpointer.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"""
|
||||
PostgreSQL-based checkpointer for LangGraph agents.
|
||||
|
||||
This module provides a persistent checkpointer using AsyncPostgresSaver
|
||||
that stores conversation state in the PostgreSQL database.
|
||||
|
||||
Uses a connection pool (psycopg_pool.AsyncConnectionPool) to handle
|
||||
connection lifecycle, health checks, and automatic reconnection,
|
||||
preventing 'the connection is closed' errors in long-running deployments.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from app.config import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global checkpointer instance (initialized lazily)
|
||||
_checkpointer: AsyncPostgresSaver | None = None
|
||||
_connection_pool: AsyncConnectionPool | None = None
|
||||
_checkpointer_initialized: bool = False
|
||||
|
||||
|
||||
def get_postgres_connection_string() -> str:
|
||||
"""
|
||||
Convert the async DATABASE_URL to a sync postgres connection string for psycopg3.
|
||||
|
||||
The DATABASE_URL is typically in format:
|
||||
postgresql+asyncpg://user:pass@host:port/dbname
|
||||
|
||||
We need to convert it to:
|
||||
postgresql://user:pass@host:port/dbname
|
||||
"""
|
||||
db_url = config.DATABASE_URL
|
||||
|
||||
# Handle asyncpg driver prefix
|
||||
if db_url.startswith("postgresql+asyncpg://"):
|
||||
return db_url.replace("postgresql+asyncpg://", "postgresql://")
|
||||
|
||||
# Handle other async prefixes
|
||||
if "+asyncpg" in db_url:
|
||||
return db_url.replace("+asyncpg", "")
|
||||
|
||||
return db_url
|
||||
|
||||
|
||||
async def _create_checkpointer() -> AsyncPostgresSaver:
|
||||
"""
|
||||
Create a new AsyncPostgresSaver backed by a connection pool.
|
||||
|
||||
The connection pool automatically handles:
|
||||
- Connection health checks before use
|
||||
- Reconnection when connections die (idle timeout, DB restart, etc.)
|
||||
- Connection lifecycle management (max_lifetime, max_idle)
|
||||
"""
|
||||
global _connection_pool
|
||||
|
||||
conn_string = get_postgres_connection_string()
|
||||
|
||||
_connection_pool = AsyncConnectionPool(
|
||||
conninfo=conn_string,
|
||||
min_size=2,
|
||||
max_size=10,
|
||||
# Connections are recycled after 30 minutes to avoid stale connections
|
||||
max_lifetime=1800,
|
||||
# Idle connections are closed after 5 minutes
|
||||
max_idle=300,
|
||||
open=False,
|
||||
# Connection kwargs required by AsyncPostgresSaver:
|
||||
# - autocommit: required for .setup() to commit checkpoint tables
|
||||
# - prepare_threshold: disable prepared statements for compatibility
|
||||
# - row_factory: checkpointer accesses rows as dicts (row["column"])
|
||||
kwargs={
|
||||
"autocommit": True,
|
||||
"prepare_threshold": 0,
|
||||
"row_factory": dict_row,
|
||||
},
|
||||
)
|
||||
await _connection_pool.open(wait=True)
|
||||
|
||||
checkpointer = AsyncPostgresSaver(conn=_connection_pool)
|
||||
logger.info("[Checkpointer] Created AsyncPostgresSaver with connection pool")
|
||||
return checkpointer
|
||||
|
||||
|
||||
async def get_checkpointer() -> AsyncPostgresSaver:
|
||||
"""
|
||||
Get or create the global AsyncPostgresSaver instance.
|
||||
|
||||
This function:
|
||||
1. Creates the checkpointer with a connection pool if it doesn't exist
|
||||
2. Sets up the required database tables on first call
|
||||
3. Returns the cached instance on subsequent calls
|
||||
|
||||
The underlying connection pool handles reconnection automatically,
|
||||
so a stale/closed connection will not cause OperationalError.
|
||||
|
||||
Returns:
|
||||
AsyncPostgresSaver: The configured checkpointer instance
|
||||
"""
|
||||
global _checkpointer, _checkpointer_initialized
|
||||
|
||||
if _checkpointer is None:
|
||||
_checkpointer = await _create_checkpointer()
|
||||
_checkpointer_initialized = False
|
||||
|
||||
# Setup tables on first call (idempotent)
|
||||
if not _checkpointer_initialized:
|
||||
await _checkpointer.setup()
|
||||
_checkpointer_initialized = True
|
||||
|
||||
return _checkpointer
|
||||
|
||||
|
||||
async def setup_checkpointer_tables() -> None:
|
||||
"""
|
||||
Explicitly setup the checkpointer tables.
|
||||
|
||||
This can be called during application startup to ensure
|
||||
tables exist before any agent calls.
|
||||
"""
|
||||
await get_checkpointer()
|
||||
logger.info("[Checkpointer] PostgreSQL checkpoint tables ready")
|
||||
|
||||
|
||||
async def close_checkpointer() -> None:
|
||||
"""
|
||||
Close the checkpointer connection pool.
|
||||
|
||||
This should be called during application shutdown.
|
||||
"""
|
||||
global _checkpointer, _connection_pool, _checkpointer_initialized
|
||||
|
||||
if _connection_pool is not None:
|
||||
await _connection_pool.close()
|
||||
logger.info("[Checkpointer] PostgreSQL connection pool closed")
|
||||
|
||||
_checkpointer = None
|
||||
_connection_pool = None
|
||||
_checkpointer_initialized = False
|
||||
|
|
@ -27,7 +27,7 @@ from litellm import get_model_info
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.prompt_caching import (
|
||||
from app.agents.chat.runtime.prompt_caching import (
|
||||
apply_litellm_prompt_caching,
|
||||
)
|
||||
from app.services.llm_router_service import (
|
||||
|
|
@ -36,7 +36,7 @@ from dataclasses import dataclass, field
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.agents.chat.multi_agent_chat.shared.path_resolver import (
|
||||
from app.agents.chat.runtime.path_resolver import (
|
||||
DOCUMENTS_ROOT,
|
||||
build_path_index,
|
||||
doc_to_virtual_path,
|
||||
|
|
@ -68,7 +68,7 @@ from typing import TYPE_CHECKING, Any
|
|||
from langchain_core.language_models import BaseChatModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.agents.chat.multi_agent_chat.shared.llm_config import AgentConfig
|
||||
from app.agents.chat.runtime.llm_config import AgentConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue