mirror of
https://github.com/open-webui/open-webui.git
synced 2026-05-06 19:08:59 -05:00
[PR #22645] [CLOSED] fix+feat: Knowledge file processing — async Docling, live status UI, error visibility, delete-race guard #65650
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/22645
Author: @jannefleischer
Created: 3/13/2026
Status: ❌ Closed
Base:
dev← Head:feat-knowledgestatus📝 Commits (1)
8ef7bd5feat+fix: Fixed handling of long-running task when adding knowledge-files; added a tooltip to the spinner with statusses, where available (currently: only docling-serve)📊 Changes
8 files changed (+333 additions, -34 deletions)
View changed files
📝
backend/open_webui/config.py(+6 -0)📝
backend/open_webui/main.py(+2 -0)📝
backend/open_webui/retrieval/loaders/main.py(+111 -17)📝
backend/open_webui/retrieval/utils.py(+7 -2)📝
backend/open_webui/routers/retrieval.py(+61 -0)📝
src/lib/apis/files/index.ts(+6 -1)📝
src/lib/components/workspace/Knowledge/KnowledgeBase.svelte(+60 -8)📝
src/lib/components/workspace/Knowledge/KnowledgeBase/Files.svelte(+80 -6)📄 Description
Summary
This PR bundles several tightly coupled improvements to the knowledge-base file pipeline. They share backend infrastructure and a common motivation: the UI had no awareness of what happened after upload, and two separate race conditions could cause files to silently disappear.
Addresses #22571 and #22573; several related issues discovered along the way are also fixed.
Issues addressed
#22571 — Silent file drop when Ollama model TTL expires during Docling processing (
IndexError)When Ollama unloads its embedding model (default TTL: 5 min) while a large file is being processed by Docling, all parallel embedding batch requests receive
503 Service Unavailable. The response body was unexpected, causing anIndexError: list index out of rangethat crashedprocess_file()silently — the file disappeared from the UI with no error shown.Fix in
retrieval/utils.py: The batch-aggregation loop that previously silently skipped non-list results now raises a descriptiveValueError(including response type and a hint to check the log). This ensures the exception propagates toprocess_file(), which marks the file asfailedwith the actual error detail, making the failure visible in the UI (see UI changes below).#22573 — Files not linked to knowledge collection when dropped simultaneously (
FILE_NOT_PROCESSEDrace condition)When multiple files were drag-dropped simultaneously with a slow extraction backend (Docling, Marker, MinerU), the frontend called
POST /knowledge/{id}/file/addimmediately after the upload returned — before Docling had actually finished. The backend correctly rejected the call with400 FILE_NOT_PROCESSED, the error was silently swallowed, and the file's embeddings (written later by the background task) were permanently orphaned with noknowledge_filerow.Root cause: The old
DoclingLoaderused the synchronous/v1/convert/fileendpoint. With large queues, nginx would drop the connection after ~120 s with a gateway timeout, the SSE stream closed early, and the frontend calledfile/addon an unprocessed file.Fix in
loaders/main.py:DoclingLoadernow uses the async/v1/convert/file/asyncendpoint.process_file()polls/v1/status/poll/{task_id}until the job completes (orDOCLING_SERVE_TIMEOUTis exceeded). The SSE stream fromGET /files/{id}/process/statusstays open for the entire polling loop, souploadFile()on the frontend only resolves — andaddFileHandleris only called — once the file is fully processed. The race window is closed.Changes by component
backend/open_webui/retrieval/loaders/main.py— DoclingLoader async rewritePOST /v1/convert/file(synchronous, drops on gateway timeout) toPOST /v1/convert/file/async+GET /v1/status/poll/{task_id}+GET /v1/result/{task_id}.?wait=ensures we never poll faster than once per 30 s.timeoutparameter: optional total seconds to wait before raising. Maps to the newDOCLING_SERVE_TIMEOUTconfig.status_callbackparameter: optionalcallable(dict)invoked after submit (withtask_id+ initialtask_position) and after each poll (with updatedtask_position). Used byprocess_file()to persist queue state intofile.datafor the UI tooltip.backend/open_webui/config.py/main.py/routers/retrieval.py—DOCLING_SERVE_TIMEOUTPersistentConfigentryDOCLING_SERVE_TIMEOUT: integer seconds, defaultNone(wait forever). Set via env var or Admin UI → Documents → Docling.GET /retrieval/configandPOST /retrieval/config/updateso it is readable/writable from the Admin Settings panel.backend/open_webui/routers/retrieval.py—process_fileenhancementsProcessing status + start time: Immediately before
loader.load(), sets{"status": "processing", "started_at": <unix timestamp>}infile.data. Applies to all extraction engines — not just Docling.Docling status callback: A closure
_docling_status_callback(data)opens a fresh DB session and mergesdataintofile.data. Passed toLoader()asDOCLING_STATUS_CALLBACKkwarg, forwarded byLoader._get_loader()toDoclingLoader. Other engines receiveNoneand are unaffected.Delete-race guard: After
save_docs_to_vector_db()returnsTruebut before writingstatus: completed, checks via a fresh DB session whether the file row still exists. If the user deleted the file while Docling/embedding was running (a window that is now unbounded with the async polling approach):form_data.collection_nameset): callsVECTOR_DB_CLIENT.delete(filter={"file_id": ...})to remove only this file's chunks.VECTOR_DB_CLIENT.delete_collection(...).{"status": False, "reason": "file_deleted_during_processing"}.A fresh session is used so the long-since-committed outer session's read cache doesn't mask the deletion.
backend/open_webui/retrieval/utils.py— batch embedding error propagationThe batch-results aggregation loop now raises
ValueErrorinstead of silently skipping batches that returned a non-list value. Error message includes the actual return type and a pointer to the log lines above for the root cause.src/lib/apis/files/index.ts—uploadFileprogress callbackAdded optional
onProgress?: (data: { status: string; error?: string }) => voidparameter. Called for every SSE event while the file is being processed, forwarding the server-sidefile.datapatch to the caller.src/lib/components/workspace/Knowledge/KnowledgeBase.svelte— polling, silent refresh, live progress$:block monitorsfileItemsand manages a 15 ssetInterval(pollingInterval). Starts when any file hasstatus: pending | processing | uploading. Stops when none remain. Cleared inonDestroyto prevent memory leaks.getItemsPagegains asilentflag. Whentrue(used by the polling interval), the current list is preserved during the fetch so files don't flash away every 15 s.uploadFileHandlerfor local files, the URL processor) now pass anonProgresscallback touploadFile. The callback patchesitem.datainfileItemsdirectly, so the tooltip reflectsprocessing,task_id, andtask_positionin real time — before the polling interval even fires.knowledgeId !== nullto the reactive debounce block so the initial evaluation (beforeonMountsetsknowledgeId) cannot schedule a non-silent refresh that blanks the list 300 ms after first load.{#key}: Changed{#key selectedFile.id}to{#key selectedFile?.id}to prevent a JS error whenselectedFileisnull.src/lib/components/workspace/Knowledge/KnowledgeBase/Files.svelte— visual status statesPer-row reactive constants:
isInFlightfile.status === 'uploading'ORfile.data.status∈{pending, processing}isFailedfile.data.status === 'failed'statusTooltipgetStatusTooltip(file)Visual states:
uploading / pending / processing<Spinner>failed<DocumentPage>text-red-500 dark:text-red-400completed<DocumentPage>HTML tooltip (in-flight and failed files, tippy.js + DOMPurify):
data.started_at, or "Uploaded: …" fromcreated_atfor pending filesdata.task_position(Docling only)data.task_id(Docling only; useful for cross-referencing docling-serve logs)data.error(failed files only)Click guard: In-flight files and placeholder items without a DB
idreturn early on click, preventing attempts to open an unprocessed file.Structured debug logging:
console.logon file click now emits a structured object withid,name,status,started_atas ISO string,task_id,task_position, anderror.Files changed
backend/open_webui/config.pyDOCLING_SERVE_TIMEOUTPersistentConfigbackend/open_webui/main.pyDOCLING_SERVE_TIMEOUTonapp.statebackend/open_webui/retrieval/loaders/main.pytimeout+status_callbackparamsbackend/open_webui/retrieval/utils.pybackend/open_webui/routers/retrieval.pyprocess_file: status/started_at, callback, delete-race guard; exposeDOCLING_SERVE_TIMEOUTin config APIsrc/lib/apis/files/index.tsonProgresscallback onuploadFilesrc/lib/components/workspace/Knowledge/KnowledgeBase.svelte{#key}src/lib/components/workspace/Knowledge/KnowledgeBase/Files.svelteWhat is NOT changed
datafield ofFileModelResponse.started_atautomatically but receive no callback.$i18n.t()in a follow-up if desired).remove_file_from_knowledge_by_id) is unchanged.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.