Skip to content

Commit b91aeaf

Browse files
committed
Removed sidecar dependency and disk links for KBs and MBs.
1 parent 12abc7e commit b91aeaf

47 files changed

Lines changed: 1845 additions & 2696 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/backend/base/langflow/__main__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,39 @@ async def _migrate_mcp(*, dry_run: bool) -> None:
10231023
)
10241024

10251025

1026+
@app.command(name="reconcile-kb-from-disk")
1027+
def reconcile_kb_from_disk(
1028+
log_level: str = typer.Option("info", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
1029+
username: str = typer.Option("", help="Only reconcile this user's knowledge bases."),
1030+
dry_run: bool = typer.Option(default=False, help="Report what would be adopted without writing."), # noqa: FBT001
1031+
) -> None:
1032+
"""Adopt knowledge base directories on disk that have no database row.
1033+
1034+
The ``knowledge_base`` table is the sole authority for KB metadata, so this scan
1035+
no longer runs on every boot. Use it to recover KBs created by a Langflow version
1036+
that still wrote the on-disk ``embedding_metadata.json`` sidecar and that are not
1037+
showing up after an upgrade.
1038+
1039+
Idempotent, and it never deletes anything: directories that already have a row,
1040+
that carry the ``.kb_deleted`` marker, or whose metadata is unreadable are skipped.
1041+
"""
1042+
configure(log_level=log_level)
1043+
asyncio.run(_reconcile_kb_from_disk(username=username or None, dry_run=dry_run))
1044+
1045+
1046+
async def _reconcile_kb_from_disk(*, username: str | None, dry_run: bool) -> None:
1047+
from langflow.api.utils import knowledge_base_service
1048+
1049+
await initialize_services()
1050+
inserted = await knowledge_base_service.backfill_all_users_from_disk(
1051+
username=username,
1052+
dry_run=dry_run,
1053+
)
1054+
scope = f"user '{username}'" if username else "all users"
1055+
verb = "would adopt" if dry_run else "adopted"
1056+
typer.echo(f"Knowledge base reconciliation complete: {verb} {inserted} knowledge base(s) for {scope}.")
1057+
1058+
10261059
# command to copy the langflow database from the cache to the current directory
10271060
# because now the database is stored per installation
10281061
@app.command()

src/backend/base/langflow/api/utils/kb_helpers.py

Lines changed: 148 additions & 267 deletions
Large diffs are not rendered by default.

src/backend/base/langflow/api/utils/knowledge_base_service.py

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,27 @@ async def list_owned_or_visible(
231231
return list(result.all())
232232

233233

234-
async def backfill_all_users_from_disk(*, kb_root: Path | None = None) -> int:
235-
"""Backfill missing KB rows for every existing user.
234+
async def backfill_all_users_from_disk(
235+
*,
236+
kb_root: Path | None = None,
237+
username: str | None = None,
238+
dry_run: bool = False,
239+
) -> int:
240+
"""Adopt KB directories that have no ``knowledge_base`` row.
241+
242+
Legacy-recovery only. The ``knowledge_base`` row is the sole authority for KB
243+
metadata and nothing in the steady-state code path reads the on-disk
244+
``embedding_metadata.json`` sidecar any more, so this exists purely to adopt
245+
directories written by a Langflow version that predates that change. It is
246+
opt-in at startup (``LANGFLOW_KB_DISK_RECONCILE_ENABLED``) and available on
247+
demand as ``langflow reconcile-kb-from-disk``.
236248
237-
Runs during application startup so list/detail endpoints can stay
238-
read-only. Returns the total number of inserted rows across all
239-
users and never raises for per-user failures.
249+
``username`` narrows the scan to a single account — the usual shape of a
250+
real recovery ("this user's KBs vanished after the upgrade"). ``dry_run``
251+
reports what would be adopted without writing.
252+
253+
Returns the number of rows inserted (or that would be inserted, under
254+
``dry_run``). Never raises for per-user failures.
240255
"""
241256
from langflow.api.utils.kb_helpers import KBStorageHelper
242257
from langflow.services.database.models.user.model import User
@@ -246,18 +261,25 @@ async def backfill_all_users_from_disk(*, kb_root: Path | None = None) -> int:
246261
return 0
247262

248263
async with session_scope() as session:
249-
users = list((await session.exec(select(User))).all())
264+
stmt = select(User)
265+
if username:
266+
stmt = stmt.where(User.username == username)
267+
users = list((await session.exec(stmt)).all())
250268

251269
inserted = 0
252270
for user in users:
253271
kb_user_root = effective_root / user.username
254272
if not kb_user_root.exists():
255273
continue
256274
try:
257-
inserted += await backfill_from_disk(user_id=user.id, kb_user_root=kb_user_root)
275+
inserted += await backfill_from_disk(
276+
user_id=user.id,
277+
kb_user_root=kb_user_root,
278+
dry_run=dry_run,
279+
)
258280
except Exception as exc: # noqa: BLE001
259281
await logger.awarning(
260-
"knowledge-base startup reconciliation failed for user %s: %s",
282+
"knowledge-base disk reconciliation failed for user %s: %s",
261283
user.username,
262284
exc,
263285
)
@@ -440,9 +462,8 @@ async def read_metadata(
440462
def record_to_metadata_dict(record: KnowledgeBaseRecord) -> dict[str, Any]:
441463
"""Serialize a row into the legacy JSON-file shape.
442464
443-
Matches the keys ``KBAnalysisHelper.get_metadata`` and the API
444-
routes expect so a DB-first migration doesn't need a parallel
445-
consumer refactor.
465+
Matches the key set the API routes and the frontend expect, so the
466+
row can back the response shape without a parallel consumer refactor.
446467
"""
447468
status = record.status
448469
if status == KnowledgeBaseStatus.READY.value and record.chunks <= 0:
@@ -497,13 +518,15 @@ async def backfill_from_disk(
497518
*,
498519
user_id: UUID,
499520
kb_user_root: Path,
521+
dry_run: bool = False,
500522
) -> int:
501523
"""Create missing ``knowledge_base`` rows for existing KB directories.
502524
503-
Called on first boot after the Phase 1.5 migration lands so every
504-
pre-existing KB gains a row. Also serves as an idempotent
505-
fallback: if a user drops an exported KB directory on disk, this
506-
upserts the corresponding row on next access.
525+
Legacy-recovery only — see :func:`backfill_all_users_from_disk`. This is the
526+
one remaining reader of the on-disk ``embedding_metadata.json`` sidecar, and
527+
it runs only when an operator asks for it.
528+
529+
``dry_run`` counts what would be adopted without writing any rows.
507530
508531
Returns the number of rows inserted. Never raises — failures are
509532
logged and skipped so one malformed KB directory doesn't block the
@@ -538,9 +561,6 @@ async def backfill_from_disk(
538561
continue
539562

540563
try:
541-
from langflow.api.utils.kb_helpers import KBAnalysisHelper
542-
543-
metadata = KBAnalysisHelper.get_metadata(kb_dir, fast=False) or metadata
544564
model_selection = _normalize_model_selection(metadata.get("model_selection"))
545565
record_id = _coerce_uuid(metadata.get("id")) or uuid4()
546566
# ``backend_type``/``backend_config`` are persisted by
@@ -569,23 +589,24 @@ async def backfill_from_disk(
569589
"provider": provider_raw,
570590
}
571591

572-
await create_record(
573-
user_id=user_id,
574-
name=name,
575-
model_selection=normalized_selection,
576-
chunk_size=int(metadata.get("chunk_size") or 1000),
577-
chunk_overlap=int(metadata.get("chunk_overlap") or 200),
578-
separator=metadata.get("separator"),
579-
column_config=metadata.get("column_config") or [],
580-
backend_type=backend_type,
581-
backend_config=backend_config,
582-
chunks=_coerce_int(metadata.get("chunks"), default=0),
583-
words=_coerce_int(metadata.get("words"), default=0),
584-
characters=_coerce_int(metadata.get("characters"), default=0),
585-
size_bytes=_coerce_int(metadata.get("size_bytes", metadata.get("size")), default=0),
586-
source_types=_coerce_source_types(metadata.get("source_types")),
587-
record_id=record_id,
588-
)
592+
if not dry_run:
593+
await create_record(
594+
user_id=user_id,
595+
name=name,
596+
model_selection=normalized_selection,
597+
chunk_size=int(metadata.get("chunk_size") or 1000),
598+
chunk_overlap=int(metadata.get("chunk_overlap") or 200),
599+
separator=metadata.get("separator"),
600+
column_config=metadata.get("column_config") or [],
601+
backend_type=backend_type,
602+
backend_config=backend_config,
603+
chunks=_coerce_int(metadata.get("chunks"), default=0),
604+
words=_coerce_int(metadata.get("words"), default=0),
605+
characters=_coerce_int(metadata.get("characters"), default=0),
606+
size_bytes=_coerce_int(metadata.get("size_bytes", metadata.get("size")), default=0),
607+
source_types=_coerce_source_types(metadata.get("source_types")),
608+
record_id=record_id,
609+
)
589610
inserted += 1
590611
except Exception as exc: # noqa: BLE001
591612
await logger.aerror("backfill: failed to upsert KB %s/%s: %s", user_id, name, exc)

0 commit comments

Comments
 (0)