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}