Skip to content

Commit 69bad2f

Browse files
committed
Langflow <> LFX Service Separation.
1 parent 8eb49d4 commit 69bad2f

30 files changed

Lines changed: 1648 additions & 330 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ instance/
192192
# Scrapy stuff:
193193
.scrapy
194194

195+
# AI Scratch work
196+
/scratch
197+
scratch/
198+
195199
# Sphinx documentation
196200
docs/_build/
197201

src/backend/base/langflow/services/database/service.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
from contextlib import asynccontextmanager, contextmanager, nullcontext
1010
from datetime import datetime, timezone
1111
from pathlib import Path
12-
from typing import TYPE_CHECKING
12+
from typing import TYPE_CHECKING, ClassVar
1313

1414
import anyio
1515
import sqlalchemy as sa
1616
from alembic import command, util
1717
from alembic.config import Config
1818
from lfx.log.logger import logger
19+
from lfx.services.capabilities import Capability, Tier
1920
from lfx.services.deps import session_scope
2021
from sqlalchemy import event, inspect
2122
from sqlalchemy.dialects import sqlite as dialect_sqlite
@@ -255,6 +256,30 @@ def check_sqlite_database_path(database_url: str) -> None:
255256
class DatabaseService(Service):
256257
name = "database_service"
257258

259+
# Tier 1 infrastructure port. A real engine persists across restarts; SHARED
260+
# (cross-process) is only true for Postgres, so the static class-level
261+
# guarantee is the intersection: {PERSISTENT}. (A backend that wants to
262+
# advertise SHARED can override per deployment.)
263+
tier: ClassVar[Tier] = Tier.INFRASTRUCTURE
264+
capabilities: ClassVar[frozenset[Capability]] = frozenset({Capability.PERSISTENT})
265+
266+
def session_scope(self):
267+
"""Async write session scope over this service (auto-commit/rollback).
268+
269+
Part of the Tier 1 database port (Option B): Tier 2 services call this on
270+
their injected ``database_service``. Delegates to the shared helper so
271+
semantics match the module-level ``lfx.services.deps.session_scope``.
272+
"""
273+
from lfx.services.database.session import session_scope_for
274+
275+
return session_scope_for(self)
276+
277+
def session_scope_readonly(self):
278+
"""Read-only session scope over this service (no commit/rollback)."""
279+
from lfx.services.database.session import session_scope_readonly_for
280+
281+
return session_scope_readonly_for(self)
282+
258283
def __init__(self, settings_service: SettingsService):
259284
self._logged_pragma = False
260285
self.settings_service = settings_service

src/backend/base/langflow/services/factory.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,23 @@ def import_all_services_into_a_dict():
7676
try:
7777
service_name = ServiceType(service_type).value.replace("_service", "")
7878

79-
# Special handling for mcp_composer which is now in lfx module
80-
if service_name == "mcp_composer":
81-
module_name = f"lfx.services.{service_name}.service"
82-
else:
83-
module_name = f"langflow.services.{service_name}.service"
79+
# Some services are hosted in lfx rather than langflow (e.g. memory,
80+
# mcp_composer). Prefer langflow's module when it exists (an override),
81+
# otherwise fall back to the lfx module. A service whose ``.service``
82+
# module exists in neither is simply skipped.
83+
module = None
84+
for module_name in (
85+
f"langflow.services.{service_name}.service",
86+
f"lfx.services.{service_name}.service",
87+
):
88+
try:
89+
module = importlib.import_module(module_name)
90+
break
91+
except ModuleNotFoundError:
92+
continue
93+
if module is None:
94+
continue
8495

85-
module = importlib.import_module(module_name)
8696
services.update(
8797
{
8898
name: obj
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
"""Langflow's DB-backed chat-message memory service."""
1+
"""Langflow chat-message memory: the converged lfx Tier 2 service.
22
3-
from langflow.services.memory.factory import MemoryServiceFactory
4-
from langflow.services.memory.service import LangflowMemoryService
3+
Langflow no longer defines its own memory service class. It *selects* lfx's
4+
``DatabaseMemoryService`` and wires it over langflow's Tier 1 ``DatabaseService``
5+
(done in ``langflow.services.utils.register_all_service_factories``). This module
6+
re-exports the lfx class for any code that imported the langflow path.
7+
"""
58

6-
__all__ = ["LangflowMemoryService", "MemoryServiceFactory"]
9+
from lfx.services.memory.database import DatabaseMemoryService
10+
11+
__all__ = ["DatabaseMemoryService"]

src/backend/base/langflow/services/memory/factory.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/backend/base/langflow/services/memory/service.py

Lines changed: 0 additions & 110 deletions
This file was deleted.

src/backend/base/langflow/services/utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ def register_all_service_factories() -> None:
566566
service_manager = get_service_manager()
567567
from lfx.services.executor import factory as executor_factory
568568
from lfx.services.mcp_composer import factory as mcp_composer_factory
569+
from lfx.services.memory.database import DatabaseMemoryService
569570
from lfx.services.settings import factory as settings_factory
570571

571572
from langflow.services.auth import factory as auth_factory
@@ -576,7 +577,6 @@ def register_all_service_factories() -> None:
576577
from langflow.services.chat import factory as chat_factory
577578
from langflow.services.database import factory as database_factory
578579
from langflow.services.job_queue import factory as job_queue_factory
579-
from langflow.services.memory import factory as memory_factory
580580
from langflow.services.session import factory as session_factory
581581
from langflow.services.shared_component_cache import factory as shared_component_cache_factory
582582
from langflow.services.state import factory as state_factory
@@ -597,7 +597,11 @@ def register_all_service_factories() -> None:
597597
service_manager.register_factory(session_factory.SessionServiceFactory())
598598
service_manager.register_factory(storage_factory.StorageServiceFactory())
599599
service_manager.register_factory(variable_factory.VariableServiceFactory())
600-
service_manager.register_factory(memory_factory.MemoryServiceFactory())
600+
# Memory (Tier 2) converges on the lfx implementation: langflow selects the
601+
# lfx DatabaseMemoryService rather than subclassing. Registered as a service
602+
# class (not a factory) so the manager injects its Tier 1 database_service
603+
# dependency (Option B). This takes precedence over lfx's InMemory default.
604+
service_manager.register_service_class(ServiceType.MEMORY_SERVICE, DatabaseMemoryService, override=True)
601605
service_manager.register_factory(telemetry_factory.TelemetryServiceFactory())
602606
service_manager.register_factory(tracing_factory.TracingServiceFactory())
603607
service_manager.register_factory(transaction_factory.TransactionServiceFactory())

0 commit comments

Comments
 (0)