refactor(gateway): simplify deps.py with getter factory + inline repos

- Replace 6 identical getter functions with _require() factory.
- Inline 3 _make_*_repo() factories into langgraph_runtime(), call
  get_session_factory() once instead of 3 times.
- Add thread_meta upsert in start_run (services.py).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
rayhpeng
2026-04-03 17:25:03 +08:00
parent 3b4622a26f
commit e362aaefbd
2 changed files with 66 additions and 95 deletions
+49 -94
View File
@@ -1,7 +1,8 @@
"""Centralized accessors for singleton objects stored on ``app.state``. """Centralized accessors for singleton objects stored on ``app.state``.
**Getters** (used by routers): raise 503 when a required dependency is **Getters** (used by routers): raise 503 when a required dependency is
missing, except ``get_store`` which returns ``None``. missing, except ``get_store`` and ``get_thread_meta_repo`` which return
``None``.
Initialization is handled directly in ``app.py`` via :class:`AsyncExitStack`. Initialization is handled directly in ``app.py`` via :class:`AsyncExitStack`.
""" """
@@ -13,9 +14,7 @@ from contextlib import AsyncExitStack, asynccontextmanager
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from deerflow.runtime import RunManager, StreamBridge from deerflow.runtime import RunManager
from deerflow.runtime.events.store.base import RunEventStore
from deerflow.runtime.runs.store.base import RunStore
@asynccontextmanager @asynccontextmanager
@@ -29,25 +28,41 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
""" """
from deerflow.agents.checkpointer.async_provider import make_checkpointer from deerflow.agents.checkpointer.async_provider import make_checkpointer
from deerflow.config import get_app_config from deerflow.config import get_app_config
from deerflow.persistence.engine import close_engine, init_engine_from_config from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config
from deerflow.runtime import make_store, make_stream_bridge from deerflow.runtime import make_store, make_stream_bridge
from deerflow.runtime.events.store import make_run_event_store
async with AsyncExitStack() as stack: async with AsyncExitStack() as stack:
app.state.stream_bridge = await stack.enter_async_context(make_stream_bridge()) app.state.stream_bridge = await stack.enter_async_context(make_stream_bridge())
app.state.checkpointer = await stack.enter_async_context(make_checkpointer())
app.state.store = await stack.enter_async_context(make_store()) # Initialize persistence engine BEFORE checkpointer so that
# Initialize persistence layer from unified database config # auto-create-database logic runs first (postgres backend).
config = get_app_config() config = get_app_config()
await init_engine_from_config(config.database) await init_engine_from_config(config.database)
# Initialize run store (RunRepository if DB available, else MemoryRunStore) app.state.checkpointer = await stack.enter_async_context(make_checkpointer())
app.state.run_store = _make_run_store() app.state.store = await stack.enter_async_context(make_store())
# Initialize run event store based on config # Initialize repositories — one get_session_factory() call for all.
app.state.run_event_store = _make_run_event_store(config) sf = get_session_factory()
if sf is not None:
from deerflow.persistence.repositories.feedback_repo import FeedbackRepository
from deerflow.persistence.repositories.run_repo import RunRepository
from deerflow.persistence.repositories.thread_meta_repo import ThreadMetaRepository
# Initialize feedback repository (None when no DB engine) app.state.run_store = RunRepository(sf)
app.state.feedback_repo = _make_feedback_repo() app.state.feedback_repo = FeedbackRepository(sf)
app.state.thread_meta_repo = ThreadMetaRepository(sf)
else:
from deerflow.runtime.runs.store.memory import MemoryRunStore
app.state.run_store = MemoryRunStore()
app.state.feedback_repo = None
app.state.thread_meta_repo = None
# Run event store (has its own factory with config-driven backend selection)
run_events_config = getattr(config, "run_events", None)
app.state.run_event_store = make_run_event_store(run_events_config)
# RunManager with store backing for persistence # RunManager with store backing for persistence
app.state.run_manager = RunManager(store=app.state.run_store) app.state.run_manager = RunManager(store=app.state.run_store)
@@ -58,71 +73,30 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
await close_engine() await close_engine()
# ---------------------------------------------------------------------------
# Factories
# ---------------------------------------------------------------------------
def _make_run_store() -> RunStore:
"""Create a RunStore: RunRepository if DB engine is available, else MemoryRunStore."""
from deerflow.persistence.engine import get_session_factory
sf = get_session_factory()
if sf is not None:
from deerflow.persistence.repositories.run_repo import RunRepository
return RunRepository(sf)
from deerflow.runtime.runs.store.memory import MemoryRunStore
return MemoryRunStore()
def _make_feedback_repo():
"""Create a FeedbackRepository if DB engine is available, else None."""
from deerflow.persistence.engine import get_session_factory
sf = get_session_factory()
if sf is not None:
from deerflow.persistence.repositories.feedback_repo import FeedbackRepository
return FeedbackRepository(sf)
return None
def _make_run_event_store(config) -> RunEventStore:
from deerflow.runtime.events.store import make_run_event_store
run_events_config = getattr(config, "run_events", None)
return make_run_event_store(run_events_config)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Getters -- called by routers per-request # Getters -- called by routers per-request
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def get_stream_bridge(request: Request) -> StreamBridge: def _require(attr: str, label: str):
"""Return the global :class:`StreamBridge`, or 503.""" """Create a FastAPI dependency that returns ``app.state.<attr>`` or 503."""
bridge = getattr(request.app.state, "stream_bridge", None)
if bridge is None: def dep(request: Request):
raise HTTPException(status_code=503, detail="Stream bridge not available") val = getattr(request.app.state, attr, None)
return bridge if val is None:
raise HTTPException(status_code=503, detail=f"{label} not available")
return val
dep.__name__ = dep.__qualname__ = f"get_{attr}"
return dep
def get_run_manager(request: Request) -> RunManager: get_stream_bridge = _require("stream_bridge", "Stream bridge")
"""Return the global :class:`RunManager`, or 503.""" get_run_manager = _require("run_manager", "Run manager")
mgr = getattr(request.app.state, "run_manager", None) get_checkpointer = _require("checkpointer", "Checkpointer")
if mgr is None: get_run_event_store = _require("run_event_store", "Run event store")
raise HTTPException(status_code=503, detail="Run manager not available") get_feedback_repo = _require("feedback_repo", "Feedback")
return mgr get_run_store = _require("run_store", "Run store")
def get_checkpointer(request: Request):
"""Return the global checkpointer, or 503."""
cp = getattr(request.app.state, "checkpointer", None)
if cp is None:
raise HTTPException(status_code=503, detail="Checkpointer not available")
return cp
def get_store(request: Request): def get_store(request: Request):
@@ -130,28 +104,9 @@ def get_store(request: Request):
return getattr(request.app.state, "store", None) return getattr(request.app.state, "store", None)
def get_run_event_store(request: Request) -> RunEventStore: def get_thread_meta_repo(request: Request):
"""Return the RunEventStore, or 503 if not available.""" """Return the ThreadMetaRepository, or None if not available."""
store = getattr(request.app.state, "run_event_store", None) return getattr(request.app.state, "thread_meta_repo", None)
if store is None:
raise HTTPException(status_code=503, detail="Run event store not available")
return store
def get_feedback_repo(request: Request):
"""Return the FeedbackRepository, or 503 if not available."""
repo = getattr(request.app.state, "feedback_repo", None)
if repo is None:
raise HTTPException(status_code=503, detail="Feedback not available")
return repo
def get_run_store(request: Request) -> RunStore:
"""Return the RunStore, or 503 if not available."""
store = getattr(request.app.state, "run_store", None)
if store is None:
raise HTTPException(status_code=503, detail="Run store not available")
return store
async def get_current_user(request: Request) -> str | None: async def get_current_user(request: Request) -> str | None:
+17 -1
View File
@@ -17,7 +17,7 @@ from typing import Any
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from app.gateway.deps import get_checkpointer, get_run_event_store, get_run_manager, get_run_store, get_store, get_stream_bridge from app.gateway.deps import get_checkpointer, get_run_event_store, get_run_manager, get_run_store, get_store, get_stream_bridge, get_thread_meta_repo
from deerflow.runtime import ( from deerflow.runtime import (
END_SENTINEL, END_SENTINEL,
HEARTBEAT_SENTINEL, HEARTBEAT_SENTINEL,
@@ -274,6 +274,22 @@ async def start_run(
if store is not None: if store is not None:
await _upsert_thread_in_store(store, thread_id, body.metadata) await _upsert_thread_in_store(store, thread_id, body.metadata)
# Upsert thread metadata in the SQL-backed threads_meta table
thread_meta_repo = get_thread_meta_repo(request)
if thread_meta_repo is not None:
try:
existing = await thread_meta_repo.get(thread_id)
if existing is None:
await thread_meta_repo.create(
thread_id,
assistant_id=body.assistant_id,
metadata=body.metadata,
)
else:
await thread_meta_repo.update_status(thread_id, "running")
except Exception:
logger.warning("Failed to upsert thread_meta for %s (non-fatal)", thread_id)
# Resolve follow_up_to_run_id: explicit from request, or auto-detect from latest successful run # Resolve follow_up_to_run_id: explicit from request, or auto-detect from latest successful run
follow_up_to_run_id = getattr(body, "follow_up_to_run_id", None) follow_up_to_run_id = getattr(body, "follow_up_to_run_id", None)
if follow_up_to_run_id is None: if follow_up_to_run_id is None: