@@ -100,6 +100,115 @@ def chunk_text_for_ingestion(
100100 return splitter .split_text (text )
101101
102102
103+ def _coerce_backend_config_value (value : Any ) -> dict [str , Any ]:
104+ """Normalize a stored ``backend_config`` into a plain dict."""
105+ if isinstance (value , dict ):
106+ return dict (value )
107+ return {}
108+
109+
110+ async def resolve_backend_selection (
111+ * ,
112+ user_id : uuid .UUID ,
113+ kb_name : str ,
114+ kb_path : Path ,
115+ ) -> tuple [str , dict [str , Any ]]:
116+ """Resolve ``(backend_type, backend_config)`` for a KB — DB row first, sidecar second.
117+
118+ The ``knowledge_base`` row is authoritative. The on-disk sidecar is a legacy
119+ fallback for KBs predating the row (or dropped in from an export), not a
120+ default: when neither source resolves this raises rather than assuming local
121+ storage. A replica that merely lacks the sidecar would otherwise write a
122+ remote-backed KB into a local Chroma directory while queries followed the row
123+ to the configured cluster and returned nothing, with no error anywhere.
124+
125+ Shared by Knowledge Bases and Memory Bases so the two cannot drift apart.
126+ """
127+ from langflow .api .utils import knowledge_base_service
128+
129+ record = await knowledge_base_service .get_by_user_and_name (user_id , kb_name )
130+ if record is not None :
131+ return (
132+ record .backend_type or BackendType .CHROMA .value ,
133+ _coerce_backend_config_value (record .backend_config ),
134+ )
135+
136+ # A sidecar with no ``backend_type`` is an explicit legacy local KB; only the
137+ # *absence* of both sources is ambiguous.
138+ metadata_file = kb_path / "embedding_metadata.json"
139+ if await asyncio .to_thread (metadata_file .exists ):
140+ metadata = KBAnalysisHelper .get_metadata (kb_path , fast = True )
141+ return (
142+ str (metadata .get ("backend_type" ) or BackendType .CHROMA .value ),
143+ _coerce_backend_config_value (metadata .get ("backend_config" )),
144+ )
145+
146+ msg = (
147+ f"Cannot determine the vector-store backend for '{ kb_name } ': it has no "
148+ f"knowledge_base record and no embedding metadata on disk. Refusing to fall "
149+ f"back to local storage, which would write to a different store than queries "
150+ f"read from."
151+ )
152+ raise ValueError (msg )
153+
154+
155+ # Default embedding used when neither the DB row nor the sidecar records one.
156+ # Matches the historical fallback in ``resolve_embedding`` so behavior is
157+ # unchanged for callers that relied on it.
158+ _DEFAULT_EMBEDDING_PROVIDER = "OpenAI"
159+ _DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small"
160+
161+
162+ async def resolve_embedding_selection (
163+ * ,
164+ user_id : uuid .UUID ,
165+ kb_name : str ,
166+ kb_path : Path | None = None ,
167+ ) -> tuple [str , str ]:
168+ """Resolve ``(embedding_provider, embedding_model)`` for a KB — DB row first, sidecar second.
169+
170+ Counterpart to :func:`resolve_backend_selection`: the ``knowledge_base`` row's
171+ ``model_selection`` is authoritative, the on-disk ``embedding_metadata.json``
172+ sidecar is a legacy fallback (KBs predating the row / shared with file-based
173+ KBs), and a hardcoded default is the last resort.
174+
175+ Memory Bases always have a row (created at MB-create and backfilled for
176+ pre-existing ones), so they resolve entirely from the DB and never touch the
177+ sidecar — which is what lets a Memory Base work on a replica whose local disk
178+ never held the KB directory. ``kb_path`` is therefore optional: pass ``None``
179+ from a fully DB-driven path and the sidecar branch is skipped entirely (no
180+ dependency on ``knowledge_bases_dir`` or the local filesystem). The sidecar
181+ branch stays for Knowledge Bases, which still write it.
182+
183+ Shared by Knowledge Bases and Memory Bases so the two cannot drift apart.
184+ """
185+ from langflow .api .utils import knowledge_base_service
186+ from langflow .api .utils .knowledge_base_service import get_embedding_model , get_embedding_provider
187+
188+ record = await knowledge_base_service .get_by_user_and_name (user_id , kb_name )
189+ if record is not None :
190+ model = get_embedding_model (record .model_selection )
191+ if model :
192+ return get_embedding_provider (record .model_selection ), model
193+
194+ # Legacy fallback: read the sidecar. Only attempted when a ``kb_path`` is
195+ # supplied and there is no row (or a row with no model recorded) — i.e. never
196+ # for a properly provisioned Memory Base.
197+ if kb_path is not None :
198+ metadata_file = kb_path / "embedding_metadata.json"
199+ if await asyncio .to_thread (metadata_file .exists ):
200+ metadata = KBAnalysisHelper .get_metadata (kb_path , fast = True )
201+ provider = str (metadata .get ("embedding_provider" ) or "" ).strip ()
202+ model = str (metadata .get ("embedding_model" ) or "" ).strip ()
203+ if model and model .lower () != "unknown" :
204+ return (
205+ provider if provider and provider .lower () != "unknown" else _DEFAULT_EMBEDDING_PROVIDER ,
206+ model ,
207+ )
208+
209+ return _DEFAULT_EMBEDDING_PROVIDER , _DEFAULT_EMBEDDING_MODEL
210+
211+
103212class KBStorageHelper :
104213 """Helper class for Knowledge Base storage and path management."""
105214
@@ -981,62 +1090,6 @@ async def cleanup_chroma_chunks_by_job(
9811090 finally :
9821091 await backend .teardown ()
9831092
984- @staticmethod
985- async def write_documents_to_chroma (
986- * ,
987- documents : list [Document ],
988- chroma : Chroma ,
989- task_job_id : uuid .UUID ,
990- job_service : JobService ,
991- ) -> int :
992- """Write pre-built Documents into an open Chroma collection.
993-
994- This is the shared primitive used by both file-based KB ingestion
995- (``perform_ingestion``) and message-based Memory Base ingestion.
996-
997- Documents must already be chunked and have their metadata populated
998- by the caller — this method only handles the batched write, cancellation
999- checking, and retry logic.
1000-
1001- Args:
1002- documents: LangChain Document objects ready for embedding.
1003- chroma: An already-constructed ``Chroma`` instance pointing at the
1004- target collection.
1005- task_job_id: Job ID used to poll for cancellation.
1006- job_service: Service for checking job status.
1007-
1008- Returns:
1009- Number of documents successfully written. If the job is cancelled
1010- mid-batch this will be less than ``len(documents)``.
1011-
1012- Raises:
1013- Exception: Re-raises any non-cancellation write failure after the
1014- retry budget is exhausted.
1015- """
1016- written = 0
1017- for i in range (0 , len (documents ), INGESTION_BATCH_SIZE ):
1018- if await KBIngestionHelper .is_job_cancelled (job_service , task_job_id ):
1019- return written
1020-
1021- batch = documents [i : i + INGESTION_BATCH_SIZE ]
1022- for attempt in range (MAX_RETRY_ATTEMPTS ):
1023- if await KBIngestionHelper .is_job_cancelled (job_service , task_job_id ):
1024- return written
1025- try :
1026- await chroma .aadd_documents (batch )
1027- break
1028- except Exception as e :
1029- if attempt == MAX_RETRY_ATTEMPTS - 1 :
1030- raise
1031- wait = (attempt + 1 ) * EXPONENTIAL_BACKOFF_MULTIPLIER
1032- await logger .awarning ("Write failed, retrying in %ds: %s" , wait , e )
1033- await asyncio .sleep (wait )
1034-
1035- written += len (batch )
1036- await asyncio .sleep (0.01 )
1037-
1038- return written
1039-
10401093 @staticmethod
10411094 async def write_documents_to_backend (
10421095 * ,
@@ -1047,12 +1100,12 @@ async def write_documents_to_backend(
10471100 ) -> int :
10481101 """Write pre-built Documents through a ``BaseVectorStoreBackend``.
10491102
1050- Backend-agnostic counterpart to :meth:`write_documents_to_chroma`.
1051- Used by the multi-backend KB ingestion path so Mongo/Astra/
1052- Postgres/OpenSearch ingestions share the same batching,
1053- cancellation-checking, and exponential-backoff retry logic that
1054- Memory Base's Chroma path gets from
1055- :meth:`write_documents_to_chroma` .
1103+ The single write primitive for every ingestion path — file-based
1104+ Knowledge Bases and message-based Memory Bases alike — so batching,
1105+ cancellation checking, and exponential-backoff retry behave identically
1106+ whatever backend the KB is on. A Chroma-only counterpart used to exist
1107+ for Memory Bases; it was removed so the local-only path cannot be
1108+ reintroduced by accident .
10561109
10571110 Documents must already be chunked with metadata populated.
10581111
0 commit comments