diff --git a/CHANGELOG.md b/CHANGELOG.md index b78fd6b99b..0b7755d7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - π **Brotli dependency update.** Brotli has been updated to address CVE-2025-6176. - π₯οΈ **Windows startup script.** The Windows startup batch script has been updated for improved compatibility. - ## [0.9.1] - 2026-04-21 ### Fixed diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py index 4592aa6cb8..c9e4f318e1 100644 --- a/backend/open_webui/internal/db.py +++ b/backend/open_webui/internal/db.py @@ -57,9 +57,7 @@ def _pop_first(params: dict[str, list[str]], key: str) -> str | None: def _is_postgres_url(url: str) -> bool: """Return True if *url* looks like a PostgreSQL connection string.""" - return bool(url) and any( - url.startswith(p) for p in ('postgresql://', 'postgresql+', 'postgres://') - ) + return bool(url) and any(url.startswith(p) for p in ('postgresql://', 'postgresql+', 'postgres://')) def extract_ssl_params_from_url(url: str) -> tuple[str, dict[str, str]]: @@ -180,9 +178,7 @@ if ENABLE_DB_MIGRATIONS: _url_without_ssl, _ssl_dict = extract_ssl_params_from_url(DATABASE_URL) # For psycopg2 (sync engine), re-append sslmode + cert-file params. -SQLALCHEMY_DATABASE_URL = ( - reattach_ssl_params_to_url(_url_without_ssl, _ssl_dict) if _ssl_dict else DATABASE_URL -) +SQLALCHEMY_DATABASE_URL = reattach_ssl_params_to_url(_url_without_ssl, _ssl_dict) if _ssl_dict else DATABASE_URL def _make_async_url(url: str) -> str: diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 9bc6b5177d..af570af0af 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1869,6 +1869,7 @@ async def chat_completion( except asyncio.CancelledError: log.info('Chat processing was cancelled') try: + async def emit_cancel_event(): event_emitter = await get_event_emitter(metadata) if event_emitter: @@ -1940,6 +1941,7 @@ async def chat_completion( try: if metadata.get('chat_id'): + async def emit_inactive_event(): try: event_emitter = await get_event_emitter(metadata, update_db=False) @@ -1947,7 +1949,7 @@ async def chat_completion( await event_emitter({'type': 'chat:active', 'data': {'active': False}}) except Exception: pass - + try: # Shield the event emission so it finishes even if the main task is cancelled await asyncio.shield(emit_inactive_event()) diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index db9459e028..79c13153ac 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -391,15 +391,11 @@ class ModelsTable: return ModelListResponse(items=models, total=total) - async def get_model_meta_by_id( - self, id: str, db: Optional[AsyncSession] = None - ) -> Optional[tuple[dict, int]]: + async def get_model_meta_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[tuple[dict, int]]: """Return (meta, updated_at) for a model, skipping access grant resolution.""" try: async with get_async_db_context(db) as db: - result = await db.execute( - select(Model.meta, Model.updated_at).filter_by(id=id) - ) + result = await db.execute(select(Model.meta, Model.updated_at).filter_by(id=id)) return result.first() except Exception: return None diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py index fce18ae586..c43567f670 100644 --- a/backend/open_webui/models/oauth_sessions.py +++ b/backend/open_webui/models/oauth_sessions.py @@ -326,9 +326,7 @@ class OAuthSessionTable: """Delete all OAuth sessions for a specific user and provider""" try: async with get_async_db_context(db) as db: - result = await db.execute( - delete(OAuthSession).filter_by(user_id=user_id, provider=provider) - ) + result = await db.execute(delete(OAuthSession).filter_by(user_id=user_id, provider=provider)) await db.commit() return result.rowcount > 0 except Exception as e: diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index 7a115ca6d7..2daa641bf2 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -400,10 +400,7 @@ class Loader: api_key=self.kwargs.get('MISTRAL_OCR_API_KEY'), file_path=file_path, ) - elif ( - self.engine == 'paddleocr_vl' - and self.kwargs.get('PADDLEOCR_VL_TOKEN') != '' - ): + elif self.engine == 'paddleocr_vl' and self.kwargs.get('PADDLEOCR_VL_TOKEN') != '': loader = PaddleOCRVLLoader( api_url=self.kwargs.get('PADDLEOCR_VL_BASE_URL'), token=self.kwargs.get('PADDLEOCR_VL_TOKEN'), diff --git a/backend/open_webui/retrieval/loaders/paddleocr_vl.py b/backend/open_webui/retrieval/loaders/paddleocr_vl.py index ab7632b3f8..b89369b2a4 100644 --- a/backend/open_webui/retrieval/loaders/paddleocr_vl.py +++ b/backend/open_webui/retrieval/loaders/paddleocr_vl.py @@ -11,6 +11,7 @@ from open_webui.env import GLOBAL_LOG_LEVEL logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) + class PaddleOCRVLLoader: """Loader that uses PaddleOCR-vl API to extract text from PDF/images.""" @@ -21,9 +22,9 @@ class PaddleOCRVLLoader: file_path: str, ): if not api_url or not token: - raise ValueError("PaddleOCR-vl API URL and Token are required.") + raise ValueError('PaddleOCR-vl API URL and Token are required.') if not os.path.exists(file_path): - raise FileNotFoundError(f"File not found at {file_path}") + raise FileNotFoundError(f'File not found at {file_path}') self.api_url = api_url.rstrip('/') self.token = token @@ -31,20 +32,17 @@ class PaddleOCRVLLoader: self.file_name = os.path.basename(file_path) def load(self) -> List[Document]: - log.info(f"Processing with PaddleOCR-vl: {self.file_path}") + log.info(f'Processing with PaddleOCR-vl: {self.file_path}') try: - with open(self.file_path, "rb") as file: + with open(self.file_path, 'rb') as file: file_bytes = file.read() - file_data = base64.b64encode(file_bytes).decode("ascii") + file_data = base64.b64encode(file_bytes).decode('ascii') except Exception as e: - log.error(f"Failed to read file {self.file_path}: {e}") + log.error(f'Failed to read file {self.file_path}: {e}') raise - headers = { - "Authorization": f"token {self.token}", - "Content-Type": "application/json" - } + headers = {'Authorization': f'token {self.token}', 'Content-Type': 'application/json'} # Detect fileType based on file extension ext = self.file_path.lower().split('.')[-1] @@ -52,76 +50,76 @@ class PaddleOCRVLLoader: file_type = 1 if ext in image_extensions else 0 payload = { - "file": file_data, - "fileType": file_type, - "useDocOrientationClassify": False, - "useDocUnwarping": False, - "useChartRecognition": False, + 'file': file_data, + 'fileType': file_type, + 'useDocOrientationClassify': False, + 'useDocUnwarping': False, + 'useChartRecognition': False, } try: - response = requests.post(f"{self.api_url}/layout-parsing", json=payload, headers=headers) + response = requests.post(f'{self.api_url}/layout-parsing', json=payload, headers=headers) response.raise_for_status() - - result = response.json().get("result", {}) - layout_results = result.get("layoutParsingResults", []) - + + result = response.json().get('result', {}) + layout_results = result.get('layoutParsingResults', []) + documents = [] total_pages = len(layout_results) skipped_pages = 0 - + for i, res in enumerate(layout_results): - markdown_text = res.get("markdown", {}).get("text", "") - + markdown_text = res.get('markdown', {}).get('text', '') + if isinstance(markdown_text, str): cleaned_content = markdown_text.strip() else: cleaned_content = str(markdown_text).strip() - + if not cleaned_content: skipped_pages += 1 continue - + documents.append( Document( page_content=cleaned_content, metadata={ - "page": i, - "page_label": i + 1, - "total_pages": total_pages, - "file_name": self.file_name, - "processing_engine": "paddleocr-vl" - } + 'page': i, + 'page_label': i + 1, + 'total_pages': total_pages, + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, ) ) - + if skipped_pages > 0: - log.info(f"PaddleOCR-vl: Processed {len(documents)} pages, skipped {skipped_pages} empty pages.") - + log.info(f'PaddleOCR-vl: Processed {len(documents)} pages, skipped {skipped_pages} empty pages.') + if not documents: - log.warning("No valid text content found by PaddleOCR-vl.") + log.warning('No valid text content found by PaddleOCR-vl.') return [ Document( - page_content="No valid text content found in document", + page_content='No valid text content found in document', metadata={ - "error": "no_valid_pages", - "file_name": self.file_name, - "processing_engine": "paddleocr-vl" - } + 'error': 'no_valid_pages', + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, ) ] - + return documents - + except Exception as e: - log.error(f"Error calling PaddleOCR-vl: {e}") + log.error(f'Error calling PaddleOCR-vl: {e}') return [ Document( - page_content=f"Error during OCR processing: {e}", + page_content=f'Error during OCR processing: {e}', metadata={ - "error": "processing_failed", - "file_name": self.file_name, - "processing_engine": "paddleocr-vl" - } + 'error': 'processing_failed', + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, ) ] diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 7cb6ca3681..6d2349f89f 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -877,9 +877,7 @@ async def delete_oauth_session_by_provider( The provider string matches the 'provider' field in the oauth_session table (e.g. 'mcp:server-id' for MCP connections). """ - result = await OAuthSessions.delete_sessions_by_user_id_and_provider( - user.id, provider, db=db - ) + result = await OAuthSessions.delete_sessions_by_user_id_and_provider(user.id, provider, db=db) if not result: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index af5e795511..04d845c3de 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -917,4 +917,3 @@ async def update_tools_user_valves_by_id( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND, ) - diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 5b945749ec..43ff4fe2e7 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -335,7 +335,9 @@ async def get_tool_module_from_cache(request, tool_id, load_from_db=True): return tool_module, frontmatter -async def get_function_module_from_cache(request, function_id, function: FunctionModel | None = None, load_from_db=True): +async def get_function_module_from_cache( + request, function_id, function: FunctionModel | None = None, load_from_db=True +): if load_from_db: # Always load from the database by default # This is useful for hooks like "inlet" or "outlet" where the content might change diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index b0dd6541ee..6b7bf6f47b 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -647,4 +647,3 @@ export const setBanners = async (token: string, banners: Banner[]) => { return res; }; - diff --git a/src/lib/apis/tools/index.ts b/src/lib/apis/tools/index.ts index 1d812b3f0f..5d26e50fee 100644 --- a/src/lib/apis/tools/index.ts +++ b/src/lib/apis/tools/index.ts @@ -483,4 +483,3 @@ export const updateUserValvesById = async (token: string, id: string, valves: ob return res; }; - diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index 13044c09d5..91b63338de 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -550,4 +550,3 @@ export const getUserGroupsById = async (token: string, userId: string) => { return res; }; - diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index 6416b2d05a..e173c74969 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -1369,12 +1369,13 @@ )} /> - - {#if RAGConfig.RAG_TEMPLATE && ((RAGConfig.RAG_TEMPLATE.match(/\[context\]/g) || []).length + (RAGConfig.RAG_TEMPLATE.match(/\{\{CONTEXT\}\}/g) || []).length) > 1} + {#if RAGConfig.RAG_TEMPLATE && (RAGConfig.RAG_TEMPLATE.match(/\[context\]/g) || []).length + (RAGConfig.RAG_TEMPLATE.match(/\{\{CONTEXT\}\}/g) || []).length > 1}