mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-15 00:19:14 +00:00
Wiring moves out of deps.py::langgraph_runtime into app/composition.py::build_domain_services, so the rule the assembly owns -- a memory database backend yields no services and the routes answer 503 -- is an assertion in tests/test_composition.py instead of a comment inside the lifespan.
70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
"""Composition root -- the one place adapters are instantiated and wired.
|
|
|
|
Every application service is assembled here and nowhere else. Ports are
|
|
declared by the domain, implemented under ``app/adapters/``, and the two are
|
|
introduced to each other in this file; no router, middleware, or lifespan hook
|
|
constructs an adapter of its own.
|
|
|
|
**Why this is a pure function rather than part of the lifespan.** Wiring used
|
|
to live inline in ``deps.py::langgraph_runtime``, tangled with engine startup
|
|
and shutdown -- so the one rule that actually governs it ("no SQL backend
|
|
means no service, and the routes answer 503") could not be tested without
|
|
driving a full application startup. ``build_domain_services`` takes what it
|
|
needs and returns what it built, so that rule is an assertion in
|
|
``tests/test_composition.py`` instead.
|
|
|
|
**What "no SQL backend" means.** ``session_factory is None`` is how a
|
|
``database.backend: memory`` deployment presents itself. The feedback context
|
|
owns a table, so it cannot run on it; the service is ``None`` and the
|
|
dependency provider translates that into 503. This is deliberately not a
|
|
silent degradation to an in-memory implementation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
from deerflow.domain.feedback import FeedbackService
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
from deerflow.runtime.runs.store import RunStore
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DomainServices:
|
|
"""Every application service the Gateway serves, or ``None`` where the
|
|
configured backend cannot support one."""
|
|
|
|
feedback: FeedbackService | None
|
|
|
|
|
|
def build_domain_services(
|
|
*,
|
|
session_factory: async_sessionmaker[AsyncSession] | None,
|
|
run_store: RunStore,
|
|
) -> DomainServices:
|
|
"""Wire the domain services from already-built infrastructure.
|
|
|
|
Takes infrastructure rather than building it: engines and stores have
|
|
lifecycles (startup, recovery, shutdown) that belong to the lifespan,
|
|
while deciding *what is assembled from them* is this function's only
|
|
job. That split is what makes it callable from a test.
|
|
"""
|
|
if session_factory is None:
|
|
return DomainServices(feedback=None)
|
|
|
|
# Imported here, not at module scope: these pull in SQLAlchemy, and the
|
|
# composition root is imported by tests that only want the memory branch.
|
|
from app.adapters.feedback.feedback_repository import SqlFeedbackRepository
|
|
from app.adapters.feedback.run_lookup import RunStoreRunLookup
|
|
|
|
return DomainServices(
|
|
feedback=FeedbackService(
|
|
repository=SqlFeedbackRepository(session_factory),
|
|
runs=RunStoreRunLookup(run_store),
|
|
),
|
|
)
|