From 7e5e62fd349f798920de8cad5caf40965ba22b7f Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Mon, 27 Jul 2026 19:37:53 -0400 Subject: [PATCH] feat: add a File Processing model capability to skip extraction and pass raw files through --- backend/open_webui/retrieval/utils.py | 2 +- backend/open_webui/tools/builtin.py | 37 ++++++++++++++++++- backend/open_webui/utils/middleware.py | 9 ++++- src/lib/components/chat/MessageInput.svelte | 15 ++++++++ .../components/common/FileItemModal.svelte | 13 +++++-- .../workspace/Models/Capabilities.svelte | 9 ++++- src/lib/constants.ts | 1 + src/lib/i18n/locales/en-US/translation.json | 3 ++ 8 files changed, 81 insertions(+), 8 deletions(-) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 1e159b01a5..48e524e699 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -1483,7 +1483,7 @@ async def get_sources_from_items( or ('file', item.get('id')) in folder_items ): query_result = { - 'documents': [[file_object.data.get('content', '')]], + 'documents': [[(file_object.data or {}).get('content', '')]], 'metadatas': [ [ { diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index c7d36992aa..7989d1b483 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -2542,7 +2542,16 @@ async def query_chat_files( if not accessible: return JSONCodec.dumps({'error': 'No accessible files found'}) - file_items = [{**item} for item, _ in accessible] + file_items = [{**item} for item, _ in accessible if item.get('processed') is not False] + if not file_items: + return json.dumps( + { + 'error': ( + 'The attached files were uploaded without content extraction ' + '(raw attachments); their content cannot be queried as text.' + ) + } + ) rag_config = await Config.get_many( 'rag.top_k', 'rag.top_k_reranker', @@ -2791,6 +2800,19 @@ async def view_file( if file.data: content = file.data.get('content', '') + if not content and 'content' not in (file.data or {}): + return json.dumps( + { + 'id': file.id, + 'filename': file.filename, + 'processed': False, + 'note': ( + 'This file was uploaded without content extraction (raw attachment); ' + 'no text content is available.' + ), + } + ) + total_chars = len(content) # Line-based addressing (overrides char-based offset/max_chars) @@ -2931,6 +2953,19 @@ async def view_knowledge_file( if file.data: content = file.data.get('content', '') + if not content and 'content' not in (file.data or {}): + return json.dumps( + { + 'id': file.id, + 'filename': file.filename, + 'processed': False, + 'note': ( + 'This file was uploaded without content extraction (raw attachment); ' + 'no text content is available.' + ), + } + ) + total_chars = len(content) # Line-based addressing (overrides char-based offset/max_chars) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 1e322838da..6c13cc45df 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -342,6 +342,8 @@ def get_citation_source_from_tool_result( elif tool_name in ('view_knowledge_file', 'view_file'): file_data = tool_result + if 'content' not in file_data: + return [] filename = file_data.get('filename', 'Unknown File') file_id = file_data.get('id', '') knowledge_name = file_data.get('knowledge_name', '') @@ -1856,7 +1858,12 @@ async def chat_completion_files_handler( __event_emitter__ = extra_params['__event_emitter__'] sources = [] - if files := body.get('metadata', {}).get('files', None): + files = [ + item + for item in (body.get('metadata', {}).get('files', None) or []) + if item.get('processed') is not False + ] + if files: # Check if all files are in full context mode all_full_context = all(item.get('context') == 'full' for item in files) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c18160331a..f9d7f0b1c7 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -719,6 +719,7 @@ type ModelCapability = | 'vision' | 'file_upload' + | 'file_processing' | 'web_search' | 'image_generation' | 'code_interpreter' @@ -746,6 +747,13 @@ modelCapabilitiesById ); + let fileProcessingCapableModels = []; + $: fileProcessingCapableModels = getCapableModelIds( + selectedModelIds, + 'file_processing', + modelCapabilitiesById + ); + let webSearchCapableModels = []; $: webSearchCapableModels = getCapableModelIds( selectedModelIds, @@ -871,6 +879,12 @@ return null; } + const skipProcessing = + process && selectedModelIds.length > 0 && fileProcessingCapableModels.length === 0; + if (skipProcessing) { + process = false; + } + const tempItemId = uuidv4(); const fileItem = { type: 'file', @@ -886,6 +900,7 @@ // Stamp the user's default upload mode so the sent payload carries it; // the per-file toggle in FileItemModal can still override it afterwards. ...($settings?.defaultUploadContext === 'full' ? { context: 'full' } : {}), + ...(skipProcessing ? { processed: false } : {}), ...itemData }; diff --git a/src/lib/components/common/FileItemModal.svelte b/src/lib/components/common/FileItemModal.svelte index ce629acdfd..317624792d 100644 --- a/src/lib/components/common/FileItemModal.svelte +++ b/src/lib/components/common/FileItemModal.svelte @@ -198,7 +198,8 @@ }; const loadContent = async () => { - selectedTab = ''; + const hasPreview = isAudio || isPDF || isExcel || isCode || isMarkdown || isDocx || isPptx; + selectedTab = item?.processed === false && hasPreview ? 'preview' : ''; expandedContent = false; docxData = null; if (item?.type === 'collection') { @@ -344,7 +345,7 @@ {/if} - {#if edit} + {#if edit && item?.processed !== false}
{:else if selectedTab === ''} - {#if item?.file?.data} + {#if item?.processed === false} +
+ {$i18n.t( + 'This file was uploaded without content extraction (raw attachment); no text content is available.' + )} +
+ {:else if item?.file?.data} {@const rawContent = (item?.file?.data?.content ?? '').trim() || 'No content'} {@const isTruncated = ($settings?.renderMarkdownInPreviews ?? true) && diff --git a/src/lib/components/workspace/Models/Capabilities.svelte b/src/lib/components/workspace/Models/Capabilities.svelte index 04be7c3939..adfd18c769 100644 --- a/src/lib/components/workspace/Models/Capabilities.svelte +++ b/src/lib/components/workspace/Models/Capabilities.svelte @@ -17,6 +17,12 @@ label: $i18n.t('File Upload'), description: $i18n.t('Model accepts file inputs') }, + file_processing: { + label: $i18n.t('File Processing'), + description: $i18n.t( + 'Extract file content on upload. When disabled, uploads skip extraction entirely and files are passed along as raw attachments for the model or pipe to handle' + ) + }, file_context: { label: $i18n.t('File Context'), description: $i18n.t('Inject file content into conversation context') @@ -74,9 +80,8 @@ capabilities = capabilities; }; - // Hide file_context when file_upload is disabled $: visibleCapabilities = (Object.keys(capabilityLabels) as Capability[]).filter((cap) => { - if (cap === 'file_context' && !capabilities.file_upload) { + if ((cap === 'file_processing' || cap === 'file_context') && !capabilities.file_upload) { return false; } return true; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f18e3620a8..6add6a4bc7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -103,6 +103,7 @@ export const SUPPORTED_FILE_EXTENSIONS = [ export const DEFAULT_CAPABILITIES = { file_context: true, + file_processing: true, vision: true, file_upload: true, web_search: true, diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index 1e04d27cca..4e7da15954 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -1152,6 +1152,7 @@ "External Web Loader URL": "", "External Web Search API Key": "", "External Web Search URL": "", + "Extract file content on upload. When disabled, uploads skip extraction entirely and files are passed along as raw attachments for the model or pipe to handle": "", "Extract action items from this note.": "", "Extract images from PDFs so OCR can process image-only pages.": "", "Fade Effect for Streaming Text": "", @@ -1224,6 +1225,7 @@ "File content": "", "File content updated successfully.": "", "File Context": "", + "File Processing": "", "File deleted successfully.": "", "File Extensions": "", "File Mode": "", @@ -2646,6 +2648,7 @@ "This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.": "", "This chat won't appear in history and your messages will not be saved.": "", "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "", + "This file was uploaded without content extraction (raw attachment); no text content is available.": "", "This folder is empty": "", "This is a default user permission and will remain enabled.": "", "This is an experimental feature, it may not function as expected and is subject to change at any time.": "",