diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index 70a19c64a6..8a2e3438a6 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -4,6 +4,7 @@ on: push: branches: - main # or whatever branch you want to use + - pypi-release jobs: release: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a39cf2df9..fa00588a22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.17] - 2024-09-04 + +### Added + +- **🔄 Import/Export Configuration**: Users can now import and export webui configurations from admin settings > Database, simplifying setup replication across systems. +- **🌍 Web Search via URL Parameter**: Added support for activating web search directly through URL by setting 'web-search=true'. +- **🌐 SearchApi Integration**: Added support for SearchApi as an alternative web search provider, enhancing search capabilities within the platform. +- **🔍 Literal Type Support in Tools**: Tools now support the Literal type. +- **🌍 Updated Translations**: Improved translations for Chinese, Ukrainian, and Catalan. + +### Fixed + +- **🔧 Pip Install Issue**: Resolved the issue where pip install failed due to missing 'alembic.ini', ensuring smoother installation processes. +- **🌃 Automatic Theme Update**: Fixed an issue where the color theme did not update dynamically with system changes. +- **🛠️ User Agent in ComfyUI**: Added default headers in ComfyUI to fix access issues, improving reliability in network communications. +- **🔄 Missing Chat Completion Response Headers**: Ensured proper return of proxied response headers during chat completion, improving API reliability. +- **🔗 Websocket Connection Prioritization**: Modified socket.io configuration to prefer websockets and more reliably fallback to polling, enhancing connection stability. +- **🎭 Accessibility Enhancements**: Added missing ARIA labels for buttons, improving accessibility for visually impaired users. +- **⚖️ Advanced Parameter**: Fixed an issue ensuring that advanced parameters are correctly applied in all scenarios, ensuring consistent behavior of user-defined settings. + +### Changed + +- **🔁 Namespace Reorganization**: Reorganized all Python files under the 'open_webui' namespace to streamline the project structure and improve maintainability. Tools and functions importing from 'utils' should now use 'open_webui.utils'. +- **🚧 Dependency Updates**: Updated several backend dependencies like 'aiohttp', 'authlib', 'duckduckgo-search', 'flask-cors', and 'langchain' to their latest versions, enhancing performance and security. + ## [0.3.16] - 2024-08-27 ### Added diff --git a/README.md b/README.md index f3cfe0d274..c178eaa665 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature- - 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query. -- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo` and `TavilySearch` and inject the results directly into your chat experience. +- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `TavilySearch` and `SearchApi` and inject the results directly into your chat experience. - 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions. diff --git a/backend/data/litellm/config.yaml b/backend/data/litellm/config.yaml deleted file mode 100644 index 7d9d2b7230..0000000000 --- a/backend/data/litellm/config.yaml +++ /dev/null @@ -1,4 +0,0 @@ -general_settings: {} -litellm_settings: {} -model_list: [] -router_settings: {} diff --git a/backend/data/readme.txt b/backend/data/readme.txt index 30c12ace0c..af1c39dcd3 100644 --- a/backend/data/readme.txt +++ b/backend/data/readme.txt @@ -1 +1 @@ -dir for backend files (db, documents, etc.) \ No newline at end of file +docker dir for backend files (db, documents, etc.) \ No newline at end of file diff --git a/backend/dev.sh b/backend/dev.sh index c66ae4ba95..5449ab7777 100755 --- a/backend/dev.sh +++ b/backend/dev.sh @@ -1,2 +1,2 @@ PORT="${PORT:-8080}" -uvicorn main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload \ No newline at end of file +uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload \ No newline at end of file diff --git a/backend/open_webui/__init__.py b/backend/open_webui/__init__.py index 1defac8247..c6578c214b 100644 --- a/backend/open_webui/__init__.py +++ b/backend/open_webui/__init__.py @@ -9,8 +9,6 @@ import uvicorn app = typer.Typer() KEY_FILE = Path.cwd() / ".webui_secret_key" -if (frontend_build_dir := Path(__file__).parent / "frontend").exists(): - os.environ["FRONTEND_BUILD_DIR"] = str(frontend_build_dir) @app.command() @@ -40,9 +38,9 @@ def serve( "/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib", ] ) - import main # we need set environment variables before importing main + import open_webui.main # we need set environment variables before importing main - uvicorn.run(main.app, host=host, port=port, forwarded_allow_ips="*") + uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*") @app.command() @@ -52,7 +50,11 @@ def dev( reload: bool = True, ): uvicorn.run( - "main:app", host=host, port=port, reload=reload, forwarded_allow_ips="*" + "open_webui.main:app", + host=host, + port=port, + reload=reload, + forwarded_allow_ips="*", ) diff --git a/backend/alembic.ini b/backend/open_webui/alembic.ini similarity index 100% rename from backend/alembic.ini rename to backend/open_webui/alembic.ini diff --git a/backend/apps/audio/main.py b/backend/open_webui/apps/audio/main.py similarity index 97% rename from backend/apps/audio/main.py rename to backend/open_webui/apps/audio/main.py index 46be15364f..1fc44b28f6 100644 --- a/backend/apps/audio/main.py +++ b/backend/open_webui/apps/audio/main.py @@ -7,46 +7,33 @@ from functools import lru_cache from pathlib import Path import requests -from fastapi import ( - FastAPI, - Request, - Depends, - HTTPException, - status, - UploadFile, - File, +from open_webui.config import ( + AUDIO_STT_ENGINE, + AUDIO_STT_MODEL, + AUDIO_STT_OPENAI_API_BASE_URL, + AUDIO_STT_OPENAI_API_KEY, + AUDIO_TTS_API_KEY, + AUDIO_TTS_ENGINE, + AUDIO_TTS_MODEL, + AUDIO_TTS_OPENAI_API_BASE_URL, + AUDIO_TTS_OPENAI_API_KEY, + AUDIO_TTS_SPLIT_ON, + AUDIO_TTS_VOICE, + CACHE_DIR, + CORS_ALLOW_ORIGIN, + DEVICE_TYPE, + WHISPER_MODEL, + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_MODEL_DIR, + AppConfig, ) +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from pydantic import BaseModel - -from config import ( - SRC_LOG_LEVELS, - CACHE_DIR, - WHISPER_MODEL, - WHISPER_MODEL_DIR, - WHISPER_MODEL_AUTO_UPDATE, - DEVICE_TYPE, - AUDIO_STT_OPENAI_API_BASE_URL, - AUDIO_STT_OPENAI_API_KEY, - AUDIO_TTS_OPENAI_API_BASE_URL, - AUDIO_TTS_OPENAI_API_KEY, - AUDIO_TTS_API_KEY, - AUDIO_STT_ENGINE, - AUDIO_STT_MODEL, - AUDIO_TTS_ENGINE, - AUDIO_TTS_MODEL, - AUDIO_TTS_VOICE, - AUDIO_TTS_SPLIT_ON, - AppConfig, - CORS_ALLOW_ORIGIN, -) -from constants import ERROR_MESSAGES -from utils.utils import ( - get_current_user, - get_verified_user, - get_admin_user, -) +from open_webui.utils.utils import get_admin_user, get_current_user, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["AUDIO"]) @@ -211,7 +198,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): body = json.loads(body) body["model"] = app.state.config.TTS_MODEL body = json.dumps(body).encode("utf-8") - except Exception as e: + except Exception: pass r = None @@ -488,7 +475,7 @@ def get_available_voices() -> dict: elif app.state.config.TTS_ENGINE == "elevenlabs": try: ret = get_elevenlabs_voices() - except Exception as e: + except Exception: # Avoided @lru_cache with exception pass diff --git a/backend/apps/images/main.py b/backend/open_webui/apps/images/main.py similarity index 97% rename from backend/apps/images/main.py rename to backend/open_webui/apps/images/main.py index ed75431063..17afd645c5 100644 --- a/backend/apps/images/main.py +++ b/backend/open_webui/apps/images/main.py @@ -1,52 +1,42 @@ -from fastapi import ( - FastAPI, - Request, - Depends, - HTTPException, -) -from fastapi.middleware.cors import CORSMiddleware -from typing import Optional -from pydantic import BaseModel -from pathlib import Path -import mimetypes -import uuid +import asyncio import base64 import json import logging +import mimetypes import re +import uuid +from pathlib import Path +from typing import Optional + import requests -import asyncio - -from utils.utils import ( - get_verified_user, - get_admin_user, -) - -from apps.images.utils.comfyui import ( - ComfyUIWorkflow, +from open_webui.apps.images.utils.comfyui import ( ComfyUIGenerateImageForm, + ComfyUIWorkflow, comfyui_generate_image, ) - -from constants import ERROR_MESSAGES -from config import ( - SRC_LOG_LEVELS, - CACHE_DIR, - IMAGE_GENERATION_ENGINE, - ENABLE_IMAGE_GENERATION, - AUTOMATIC1111_BASE_URL, +from open_webui.config import ( AUTOMATIC1111_API_AUTH, + AUTOMATIC1111_BASE_URL, + CACHE_DIR, COMFYUI_BASE_URL, COMFYUI_WORKFLOW, COMFYUI_WORKFLOW_NODES, - IMAGES_OPENAI_API_BASE_URL, - IMAGES_OPENAI_API_KEY, + CORS_ALLOW_ORIGIN, + ENABLE_IMAGE_GENERATION, + IMAGE_GENERATION_ENGINE, IMAGE_GENERATION_MODEL, IMAGE_SIZE, IMAGE_STEPS, - CORS_ALLOW_ORIGIN, + IMAGES_OPENAI_API_BASE_URL, + IMAGES_OPENAI_API_KEY, AppConfig, ) +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import Depends, FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from open_webui.utils.utils import get_admin_user, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["IMAGES"]) @@ -143,7 +133,7 @@ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)): form_data.automatic1111.AUTOMATIC1111_API_AUTH ) - app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL + app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL.strip("/") app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES @@ -186,7 +176,7 @@ async def verify_url(user=Depends(get_admin_user)): ) r.raise_for_status() return True - except Exception as e: + except Exception: app.state.config.ENABLED = False raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) elif app.state.config.ENGINE == "comfyui": @@ -194,7 +184,7 @@ async def verify_url(user=Depends(get_admin_user)): r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info") r.raise_for_status() return True - except Exception as e: + except Exception: app.state.config.ENABLED = False raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) else: @@ -202,6 +192,7 @@ async def verify_url(user=Depends(get_admin_user)): def set_image_model(model: str): + log.info(f"Setting image model to {model}") app.state.config.MODEL = model if app.state.config.ENGINE in ["", "automatic1111"]: api_auth = get_automatic1111_api_auth() @@ -255,7 +246,8 @@ async def get_image_config(user=Depends(get_admin_user)): @app.post("/image/config/update") async def update_image_config(form_data: ImageConfigForm, user=Depends(get_admin_user)): - app.state.config.MODEL = form_data.MODEL + + set_image_model(form_data.MODEL) pattern = r"^\d+x\d+$" if re.match(pattern, form_data.IMAGE_SIZE): @@ -397,7 +389,6 @@ def save_url_image(url): r = requests.get(url) r.raise_for_status() if r.headers["content-type"].split("/")[0] == "image": - mime_type = r.headers["content-type"] image_format = mimetypes.guess_extension(mime_type) @@ -412,7 +403,7 @@ def save_url_image(url): image_file.write(chunk) return image_filename else: - log.error(f"Url does not point to an image.") + log.error("Url does not point to an image.") return None except Exception as e: @@ -430,7 +421,6 @@ async def image_generations( r = None try: if app.state.config.ENGINE == "openai": - headers = {} headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}" headers["Content-Type"] = "application/json" diff --git a/backend/apps/images/utils/comfyui.py b/backend/open_webui/apps/images/utils/comfyui.py similarity index 87% rename from backend/apps/images/utils/comfyui.py rename to backend/open_webui/apps/images/utils/comfyui.py index 1584842236..0a3e3a1d9b 100644 --- a/backend/apps/images/utils/comfyui.py +++ b/backend/open_webui/apps/images/utils/comfyui.py @@ -1,34 +1,45 @@ import asyncio -import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client) import json -import urllib.request -import urllib.parse -import random import logging +import random +import urllib.parse +import urllib.request +from typing import Optional -from config import SRC_LOG_LEVELS +import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client) +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["COMFYUI"]) -from pydantic import BaseModel - -from typing import Optional +default_headers = {"User-Agent": "Mozilla/5.0"} def queue_prompt(prompt, client_id, base_url): log.info("queue_prompt") p = {"prompt": prompt, "client_id": client_id} data = json.dumps(p).encode("utf-8") - req = urllib.request.Request(f"{base_url}/prompt", data=data) - return json.loads(urllib.request.urlopen(req).read()) + log.debug(f"queue_prompt data: {data}") + try: + req = urllib.request.Request( + f"{base_url}/prompt", data=data, headers=default_headers + ) + response = urllib.request.urlopen(req).read() + return json.loads(response) + except Exception as e: + log.exception(f"Error while queuing prompt: {e}") + raise e def get_image(filename, subfolder, folder_type, base_url): log.info("get_image") data = {"filename": filename, "subfolder": subfolder, "type": folder_type} url_values = urllib.parse.urlencode(data) - with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response: + req = urllib.request.Request( + f"{base_url}/view?{url_values}", headers=default_headers + ) + with urllib.request.urlopen(req) as response: return response.read() @@ -41,7 +52,11 @@ def get_image_url(filename, subfolder, folder_type, base_url): def get_history(prompt_id, base_url): log.info("get_history") - with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response: + + req = urllib.request.Request( + f"{base_url}/history/{prompt_id}", headers=default_headers + ) + with urllib.request.urlopen(req) as response: return json.loads(response.read()) diff --git a/backend/apps/ollama/main.py b/backend/open_webui/apps/ollama/main.py similarity index 98% rename from backend/apps/ollama/main.py rename to backend/open_webui/apps/ollama/main.py index db677e84cb..44b5667d5d 100644 --- a/backend/apps/ollama/main.py +++ b/backend/open_webui/apps/ollama/main.py @@ -1,54 +1,40 @@ -from fastapi import ( - FastAPI, - Request, - HTTPException, - Depends, - UploadFile, - File, -) -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse - -from pydantic import BaseModel, ConfigDict - -import os -import re -import random -import requests -import json -import aiohttp import asyncio +import json import logging +import os +import random +import re import time -from urllib.parse import urlparse from typing import Optional, Union +from urllib.parse import urlparse -from starlette.background import BackgroundTask - -from apps.webui.models.models import Models -from constants import ERROR_MESSAGES -from utils.utils import ( - get_verified_user, - get_admin_user, -) - -from config import ( - SRC_LOG_LEVELS, - OLLAMA_BASE_URLS, - ENABLE_OLLAMA_API, +import aiohttp +import requests +from open_webui.apps.webui.models.models import Models +from open_webui.config import ( AIOHTTP_CLIENT_TIMEOUT, + CORS_ALLOW_ORIGIN, ENABLE_MODEL_FILTER, + ENABLE_OLLAMA_API, MODEL_FILTER_LIST, + OLLAMA_BASE_URLS, UPLOAD_DIR, AppConfig, - CORS_ALLOW_ORIGIN, ) -from utils.misc import ( - calculate_sha256, +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from pydantic import BaseModel, ConfigDict +from starlette.background import BackgroundTask +from open_webui.utils.misc import ( apply_model_params_to_body_ollama, apply_model_params_to_body_openai, apply_model_system_prompt_to_body, + calculate_sha256, ) +from open_webui.utils.utils import get_admin_user, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["OLLAMA"]) diff --git a/backend/apps/openai/main.py b/backend/open_webui/apps/openai/main.py similarity index 97% rename from backend/apps/openai/main.py rename to backend/open_webui/apps/openai/main.py index 9ad67c40c7..53d1f4534c 100644 --- a/backend/apps/openai/main.py +++ b/backend/open_webui/apps/openai/main.py @@ -1,44 +1,36 @@ -from fastapi import FastAPI, Request, HTTPException, Depends -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse, FileResponse - -import requests -import aiohttp import asyncio +import hashlib import json import logging +from pathlib import Path +from typing import Literal, Optional, overload +import aiohttp +import requests +from open_webui.apps.webui.models.models import Models +from open_webui.config import ( + AIOHTTP_CLIENT_TIMEOUT, + CACHE_DIR, + CORS_ALLOW_ORIGIN, + ENABLE_MODEL_FILTER, + ENABLE_OPENAI_API, + MODEL_FILTER_LIST, + OPENAI_API_BASE_URLS, + OPENAI_API_KEYS, + AppConfig, +) +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import Depends, FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, StreamingResponse from pydantic import BaseModel from starlette.background import BackgroundTask - -from apps.webui.models.models import Models -from constants import ERROR_MESSAGES -from utils.utils import ( - get_verified_user, - get_admin_user, -) -from utils.misc import ( +from open_webui.utils.misc import ( apply_model_params_to_body_openai, apply_model_system_prompt_to_body, ) - -from config import ( - SRC_LOG_LEVELS, - ENABLE_OPENAI_API, - AIOHTTP_CLIENT_TIMEOUT, - OPENAI_API_BASE_URLS, - OPENAI_API_KEYS, - CACHE_DIR, - ENABLE_MODEL_FILTER, - MODEL_FILTER_LIST, - AppConfig, - CORS_ALLOW_ORIGIN, -) -from typing import Optional, Literal, overload - - -import hashlib -from pathlib import Path +from open_webui.utils.utils import get_admin_user, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["OPENAI"]) diff --git a/backend/apps/rag/main.py b/backend/open_webui/apps/rag/main.py similarity index 95% rename from backend/apps/rag/main.py rename to backend/open_webui/apps/rag/main.py index c3cee8dcca..6c064fe81a 100644 --- a/backend/apps/rag/main.py +++ b/backend/open_webui/apps/rag/main.py @@ -1,140 +1,118 @@ -from fastapi import ( - FastAPI, - Depends, - HTTPException, - status, - UploadFile, - File, - Form, -) -from fastapi.middleware.cors import CORSMiddleware -import requests -import os, shutil, logging, re -from datetime import datetime - -from pathlib import Path -from typing import Union, Sequence, Iterator, Any - -from chromadb.utils.batch_utils import create_batches -from langchain_core.documents import Document - -from langchain_community.document_loaders import ( - WebBaseLoader, - TextLoader, - PyPDFLoader, - CSVLoader, - BSHTMLLoader, - Docx2txtLoader, - UnstructuredEPubLoader, - UnstructuredWordDocumentLoader, - UnstructuredMarkdownLoader, - UnstructuredXMLLoader, - UnstructuredRSTLoader, - UnstructuredExcelLoader, - UnstructuredPowerPointLoader, - YoutubeLoader, - OutlookMessageLoader, -) -from langchain.text_splitter import RecursiveCharacterTextSplitter - -import validators -import urllib.parse -import socket - - -from pydantic import BaseModel -from typing import Optional -import mimetypes -import uuid import json +import logging +import mimetypes +import os +import shutil +import socket +import urllib.parse +import uuid +from datetime import datetime +from pathlib import Path +from typing import Iterator, Optional, Sequence, Union -from apps.webui.models.documents import ( - Documents, - DocumentForm, - DocumentResponse, -) -from apps.webui.models.files import ( - Files, -) - -from apps.rag.utils import ( - get_model_path, +import requests +import validators +from open_webui.apps.rag.search.brave import search_brave +from open_webui.apps.rag.search.duckduckgo import search_duckduckgo +from open_webui.apps.rag.search.google_pse import search_google_pse +from open_webui.apps.rag.search.jina_search import search_jina +from open_webui.apps.rag.search.main import SearchResult +from open_webui.apps.rag.search.searchapi import search_searchapi +from open_webui.apps.rag.search.searxng import search_searxng +from open_webui.apps.rag.search.serper import search_serper +from open_webui.apps.rag.search.serply import search_serply +from open_webui.apps.rag.search.serpstack import search_serpstack +from open_webui.apps.rag.search.tavily import search_tavily +from open_webui.apps.rag.utils import ( get_embedding_function, - query_doc, - query_doc_with_hybrid_search, + get_model_path, query_collection, query_collection_with_hybrid_search, + query_doc, + query_doc_with_hybrid_search, ) - -from apps.rag.search.brave import search_brave -from apps.rag.search.google_pse import search_google_pse -from apps.rag.search.main import SearchResult -from apps.rag.search.searxng import search_searxng -from apps.rag.search.serper import search_serper -from apps.rag.search.serpstack import search_serpstack -from apps.rag.search.serply import search_serply -from apps.rag.search.duckduckgo import search_duckduckgo -from apps.rag.search.tavily import search_tavily -from apps.rag.search.jina_search import search_jina - -from utils.misc import ( - calculate_sha256, - calculate_sha256_string, - sanitize_filename, - extract_folders_after_data_docs, -) -from utils.utils import get_verified_user, get_admin_user - -from config import ( - AppConfig, - ENV, - SRC_LOG_LEVELS, - UPLOAD_DIR, - DOCS_DIR, +from open_webui.apps.webui.models.documents import DocumentForm, Documents +from open_webui.apps.webui.models.files import Files +from chromadb.utils.batch_utils import create_batches +from open_webui.config import ( + BRAVE_SEARCH_API_KEY, + CHROMA_CLIENT, + CHUNK_OVERLAP, + CHUNK_SIZE, CONTENT_EXTRACTION_ENGINE, - TIKA_SERVER_URL, - RAG_TOP_K, - RAG_RELEVANCE_THRESHOLD, - RAG_FILE_MAX_SIZE, - RAG_FILE_MAX_COUNT, + CORS_ALLOW_ORIGIN, + DEVICE_TYPE, + DOCS_DIR, + ENABLE_RAG_HYBRID_SEARCH, + ENABLE_RAG_LOCAL_WEB_FETCH, + ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + ENABLE_RAG_WEB_SEARCH, + ENV, + GOOGLE_PSE_API_KEY, + GOOGLE_PSE_ENGINE_ID, + PDF_EXTRACT_IMAGES, RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_MODEL, RAG_EMBEDDING_MODEL_AUTO_UPDATE, RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, - ENABLE_RAG_HYBRID_SEARCH, - ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, - RAG_RERANKING_MODEL, - PDF_EXTRACT_IMAGES, - RAG_RERANKING_MODEL_AUTO_UPDATE, - RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + RAG_EMBEDDING_OPENAI_BATCH_SIZE, + RAG_FILE_MAX_COUNT, + RAG_FILE_MAX_SIZE, RAG_OPENAI_API_BASE_URL, RAG_OPENAI_API_KEY, - DEVICE_TYPE, - CHROMA_CLIENT, - CHUNK_SIZE, - CHUNK_OVERLAP, + RAG_RELEVANCE_THRESHOLD, + RAG_RERANKING_MODEL, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, RAG_TEMPLATE, - ENABLE_RAG_LOCAL_WEB_FETCH, - YOUTUBE_LOADER_LANGUAGE, - ENABLE_RAG_WEB_SEARCH, - RAG_WEB_SEARCH_ENGINE, + RAG_TOP_K, + RAG_WEB_SEARCH_CONCURRENT_REQUESTS, RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + RAG_WEB_SEARCH_ENGINE, + RAG_WEB_SEARCH_RESULT_COUNT, + SEARCHAPI_API_KEY, + SEARCHAPI_ENGINE, SEARXNG_QUERY_URL, - GOOGLE_PSE_API_KEY, - GOOGLE_PSE_ENGINE_ID, - BRAVE_SEARCH_API_KEY, - SERPSTACK_API_KEY, - SERPSTACK_HTTPS, SERPER_API_KEY, SERPLY_API_KEY, + SERPSTACK_API_KEY, + SERPSTACK_HTTPS, TAVILY_API_KEY, - RAG_WEB_SEARCH_RESULT_COUNT, - RAG_WEB_SEARCH_CONCURRENT_REQUESTS, - RAG_EMBEDDING_OPENAI_BATCH_SIZE, - CORS_ALLOW_ORIGIN, + TIKA_SERVER_URL, + UPLOAD_DIR, + YOUTUBE_LOADER_LANGUAGE, + AppConfig, ) - -from constants import ERROR_MESSAGES +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status +from fastapi.middleware.cors import CORSMiddleware +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_community.document_loaders import ( + BSHTMLLoader, + CSVLoader, + Docx2txtLoader, + OutlookMessageLoader, + PyPDFLoader, + TextLoader, + UnstructuredEPubLoader, + UnstructuredExcelLoader, + UnstructuredMarkdownLoader, + UnstructuredPowerPointLoader, + UnstructuredRSTLoader, + UnstructuredXMLLoader, + WebBaseLoader, + YoutubeLoader, +) +from langchain_core.documents import Document +from pydantic import BaseModel +from open_webui.utils.misc import ( + calculate_sha256, + calculate_sha256_string, + extract_folders_after_data_docs, + sanitize_filename, +) +from open_webui.utils.utils import get_admin_user, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) @@ -189,6 +167,8 @@ app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS app.state.config.SERPER_API_KEY = SERPER_API_KEY app.state.config.SERPLY_API_KEY = SERPLY_API_KEY app.state.config.TAVILY_API_KEY = TAVILY_API_KEY +app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY +app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS @@ -427,6 +407,8 @@ async def get_rag_config(user=Depends(get_admin_user)): "serper_api_key": app.state.config.SERPER_API_KEY, "serply_api_key": app.state.config.SERPLY_API_KEY, "tavily_api_key": app.state.config.TAVILY_API_KEY, + "searchapi_api_key": app.state.config.SEARCHAPI_API_KEY, + "seaarchapi_engine": app.state.config.SEARCHAPI_ENGINE, "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, }, @@ -466,6 +448,8 @@ class WebSearchConfig(BaseModel): serper_api_key: Optional[str] = None serply_api_key: Optional[str] = None tavily_api_key: Optional[str] = None + searchapi_api_key: Optional[str] = None + searchapi_engine: Optional[str] = None result_count: Optional[int] = None concurrent_requests: Optional[int] = None @@ -529,6 +513,8 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_ app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key + app.state.config.SEARCHAPI_API_KEY = form_data.web.search.searchapi_api_key + app.state.config.SEARCHAPI_ENGINE = form_data.web.search.searchapi_engine app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = ( form_data.web.search.concurrent_requests @@ -566,6 +552,8 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_ "serpstack_https": app.state.config.SERPSTACK_HTTPS, "serper_api_key": app.state.config.SERPER_API_KEY, "serply_api_key": app.state.config.SERPLY_API_KEY, + "serachapi_api_key": app.state.config.SEARCHAPI_API_KEY, + "searchapi_engine": app.state.config.SEARCHAPI_ENGINE, "tavily_api_key": app.state.config.TAVILY_API_KEY, "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, @@ -817,6 +805,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]: - SERPER_API_KEY - SERPLY_API_KEY - TAVILY_API_KEY + - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`) Args: query (str): The query to search for """ @@ -904,6 +893,17 @@ def search_web(engine: str, query: str) -> list[SearchResult]: ) else: raise Exception("No TAVILY_API_KEY found in environment variables") + elif engine == "searchapi": + if app.state.config.SEARCHAPI_API_KEY: + return search_searchapi( + app.state.config.SEARCHAPI_API_KEY, + app.state.config.SEARCHAPI_ENGINE, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception("No SEARCHAPI_API_KEY found in environment variables") elif engine == "jina": return search_jina(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT) else: @@ -954,7 +954,6 @@ def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)): def store_data_in_vector_db( data, collection_name, metadata: Optional[dict] = None, overwrite: bool = False ) -> bool: - text_splitter = RecursiveCharacterTextSplitter( chunk_size=app.state.config.CHUNK_SIZE, chunk_overlap=app.state.config.CHUNK_OVERLAP, @@ -1315,7 +1314,6 @@ def store_text( form_data: TextRAGForm, user=Depends(get_verified_user), ): - collection_name = form_data.collection_name if collection_name is None: collection_name = calculate_sha256_string(form_data.content) diff --git a/backend/apps/rag/search/brave.py b/backend/open_webui/apps/rag/search/brave.py similarity index 90% rename from backend/apps/rag/search/brave.py rename to backend/open_webui/apps/rag/search/brave.py index 681caa9761..2eb256b4bc 100644 --- a/backend/apps/rag/search/brave.py +++ b/backend/open_webui/apps/rag/search/brave.py @@ -1,9 +1,9 @@ import logging from typing import Optional -import requests -from apps.rag.search.main import SearchResult, get_filtered_results -from config import SRC_LOG_LEVELS +import requests +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/duckduckgo.py b/backend/open_webui/apps/rag/search/duckduckgo.py similarity index 92% rename from backend/apps/rag/search/duckduckgo.py rename to backend/open_webui/apps/rag/search/duckduckgo.py index e994ef47a9..a8a580acad 100644 --- a/backend/apps/rag/search/duckduckgo.py +++ b/backend/open_webui/apps/rag/search/duckduckgo.py @@ -1,8 +1,9 @@ import logging from typing import Optional -from apps.rag.search.main import SearchResult, get_filtered_results + +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results from duckduckgo_search import DDGS -from config import SRC_LOG_LEVELS +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/google_pse.py b/backend/open_webui/apps/rag/search/google_pse.py similarity index 91% rename from backend/apps/rag/search/google_pse.py rename to backend/open_webui/apps/rag/search/google_pse.py index 7fedb3dad9..a7f75a6c6d 100644 --- a/backend/apps/rag/search/google_pse.py +++ b/backend/open_webui/apps/rag/search/google_pse.py @@ -1,10 +1,9 @@ -import json import logging from typing import Optional -import requests -from apps.rag.search.main import SearchResult, get_filtered_results -from config import SRC_LOG_LEVELS +import requests +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/jina_search.py b/backend/open_webui/apps/rag/search/jina_search.py similarity index 91% rename from backend/apps/rag/search/jina_search.py rename to backend/open_webui/apps/rag/search/jina_search.py index 8d1c582a1e..41cde679d1 100644 --- a/backend/apps/rag/search/jina_search.py +++ b/backend/open_webui/apps/rag/search/jina_search.py @@ -1,9 +1,9 @@ import logging -import requests -from yarl import URL -from apps.rag.search.main import SearchResult -from config import SRC_LOG_LEVELS +import requests +from open_webui.apps.rag.search.main import SearchResult +from open_webui.env import SRC_LOG_LEVELS +from yarl import URL log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/main.py b/backend/open_webui/apps/rag/search/main.py similarity index 83% rename from backend/apps/rag/search/main.py rename to backend/open_webui/apps/rag/search/main.py index 49056f1fd1..1af8a70aa1 100644 --- a/backend/apps/rag/search/main.py +++ b/backend/open_webui/apps/rag/search/main.py @@ -1,5 +1,6 @@ from typing import Optional from urllib.parse import urlparse + from pydantic import BaseModel @@ -8,7 +9,8 @@ def get_filtered_results(results, filter_list): return results filtered_results = [] for result in results: - domain = urlparse(result["url"]).netloc + url = result.get("url") or result.get("link", "") + domain = urlparse(url).netloc if any(domain.endswith(filtered_domain) for filtered_domain in filter_list): filtered_results.append(result) return filtered_results diff --git a/backend/open_webui/apps/rag/search/searchapi.py b/backend/open_webui/apps/rag/search/searchapi.py new file mode 100644 index 0000000000..9ec9a07476 --- /dev/null +++ b/backend/open_webui/apps/rag/search/searchapi.py @@ -0,0 +1,48 @@ +import logging +from typing import Optional +from urllib.parse import urlencode + +import requests +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_searchapi( + api_key: str, + engine: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using searchapi.io's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A searchapi.io API key + query (str): The query to search for + """ + url = "https://www.searchapi.io/api/v1/search" + + engine = engine or "google" + + payload = {"engine": engine, "q": query, "api_key": api_key} + + url = f"{url}?{urlencode(payload)}" + response = requests.request("GET", url) + + json_response = response.json() + log.info(f"results from searchapi search: {json_response}") + + results = sorted( + json_response.get("organic_results", []), key=lambda x: x.get("position", 0) + ) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result["link"], title=result["title"], snippet=result["snippet"] + ) + for result in results[:count] + ] diff --git a/backend/apps/rag/search/searxng.py b/backend/open_webui/apps/rag/search/searxng.py similarity index 96% rename from backend/apps/rag/search/searxng.py rename to backend/open_webui/apps/rag/search/searxng.py index 94bed2857b..26c534aa3c 100644 --- a/backend/apps/rag/search/searxng.py +++ b/backend/open_webui/apps/rag/search/searxng.py @@ -1,10 +1,9 @@ import logging -import requests - from typing import Optional -from apps.rag.search.main import SearchResult, get_filtered_results -from config import SRC_LOG_LEVELS +import requests +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/serper.py b/backend/open_webui/apps/rag/search/serper.py similarity index 90% rename from backend/apps/rag/search/serper.py rename to backend/open_webui/apps/rag/search/serper.py index e71fbb6283..ed7cc2c5fb 100644 --- a/backend/apps/rag/search/serper.py +++ b/backend/open_webui/apps/rag/search/serper.py @@ -1,10 +1,10 @@ import json import logging from typing import Optional -import requests -from apps.rag.search.main import SearchResult, get_filtered_results -from config import SRC_LOG_LEVELS +import requests +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/serply.py b/backend/open_webui/apps/rag/search/serply.py similarity index 94% rename from backend/apps/rag/search/serply.py rename to backend/open_webui/apps/rag/search/serply.py index 28c15fd788..260e9b30e2 100644 --- a/backend/apps/rag/search/serply.py +++ b/backend/open_webui/apps/rag/search/serply.py @@ -1,11 +1,10 @@ -import json import logging from typing import Optional -import requests from urllib.parse import urlencode -from apps.rag.search.main import SearchResult, get_filtered_results -from config import SRC_LOG_LEVELS +import requests +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/serpstack.py b/backend/open_webui/apps/rag/search/serpstack.py similarity index 91% rename from backend/apps/rag/search/serpstack.py rename to backend/open_webui/apps/rag/search/serpstack.py index 5c19bd1342..962c1a5b30 100644 --- a/backend/apps/rag/search/serpstack.py +++ b/backend/open_webui/apps/rag/search/serpstack.py @@ -1,10 +1,9 @@ -import json import logging from typing import Optional -import requests -from apps.rag.search.main import SearchResult, get_filtered_results -from config import SRC_LOG_LEVELS +import requests +from open_webui.apps.rag.search.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/tavily.py b/backend/open_webui/apps/rag/search/tavily.py similarity index 90% rename from backend/apps/rag/search/tavily.py rename to backend/open_webui/apps/rag/search/tavily.py index ed4ab6e084..a619d29edb 100644 --- a/backend/apps/rag/search/tavily.py +++ b/backend/open_webui/apps/rag/search/tavily.py @@ -1,9 +1,8 @@ import logging import requests - -from apps.rag.search.main import SearchResult -from config import SRC_LOG_LEVELS +from open_webui.apps.rag.search.main import SearchResult +from open_webui.env import SRC_LOG_LEVELS log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) diff --git a/backend/apps/rag/search/testdata/brave.json b/backend/open_webui/apps/rag/search/testdata/brave.json similarity index 100% rename from backend/apps/rag/search/testdata/brave.json rename to backend/open_webui/apps/rag/search/testdata/brave.json diff --git a/backend/apps/rag/search/testdata/google_pse.json b/backend/open_webui/apps/rag/search/testdata/google_pse.json similarity index 100% rename from backend/apps/rag/search/testdata/google_pse.json rename to backend/open_webui/apps/rag/search/testdata/google_pse.json diff --git a/backend/open_webui/apps/rag/search/testdata/searchapi.json b/backend/open_webui/apps/rag/search/testdata/searchapi.json new file mode 100644 index 0000000000..fa3d1c3d74 --- /dev/null +++ b/backend/open_webui/apps/rag/search/testdata/searchapi.json @@ -0,0 +1,357 @@ +{ + "search_metadata": { + "id": "search_VW19X7MebbAtdMwoQe68NbDz", + "status": "Success", + "created_at": "2024-08-27T13:43:20Z", + "request_time_taken": 0.6, + "parsing_time_taken": 0.72, + "total_time_taken": 1.32, + "request_url": "https://www.google.com/search?q=chatgpt&oq=chatgpt&gl=us&hl=en&ie=UTF-8", + "html_url": "https://www.searchapi.io/api/v1/searches/search_VW19X7MebbAtdMwoQe68NbDz.html", + "json_url": "https://www.searchapi.io/api/v1/searches/search_VW19X7MebbAtdMwoQe68NbDz" + }, + "search_parameters": { + "engine": "google", + "q": "chatgpt", + "device": "desktop", + "google_domain": "google.com", + "hl": "en", + "gl": "us" + }, + "search_information": { + "query_displayed": "chatgpt", + "total_results": 1010000000, + "time_taken_displayed": 0.37, + "detected_location": "United States" + }, + "knowledge_graph": { + "kgmid": "/g/11khcfz0y2", + "knowledge_graph_type": "Kp3 verticals", + "title": "ChatGPT", + "type": "Software", + "description": "ChatGPT is a chatbot and virtual assistant developed by OpenAI and launched on November 30, 2022. Based on large language models, it enables users to refine and steer a conversation towards a desired length, format, style, level of detail, and language.", + "source": { + "name": "Wikipedia", + "link": "https://en.wikipedia.org/wiki/ChatGPT" + }, + "developer": "OpenAI, Microsoft", + "developer_links": [ + { + "text": "OpenAI", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=OpenAI&si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgItjbuK5dmA2L3ta2Ero3Ypd_sib6W4Pr5sCi7O_W3yzdqxwyrjzsYeYOtNg2ogL1xVq9TKwgD48tL7rygfkRfNyy4k-R5yQgywoFukoCUths6NdRX69gl50cvd6dpZcMzVelCxT7mxXlRchl6XkueG326znDiZL-ODNOysdnCc4XoeAQUFtbaVjja6Vc7WkQF4X8rUdbDKPVU9WyLOV765d8Y777kMI7-nXGGyD7xXJX5E3HA%3D%3D&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECD0QAg" + }, + { + "text": "Microsoft", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Microsoft&si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-SdjhIP74XAMBYys4zy1Z9yzXEom04F9Qy-tMOt2d-L6jIC5cXse6I528G870-4sF-DZYAPj0F1HoGTUOqpWuP7jbEPm3w_-mCH0wVgBHBGCgxRrCaUn8_k2-aga9V9JD6hkq2kM8zVmERCqCM8rqo3bNfbPdJ-baTq4w8Pkxdan3K--CfOtXX--lTjJtO6BnfG2RdpY_jBfy3uZZ7DeAE4-P4rvKuty6UL6le4dqqDt-kLQA%3D%3D&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAXoECD0QAw" + } + ], + "initial_release_date": "November 30, 2022", + "programming_language": "Python", + "programming_language_links": [ + { + "text": "Python", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Python&si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqmwbtPHPEcZi5JOYKaqe_iu1m4TVPotntrDVKbuXCkoFhx-K-Dp6PbewOILPFWjhDofHha-WRuSQCgY7LnBkzXtVH7pxiRdHONv3wpVsflGBg_EdTHCxOnyWt1nDgBmCjsfchXU7DKtJq159-V0-seE_cp7VV&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDYQAg" + } + ], + "engine": "GPT-4; GPT-4o; GPT-4o mini", + "engine_links": [ + { + "text": "GPT-4", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4&stick=H4sIAAAAAAAAAONgVuLVT9c3NMy2TI_PNUtOX8TK6h4QomsCAKiBOxkZAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDUQAg" + }, + { + "text": "GPT-4o", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4o&stick=H4sIAAAAAAAAAONgVuLVT9c3NCyryEg3rMooWMTK5h4QomuSDwC3NAfvGgAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAXoECDUQAw" + }, + { + "text": "GPT-4o", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4o&stick=H4sIAAAAAAAAAONgVuLVT9c3NCyryEg3rMooWMTK5h4QomuSDwC3NAfvGgAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAnoECDUQBA" + } + ], + "license": "Proprietary", + "platform": "Cloud computing platforms", + "platform_links": [ + { + "text": "Cloud computing", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Cloud+computing&stick=H4sIAAAAAAAAAONgVuLSz9U3MKqMt8w1XsTK75yTX5qikJyfW1BakpmXDgB-4JvxIAAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDcQAg" + } + ], + "stable_release": "July 18, 2024; 40 days ago", + "image": "data:image/png;base64,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" + }, + "organic_results": [ + { + "position": 1, + "title": "ChatGPT | OpenAI", + "link": "https://openai.com/chatgpt/", + "source": "OpenAI", + "domain": "openai.com", + "displayed_link": "https://openai.com › chatgpt", + "snippet": "ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, ...", + "snippet_highlighted_words": ["ChatGPT", "ChatGPT"], + "sitelinks": { + "expanded": [ + { + "title": "Introducing ChatGPT", + "link": "https://openai.com/index/chatgpt/", + "snippet": "We've trained a model called ChatGPT which interacts in a ..." + }, + { + "title": "Download ChatGPT", + "link": "https://openai.com/chatgpt/download/", + "snippet": "Download ChatGPT Use ChatGPT your way. Talk to type or have a ..." + }, + { + "title": "Pricing", + "link": "https://openai.com/chatgpt/pricing/", + "snippet": "Pricing · $25per user / month billed annually · $30per user / month ..." + }, + { + "title": "“What is ChatGPT?” article", + "link": "https://help.openai.com/en/articles/6783457-what-is-chatgpt", + "snippet": "How does ChatGPT work? ChatGPT is fine-tuned from ..." + }, + { + "title": "For Teams", + "link": "https://openai.com/chatgpt/team/", + "snippet": "ChatGPT simplifies lead qualification and forecasting ..." + } + ] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAABQElEQVR4Ac3PIYyDMBiG4VefPDtxEj0xM39qZl40mcPhMzONOjWNrqxA4UgmqklweBQKVfFdGhbSZZvfY5qmb35++DAbO4XQF7xjpN42s1oyXtlr2gN4SRpynnTaANtesy1tkOOR8aoAJ12J6ngmGkknCqn5gv0y8Jv03eYy+PEAu07jCQ66sDqqpohBCVb2PMtvSbeoxRJcLlIFVFKVBuOwBDdNxkzjEbKbVDwHvgZw8j+Qq2fVhhjkxB2g7JwqKJMRhUqo5Lol8OTxMbSsehXw45e9ao+J92EkGaFbBscxLqnbPRhYOVXr/53L+wTVaUDmNZ+tLNyDWgdWl3gxo7otHMYY5DYdwLc6gB18tVLBSVJD6qr6fsoBVt7wyCm4PxfiRyBTx5N8kCQP8DtrzysZrebG9ZLhnaILYbIbPss/4c/row+G/FAAAAAASUVORK5CYII=" + }, + { + "position": 2, + "title": "ChatGPT", + "link": "https://chatgpt.com/", + "source": "ChatGPT", + "domain": "chatgpt.com", + "displayed_link": "https://chatgpt.com", + "snippet": "ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, learning,", + "snippet_highlighted_words": ["ChatGPT", "ChatGPT"], + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAAAAABXZoBIAAABGElEQVR4Aa3SIWzCUBSF4d8rDA6LnMfiMPjU18xiJjHPCxzBVePqaqsrK6sqK5qgnmjybzShzQKb4tjv3mvuwX/yHhya9i8cDgCXlziwKm99TnIM5RN+rlQvkO5Z97+wP1FpAbkadwwzWgAOW4L2rcppxoZLjc2i1xMEzZYzblMrbBILzpaQV0wYqUfcbNNk3+kZPibsaEek1oqjxj3DA6W8Y5uobs7kuggTphvNOKWq6/HQlQl70sF4oNaS2NNaMzxQ4Krt9rBPliMW82akubKqDFSuR9x9TiiF8QsybfnBLtDNePhQm3ifSOyAyhlvpKoZy0pzsuiM2kKSwlWNhKd/FiHsFsXtVrB5XbAAEHyN2jTv7+1TvgE1rn+XcUk3JAAAAABJRU5ErkJggg==" + }, + { + "position": 3, + "title": "OpenAI", + "link": "https://openai.com/", + "source": "OpenAI", + "domain": "openai.com", + "displayed_link": "https://openai.com", + "snippet": "ChatGPT on your desktop. Chat about email, screenshots, files, and anything on your screen. Chat about email, screenshots, files ...", + "snippet_highlighted_words": ["ChatGPT"], + "sitelinks": { + "inline": [ + { + "title": "ChatGPT", + "link": "https://openai.com/chatgpt/" + }, + { + "title": "Introducing ChatGPT", + "link": "https://openai.com/index/chatgpt/" + }, + { + "title": "Download ChatGPT", + "link": "https://openai.com/chatgpt/download/" + }, + { + "title": "ChatGPT for teams", + "link": "https://openai.com/chatgpt/team/" + } + ] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAABQElEQVR4Ac3PIYyDMBiG4VefPDtxEj0xM39qZl40mcPhMzONOjWNrqxA4UgmqklweBQKVfFdGhbSZZvfY5qmb35++DAbO4XQF7xjpN42s1oyXtlr2gN4SRpynnTaANtesy1tkOOR8aoAJ12J6ngmGkknCqn5gv0y8Jv03eYy+PEAu07jCQ66sDqqpohBCVb2PMtvSbeoxRJcLlIFVFKVBuOwBDdNxkzjEbKbVDwHvgZw8j+Qq2fVhhjkxB2g7JwqKJMRhUqo5Lol8OTxMbSsehXw45e9ao+J92EkGaFbBscxLqnbPRhYOVXr/53L+wTVaUDmNZ+tLNyDWgdWl3gxo7otHMYY5DYdwLc6gB18tVLBSVJD6qr6fsoBVt7wyCm4PxfiRyBTx5N8kCQP8DtrzysZrebG9ZLhnaILYbIbPss/4c/row+G/FAAAAAASUVORK5CYII=" + }, + { + "position": 4, + "title": "ChatGPT - Apps on Google Play", + "link": "https://play.google.com/store/apps/details?id=com.openai.chatgpt&hl=en_US", + "source": "Google Play", + "domain": "play.google.com", + "displayed_link": "https://play.google.com › store › apps › details › id=com...", + "snippet": "With the official ChatGPT app, get instant answers and inspiration wherever you are. This app is free and brings you the newest model improvements from ...", + "snippet_highlighted_words": ["ChatGPT"], + "rich_snippet": { + "detected_extensions": { + "rating": 4.8, + "reviews": 3113820 + }, + "extensions": ["Rating: 4.8", "3,113,820 votes", "Free", "Android", "Business/Productivity"] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADNklEQVR4AcXUA4wdURiG4VPbtu32mmvUtm3bts2gnNq2bbtBbSzn3n79M8nZrvdcZZO8yUTzPUMGIFmLOlBJTXhtqcPUUaoDxRwp16aObORuHcNpxmwnUjM5/uICam0PyKza2miJSmoGOlH0HlIGjwPUm9tOqrXdD6qt9RANEb39VEGPAXym/5YMK9ehxu5ahKgbH4I3m0rjdoDfBEj+EwDDyrUiiO9UR7ffAd8pMvzHAyJ3Ivr74R7AtD8SIRATURO1ttWBakuCCN4BqrDLAApRiAmAcflm1NilJUSwCAJqqcm8nBs7pRu4y+gCIAoRqdwJ07LtdCfUSSLUlEZqjBQbevwctVvX1QUA7zd8pkYIIfh41o2d0Xh7ICJOpAFOsic0ZHYBIIZQKynjaLQtCPLJVKCrRyQhaAjUYaqIMCBxxA5CqAgRRIjmUMapzBu7oHG0cZmPx2welU4AkARi6T7U3KWHanuAgshCV952hy/sJ1MmNs77Q5mcAHBEuIKwLD6Mmrv1yCS1QMftfsApJjLO+0A1cxjAEb5TwxRE/uX30PmkFrjAgJPRn7lQklMAXwL4TAtB8UVA927PIA8sBNxikC+mhHzMgwA+7j0tFEWXAN1GXkGksSzkMpXxdWBZ/L3LYLvMRBHfqLbCAD7uNT0MJRYDfYedQ4S5FOymonjvbcD7Slb86Fsa9psM9itJIuxUsPhLGG282BJg8ODjgL4QbKbieO+nxyc/NT55a/CughXfu5dLCrGGyir2GcYzPoTGYSiESFNJGtfhk69aSUH4qPG+YoKIc1Q58R+R+Hj8iJ5lYbuUArazKd/QkL9Tv2Lfqb9hpfGiSYzHh3hXxjv05/Di/e23GB8TB/Bxy4xw5YUbMfAwjRdJajw6Yun7KpaM37uWY/YbBDjpIICP023HxH47AV0ehJtLi4wfp0pR7H01M6OvwnGA/9Rf0v/xHTSeF2GWMviQ+PhzykoxJVcA1eZBKh5jvGxi47+oHnzULYCacyFN7is0Po9KRzG3Am7XbzopxFoeoZZyCY0foIrwIbcDZFPxzN+9qy2JZ/whZeADngJEP0k76gB1kOooOuwKIFn7B3LHHIJtp64TAAAAAElFTkSuQmCC", + "thumbnail": "data:image/jpeg;base64,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" + }, + { + "position": 5, + "title": "ChatGPT on the App Store - Apple", + "link": "https://apps.apple.com/us/app/chatgpt/id6448311069", + "source": "Apple", + "domain": "apps.apple.com", + "displayed_link": "https://apps.apple.com › app › chatgpt", + "snippet": "This official app is free, syncs your history across devices, and brings you the newest model improvements from OpenAI. With ChatGPT in your pocket ...", + "snippet_highlighted_words": ["ChatGPT"], + "rich_snippet": { + "detected_extensions": { + "rating": 4.9, + "reviews": 1026513 + }, + "extensions": ["Rating: 4.9", "1,026,513 reviews", "Free", "iOS", "Business/Productivity"] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC5UlEQVR4Aa1XQ3hkQRjc+ynX2OZtbfu+tm3b1nlt27a9O4qNS5xxbdd+cTKvXydT31fJoPuvmvf6/ejw86dBlX6CwwQXCq6t5cLaz/xV4+ld6F8r9NdgsCAjIwf5+UUoLCwBydf8jN+JNQbBddzjDQM+gocErRSyWm2QgWu4lntq9/q01UAfwYKCgmK43W6ognu4lzEE+6oamCboLC0tR3vBGIwlOF2vgZm5uQWoqamBXrhcLpw5cxZ79uxFKxCxrGBMxpYZ6Eu33KAXNDp+/AQEBgbzv8Y6Kxi7+e1ofuAKVS/7zp27KE7i6dNnem5HAbVaM3CYh0YF/PWRkdEUpxHoQe3BPNTcQJCgTc9pT0tLh8VigdPpBLFv3368evVKBC7A16/fkJmZKX06qCXo39jAej67Wnjx4iVGjBiJ0NBwBAeHYsCAgTh48BCuXLmCKVOmIioqBrwS4eGRGDduPMxmMzyBWtRsbMCglWSePXuOkJAwCuhmnz79YLVaPSUrGjDWGQhgCvWEyspKdOrURUk8JiYO799/0Exg1KQ2DQxjHveEO3fuKomTPBcyUJPaNLCQxcQTNm3arGzAYDBABmoK7UU0sE7rAC5dukxJPCgoRPy6DMhATWpLDWzbtl35Cty//0DBgOQW3LhxU9nAsGEj4HA4dN0CySHkwvy6bKfECRMmISsrS34IZY8hMXnyFAZV5rFjx6WPoa5E9PnzZ2XxpKQUlJaWaiUik1IqXrBgkZKB06fPwBOKiv4fwA3Ni5FdK3NVVFSgd+++usRnzJilXIzII7JynJOTAxaa7t17Yt68+bh37z6+fPmKCxcuYvToMejVqzdWrVrNMi0rx4cVGxIFKDQkCi2ZAhRaMklTavWqeF6epCltxuneasvLyurb8lmqg0lfLw4m/dozmh0RtBUV6R/NuJZ7avf6eGs4ZeIwMoVmZrYcTvkZv+MarlUZTlUZIDi8diRfX8uFtZ8FqMb7Bx+2VJbBTrlcAAAAAElFTkSuQmCC" + }, + { + "position": 6, + "title": "What is ChatGPT and why does it matter? Here's what you ...", + "link": "https://www.zdnet.com/article/what-is-chatgpt-and-why-does-it-matter-heres-everything-you-need-to-know/", + "source": "ZDNET", + "domain": "www.zdnet.com", + "displayed_link": "https://www.zdnet.com › ... › Artificial Intelligence", + "snippet": "ChatGPT is an AI chatbot with natural language processing (NLP) that allows you to have human-like conversations to complete various tasks. The ...", + "snippet_highlighted_words": ["ChatGPT"], + "date": "Jun 17, 2024", + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAMFBMVEXQ/0rQ/0vM+0oDAxDV/0yixjxlfCpTZiV/nDK75USTtTg7SR/F8Uiu1UAfJhhyjC65DF56AAAAAXRSTlP3Yz+/2QAAANRJREFUKJHdkkGSxCAIRaOAAore/7YDJNPVSVfPAeYtdPEK5FMex1/U8pV/JutMatwVH9JaouTH1ok3SWsEjWGNBXve5JQ5qS+XJLJB8V3iZK8AZhBEAb5JBVjNEFPaU65tIibR1saiZ2XgbzpDH1H2ZtWttB316SSpZ5TeWymNtSfK501X2wFWo23mVR7DE49f2VYrkdPONVaEXmq9JHVIGUQSl/ia1jxFyOYT2YeUletTIyJ5SEIf4WoLfJNCE73EhJKoJHv9hHjFyQNzeefp8jvHD3ZbC4DWezICAAAAAElFTkSuQmCC" + } + ], + "inline_images": { + "images": [ + { + "title": "upload.wikimedia.org/wikipedia/commons/e/ef/ChatGP...", + "source": { + "name": "en.wikipedia.org", + "link": "https://en.wikipedia.org/wiki/ChatGPT" + }, + "original": { + "link": "https://upload.wikimedia.org/wikipedia/commons/e/ef/ChatGPT-Logo.svg", + "height": 800, + "width": 800, + "size": "1KB" + }, + "thumbnail": "data:image/png;base64,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" + }, + { + "title": "Introducing ChatGPT | OpenAI", + "source": { + "name": "OpenAI", + "link": "https://openai.com/index/chatgpt/" + }, + "original": { + "link": "https://images.ctfassets.net/kftzwdyauwt9/40in10B8KtAGrQvwRv5cop/8241bb17c283dced48ea034a41d7464a/chatgpt_diagram_light.png?w=3840&q=90&fm=webp", + "height": 1153, + "width": 1940, + "size": "93KB" + }, + "thumbnail": "data:image/png;base64,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" + }, + { + "title": "What Is ChatGPT? Everything You Need to Know | TechTarget", + "source": { + "name": "TechTarget", + "link": "https://www.techtarget.com/whatis/definition/ChatGPT" + }, + "original": { + "link": "https://cdn.ttgtmedia.com/rms/onlineimages/chatgpt_screenshot-f_mobile.jpg", + "height": 252, + "width": 559, + "size": "12KB" + }, + "thumbnail": "data:image/jpeg;base64,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" + }, + { + "title": "ChatGPT Tutorial - A Crash Course on Chat GPT for Beginners", + "source": { + "name": "YouTube", + "link": "https://m.youtube.com/watch?v=JTxsNm9IdYU" + }, + "original": { + "link": "https://i.ytimg.com/vi/JTxsNm9IdYU/maxresdefault.jpg", + "height": 720, + "width": 1280, + "size": "134KB" + }, + "thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRIDD6PSH-o5_a4uY4vMZypbGD47mIWLL6VsTXNuADpOw&s" + }, + { + "title": "Introducing ChatGPT and Whisper APIs | OpenAI", + "source": { + "name": "OpenAI", + "link": "https://openai.com/index/introducing-chatgpt-and-whisper-apis/" + }, + "original": { + "link": "https://images.ctfassets.net/kftzwdyauwt9/44fefabe-41f8-4dbf-d80656c1f876/8dec20d14a894ae52ae07449452a89c5/introducing-chatgpt-and-whisper-apis.jpg?w=3840&q=90&fm=webp", + "height": 2048, + "width": 2048, + "size": "93KB" + }, + "thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ_IQLO0924Gl1jYnj0yWaeKwSWj8tbTbk0Jc6cAvQv6A&s" + } + ] + }, + "inline_videos": [ + { + "position": 1, + "title": "2 MINUTES AGO: OpenAI Just Released the Most Powerful ...", + "link": "https://www.youtube.com/watch?v=7idowVzHZ9g", + "source": "YouTube", + "channel": "AI Uncovered", + "date": "1 day ago", + "image": "data:image/jpeg;base64,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" + }, + { + "position": 2, + "title": "OpenAI Secretly Released a NEW ChatGPT Model and It’s ...", + "link": "https://www.youtube.com/watch?v=uh4baKXL6K4", + "source": "YouTube", + "channel": "Unveiling AI News", + "date": "21 hours ago", + "image": "data:image/jpeg;base64,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" + }, + { + "position": 3, + "title": "OpenAI's ChatGPT Does Research… And Breaks Itself!", + "link": "https://www.youtube.com/watch?v=iC-wRBsAhEs", + "source": "YouTube", + "channel": "Two Minute Papers", + "date": "2 days ago", + "image": "data:image/jpeg;base64,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" + } + ], + "inline_videos_more_link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&tbm=vid&q=chatgpt&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ8ccDegQIIhAH", + "related_searches": [ + { + "query": "ChatGPT login", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+login&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhUEAE" + }, + { + "query": "ChatGPT free", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+free&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhXEAE" + }, + { + "query": "ChatGPT 4", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+4&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhREAE" + }, + { + "query": "ChatGPT app", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+app&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhQEAE" + }, + { + "query": "ChatGPT download", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+download&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhPEAE" + }, + { + "query": "ChatGPT OpenAI", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+OpenAI&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhOEAE" + }, + { + "query": "ChatGPT website", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+website&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhVEAE" + }, + { + "query": "ChatGPT free online", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+free+online&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhWEAE" + } + ], + "pagination": { + "current": 1, + "next": "https://www.google.com/search?q=chatgpt&oq=chatgpt&gl=us&hl=en&start=10&ie=UTF-8" + } +} diff --git a/backend/apps/rag/search/testdata/searxng.json b/backend/open_webui/apps/rag/search/testdata/searxng.json similarity index 100% rename from backend/apps/rag/search/testdata/searxng.json rename to backend/open_webui/apps/rag/search/testdata/searxng.json diff --git a/backend/apps/rag/search/testdata/serper.json b/backend/open_webui/apps/rag/search/testdata/serper.json similarity index 100% rename from backend/apps/rag/search/testdata/serper.json rename to backend/open_webui/apps/rag/search/testdata/serper.json diff --git a/backend/apps/rag/search/testdata/serply.json b/backend/open_webui/apps/rag/search/testdata/serply.json similarity index 100% rename from backend/apps/rag/search/testdata/serply.json rename to backend/open_webui/apps/rag/search/testdata/serply.json diff --git a/backend/apps/rag/search/testdata/serpstack.json b/backend/open_webui/apps/rag/search/testdata/serpstack.json similarity index 100% rename from backend/apps/rag/search/testdata/serpstack.json rename to backend/open_webui/apps/rag/search/testdata/serpstack.json diff --git a/backend/apps/rag/utils.py b/backend/open_webui/apps/rag/utils.py similarity index 97% rename from backend/apps/rag/utils.py rename to backend/open_webui/apps/rag/utils.py index 82bead0126..2bf8a02e45 100644 --- a/backend/apps/rag/utils.py +++ b/backend/open_webui/apps/rag/utils.py @@ -1,27 +1,19 @@ -import os import logging +import os +from typing import Optional, Union + import requests - -from typing import Union - -from apps.ollama.main import ( - generate_ollama_embeddings, +from open_webui.apps.ollama.main import ( GenerateEmbeddingsForm, + generate_ollama_embeddings, ) - +from open_webui.config import CHROMA_CLIENT +from open_webui.env import SRC_LOG_LEVELS from huggingface_hub import snapshot_download - -from langchain_core.documents import Document +from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever from langchain_community.retrievers import BM25Retriever -from langchain.retrievers import ( - ContextualCompressionRetriever, - EnsembleRetriever, -) - -from typing import Optional - -from utils.misc import get_last_user_message, add_or_update_system_message -from config import SRC_LOG_LEVELS, CHROMA_CLIENT +from langchain_core.documents import Document +from open_webui.utils.misc import get_last_user_message log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) @@ -401,8 +393,8 @@ def generate_openai_batch_embeddings( from typing import Any -from langchain_core.retrievers import BaseRetriever from langchain_core.callbacks import CallbackManagerForRetrieverRun +from langchain_core.retrievers import BaseRetriever class ChromaRetriever(BaseRetriever): @@ -439,11 +431,10 @@ class ChromaRetriever(BaseRetriever): import operator - from typing import Optional, Sequence -from langchain_core.documents import BaseDocumentCompressor, Document from langchain_core.callbacks import Callbacks +from langchain_core.documents import BaseDocumentCompressor, Document from langchain_core.pydantic_v1 import Extra diff --git a/backend/apps/socket/main.py b/backend/open_webui/apps/socket/main.py similarity index 97% rename from backend/apps/socket/main.py rename to backend/open_webui/apps/socket/main.py index fcffca4209..5985bc5240 100644 --- a/backend/apps/socket/main.py +++ b/backend/open_webui/apps/socket/main.py @@ -1,9 +1,8 @@ -import socketio import asyncio - -from apps.webui.models.users import Users -from utils.utils import decode_token +import socketio +from open_webui.apps.webui.models.users import Users +from open_webui.utils.utils import decode_token sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi") app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io") diff --git a/backend/apps/webui/internal/db.py b/backend/open_webui/apps/webui/internal/db.py similarity index 86% rename from backend/apps/webui/internal/db.py rename to backend/open_webui/apps/webui/internal/db.py index db8df5ee58..82dba50318 100644 --- a/backend/apps/webui/internal/db.py +++ b/backend/open_webui/apps/webui/internal/db.py @@ -1,21 +1,16 @@ -import os -import logging import json +import logging from contextlib import contextmanager +from typing import Any, Optional - -from typing import Optional, Any -from typing_extensions import Self - -from sqlalchemy import create_engine, types, Dialect -from sqlalchemy.sql.type_api import _T -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, scoped_session - - +from open_webui.apps.webui.internal.wrappers import register_connection +from open_webui.env import OPEN_WEBUI_DIR, DATABASE_URL, SRC_LOG_LEVELS from peewee_migrate import Router -from apps.webui.internal.wrappers import register_connection -from env import SRC_LOG_LEVELS, BACKEND_DIR, DATABASE_URL +from sqlalchemy import Dialect, create_engine, types +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.sql.type_api import _T +from typing_extensions import Self log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["DB"]) @@ -50,7 +45,7 @@ def handle_peewee_migration(DATABASE_URL): try: # Replace the postgresql:// with postgres:// to handle the peewee migration db = register_connection(DATABASE_URL.replace("postgresql://", "postgres://")) - migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations" + migrate_dir = OPEN_WEBUI_DIR / "apps" / "webui" / "internal" / "migrations" router = Router(db, logger=log, migrate_dir=migrate_dir) router.run() db.close() diff --git a/backend/apps/webui/internal/migrations/001_initial_schema.py b/backend/open_webui/apps/webui/internal/migrations/001_initial_schema.py similarity index 100% rename from backend/apps/webui/internal/migrations/001_initial_schema.py rename to backend/open_webui/apps/webui/internal/migrations/001_initial_schema.py diff --git a/backend/apps/webui/internal/migrations/002_add_local_sharing.py b/backend/open_webui/apps/webui/internal/migrations/002_add_local_sharing.py similarity index 100% rename from backend/apps/webui/internal/migrations/002_add_local_sharing.py rename to backend/open_webui/apps/webui/internal/migrations/002_add_local_sharing.py diff --git a/backend/apps/webui/internal/migrations/003_add_auth_api_key.py b/backend/open_webui/apps/webui/internal/migrations/003_add_auth_api_key.py similarity index 100% rename from backend/apps/webui/internal/migrations/003_add_auth_api_key.py rename to backend/open_webui/apps/webui/internal/migrations/003_add_auth_api_key.py diff --git a/backend/apps/webui/internal/migrations/004_add_archived.py b/backend/open_webui/apps/webui/internal/migrations/004_add_archived.py similarity index 100% rename from backend/apps/webui/internal/migrations/004_add_archived.py rename to backend/open_webui/apps/webui/internal/migrations/004_add_archived.py diff --git a/backend/apps/webui/internal/migrations/005_add_updated_at.py b/backend/open_webui/apps/webui/internal/migrations/005_add_updated_at.py similarity index 100% rename from backend/apps/webui/internal/migrations/005_add_updated_at.py rename to backend/open_webui/apps/webui/internal/migrations/005_add_updated_at.py diff --git a/backend/apps/webui/internal/migrations/006_migrate_timestamps_and_charfields.py b/backend/open_webui/apps/webui/internal/migrations/006_migrate_timestamps_and_charfields.py similarity index 100% rename from backend/apps/webui/internal/migrations/006_migrate_timestamps_and_charfields.py rename to backend/open_webui/apps/webui/internal/migrations/006_migrate_timestamps_and_charfields.py diff --git a/backend/apps/webui/internal/migrations/007_add_user_last_active_at.py b/backend/open_webui/apps/webui/internal/migrations/007_add_user_last_active_at.py similarity index 100% rename from backend/apps/webui/internal/migrations/007_add_user_last_active_at.py rename to backend/open_webui/apps/webui/internal/migrations/007_add_user_last_active_at.py diff --git a/backend/apps/webui/internal/migrations/008_add_memory.py b/backend/open_webui/apps/webui/internal/migrations/008_add_memory.py similarity index 100% rename from backend/apps/webui/internal/migrations/008_add_memory.py rename to backend/open_webui/apps/webui/internal/migrations/008_add_memory.py diff --git a/backend/apps/webui/internal/migrations/009_add_models.py b/backend/open_webui/apps/webui/internal/migrations/009_add_models.py similarity index 100% rename from backend/apps/webui/internal/migrations/009_add_models.py rename to backend/open_webui/apps/webui/internal/migrations/009_add_models.py diff --git a/backend/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py b/backend/open_webui/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py similarity index 98% rename from backend/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py rename to backend/open_webui/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py index 2ef814c06b..322ddd44ec 100644 --- a/backend/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py +++ b/backend/open_webui/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py @@ -30,7 +30,7 @@ import peewee as pw from peewee_migrate import Migrator import json -from utils.misc import parse_ollama_modelfile +from open_webui.utils.misc import parse_ollama_modelfile with suppress(ImportError): import playhouse.postgres_ext as pw_pext diff --git a/backend/apps/webui/internal/migrations/011_add_user_settings.py b/backend/open_webui/apps/webui/internal/migrations/011_add_user_settings.py similarity index 100% rename from backend/apps/webui/internal/migrations/011_add_user_settings.py rename to backend/open_webui/apps/webui/internal/migrations/011_add_user_settings.py diff --git a/backend/apps/webui/internal/migrations/012_add_tools.py b/backend/open_webui/apps/webui/internal/migrations/012_add_tools.py similarity index 100% rename from backend/apps/webui/internal/migrations/012_add_tools.py rename to backend/open_webui/apps/webui/internal/migrations/012_add_tools.py diff --git a/backend/apps/webui/internal/migrations/013_add_user_info.py b/backend/open_webui/apps/webui/internal/migrations/013_add_user_info.py similarity index 100% rename from backend/apps/webui/internal/migrations/013_add_user_info.py rename to backend/open_webui/apps/webui/internal/migrations/013_add_user_info.py diff --git a/backend/apps/webui/internal/migrations/014_add_files.py b/backend/open_webui/apps/webui/internal/migrations/014_add_files.py similarity index 100% rename from backend/apps/webui/internal/migrations/014_add_files.py rename to backend/open_webui/apps/webui/internal/migrations/014_add_files.py diff --git a/backend/apps/webui/internal/migrations/015_add_functions.py b/backend/open_webui/apps/webui/internal/migrations/015_add_functions.py similarity index 100% rename from backend/apps/webui/internal/migrations/015_add_functions.py rename to backend/open_webui/apps/webui/internal/migrations/015_add_functions.py diff --git a/backend/apps/webui/internal/migrations/016_add_valves_and_is_active.py b/backend/open_webui/apps/webui/internal/migrations/016_add_valves_and_is_active.py similarity index 100% rename from backend/apps/webui/internal/migrations/016_add_valves_and_is_active.py rename to backend/open_webui/apps/webui/internal/migrations/016_add_valves_and_is_active.py diff --git a/backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py b/backend/open_webui/apps/webui/internal/migrations/017_add_user_oauth_sub.py similarity index 100% rename from backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py rename to backend/open_webui/apps/webui/internal/migrations/017_add_user_oauth_sub.py diff --git a/backend/apps/webui/internal/migrations/018_add_function_is_global.py b/backend/open_webui/apps/webui/internal/migrations/018_add_function_is_global.py similarity index 100% rename from backend/apps/webui/internal/migrations/018_add_function_is_global.py rename to backend/open_webui/apps/webui/internal/migrations/018_add_function_is_global.py diff --git a/backend/apps/webui/internal/wrappers.py b/backend/open_webui/apps/webui/internal/wrappers.py similarity index 93% rename from backend/apps/webui/internal/wrappers.py rename to backend/open_webui/apps/webui/internal/wrappers.py index 19523064af..ccc62b9a57 100644 --- a/backend/apps/webui/internal/wrappers.py +++ b/backend/open_webui/apps/webui/internal/wrappers.py @@ -1,13 +1,13 @@ -from contextvars import ContextVar -from peewee import * -from peewee import PostgresqlDatabase, InterfaceError as PeeWeeInterfaceError - import logging +from contextvars import ContextVar + +from open_webui.env import SRC_LOG_LEVELS +from peewee import * +from peewee import InterfaceError as PeeWeeInterfaceError +from peewee import PostgresqlDatabase from playhouse.db_url import connect, parse from playhouse.shortcuts import ReconnectMixin -from env import SRC_LOG_LEVELS - log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["DB"]) diff --git a/backend/apps/webui/main.py b/backend/open_webui/apps/webui/main.py similarity index 95% rename from backend/apps/webui/main.py rename to backend/open_webui/apps/webui/main.py index 00963def64..074b3144cf 100644 --- a/backend/apps/webui/main.py +++ b/backend/open_webui/apps/webui/main.py @@ -1,65 +1,62 @@ -from fastapi import FastAPI -from fastapi.responses import StreamingResponse -from fastapi.middleware.cors import CORSMiddleware -from apps.webui.routers import ( - auths, - users, - chats, - documents, - tools, - models, - prompts, - configs, - memories, - utils, - files, - functions, -) -from apps.webui.models.functions import Functions -from apps.webui.models.models import Models -from apps.webui.utils import load_function_module_by_id - -from utils.misc import ( - openai_chat_chunk_message_template, - openai_chat_completion_message_template, - apply_model_params_to_body_openai, - apply_model_system_prompt_to_body, -) - -from utils.tools import get_tools - -from config import ( - SHOW_ADMIN_DETAILS, - ADMIN_EMAIL, - WEBUI_AUTH, - DEFAULT_MODELS, - DEFAULT_PROMPT_SUGGESTIONS, - DEFAULT_USER_ROLE, - ENABLE_SIGNUP, - ENABLE_LOGIN_FORM, - USER_PERMISSIONS, - WEBHOOK_URL, - WEBUI_AUTH_TRUSTED_EMAIL_HEADER, - WEBUI_AUTH_TRUSTED_NAME_HEADER, - JWT_EXPIRES_IN, - WEBUI_BANNERS, - ENABLE_COMMUNITY_SHARING, - ENABLE_MESSAGE_RATING, - AppConfig, - OAUTH_USERNAME_CLAIM, - OAUTH_PICTURE_CLAIM, - OAUTH_EMAIL_CLAIM, - CORS_ALLOW_ORIGIN, -) - -from apps.socket.main import get_event_call, get_event_emitter - import inspect import json import logging +from typing import AsyncGenerator, Generator, Iterator -from typing import Iterator, Generator, AsyncGenerator +from open_webui.apps.socket.main import get_event_call, get_event_emitter +from open_webui.apps.webui.models.functions import Functions +from open_webui.apps.webui.models.models import Models +from open_webui.apps.webui.routers import ( + auths, + chats, + configs, + documents, + files, + functions, + memories, + models, + prompts, + tools, + users, + utils, +) +from open_webui.apps.webui.utils import load_function_module_by_id +from open_webui.config import ( + ADMIN_EMAIL, + CORS_ALLOW_ORIGIN, + DEFAULT_MODELS, + DEFAULT_PROMPT_SUGGESTIONS, + DEFAULT_USER_ROLE, + ENABLE_COMMUNITY_SHARING, + ENABLE_LOGIN_FORM, + ENABLE_MESSAGE_RATING, + ENABLE_SIGNUP, + JWT_EXPIRES_IN, + OAUTH_EMAIL_CLAIM, + OAUTH_PICTURE_CLAIM, + OAUTH_USERNAME_CLAIM, + SHOW_ADMIN_DETAILS, + USER_PERMISSIONS, + WEBHOOK_URL, + WEBUI_AUTH, + WEBUI_BANNERS, + AppConfig, +) +from open_webui.env import ( + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, +) +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse from pydantic import BaseModel +from open_webui.utils.misc import ( + apply_model_params_to_body_openai, + apply_model_system_prompt_to_body, + openai_chat_chunk_message_template, + openai_chat_completion_message_template, +) +from open_webui.utils.tools import get_tools app = FastAPI() diff --git a/backend/apps/webui/models/auths.py b/backend/open_webui/apps/webui/models/auths.py similarity index 94% rename from backend/apps/webui/models/auths.py rename to backend/open_webui/apps/webui/models/auths.py index 601c7c9a4c..167b9f6dcb 100644 --- a/backend/apps/webui/models/auths.py +++ b/backend/open_webui/apps/webui/models/auths.py @@ -1,15 +1,13 @@ -from pydantic import BaseModel -from typing import Optional -import uuid import logging -from sqlalchemy import String, Column, Boolean, Text +import uuid +from typing import Optional -from utils.utils import verify_password - -from apps.webui.models.users import UserModel, Users -from apps.webui.internal.db import Base, get_db - -from env import SRC_LOG_LEVELS +from open_webui.apps.webui.internal.db import Base, get_db +from open_webui.apps.webui.models.users import UserModel, Users +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel +from sqlalchemy import Boolean, Column, String, Text +from open_webui.utils.utils import verify_password log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -92,7 +90,6 @@ class AddUserForm(SignupForm): class AuthsTable: - def insert_new_auth( self, email: str, @@ -103,7 +100,6 @@ class AuthsTable: oauth_sub: Optional[str] = None, ) -> Optional[UserModel]: with get_db() as db: - log.info("insert_new_auth") id = str(uuid.uuid4()) @@ -130,7 +126,6 @@ class AuthsTable: log.info(f"authenticate_user: {email}") try: with get_db() as db: - auth = db.query(Auth).filter_by(email=email, active=True).first() if auth: if verify_password(password, auth.password): @@ -189,7 +184,6 @@ class AuthsTable: def delete_auth_by_id(self, id: str) -> bool: try: with get_db() as db: - # Delete User result = Users.delete_user_by_id(id) diff --git a/backend/apps/webui/models/chats.py b/backend/open_webui/apps/webui/models/chats.py similarity index 98% rename from backend/apps/webui/models/chats.py rename to backend/open_webui/apps/webui/models/chats.py index 164be06466..f364dcc700 100644 --- a/backend/apps/webui/models/chats.py +++ b/backend/open_webui/apps/webui/models/chats.py @@ -1,14 +1,11 @@ -from pydantic import BaseModel, ConfigDict -from typing import Union, Optional - import json -import uuid import time +import uuid +from typing import Optional -from sqlalchemy import Column, String, BigInteger, Boolean, Text - -from apps.webui.internal.db import Base, get_db - +from open_webui.apps.webui.internal.db import Base, get_db +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Boolean, Column, String, Text #################### # Chat DB Schema @@ -77,10 +74,8 @@ class ChatTitleIdResponse(BaseModel): class ChatTable: - def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]: with get_db() as db: - id = str(uuid.uuid4()) chat = ChatModel( **{ @@ -106,7 +101,6 @@ class ChatTable: def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: try: with get_db() as db: - chat_obj = db.get(Chat, id) chat_obj.chat = json.dumps(chat) chat_obj.title = chat["title"] if "title" in chat else "New Chat" @@ -115,12 +109,11 @@ class ChatTable: db.refresh(chat_obj) return ChatModel.model_validate(chat_obj) - except Exception as e: + except Exception: return None def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: with get_db() as db: - # Get the existing chat to share chat = db.get(Chat, chat_id) # Check if the chat is already shared @@ -154,7 +147,6 @@ class ChatTable: def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: try: with get_db() as db: - print("update_shared_chat_by_id") chat = db.get(Chat, chat_id) print(chat) @@ -170,7 +162,6 @@ class ChatTable: def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool: try: with get_db() as db: - db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete() db.commit() @@ -183,7 +174,6 @@ class ChatTable: ) -> Optional[ChatModel]: try: with get_db() as db: - chat = db.get(Chat, id) chat.share_id = share_id db.commit() @@ -195,7 +185,6 @@ class ChatTable: def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]: try: with get_db() as db: - chat = db.get(Chat, id) chat.archived = not chat.archived db.commit() @@ -217,7 +206,6 @@ class ChatTable: self, user_id: str, skip: int = 0, limit: int = 50 ) -> list[ChatModel]: with get_db() as db: - all_chats = ( db.query(Chat) .filter_by(user_id=user_id, archived=True) @@ -297,7 +285,6 @@ class ChatTable: def get_chat_by_id(self, id: str) -> Optional[ChatModel]: try: with get_db() as db: - chat = db.get(Chat, id) return ChatModel.model_validate(chat) except Exception: @@ -306,20 +293,18 @@ class ChatTable: def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]: try: with get_db() as db: - chat = db.query(Chat).filter_by(share_id=id).first() if chat: return self.get_chat_by_id(id) else: return None - except Exception as e: + except Exception: return None def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: try: with get_db() as db: - chat = db.query(Chat).filter_by(id=id, user_id=user_id).first() return ChatModel.model_validate(chat) except Exception: @@ -327,7 +312,6 @@ class ChatTable: def get_chats(self, skip: int = 0, limit: int = 50) -> list[ChatModel]: with get_db() as db: - all_chats = ( db.query(Chat) # .limit(limit).offset(skip) @@ -337,7 +321,6 @@ class ChatTable: def get_chats_by_user_id(self, user_id: str) -> list[ChatModel]: with get_db() as db: - all_chats = ( db.query(Chat) .filter_by(user_id=user_id) @@ -347,7 +330,6 @@ class ChatTable: def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]: with get_db() as db: - all_chats = ( db.query(Chat) .filter_by(user_id=user_id, archived=True) @@ -358,7 +340,6 @@ class ChatTable: def delete_chat_by_id(self, id: str) -> bool: try: with get_db() as db: - db.query(Chat).filter_by(id=id).delete() db.commit() @@ -369,7 +350,6 @@ class ChatTable: def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool: try: with get_db() as db: - db.query(Chat).filter_by(id=id, user_id=user_id).delete() db.commit() @@ -379,9 +359,7 @@ class ChatTable: def delete_chats_by_user_id(self, user_id: str) -> bool: try: - with get_db() as db: - self.delete_shared_chats_by_user_id(user_id) db.query(Chat).filter_by(user_id=user_id).delete() @@ -393,9 +371,7 @@ class ChatTable: def delete_shared_chats_by_user_id(self, user_id: str) -> bool: try: - with get_db() as db: - chats_by_user = db.query(Chat).filter_by(user_id=user_id).all() shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user] diff --git a/backend/apps/webui/models/documents.py b/backend/open_webui/apps/webui/models/documents.py similarity index 96% rename from backend/apps/webui/models/documents.py rename to backend/open_webui/apps/webui/models/documents.py index 15dd636630..0b96c25744 100644 --- a/backend/apps/webui/models/documents.py +++ b/backend/open_webui/apps/webui/models/documents.py @@ -1,15 +1,12 @@ -from pydantic import BaseModel, ConfigDict -from typing import Optional -import time -import logging - -from sqlalchemy import String, Column, BigInteger, Text - -from apps.webui.internal.db import Base, get_db - import json +import logging +import time +from typing import Optional -from env import SRC_LOG_LEVELS +from open_webui.apps.webui.internal.db import Base, get_db +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -70,12 +67,10 @@ class DocumentForm(DocumentUpdateForm): class DocumentsTable: - def insert_new_doc( self, user_id: str, form_data: DocumentForm ) -> Optional[DocumentModel]: with get_db() as db: - document = DocumentModel( **{ **form_data.model_dump(), @@ -99,7 +94,6 @@ class DocumentsTable: def get_doc_by_name(self, name: str) -> Optional[DocumentModel]: try: with get_db() as db: - document = db.query(Document).filter_by(name=name).first() return DocumentModel.model_validate(document) if document else None except Exception: @@ -107,7 +101,6 @@ class DocumentsTable: def get_docs(self) -> list[DocumentModel]: with get_db() as db: - return [ DocumentModel.model_validate(doc) for doc in db.query(Document).all() ] @@ -117,7 +110,6 @@ class DocumentsTable: ) -> Optional[DocumentModel]: try: with get_db() as db: - db.query(Document).filter_by(name=name).update( { "title": form_data.title, @@ -140,7 +132,6 @@ class DocumentsTable: doc_content = {**doc_content, **updated} with get_db() as db: - db.query(Document).filter_by(name=name).update( { "content": json.dumps(doc_content), @@ -156,7 +147,6 @@ class DocumentsTable: def delete_doc_by_name(self, name: str) -> bool: try: with get_db() as db: - db.query(Document).filter_by(name=name).delete() db.commit() return True diff --git a/backend/apps/webui/models/files.py b/backend/open_webui/apps/webui/models/files.py similarity index 93% rename from backend/apps/webui/models/files.py rename to backend/open_webui/apps/webui/models/files.py index 1b71751244..7fba74479d 100644 --- a/backend/apps/webui/models/files.py +++ b/backend/open_webui/apps/webui/models/files.py @@ -1,15 +1,11 @@ -from pydantic import BaseModel, ConfigDict -from typing import Union, Optional -import time import logging +import time +from typing import Optional -from sqlalchemy import Column, String, BigInteger, Text - -from apps.webui.internal.db import JSONField, Base, get_db - -import json - -from env import SRC_LOG_LEVELS +from open_webui.apps.webui.internal.db import Base, JSONField, get_db +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -59,10 +55,8 @@ class FileForm(BaseModel): class FilesTable: - def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]: with get_db() as db: - file = FileModel( **{ **form_data.model_dump(), @@ -86,7 +80,6 @@ class FilesTable: def get_file_by_id(self, id: str) -> Optional[FileModel]: with get_db() as db: - try: file = db.get(File, id) return FileModel.model_validate(file) @@ -95,7 +88,6 @@ class FilesTable: def get_files(self) -> list[FileModel]: with get_db() as db: - return [FileModel.model_validate(file) for file in db.query(File).all()] def get_files_by_user_id(self, user_id: str) -> list[FileModel]: @@ -106,9 +98,7 @@ class FilesTable: ] def delete_file_by_id(self, id: str) -> bool: - with get_db() as db: - try: db.query(File).filter_by(id=id).delete() db.commit() @@ -118,9 +108,7 @@ class FilesTable: return False def delete_all_files(self) -> bool: - with get_db() as db: - try: db.query(File).delete() db.commit() diff --git a/backend/apps/webui/models/functions.py b/backend/open_webui/apps/webui/models/functions.py similarity index 96% rename from backend/apps/webui/models/functions.py rename to backend/open_webui/apps/webui/models/functions.py index 10d8111485..fda1550750 100644 --- a/backend/apps/webui/models/functions.py +++ b/backend/open_webui/apps/webui/models/functions.py @@ -1,18 +1,12 @@ -from pydantic import BaseModel, ConfigDict -from typing import Union, Optional -import time import logging +import time +from typing import Optional -from sqlalchemy import Column, String, Text, BigInteger, Boolean - -from apps.webui.internal.db import JSONField, Base, get_db -from apps.webui.models.users import Users - -import json -import copy - - -from env import SRC_LOG_LEVELS +from open_webui.apps.webui.internal.db import Base, JSONField, get_db +from open_webui.apps.webui.models.users import Users +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Boolean, Column, String, Text log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -87,11 +81,9 @@ class FunctionValves(BaseModel): class FunctionsTable: - def insert_new_function( self, user_id: str, type: str, form_data: FunctionForm ) -> Optional[FunctionModel]: - function = FunctionModel( **{ **form_data.model_dump(), @@ -119,7 +111,6 @@ class FunctionsTable: def get_function_by_id(self, id: str) -> Optional[FunctionModel]: try: with get_db() as db: - function = db.get(Function, id) return FunctionModel.model_validate(function) except Exception: @@ -127,7 +118,6 @@ class FunctionsTable: def get_functions(self, active_only=False) -> list[FunctionModel]: with get_db() as db: - if active_only: return [ FunctionModel.model_validate(function) @@ -143,7 +133,6 @@ class FunctionsTable: self, type: str, active_only=False ) -> list[FunctionModel]: with get_db() as db: - if active_only: return [ FunctionModel.model_validate(function) @@ -159,7 +148,6 @@ class FunctionsTable: def get_global_filter_functions(self) -> list[FunctionModel]: with get_db() as db: - return [ FunctionModel.model_validate(function) for function in db.query(Function) @@ -178,7 +166,6 @@ class FunctionsTable: def get_function_valves_by_id(self, id: str) -> Optional[dict]: with get_db() as db: - try: function = db.get(Function, id) return function.valves if function.valves else {} @@ -190,7 +177,6 @@ class FunctionsTable: self, id: str, valves: dict ) -> Optional[FunctionValves]: with get_db() as db: - try: function = db.get(Function, id) function.valves = valves @@ -204,7 +190,6 @@ class FunctionsTable: def get_user_valves_by_id_and_user_id( self, id: str, user_id: str ) -> Optional[dict]: - try: user = Users.get_user_by_id(user_id) user_settings = user.settings.model_dump() if user.settings else {} @@ -223,7 +208,6 @@ class FunctionsTable: def update_user_valves_by_id_and_user_id( self, id: str, user_id: str, valves: dict ) -> Optional[dict]: - try: user = Users.get_user_by_id(user_id) user_settings = user.settings.model_dump() if user.settings else {} @@ -246,7 +230,6 @@ class FunctionsTable: def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]: with get_db() as db: - try: db.query(Function).filter_by(id=id).update( { @@ -261,7 +244,6 @@ class FunctionsTable: def deactivate_all_functions(self) -> Optional[bool]: with get_db() as db: - try: db.query(Function).update( { diff --git a/backend/apps/webui/models/memories.py b/backend/open_webui/apps/webui/models/memories.py similarity index 96% rename from backend/apps/webui/models/memories.py rename to backend/open_webui/apps/webui/models/memories.py index 41bb11ccf4..6686058d36 100644 --- a/backend/apps/webui/models/memories.py +++ b/backend/open_webui/apps/webui/models/memories.py @@ -1,12 +1,10 @@ -from pydantic import BaseModel, ConfigDict -from typing import Union, Optional - -from sqlalchemy import Column, String, BigInteger, Text - -from apps.webui.internal.db import Base, get_db - import time import uuid +from typing import Optional + +from open_webui.apps.webui.internal.db import Base, get_db +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text #################### # Memory DB Schema @@ -39,13 +37,11 @@ class MemoryModel(BaseModel): class MemoriesTable: - def insert_new_memory( self, user_id: str, content: str, ) -> Optional[MemoryModel]: - with get_db() as db: id = str(uuid.uuid4()) @@ -73,7 +69,6 @@ class MemoriesTable: content: str, ) -> Optional[MemoryModel]: with get_db() as db: - try: db.query(Memory).filter_by(id=id).update( {"content": content, "updated_at": int(time.time())} @@ -85,7 +80,6 @@ class MemoriesTable: def get_memories(self) -> list[MemoryModel]: with get_db() as db: - try: memories = db.query(Memory).all() return [MemoryModel.model_validate(memory) for memory in memories] @@ -94,7 +88,6 @@ class MemoriesTable: def get_memories_by_user_id(self, user_id: str) -> list[MemoryModel]: with get_db() as db: - try: memories = db.query(Memory).filter_by(user_id=user_id).all() return [MemoryModel.model_validate(memory) for memory in memories] @@ -103,7 +96,6 @@ class MemoriesTable: def get_memory_by_id(self, id: str) -> Optional[MemoryModel]: with get_db() as db: - try: memory = db.get(Memory, id) return MemoryModel.model_validate(memory) @@ -112,7 +104,6 @@ class MemoriesTable: def delete_memory_by_id(self, id: str) -> bool: with get_db() as db: - try: db.query(Memory).filter_by(id=id).delete() db.commit() @@ -124,7 +115,6 @@ class MemoriesTable: def delete_memories_by_user_id(self, user_id: str) -> bool: with get_db() as db: - try: db.query(Memory).filter_by(user_id=user_id).delete() db.commit() @@ -135,7 +125,6 @@ class MemoriesTable: def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool: with get_db() as db: - try: db.query(Memory).filter_by(id=id, user_id=user_id).delete() db.commit() diff --git a/backend/apps/webui/models/models.py b/backend/open_webui/apps/webui/models/models.py similarity index 95% rename from backend/apps/webui/models/models.py rename to backend/open_webui/apps/webui/models/models.py index 0a36da9878..9bdffb9bcc 100644 --- a/backend/apps/webui/models/models.py +++ b/backend/open_webui/apps/webui/models/models.py @@ -1,14 +1,11 @@ import logging -from typing import Optional, List - -from pydantic import BaseModel, ConfigDict -from sqlalchemy import Column, BigInteger, Text - -from apps.webui.internal.db import Base, JSONField, get_db - -from env import SRC_LOG_LEVELS - import time +from typing import Optional + +from open_webui.apps.webui.internal.db import Base, JSONField, get_db +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, Text log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) diff --git a/backend/apps/webui/models/prompts.py b/backend/open_webui/apps/webui/models/prompts.py similarity index 94% rename from backend/apps/webui/models/prompts.py rename to backend/open_webui/apps/webui/models/prompts.py index 942f64a435..6b98e5c535 100644 --- a/backend/apps/webui/models/prompts.py +++ b/backend/open_webui/apps/webui/models/prompts.py @@ -1,12 +1,9 @@ -from pydantic import BaseModel, ConfigDict -from typing import Optional import time +from typing import Optional -from sqlalchemy import String, Column, BigInteger, Text - -from apps.webui.internal.db import Base, get_db - -import json +from open_webui.apps.webui.internal.db import Base, get_db +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text #################### # Prompts DB Schema @@ -45,7 +42,6 @@ class PromptForm(BaseModel): class PromptsTable: - def insert_new_prompt( self, user_id: str, form_data: PromptForm ) -> Optional[PromptModel]: @@ -61,7 +57,6 @@ class PromptsTable: try: with get_db() as db: - result = Prompt(**prompt.dict()) db.add(result) db.commit() @@ -70,13 +65,12 @@ class PromptsTable: return PromptModel.model_validate(result) else: return None - except Exception as e: + except Exception: return None def get_prompt_by_command(self, command: str) -> Optional[PromptModel]: try: with get_db() as db: - prompt = db.query(Prompt).filter_by(command=command).first() return PromptModel.model_validate(prompt) except Exception: @@ -84,7 +78,6 @@ class PromptsTable: def get_prompts(self) -> list[PromptModel]: with get_db() as db: - return [ PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all() ] @@ -94,7 +87,6 @@ class PromptsTable: ) -> Optional[PromptModel]: try: with get_db() as db: - prompt = db.query(Prompt).filter_by(command=command).first() prompt.title = form_data.title prompt.content = form_data.content @@ -107,7 +99,6 @@ class PromptsTable: def delete_prompt_by_command(self, command: str) -> bool: try: with get_db() as db: - db.query(Prompt).filter_by(command=command).delete() db.commit() diff --git a/backend/apps/webui/models/tags.py b/backend/open_webui/apps/webui/models/tags.py similarity index 97% rename from backend/apps/webui/models/tags.py rename to backend/open_webui/apps/webui/models/tags.py index 605cca2e79..985273ff1b 100644 --- a/backend/apps/webui/models/tags.py +++ b/backend/open_webui/apps/webui/models/tags.py @@ -1,16 +1,12 @@ -from pydantic import BaseModel, ConfigDict +import logging +import time +import uuid from typing import Optional -import json -import uuid -import time -import logging - -from sqlalchemy import String, Column, BigInteger, Text - -from apps.webui.internal.db import Base, get_db - -from env import SRC_LOG_LEVELS +from open_webui.apps.webui.internal.db import Base, get_db +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -77,10 +73,8 @@ class ChatTagsResponse(BaseModel): class TagTable: - def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]: with get_db() as db: - id = str(uuid.uuid4()) tag = TagModel(**{"id": id, "user_id": user_id, "name": name}) try: @@ -92,7 +86,7 @@ class TagTable: return TagModel.model_validate(result) else: return None - except Exception as e: + except Exception: return None def get_tag_by_name_and_user_id( @@ -102,7 +96,7 @@ class TagTable: with get_db() as db: tag = db.query(Tag).filter_by(name=name, user_id=user_id).first() return TagModel.model_validate(tag) - except Exception as e: + except Exception: return None def add_tag_to_chat( @@ -161,7 +155,6 @@ class TagTable: self, chat_id: str, user_id: str ) -> list[TagModel]: with get_db() as db: - tag_names = [ chat_id_tag.tag_name for chat_id_tag in ( @@ -186,7 +179,6 @@ class TagTable: self, tag_name: str, user_id: str ) -> list[ChatIdTagModel]: with get_db() as db: - return [ ChatIdTagModel.model_validate(chat_id_tag) for chat_id_tag in ( @@ -201,7 +193,6 @@ class TagTable: self, tag_name: str, user_id: str ) -> int: with get_db() as db: - return ( db.query(ChatIdTag) .filter_by(tag_name=tag_name, user_id=user_id) @@ -236,7 +227,6 @@ class TagTable: ) -> bool: try: with get_db() as db: - res = ( db.query(ChatIdTag) .filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id) diff --git a/backend/apps/webui/models/tools.py b/backend/open_webui/apps/webui/models/tools.py similarity index 96% rename from backend/apps/webui/models/tools.py rename to backend/open_webui/apps/webui/models/tools.py index 2f4c532b86..e06f83452b 100644 --- a/backend/apps/webui/models/tools.py +++ b/backend/open_webui/apps/webui/models/tools.py @@ -1,17 +1,12 @@ -from pydantic import BaseModel, ConfigDict -from typing import Optional -import time import logging -from sqlalchemy import String, Column, BigInteger, Text +import time +from typing import Optional -from apps.webui.internal.db import Base, JSONField, get_db -from apps.webui.models.users import Users - -import json -import copy - - -from env import SRC_LOG_LEVELS +from open_webui.apps.webui.internal.db import Base, JSONField, get_db +from open_webui.apps.webui.models.users import Users +from open_webui.env import SRC_LOG_LEVELS +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -79,13 +74,10 @@ class ToolValves(BaseModel): class ToolsTable: - def insert_new_tool( self, user_id: str, form_data: ToolForm, specs: list[dict] ) -> Optional[ToolModel]: - with get_db() as db: - tool = ToolModel( **{ **form_data.model_dump(), @@ -112,7 +104,6 @@ class ToolsTable: def get_tool_by_id(self, id: str) -> Optional[ToolModel]: try: with get_db() as db: - tool = db.get(Tool, id) return ToolModel.model_validate(tool) except Exception: @@ -125,7 +116,6 @@ class ToolsTable: def get_tool_valves_by_id(self, id: str) -> Optional[dict]: try: with get_db() as db: - tool = db.get(Tool, id) return tool.valves if tool.valves else {} except Exception as e: @@ -135,7 +125,6 @@ class ToolsTable: def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]: try: with get_db() as db: - db.query(Tool).filter_by(id=id).update( {"valves": valves, "updated_at": int(time.time())} ) diff --git a/backend/apps/webui/models/users.py b/backend/open_webui/apps/webui/models/users.py similarity index 96% rename from backend/apps/webui/models/users.py rename to backend/open_webui/apps/webui/models/users.py index b6e85e2ca2..328618a671 100644 --- a/backend/apps/webui/models/users.py +++ b/backend/open_webui/apps/webui/models/users.py @@ -1,11 +1,10 @@ -from pydantic import BaseModel, ConfigDict -from typing import Optional import time +from typing import Optional -from sqlalchemy import String, Column, BigInteger, Text - -from apps.webui.internal.db import Base, JSONField, get_db -from apps.webui.models.chats import Chats +from open_webui.apps.webui.internal.db import Base, JSONField, get_db +from open_webui.apps.webui.models.chats import Chats +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text #################### # User DB Schema @@ -113,7 +112,7 @@ class UsersTable: with get_db() as db: user = db.query(User).filter_by(id=id).first() return UserModel.model_validate(user) - except Exception as e: + except Exception: return None def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]: @@ -221,7 +220,7 @@ class UsersTable: user = db.query(User).filter_by(id=id).first() return UserModel.model_validate(user) # return UserModel(**user.dict()) - except Exception as e: + except Exception: return None def delete_user_by_id(self, id: str) -> bool: @@ -255,7 +254,7 @@ class UsersTable: with get_db() as db: user = db.query(User).filter_by(id=id).first() return user.api_key - except Exception as e: + except Exception: return None diff --git a/backend/apps/webui/routers/auths.py b/backend/open_webui/apps/webui/routers/auths.py similarity index 96% rename from backend/apps/webui/routers/auths.py rename to backend/open_webui/apps/webui/routers/auths.py index 8909b1e059..2366841e17 100644 --- a/backend/apps/webui/routers/auths.py +++ b/backend/open_webui/apps/webui/routers/auths.py @@ -1,43 +1,36 @@ -import logging - -from fastapi import Request, UploadFile, File -from fastapi import Depends, HTTPException, status -from fastapi.responses import Response - -from fastapi import APIRouter -from pydantic import BaseModel import re import uuid -import csv -from apps.webui.models.auths import ( - SigninForm, - SignupForm, +from open_webui.apps.webui.models.auths import ( AddUserForm, - UpdateProfileForm, - UpdatePasswordForm, - UserResponse, - SigninResponse, - Auths, ApiKey, + Auths, + SigninForm, + SigninResponse, + SignupForm, + UpdatePasswordForm, + UpdateProfileForm, + UserResponse, ) -from apps.webui.models.users import Users - -from utils.utils import ( - get_password_hash, - get_current_user, - get_admin_user, - create_token, - create_api_key, -) -from utils.misc import parse_duration, validate_email_format -from utils.webhook import post_webhook -from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES -from config import ( - WEBUI_AUTH, +from open_webui.apps.webui.models.users import Users +from open_webui.config import WEBUI_AUTH +from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES +from open_webui.env import ( WEBUI_AUTH_TRUSTED_EMAIL_HEADER, WEBUI_AUTH_TRUSTED_NAME_HEADER, ) +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import Response +from pydantic import BaseModel +from open_webui.utils.misc import parse_duration, validate_email_format +from open_webui.utils.utils import ( + create_api_key, + create_token, + get_admin_user, + get_current_user, + get_password_hash, +) +from open_webui.utils.webhook import post_webhook router = APIRouter() @@ -273,7 +266,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm): @router.post("/add", response_model=SigninResponse) async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)): - if not validate_email_format(form_data.email.lower()): raise HTTPException( status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT @@ -283,7 +275,6 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)): raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) try: - print(form_data) hashed = get_password_hash(form_data.password) user = Auths.insert_new_auth( diff --git a/backend/apps/webui/routers/chats.py b/backend/open_webui/apps/webui/routers/chats.py similarity index 96% rename from backend/apps/webui/routers/chats.py rename to backend/open_webui/apps/webui/routers/chats.py index 6621e73372..21f95d9fe8 100644 --- a/backend/apps/webui/routers/chats.py +++ b/backend/open_webui/apps/webui/routers/chats.py @@ -1,34 +1,25 @@ -from fastapi import Depends, Request, HTTPException, status -from datetime import datetime, timedelta -from typing import Union, Optional -from utils.utils import get_verified_user, get_admin_user -from fastapi import APIRouter -from pydantic import BaseModel import json import logging +from typing import Optional -from apps.webui.models.users import Users -from apps.webui.models.chats import ( - ChatModel, - ChatResponse, - ChatTitleForm, +from open_webui.apps.webui.models.chats import ( ChatForm, - ChatTitleIdResponse, + ChatResponse, Chats, + ChatTitleIdResponse, ) - - -from apps.webui.models.tags import ( - TagModel, - ChatIdTagModel, +from open_webui.apps.webui.models.tags import ( ChatIdTagForm, - ChatTagsResponse, + ChatIdTagModel, + TagModel, Tags, ) - -from constants import ERROR_MESSAGES - -from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_CHAT_ACCESS +from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import APIRouter, Depends, HTTPException, Request, status +from pydantic import BaseModel +from open_webui.utils.utils import get_admin_user, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -61,7 +52,6 @@ async def get_session_user_chat_list( @router.delete("/", response_model=bool) async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)): - if ( user.role == "user" and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"] @@ -220,7 +210,6 @@ class TagNameForm(BaseModel): async def get_user_chat_list_by_tag_name( form_data: TagNameForm, user=Depends(get_verified_user) ): - chat_ids = [ chat_id_tag.chat_id for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id( @@ -299,7 +288,6 @@ async def update_chat_by_id( @router.delete("/{id}", response_model=bool) async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)): - if user.role == "admin": result = Chats.delete_chat_by_id(id) return result @@ -323,7 +311,6 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified async def clone_chat_by_id(id: str, user=Depends(get_verified_user)): chat = Chats.get_chat_by_id_and_user_id(id, user.id) if chat: - chat_body = json.loads(chat.chat) updated_chat = { **chat_body, diff --git a/backend/apps/webui/routers/configs.py b/backend/open_webui/apps/webui/routers/configs.py similarity index 69% rename from backend/apps/webui/routers/configs.py rename to backend/open_webui/apps/webui/routers/configs.py index 68c6873742..1c30b0b3bb 100644 --- a/backend/apps/webui/routers/configs.py +++ b/backend/open_webui/apps/webui/routers/configs.py @@ -1,29 +1,39 @@ -from fastapi import Response, Request -from fastapi import Depends, FastAPI, HTTPException, status -from datetime import datetime, timedelta -from typing import Union - -from fastapi import APIRouter +from open_webui.config import BannerModel +from fastapi import APIRouter, Depends, Request from pydantic import BaseModel -import time -import uuid +from open_webui.utils.utils import get_admin_user, get_verified_user -from config import BannerModel -from apps.webui.models.users import Users - -from utils.utils import ( - get_password_hash, - get_verified_user, - get_admin_user, - create_token, -) -from utils.misc import get_gravatar_url, validate_email_format -from constants import ERROR_MESSAGES +from open_webui.config import get_config, save_config router = APIRouter() +############################ +# ImportConfig +############################ + + +class ImportConfigForm(BaseModel): + config: dict + + +@router.post("/import", response_model=dict) +async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user)): + save_config(form_data.config) + return get_config() + + +############################ +# ExportConfig +############################ + + +@router.get("/export", response_model=dict) +async def export_config(user=Depends(get_admin_user)): + return get_config() + + class SetDefaultModelsForm(BaseModel): models: str diff --git a/backend/apps/webui/routers/documents.py b/backend/open_webui/apps/webui/routers/documents.py similarity index 92% rename from backend/apps/webui/routers/documents.py rename to backend/open_webui/apps/webui/routers/documents.py index 3bb2aa15b5..c8f27852f4 100644 --- a/backend/apps/webui/routers/documents.py +++ b/backend/open_webui/apps/webui/routers/documents.py @@ -1,21 +1,16 @@ -from fastapi import Depends, FastAPI, HTTPException, status -from datetime import datetime, timedelta -from typing import Union, Optional - -from fastapi import APIRouter -from pydantic import BaseModel import json +from typing import Optional -from apps.webui.models.documents import ( - Documents, +from open_webui.apps.webui.models.documents import ( DocumentForm, - DocumentUpdateForm, - DocumentModel, DocumentResponse, + Documents, + DocumentUpdateForm, ) - -from utils.utils import get_verified_user, get_admin_user -from constants import ERROR_MESSAGES +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel +from open_webui.utils.utils import get_admin_user, get_verified_user router = APIRouter() diff --git a/backend/apps/webui/routers/files.py b/backend/open_webui/apps/webui/routers/files.py similarity index 90% rename from backend/apps/webui/routers/files.py rename to backend/open_webui/apps/webui/routers/files.py index 48ca366d88..1a326bcd8c 100644 --- a/backend/apps/webui/routers/files.py +++ b/backend/open_webui/apps/webui/routers/files.py @@ -1,42 +1,17 @@ -from fastapi import ( - Depends, - FastAPI, - HTTPException, - status, - Request, - UploadFile, - File, - Form, -) - - -from datetime import datetime, timedelta -from typing import Union, Optional -from pathlib import Path - -from fastapi import APIRouter -from fastapi.responses import StreamingResponse, JSONResponse, FileResponse - -from pydantic import BaseModel -import json - -from apps.webui.models.files import ( - Files, - FileForm, - FileModel, - FileModelResponse, -) -from utils.utils import get_verified_user, get_admin_user -from constants import ERROR_MESSAGES - -from importlib import util +import logging import os +import shutil import uuid -import os, shutil, logging, re - - -from config import SRC_LOG_LEVELS, UPLOAD_DIR +from pathlib import Path +from typing import Optional +from open_webui.apps.webui.models.files import FileForm, FileModel, Files +from open_webui.config import UPLOAD_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status +from fastapi.responses import FileResponse +from open_webui.utils.utils import get_admin_user, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) diff --git a/backend/apps/webui/routers/functions.py b/backend/open_webui/apps/webui/routers/functions.py similarity index 96% rename from backend/apps/webui/routers/functions.py rename to backend/open_webui/apps/webui/routers/functions.py index f40d282645..c0ec32c90a 100644 --- a/backend/apps/webui/routers/functions.py +++ b/backend/open_webui/apps/webui/routers/functions.py @@ -1,27 +1,18 @@ -from fastapi import Depends, FastAPI, HTTPException, status, Request -from datetime import datetime, timedelta -from typing import Union, Optional +import os +from pathlib import Path +from typing import Optional -from fastapi import APIRouter -from pydantic import BaseModel -import json - -from apps.webui.models.functions import ( - Functions, +from open_webui.apps.webui.models.functions import ( FunctionForm, FunctionModel, FunctionResponse, + Functions, ) -from apps.webui.utils import load_function_module_by_id -from utils.utils import get_verified_user, get_admin_user -from constants import ERROR_MESSAGES - -from importlib import util -import os -from pathlib import Path - -from config import DATA_DIR, CACHE_DIR, FUNCTIONS_DIR - +from open_webui.apps.webui.utils import load_function_module_by_id +from open_webui.config import CACHE_DIR, FUNCTIONS_DIR +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.utils.utils import get_admin_user, get_verified_user router = APIRouter() @@ -304,7 +295,6 @@ async def update_function_valves_by_id( ): function = Functions.get_function_by_id(id) if function: - if id in request.app.state.FUNCTIONS: function_module = request.app.state.FUNCTIONS[id] else: diff --git a/backend/apps/webui/routers/memories.py b/backend/open_webui/apps/webui/routers/memories.py similarity index 92% rename from backend/apps/webui/routers/memories.py rename to backend/open_webui/apps/webui/routers/memories.py index ae0a9efcb2..914b69e7e1 100644 --- a/backend/apps/webui/routers/memories.py +++ b/backend/open_webui/apps/webui/routers/memories.py @@ -1,18 +1,12 @@ -from fastapi import Response, Request -from fastapi import Depends, FastAPI, HTTPException, status -from datetime import datetime, timedelta -from typing import Union, Optional - -from fastapi import APIRouter -from pydantic import BaseModel import logging +from typing import Optional -from apps.webui.models.memories import Memories, MemoryModel - -from utils.utils import get_verified_user -from constants import ERROR_MESSAGES - -from config import SRC_LOG_LEVELS, CHROMA_CLIENT +from open_webui.apps.webui.models.memories import Memories, MemoryModel +from open_webui.config import CHROMA_CLIENT +from open_webui.env import SRC_LOG_LEVELS +from fastapi import APIRouter, Depends, HTTPException, Request +from pydantic import BaseModel +from open_webui.utils.utils import get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) diff --git a/backend/apps/webui/routers/models.py b/backend/open_webui/apps/webui/routers/models.py similarity index 86% rename from backend/apps/webui/routers/models.py rename to backend/open_webui/apps/webui/routers/models.py index 8faeed7a64..a99c65d760 100644 --- a/backend/apps/webui/routers/models.py +++ b/backend/open_webui/apps/webui/routers/models.py @@ -1,15 +1,14 @@ -from fastapi import Depends, FastAPI, HTTPException, status, Request -from datetime import datetime, timedelta -from typing import Union, Optional +from typing import Optional -from fastapi import APIRouter -from pydantic import BaseModel -import json - -from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse - -from utils.utils import get_verified_user, get_admin_user -from constants import ERROR_MESSAGES +from open_webui.apps.webui.models.models import ( + ModelForm, + ModelModel, + ModelResponse, + Models, +) +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.utils.utils import get_admin_user, get_verified_user router = APIRouter() diff --git a/backend/apps/webui/routers/prompts.py b/backend/open_webui/apps/webui/routers/prompts.py similarity index 85% rename from backend/apps/webui/routers/prompts.py rename to backend/open_webui/apps/webui/routers/prompts.py index 39d79362af..593c643b97 100644 --- a/backend/apps/webui/routers/prompts.py +++ b/backend/open_webui/apps/webui/routers/prompts.py @@ -1,15 +1,9 @@ -from fastapi import Depends, FastAPI, HTTPException, status -from datetime import datetime, timedelta -from typing import Union, Optional +from typing import Optional -from fastapi import APIRouter -from pydantic import BaseModel -import json - -from apps.webui.models.prompts import Prompts, PromptForm, PromptModel - -from utils.utils import get_verified_user, get_admin_user -from constants import ERROR_MESSAGES +from open_webui.apps.webui.models.prompts import PromptForm, PromptModel, Prompts +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, status +from open_webui.utils.utils import get_admin_user, get_verified_user router = APIRouter() diff --git a/backend/apps/webui/routers/tools.py b/backend/open_webui/apps/webui/routers/tools.py similarity index 96% rename from backend/apps/webui/routers/tools.py rename to backend/open_webui/apps/webui/routers/tools.py index d6da7ae922..eece5e78ed 100644 --- a/backend/apps/webui/routers/tools.py +++ b/backend/open_webui/apps/webui/routers/tools.py @@ -1,20 +1,14 @@ -from fastapi import Depends, HTTPException, status, Request -from typing import Optional - -from fastapi import APIRouter - -from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse -from apps.webui.utils import load_toolkit_module_by_id - -from utils.utils import get_admin_user, get_verified_user -from utils.tools import get_tools_specs -from constants import ERROR_MESSAGES - import os from pathlib import Path +from typing import Optional -from config import DATA_DIR, CACHE_DIR - +from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools +from open_webui.apps.webui.utils import load_toolkit_module_by_id +from open_webui.config import CACHE_DIR, DATA_DIR +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.utils.tools import get_tools_specs +from open_webui.utils.utils import get_admin_user, get_verified_user TOOLS_DIR = f"{DATA_DIR}/tools" os.makedirs(TOOLS_DIR, exist_ok=True) diff --git a/backend/apps/webui/routers/users.py b/backend/open_webui/apps/webui/routers/users.py similarity index 92% rename from backend/apps/webui/routers/users.py rename to backend/open_webui/apps/webui/routers/users.py index 543757275a..abc540efa8 100644 --- a/backend/apps/webui/routers/users.py +++ b/backend/open_webui/apps/webui/routers/users.py @@ -1,33 +1,20 @@ -from fastapi import Response, Request -from fastapi import Depends, FastAPI, HTTPException, status -from datetime import datetime, timedelta -from typing import Union, Optional - -from fastapi import APIRouter -from pydantic import BaseModel -import time -import uuid import logging +from typing import Optional -from apps.webui.models.users import ( +from open_webui.apps.webui.models.auths import Auths +from open_webui.apps.webui.models.chats import Chats +from open_webui.apps.webui.models.users import ( UserModel, - UserUpdateForm, UserRoleUpdateForm, - UserSettings, Users, + UserSettings, + UserUpdateForm, ) -from apps.webui.models.auths import Auths -from apps.webui.models.chats import Chats - -from utils.utils import ( - get_verified_user, - get_password_hash, - get_current_user, - get_admin_user, -) -from constants import ERROR_MESSAGES - -from config import SRC_LOG_LEVELS +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import APIRouter, Depends, HTTPException, Request, status +from pydantic import BaseModel +from open_webui.utils.utils import get_admin_user, get_password_hash, get_verified_user log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -69,7 +56,6 @@ async def update_user_permissions( @router.post("/update/role", response_model=Optional[UserModel]) async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)): - if user.id != form_data.id and form_data.id != Users.get_first_user().id: return Users.update_user_role_by_id(form_data.id, form_data.role) @@ -173,7 +159,6 @@ class UserResponse(BaseModel): @router.get("/{user_id}", response_model=UserResponse) async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): - # Check if user_id is a shared chat # If it is, get the user_id from the chat if user_id.startswith("shared-"): diff --git a/backend/apps/webui/routers/utils.py b/backend/open_webui/apps/webui/routers/utils.py similarity index 88% rename from backend/apps/webui/routers/utils.py rename to backend/open_webui/apps/webui/routers/utils.py index 8bf8267da1..731f987843 100644 --- a/backend/apps/webui/routers/utils.py +++ b/backend/open_webui/apps/webui/routers/utils.py @@ -1,23 +1,16 @@ -from pathlib import Path import site +from pathlib import Path -from fastapi import APIRouter, UploadFile, File, Response -from fastapi import Depends, HTTPException, status -from starlette.responses import StreamingResponse, FileResponse -from pydantic import BaseModel - - -from fpdf import FPDF -import markdown import black - - -from utils.utils import get_admin_user -from utils.misc import calculate_sha256, get_gravatar_url - -from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT -from constants import ERROR_MESSAGES - +import markdown +from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Response, status +from fpdf import FPDF +from pydantic import BaseModel +from starlette.responses import FileResponse +from open_webui.utils.misc import get_gravatar_url +from open_webui.utils.utils import get_admin_user router = APIRouter() @@ -115,7 +108,7 @@ async def download_chat_as_pdf( return Response( content=bytes(pdf_bytes), media_type="application/pdf", - headers={"Content-Disposition": f"attachment;filename=chat.pdf"}, + headers={"Content-Disposition": "attachment;filename=chat.pdf"}, ) @@ -126,7 +119,7 @@ async def download_db(user=Depends(get_admin_user)): status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - from apps.webui.internal.db import engine + from open_webui.apps.webui.internal.db import engine if engine.name != "sqlite": raise HTTPException( diff --git a/backend/apps/webui/utils.py b/backend/open_webui/apps/webui/utils.py similarity index 96% rename from backend/apps/webui/utils.py rename to backend/open_webui/apps/webui/utils.py index a556b8e8c1..6b66258daf 100644 --- a/backend/apps/webui/utils.py +++ b/backend/open_webui/apps/webui/utils.py @@ -1,13 +1,12 @@ -from importlib import util import os import re -import sys import subprocess +import sys +from importlib import util - -from apps.webui.models.tools import Tools -from apps.webui.models.functions import Functions -from config import TOOLS_DIR, FUNCTIONS_DIR +from open_webui.apps.webui.models.functions import Functions +from open_webui.apps.webui.models.tools import Tools +from open_webui.config import FUNCTIONS_DIR, TOOLS_DIR def extract_frontmatter(file_path): diff --git a/backend/config.py b/backend/open_webui/config.py similarity index 96% rename from backend/config.py rename to backend/open_webui/config.py index 9cdcbe474e..2e3f1b2b8d 100644 --- a/backend/config.py +++ b/backend/open_webui/config.py @@ -1,58 +1,29 @@ -from sqlalchemy import create_engine, Column, Integer, DateTime, JSON, func -from contextlib import contextmanager - - -import os -import sys +import json import logging -import importlib.metadata -import pkgutil -from urllib.parse import urlparse +import os +import shutil from datetime import datetime +from pathlib import Path +from typing import Generic, Optional, TypeVar +from urllib.parse import urlparse import chromadb -from chromadb import Settings -from typing import TypeVar, Generic -from pydantic import BaseModel -from typing import Optional - -from pathlib import Path -import json -import yaml - import requests -import shutil - - -from apps.webui.internal.db import Base, get_db - -from constants import ERROR_MESSAGES - -from env import ( - ENV, - VERSION, - SAFE_MODE, - GLOBAL_LOG_LEVEL, - SRC_LOG_LEVELS, - BASE_DIR, +import yaml +from open_webui.apps.webui.internal.db import Base, get_db +from chromadb import Settings +from open_webui.env import ( + OPEN_WEBUI_DIR, DATA_DIR, - BACKEND_DIR, + ENV, FRONTEND_BUILD_DIR, - WEBUI_NAME, - WEBUI_URL, - WEBUI_FAVICON_URL, - WEBUI_BUILD_HASH, - CONFIG_DATA, - DATABASE_URL, - CHANGELOG, WEBUI_AUTH, - WEBUI_AUTH_TRUSTED_EMAIL_HEADER, - WEBUI_AUTH_TRUSTED_NAME_HEADER, - WEBUI_SECRET_KEY, - WEBUI_SESSION_COOKIE_SAME_SITE, - WEBUI_SESSION_COOKIE_SECURE, + WEBUI_FAVICON_URL, + WEBUI_NAME, log, ) +from pydantic import BaseModel +from sqlalchemy import JSON, Column, DateTime, Integer, func class EndpointFilter(logging.Filter): @@ -72,10 +43,15 @@ logging.getLogger("uvicorn.access").addFilter(EndpointFilter()) def run_migrations(): print("Running migrations") try: - from alembic.config import Config from alembic import command + from alembic.config import Config + + alembic_cfg = Config(OPEN_WEBUI_DIR / "alembic.ini") + + # Set the script location dynamically + migrations_path = OPEN_WEBUI_DIR / "migrations" + alembic_cfg.set_main_option("script_location", str(migrations_path)) - alembic_cfg = Config("alembic.ini") command.upgrade(alembic_cfg, "head") except Exception as e: print(f"Error: {e}") @@ -118,15 +94,6 @@ if os.path.exists(f"{DATA_DIR}/config.json"): save_to_db(data) os.rename(f"{DATA_DIR}/config.json", f"{DATA_DIR}/old_config.json") - -def save_config(): - try: - with open(f"{DATA_DIR}/config.json", "w") as f: - json.dump(CONFIG_DATA, f, indent="\t") - except Exception as e: - log.exception(e) - - DEFAULT_CONFIG = { "version": 0, "ui": { @@ -200,6 +167,25 @@ def get_config_value(config_path: str): return cur_config +PERSISTENT_CONFIG_REGISTRY = [] + + +def save_config(config): + global CONFIG_DATA + global PERSISTENT_CONFIG_REGISTRY + try: + save_to_db(config) + CONFIG_DATA = config + + # Trigger updates on all registered PersistentConfig entries + for config_item in PERSISTENT_CONFIG_REGISTRY: + config_item.update() + except Exception as e: + log.exception(e) + return False + return True + + T = TypeVar("T") @@ -215,6 +201,8 @@ class PersistentConfig(Generic[T]): else: self.value = env_value + PERSISTENT_CONFIG_REGISTRY.append(self) + def __str__(self): return str(self.value) @@ -231,6 +219,12 @@ class PersistentConfig(Generic[T]): ) return super().__getattribute__(item) + def update(self): + new_value = get_config_value(self.config_path) + if new_value is not None: + self.value = new_value + log.info(f"Updated {self.env_name} to new value {self.value}") + def save(self): log.info(f"Saving '{self.env_name}' to the database") path_parts = self.config_path.split(".") @@ -441,7 +435,7 @@ load_oauth_providers() # Static DIR #################################### -STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve() +STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve() frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png" @@ -1237,6 +1231,18 @@ TAVILY_API_KEY = PersistentConfig( os.getenv("TAVILY_API_KEY", ""), ) +SEARCHAPI_API_KEY = PersistentConfig( + "SEARCHAPI_API_KEY", + "rag.web.search.searchapi_api_key", + os.getenv("SEARCHAPI_API_KEY", ""), +) + +SEARCHAPI_ENGINE = PersistentConfig( + "SEARCHAPI_ENGINE", + "rag.web.search.searchapi_engine", + os.getenv("SEARCHAPI_ENGINE", ""), +) + RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig( "RAG_WEB_SEARCH_RESULT_COUNT", "rag.web.search.result_count", diff --git a/backend/constants.py b/backend/open_webui/constants.py similarity index 100% rename from backend/constants.py rename to backend/open_webui/constants.py diff --git a/backend/open_webui/data/readme.txt b/backend/open_webui/data/readme.txt new file mode 100644 index 0000000000..7902100912 --- /dev/null +++ b/backend/open_webui/data/readme.txt @@ -0,0 +1 @@ +pip install dir for backend files (db, documents, etc.) \ No newline at end of file diff --git a/backend/env.py b/backend/open_webui/env.py similarity index 82% rename from backend/env.py rename to backend/open_webui/env.py index 689dc1b6d5..b803b58636 100644 --- a/backend/env.py +++ b/backend/open_webui/env.py @@ -1,32 +1,31 @@ -from pathlib import Path -import os -import logging -import sys -import json - - import importlib.metadata +import json +import logging +import os import pkgutil -from urllib.parse import urlparse -from datetime import datetime - +import sys +import shutil +from pathlib import Path import markdown from bs4 import BeautifulSoup - -from constants import ERROR_MESSAGES +from open_webui.constants import ERROR_MESSAGES #################################### # Load .env file #################################### -BACKEND_DIR = Path(__file__).parent # the path containing this file +OPEN_WEBUI_DIR = Path(__file__).parent # the path containing this file +print(OPEN_WEBUI_DIR) + +BACKEND_DIR = OPEN_WEBUI_DIR.parent # the path containing this file BASE_DIR = BACKEND_DIR.parent # the path containing the backend/ +print(BACKEND_DIR) print(BASE_DIR) try: - from dotenv import load_dotenv, find_dotenv + from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv(str(BASE_DIR / ".env"))) except ImportError: @@ -89,14 +88,23 @@ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" ENV = os.environ.get("ENV", "dev") +PIP_INSTALL = False try: - PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text()) -except Exception: + importlib.metadata.version("open-webui") + PIP_INSTALL = True +except importlib.metadata.PackageNotFoundError: + pass + + +if PIP_INSTALL: + PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")} +else: try: - PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")} - except importlib.metadata.PackageNotFoundError: + PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text()) + except Exception: PACKAGE_DATA = {"version": "0.0.0"} + VERSION = PACKAGE_DATA["version"] @@ -178,11 +186,35 @@ WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build") #################################### DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve() + +if PIP_INSTALL: + NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve() + NEW_DATA_DIR.mkdir(parents=True, exist_ok=True) + + # Check if the data directory exists in the package directory + if DATA_DIR.exists(): + log.info(f"Moving {DATA_DIR} to {NEW_DATA_DIR}") + for item in DATA_DIR.iterdir(): + dest = NEW_DATA_DIR / item.name + if item.is_dir(): + shutil.copytree(item, dest, dirs_exist_ok=True) + else: + shutil.copy2(item, dest) + + DATA_DIR = OPEN_WEBUI_DIR / "data" + + FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve() +if PIP_INSTALL: + FRONTEND_BUILD_DIR = Path( + os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend") + ).resolve() + RESET_CONFIG_ON_START = ( os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true" ) + if RESET_CONFIG_ON_START: try: os.remove(f"{DATA_DIR}/config.json") @@ -191,12 +223,6 @@ if RESET_CONFIG_ON_START: except Exception: pass -try: - CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text()) -except Exception: - CONFIG_DATA = {} - - #################################### # Database #################################### diff --git a/backend/main.py b/backend/open_webui/main.py similarity index 95% rename from backend/main.py rename to backend/open_webui/main.py index 4b91cdc84b..12d144342f 100644 --- a/backend/main.py +++ b/backend/open_webui/main.py @@ -1,131 +1,138 @@ import base64 +import inspect +import json +import logging +import mimetypes +import os +import shutil +import sys +import time import uuid from contextlib import asynccontextmanager -from authlib.integrations.starlette_client import OAuth -from authlib.oidc.core import UserInfo -import json -import time -import os -import sys -import logging -import aiohttp -import requests -import mimetypes -import shutil -import inspect from typing import Optional -from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form -from fastapi.staticfiles import StaticFiles -from fastapi.responses import JSONResponse -from fastapi import HTTPException +import aiohttp +import requests + + +from open_webui.apps.audio.main import app as audio_app +from open_webui.apps.images.main import app as images_app +from open_webui.apps.ollama.main import app as ollama_app +from open_webui.apps.ollama.main import ( + generate_openai_chat_completion as generate_ollama_chat_completion, +) +from open_webui.apps.ollama.main import get_all_models as get_ollama_models +from open_webui.apps.openai.main import app as openai_app +from open_webui.apps.openai.main import ( + generate_chat_completion as generate_openai_chat_completion, +) +from open_webui.apps.openai.main import get_all_models as get_openai_models +from open_webui.apps.rag.main import app as rag_app +from open_webui.apps.rag.utils import get_rag_context, rag_template +from open_webui.apps.socket.main import app as socket_app +from open_webui.apps.socket.main import get_event_call, get_event_emitter +from open_webui.apps.webui.internal.db import Session +from open_webui.apps.webui.main import app as webui_app +from open_webui.apps.webui.main import ( + generate_function_chat_completion, + get_pipe_models, +) +from open_webui.apps.webui.models.auths import Auths +from open_webui.apps.webui.models.functions import Functions +from open_webui.apps.webui.models.models import Models +from open_webui.apps.webui.models.users import UserModel, Users +from open_webui.apps.webui.utils import load_function_module_by_id + + +from authlib.integrations.starlette_client import OAuth +from authlib.oidc.core import UserInfo + + +from open_webui.config import ( + CACHE_DIR, + CORS_ALLOW_ORIGIN, + DEFAULT_LOCALE, + ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_EXPORT, + ENABLE_MODEL_FILTER, + ENABLE_OAUTH_SIGNUP, + ENABLE_OLLAMA_API, + ENABLE_OPENAI_API, + ENV, + FRONTEND_BUILD_DIR, + MODEL_FILTER_LIST, + OAUTH_MERGE_ACCOUNTS_BY_EMAIL, + OAUTH_PROVIDERS, + SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE, + SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD, + STATIC_DIR, + TASK_MODEL, + TASK_MODEL_EXTERNAL, + TITLE_GENERATION_PROMPT_TEMPLATE, + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + WEBHOOK_URL, + WEBUI_AUTH, + WEBUI_NAME, + AppConfig, + run_migrations, +) +from open_webui.constants import ERROR_MESSAGES, TASKS, WEBHOOK_MESSAGES +from open_webui.env import ( + CHANGELOG, + GLOBAL_LOG_LEVEL, + SAFE_MODE, + SRC_LOG_LEVELS, + VERSION, + WEBUI_BUILD_HASH, + WEBUI_SECRET_KEY, + WEBUI_SESSION_COOKIE_SAME_SITE, + WEBUI_SESSION_COOKIE_SECURE, + WEBUI_URL, +) +from fastapi import ( + Depends, + FastAPI, + File, + Form, + HTTPException, + Request, + UploadFile, + status, +) from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel from sqlalchemy import text from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.sessions import SessionMiddleware -from starlette.responses import StreamingResponse, Response, RedirectResponse +from starlette.responses import RedirectResponse, Response, StreamingResponse -from apps.socket.main import app as socket_app, get_event_emitter, get_event_call -from apps.ollama.main import ( - app as ollama_app, - get_all_models as get_ollama_models, - generate_openai_chat_completion as generate_ollama_chat_completion, +from open_webui.utils.misc import ( + add_or_update_system_message, + get_last_user_message, + parse_duration, + prepend_to_first_user_message_content, ) -from apps.openai.main import ( - app as openai_app, - get_all_models as get_openai_models, - generate_chat_completion as generate_openai_chat_completion, +from open_webui.utils.task import ( + moa_response_generation_template, + search_query_generation_template, + title_generation_template, + tools_function_calling_generation_template, ) - -from apps.audio.main import app as audio_app -from apps.images.main import app as images_app -from apps.rag.main import app as rag_app -from apps.webui.main import ( - app as webui_app, - get_pipe_models, - generate_function_chat_completion, -) -from apps.webui.internal.db import Session - - -from pydantic import BaseModel - -from apps.webui.models.auths import Auths -from apps.webui.models.models import Models -from apps.webui.models.functions import Functions -from apps.webui.models.users import Users, UserModel - -from apps.webui.utils import load_function_module_by_id - -from utils.utils import ( +from open_webui.utils.tools import get_tools +from open_webui.utils.utils import ( + create_token, + decode_token, get_admin_user, - get_verified_user, get_current_user, get_http_authorization_cred, get_password_hash, - create_token, - decode_token, + get_verified_user, ) -from utils.task import ( - title_generation_template, - search_query_generation_template, - tools_function_calling_generation_template, - moa_response_generation_template, -) - -from utils.tools import get_tools -from utils.misc import ( - get_last_user_message, - add_or_update_system_message, - prepend_to_first_user_message_content, - parse_duration, -) - -from apps.rag.utils import get_rag_context, rag_template - -from config import ( - run_migrations, - WEBUI_NAME, - WEBUI_URL, - WEBUI_AUTH, - ENV, - VERSION, - CHANGELOG, - FRONTEND_BUILD_DIR, - CACHE_DIR, - STATIC_DIR, - DEFAULT_LOCALE, - ENABLE_OPENAI_API, - ENABLE_OLLAMA_API, - ENABLE_MODEL_FILTER, - MODEL_FILTER_LIST, - GLOBAL_LOG_LEVEL, - SRC_LOG_LEVELS, - WEBHOOK_URL, - ENABLE_ADMIN_EXPORT, - WEBUI_BUILD_HASH, - TASK_MODEL, - TASK_MODEL_EXTERNAL, - TITLE_GENERATION_PROMPT_TEMPLATE, - SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE, - SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD, - TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, - SAFE_MODE, - OAUTH_PROVIDERS, - ENABLE_OAUTH_SIGNUP, - OAUTH_MERGE_ACCOUNTS_BY_EMAIL, - WEBUI_SECRET_KEY, - WEBUI_SESSION_COOKIE_SAME_SITE, - WEBUI_SESSION_COOKIE_SECURE, - ENABLE_ADMIN_CHAT_ACCESS, - AppConfig, - CORS_ALLOW_ORIGIN, -) - -from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES, TASKS -from utils.webhook import post_webhook +from open_webui.utils.webhook import post_webhook if SAFE_MODE: print("SAFE MODE ENABLED") @@ -628,7 +635,10 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware): async for data in original_generator: yield data - return StreamingResponse(stream_wrapper(response.body_iterator, data_items)) + return StreamingResponse( + stream_wrapper(response.body_iterator, data_items), + headers=dict(response.headers), + ) async def _receive(self, body: bytes): return {"type": "http.request", "body": body, "more_body": False} @@ -729,10 +739,16 @@ class PipelineMiddleware(BaseHTTPMiddleware): try: data = filter_pipeline(data, user) except Exception as e: - return JSONResponse( - status_code=e.args[0], - content={"detail": e.args[1]}, - ) + if len(e.args) > 1: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) modified_body_bytes = json.dumps(data).encode("utf-8") # Replace the request body with the modified one @@ -1380,10 +1396,16 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)): try: payload = filter_pipeline(payload, user) except Exception as e: - return JSONResponse( - status_code=e.args[0], - content={"detail": e.args[1]}, - ) + if len(e.args) > 1: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) if "chat_id" in payload: del payload["chat_id"] @@ -1433,10 +1455,16 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user) try: payload = filter_pipeline(payload, user) except Exception as e: - return JSONResponse( - status_code=e.args[0], - content={"detail": e.args[1]}, - ) + if len(e.args) > 1: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) if "chat_id" in payload: del payload["chat_id"] @@ -1490,10 +1518,16 @@ Message: """{{prompt}}""" try: payload = filter_pipeline(payload, user) except Exception as e: - return JSONResponse( - status_code=e.args[0], - content={"detail": e.args[1]}, - ) + if len(e.args) > 1: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) if "chat_id" in payload: del payload["chat_id"] @@ -1542,10 +1576,16 @@ Responses from models: {{responses}}""" try: payload = filter_pipeline(payload, user) except Exception as e: - return JSONResponse( - status_code=e.args[0], - content={"detail": e.args[1]}, - ) + if len(e.args) > 1: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + else: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) if "chat_id" in payload: del payload["chat_id"] diff --git a/backend/migrations/README b/backend/open_webui/migrations/README similarity index 100% rename from backend/migrations/README rename to backend/open_webui/migrations/README diff --git a/backend/migrations/env.py b/backend/open_webui/migrations/env.py similarity index 78% rename from backend/migrations/env.py rename to backend/open_webui/migrations/env.py index b3b3407fa8..5e860c8a05 100644 --- a/backend/migrations/env.py +++ b/backend/open_webui/migrations/env.py @@ -1,24 +1,9 @@ -import os from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool - from alembic import context - -from apps.webui.models.auths import Auth -from apps.webui.models.chats import Chat -from apps.webui.models.documents import Document -from apps.webui.models.memories import Memory -from apps.webui.models.models import Model -from apps.webui.models.prompts import Prompt -from apps.webui.models.tags import Tag, ChatIdTag -from apps.webui.models.tools import Tool -from apps.webui.models.users import User -from apps.webui.models.files import File -from apps.webui.models.functions import Function - -from env import DATABASE_URL +from open_webui.apps.webui.models.auths import Auth +from open_webui.env import DATABASE_URL +from sqlalchemy import engine_from_config, pool # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/backend/migrations/script.py.mako b/backend/open_webui/migrations/script.py.mako similarity index 93% rename from backend/migrations/script.py.mako rename to backend/open_webui/migrations/script.py.mako index 5f667ccfe0..01e730e77d 100644 --- a/backend/migrations/script.py.mako +++ b/backend/open_webui/migrations/script.py.mako @@ -9,7 +9,7 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa -import apps.webui.internal.db +import open_webui.apps.webui.internal.db ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/backend/migrations/util.py b/backend/open_webui/migrations/util.py similarity index 100% rename from backend/migrations/util.py rename to backend/open_webui/migrations/util.py diff --git a/backend/migrations/versions/7e5b5dc7342b_init.py b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py similarity index 98% rename from backend/migrations/versions/7e5b5dc7342b_init.py rename to backend/open_webui/migrations/versions/7e5b5dc7342b_init.py index b82627f5bc..53bfc3108d 100644 --- a/backend/migrations/versions/7e5b5dc7342b_init.py +++ b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py @@ -1,17 +1,19 @@ """init Revision ID: 7e5b5dc7342b -Revises: +Revises: Create Date: 2024-06-24 13:15:33.808998 """ from typing import Sequence, Union -from alembic import op import sqlalchemy as sa -import apps.webui.internal.db -from migrations.util import get_existing_tables +from alembic import op + + +import open_webui.apps.webui.internal.db +from open_webui.migrations.util import get_existing_tables # revision identifiers, used by Alembic. revision: str = "7e5b5dc7342b" diff --git a/backend/migrations/versions/ca81bd47c050_add_config_table.py b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py similarity index 97% rename from backend/migrations/versions/ca81bd47c050_add_config_table.py rename to backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py index b9f708240b..1540aa6a7f 100644 --- a/backend/migrations/versions/ca81bd47c050_add_config_table.py +++ b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py @@ -8,10 +8,8 @@ Create Date: 2024-08-25 15:26:35.241684 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa -import apps.webui.internal.db - +from alembic import op # revision identifiers, used by Alembic. revision: str = "ca81bd47c050" diff --git a/backend/static/favicon.png b/backend/open_webui/static/favicon.png similarity index 100% rename from backend/static/favicon.png rename to backend/open_webui/static/favicon.png diff --git a/backend/static/fonts/NotoSans-Bold.ttf b/backend/open_webui/static/fonts/NotoSans-Bold.ttf similarity index 100% rename from backend/static/fonts/NotoSans-Bold.ttf rename to backend/open_webui/static/fonts/NotoSans-Bold.ttf diff --git a/backend/static/fonts/NotoSans-Italic.ttf b/backend/open_webui/static/fonts/NotoSans-Italic.ttf similarity index 100% rename from backend/static/fonts/NotoSans-Italic.ttf rename to backend/open_webui/static/fonts/NotoSans-Italic.ttf diff --git a/backend/static/fonts/NotoSans-Regular.ttf b/backend/open_webui/static/fonts/NotoSans-Regular.ttf similarity index 100% rename from backend/static/fonts/NotoSans-Regular.ttf rename to backend/open_webui/static/fonts/NotoSans-Regular.ttf diff --git a/backend/static/fonts/NotoSansJP-Regular.ttf b/backend/open_webui/static/fonts/NotoSansJP-Regular.ttf similarity index 100% rename from backend/static/fonts/NotoSansJP-Regular.ttf rename to backend/open_webui/static/fonts/NotoSansJP-Regular.ttf diff --git a/backend/static/fonts/NotoSansKR-Regular.ttf b/backend/open_webui/static/fonts/NotoSansKR-Regular.ttf similarity index 100% rename from backend/static/fonts/NotoSansKR-Regular.ttf rename to backend/open_webui/static/fonts/NotoSansKR-Regular.ttf diff --git a/backend/static/fonts/NotoSansSC-Regular.ttf b/backend/open_webui/static/fonts/NotoSansSC-Regular.ttf similarity index 100% rename from backend/static/fonts/NotoSansSC-Regular.ttf rename to backend/open_webui/static/fonts/NotoSansSC-Regular.ttf diff --git a/backend/static/logo.png b/backend/open_webui/static/logo.png similarity index 100% rename from backend/static/logo.png rename to backend/open_webui/static/logo.png diff --git a/backend/static/splash.png b/backend/open_webui/static/splash.png similarity index 100% rename from backend/static/splash.png rename to backend/open_webui/static/splash.png diff --git a/backend/static/user-import.csv b/backend/open_webui/static/user-import.csv similarity index 100% rename from backend/static/user-import.csv rename to backend/open_webui/static/user-import.csv diff --git a/backend/test/__init__.py b/backend/open_webui/test/__init__.py similarity index 100% rename from backend/test/__init__.py rename to backend/open_webui/test/__init__.py diff --git a/backend/test/apps/webui/routers/test_auths.py b/backend/open_webui/test/apps/webui/routers/test_auths.py similarity index 95% rename from backend/test/apps/webui/routers/test_auths.py rename to backend/open_webui/test/apps/webui/routers/test_auths.py index 3a8695a693..bc14fb8ddc 100644 --- a/backend/test/apps/webui/routers/test_auths.py +++ b/backend/open_webui/test/apps/webui/routers/test_auths.py @@ -1,5 +1,3 @@ -import pytest - from test.util.abstract_integration_test import AbstractPostgresTest from test.util.mock_user import mock_webui_user @@ -9,8 +7,8 @@ class TestAuths(AbstractPostgresTest): def setup_class(cls): super().setup_class() - from apps.webui.models.users import Users - from apps.webui.models.auths import Auths + from open_webui.apps.webui.models.auths import Auths + from open_webui.apps.webui.models.users import Users cls.users = Users cls.auths = Auths @@ -28,7 +26,7 @@ class TestAuths(AbstractPostgresTest): } def test_update_profile(self): - from utils.utils import get_password_hash + from open_webui.utils.utils import get_password_hash user = self.auths.insert_new_auth( email="john.doe@openwebui.com", @@ -49,7 +47,7 @@ class TestAuths(AbstractPostgresTest): assert db_user.profile_image_url == "/user2.png" def test_update_password(self): - from utils.utils import get_password_hash + from open_webui.utils.utils import get_password_hash user = self.auths.insert_new_auth( email="john.doe@openwebui.com", @@ -76,7 +74,7 @@ class TestAuths(AbstractPostgresTest): assert new_auth is not None def test_signin(self): - from utils.utils import get_password_hash + from open_webui.utils.utils import get_password_hash user = self.auths.insert_new_auth( email="john.doe@openwebui.com", diff --git a/backend/test/apps/webui/routers/test_chats.py b/backend/open_webui/test/apps/webui/routers/test_chats.py similarity index 98% rename from backend/test/apps/webui/routers/test_chats.py rename to backend/open_webui/test/apps/webui/routers/test_chats.py index f4661b6257..935316fd8f 100644 --- a/backend/test/apps/webui/routers/test_chats.py +++ b/backend/open_webui/test/apps/webui/routers/test_chats.py @@ -5,7 +5,6 @@ from test.util.mock_user import mock_webui_user class TestChats(AbstractPostgresTest): - BASE_PATH = "/api/v1/chats" def setup_class(cls): @@ -13,8 +12,7 @@ class TestChats(AbstractPostgresTest): def setup_method(self): super().setup_method() - from apps.webui.models.chats import ChatForm - from apps.webui.models.chats import Chats + from open_webui.apps.webui.models.chats import ChatForm, Chats self.chats = Chats self.chats.insert_new_chat( @@ -90,7 +88,7 @@ class TestChats(AbstractPostgresTest): def test_get_user_archived_chats(self): self.chats.archive_all_chats_by_user_id("2") - from apps.webui.internal.db import Session + from open_webui.apps.webui.internal.db import Session Session.commit() with mock_webui_user(id="2"): diff --git a/backend/test/apps/webui/routers/test_documents.py b/backend/open_webui/test/apps/webui/routers/test_documents.py similarity index 98% rename from backend/test/apps/webui/routers/test_documents.py rename to backend/open_webui/test/apps/webui/routers/test_documents.py index 14ca339fd0..4d30b35e41 100644 --- a/backend/test/apps/webui/routers/test_documents.py +++ b/backend/open_webui/test/apps/webui/routers/test_documents.py @@ -3,12 +3,11 @@ from test.util.mock_user import mock_webui_user class TestDocuments(AbstractPostgresTest): - BASE_PATH = "/api/v1/documents" def setup_class(cls): super().setup_class() - from apps.webui.models.documents import Documents + from open_webui.apps.webui.models.documents import Documents cls.documents = Documents diff --git a/backend/test/apps/webui/routers/test_models.py b/backend/open_webui/test/apps/webui/routers/test_models.py similarity index 97% rename from backend/test/apps/webui/routers/test_models.py rename to backend/open_webui/test/apps/webui/routers/test_models.py index 410c4516a2..1d52658b8f 100644 --- a/backend/test/apps/webui/routers/test_models.py +++ b/backend/open_webui/test/apps/webui/routers/test_models.py @@ -3,12 +3,11 @@ from test.util.mock_user import mock_webui_user class TestModels(AbstractPostgresTest): - BASE_PATH = "/api/v1/models" def setup_class(cls): super().setup_class() - from apps.webui.models.models import Model + from open_webui.apps.webui.models.models import Model cls.models = Model diff --git a/backend/test/apps/webui/routers/test_prompts.py b/backend/open_webui/test/apps/webui/routers/test_prompts.py similarity index 99% rename from backend/test/apps/webui/routers/test_prompts.py rename to backend/open_webui/test/apps/webui/routers/test_prompts.py index 9f47be9923..d91bf77dc5 100644 --- a/backend/test/apps/webui/routers/test_prompts.py +++ b/backend/open_webui/test/apps/webui/routers/test_prompts.py @@ -3,7 +3,6 @@ from test.util.mock_user import mock_webui_user class TestPrompts(AbstractPostgresTest): - BASE_PATH = "/api/v1/prompts" def test_prompts(self): diff --git a/backend/test/apps/webui/routers/test_users.py b/backend/open_webui/test/apps/webui/routers/test_users.py similarity index 98% rename from backend/test/apps/webui/routers/test_users.py rename to backend/open_webui/test/apps/webui/routers/test_users.py index 9736b4d32a..6facf7055a 100644 --- a/backend/test/apps/webui/routers/test_users.py +++ b/backend/open_webui/test/apps/webui/routers/test_users.py @@ -21,12 +21,11 @@ def _assert_user(data, id, **kwargs): class TestUsers(AbstractPostgresTest): - BASE_PATH = "/api/v1/users" def setup_class(cls): super().setup_class() - from apps.webui.models.users import Users + from open_webui.apps.webui.models.users import Users cls.users = Users diff --git a/backend/test/util/abstract_integration_test.py b/backend/open_webui/test/util/abstract_integration_test.py similarity index 95% rename from backend/test/util/abstract_integration_test.py rename to backend/open_webui/test/util/abstract_integration_test.py index 8535221a85..2814731e06 100644 --- a/backend/test/util/abstract_integration_test.py +++ b/backend/open_webui/test/util/abstract_integration_test.py @@ -92,7 +92,7 @@ class AbstractPostgresTest(AbstractIntegrationTest): db = None while retries > 0: try: - from config import BACKEND_DIR + from open_webui.config import OPEN_WEBUI_DIR db = create_engine(database_url, pool_pre_ping=True) db = db.connect() @@ -115,7 +115,7 @@ class AbstractPostgresTest(AbstractIntegrationTest): pytest.fail(f"Could not setup test environment: {ex}") def _check_db_connection(self): - from apps.webui.internal.db import Session + from open_webui.apps.webui.internal.db import Session retries = 10 while retries > 0: @@ -139,7 +139,7 @@ class AbstractPostgresTest(AbstractIntegrationTest): cls.docker_client.containers.get(cls.DOCKER_CONTAINER_NAME).remove(force=True) def teardown_method(self): - from apps.webui.internal.db import Session + from open_webui.apps.webui.internal.db import Session # rollback everything not yet committed Session.commit() diff --git a/backend/test/util/mock_user.py b/backend/open_webui/test/util/mock_user.py similarity index 87% rename from backend/test/util/mock_user.py rename to backend/open_webui/test/util/mock_user.py index 8d0300d3f9..96456a2c81 100644 --- a/backend/test/util/mock_user.py +++ b/backend/open_webui/test/util/mock_user.py @@ -5,7 +5,7 @@ from fastapi import FastAPI @contextmanager def mock_webui_user(**kwargs): - from apps.webui.main import app + from open_webui.apps.webui.main import app with mock_user(app, **kwargs): yield @@ -13,13 +13,13 @@ def mock_webui_user(**kwargs): @contextmanager def mock_user(app: FastAPI, **kwargs): - from utils.utils import ( + from open_webui.utils.utils import ( get_current_user, get_verified_user, get_admin_user, get_current_user_by_api_key, ) - from apps.webui.models.users import User + from open_webui.apps.webui.models.users import User def create_user(): user_parameters = { diff --git a/backend/utils/logo.png b/backend/open_webui/utils/logo.png similarity index 100% rename from backend/utils/logo.png rename to backend/open_webui/utils/logo.png diff --git a/backend/utils/misc.py b/backend/open_webui/utils/misc.py similarity index 99% rename from backend/utils/misc.py rename to backend/open_webui/utils/misc.py index df35732c05..8b72983f17 100644 --- a/backend/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -1,12 +1,12 @@ -from pathlib import Path import hashlib import re -from datetime import timedelta -from typing import Optional, Callable -import uuid import time +import uuid +from datetime import timedelta +from pathlib import Path +from typing import Callable, Optional -from utils.task import prompt_template +from open_webui.utils.task import prompt_template def get_last_user_message_item(messages: list[dict]) -> Optional[dict]: diff --git a/backend/utils/schemas.py b/backend/open_webui/utils/schemas.py similarity index 94% rename from backend/utils/schemas.py rename to backend/open_webui/utils/schemas.py index 452f95bc73..958e57318d 100644 --- a/backend/utils/schemas.py +++ b/backend/open_webui/utils/schemas.py @@ -1,5 +1,7 @@ +from ast import literal_eval +from typing import Any, Literal, Optional, Type + from pydantic import BaseModel, Field, create_model -from typing import Any, Optional, Type def json_schema_to_model(tool_dict: dict[str, Any]) -> Type[BaseModel]: @@ -100,5 +102,7 @@ def json_schema_to_pydantic_type(json_schema: dict[str, Any]) -> Any: return dict elif type_ == "null": return Optional[Any] # Use Optional[Any] for nullable fields + elif type_ == "literal": + return Literal[literal_eval(json_schema.get("enum"))] else: raise ValueError(f"Unsupported JSON schema type: {type_}") diff --git a/backend/utils/task.py b/backend/open_webui/utils/task.py similarity index 99% rename from backend/utils/task.py rename to backend/open_webui/utils/task.py index ea9254c4f7..cf3d8a10c2 100644 --- a/backend/utils/task.py +++ b/backend/open_webui/utils/task.py @@ -1,6 +1,5 @@ -import re import math - +import re from datetime import datetime from typing import Optional diff --git a/backend/utils/tools.py b/backend/open_webui/utils/tools.py similarity index 96% rename from backend/utils/tools.py rename to backend/open_webui/utils/tools.py index 1a2fea32b0..0b57eb35b6 100644 --- a/backend/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -2,11 +2,10 @@ import inspect import logging from typing import Awaitable, Callable, get_type_hints -from apps.webui.models.tools import Tools -from apps.webui.models.users import UserModel -from apps.webui.utils import load_toolkit_module_by_id - -from utils.schemas import json_schema_to_model +from open_webui.apps.webui.models.tools import Tools +from open_webui.apps.webui.models.users import UserModel +from open_webui.apps.webui.utils import load_toolkit_module_by_id +from open_webui.utils.schemas import json_schema_to_model log = logging.getLogger(__name__) diff --git a/backend/utils/utils.py b/backend/open_webui/utils/utils.py similarity index 90% rename from backend/utils/utils.py rename to backend/open_webui/utils/utils.py index 4c15ea237a..45a7eef305 100644 --- a/backend/utils/utils.py +++ b/backend/open_webui/utils/utils.py @@ -1,16 +1,15 @@ -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from fastapi import HTTPException, status, Depends, Request - -from apps.webui.models.users import Users - -from typing import Union, Optional -from constants import ERROR_MESSAGES -from passlib.context import CryptContext -from datetime import datetime, timedelta, UTC -import jwt -import uuid import logging -from env import WEBUI_SECRET_KEY +import uuid +from datetime import UTC, datetime, timedelta +from typing import Optional, Union + +import jwt +from open_webui.apps.webui.models.users import Users +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import WEBUI_SECRET_KEY +from fastapi import Depends, HTTPException, Request, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from passlib.context import CryptContext logging.getLogger("passlib").setLevel(logging.ERROR) diff --git a/backend/utils/webhook.py b/backend/open_webui/utils/webhook.py similarity index 93% rename from backend/utils/webhook.py rename to backend/open_webui/utils/webhook.py index b6692e53a7..234209884f 100644 --- a/backend/utils/webhook.py +++ b/backend/open_webui/utils/webhook.py @@ -1,8 +1,9 @@ import json -import requests import logging -from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME +import requests +from open_webui.config import WEBUI_FAVICON_URL, WEBUI_NAME +from open_webui.env import SRC_LOG_LEVELS, VERSION log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["WEBHOOK"]) diff --git a/backend/requirements.txt b/backend/requirements.txt index c597947e4c..93720cc849 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,14 +4,14 @@ pydantic==2.8.2 python-multipart==0.0.9 Flask==3.0.3 -Flask-Cors==4.0.1 +Flask-Cors==5.0.0 python-socketio==5.11.3 python-jose==3.3.0 passlib[bcrypt]==1.7.4 requests==2.32.3 -aiohttp==3.10.2 +aiohttp==3.10.5 sqlalchemy==2.0.32 alembic==1.13.2 @@ -34,7 +34,7 @@ anthropic google-generativeai==0.7.2 tiktoken -langchain==0.2.14 +langchain==0.2.15 langchain-community==0.2.12 langchain-chroma==0.1.2 @@ -44,7 +44,7 @@ sentence-transformers==3.0.1 pypdf==4.3.1 docx2txt==0.8 python-pptx==1.0.0 -unstructured==0.15.7 +unstructured==0.15.9 nltk==3.9.1 Markdown==3.7 pypandoc==1.13 @@ -64,7 +64,7 @@ rank-bm25==0.2.2 faster-whisper==1.0.3 PyJWT[crypto]==2.9.0 -authlib==1.3.1 +authlib==1.3.2 black==24.8.0 langfuse==2.44.0 @@ -73,7 +73,7 @@ pytube==15.0.0 extract_msg pydub -duckduckgo-search~=6.2.1 +duckduckgo-search~=6.2.11 ## Tests docker~=7.1.0 diff --git a/backend/start.sh b/backend/start.sh index 0a5c48e8c4..a945acb62e 100755 --- a/backend/start.sh +++ b/backend/start.sh @@ -35,7 +35,7 @@ if [ -n "$SPACE_ID" ]; then echo "Configuring for HuggingFace Space deployment" if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then echo "Admin user configured, creating" - WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' & + WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' & webui_pid=$! echo "Waiting for webui to start..." while ! curl -s http://localhost:8080/health > /dev/null; do @@ -54,4 +54,4 @@ if [ -n "$SPACE_ID" ]; then export WEBUI_URL=${SPACE_HOST} fi -WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' +WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' diff --git a/package-lock.json b/package-lock.json index c32a7adf80..2e26aa010b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.3.16", + "version": "0.3.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.3.16", + "version": "0.3.17", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", @@ -8503,9 +8503,9 @@ } }, "node_modules/svelte": { - "version": "4.2.18", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", - "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", diff --git a/package.json b/package.json index 175830ef31..0214d613f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.3.16", + "version": "0.3.17", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index 98f9ccda03..057ef14754 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,14 +12,14 @@ dependencies = [ "python-multipart==0.0.9", "Flask==3.0.3", - "Flask-Cors==4.0.1", + "Flask-Cors==5.0.0", "python-socketio==5.11.3", "python-jose==3.3.0", "passlib[bcrypt]==1.7.4", "requests==2.32.3", - "aiohttp==3.10.2", + "aiohttp==3.10.5", "sqlalchemy==2.0.32", "alembic==1.13.2", @@ -41,7 +41,7 @@ dependencies = [ "google-generativeai==0.7.2", "tiktoken", - "langchain==0.2.14", + "langchain==0.2.15", "langchain-community==0.2.12", "langchain-chroma==0.1.2", @@ -51,7 +51,7 @@ dependencies = [ "pypdf==4.3.1", "docx2txt==0.8", "python-pptx==1.0.0", - "unstructured==0.15.7", + "unstructured==0.15.9", "nltk==3.9.1", "Markdown==3.7", "pypandoc==1.13", @@ -71,7 +71,7 @@ dependencies = [ "faster-whisper==1.0.3", "PyJWT[crypto]==2.9.0", - "authlib==1.3.1", + "authlib==1.3.2", "black==24.8.0", "langfuse==2.44.0", @@ -80,7 +80,7 @@ dependencies = [ "extract_msg", "pydub", - "duckduckgo-search~=6.2.1", + "duckduckgo-search~=6.2.11", "docker~=7.1.0", "pytest~=8.2.2", diff --git a/src/app.html b/src/app.html index 718f7e194c..59fd7c5ed3 100644 --- a/src/app.html +++ b/src/app.html @@ -6,6 +6,7 @@ + // On page load or when changing themes, best to add inline in `head` to avoid FOUC (() => { + if (!localStorage?.theme) { + localStorage.theme = 'system'; + } + if (localStorage?.theme && localStorage?.theme.includes('oled')) { document.documentElement.style.setProperty('--color-gray-800', '#101010'); document.documentElement.style.setProperty('--color-gray-850', '#050505'); diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index 4f53c53c89..0c4de6ad65 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -1,6 +1,63 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; import type { Banner } from '$lib/types'; +export const importConfig = async (token: string, config) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/import`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + config: config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/export`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const setDefaultModels = async (token: string, models: string) => { let error = null; diff --git a/src/lib/components/admin/Settings/Database.svelte b/src/lib/components/admin/Settings/Database.svelte index 0ba45263e1..3c376cb80e 100644 --- a/src/lib/components/admin/Settings/Database.svelte +++ b/src/lib/components/admin/Settings/Database.svelte @@ -7,6 +7,7 @@ import { config, user } from '$lib/stores'; import { toast } from 'svelte-sonner'; import { getAllUserChats } from '$lib/apis/chats'; + import { exportConfig, importConfig } from '$lib/apis/configs'; const i18n = getContext('i18n'); @@ -34,6 +35,92 @@
{$i18n.t('Database')}
+ { + const file = e.target.files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const res = await importConfig(localStorage.token, JSON.parse(e.target.result)).catch( + (error) => { + toast.error(error); + } + ); + + if (res) { + toast.success('Config imported successfully'); + } + e.target.value = null; + }; + + reader.readAsText(file); + }} + /> + + + + + +
+ {#if $config?.features.enable_admin_export ?? true}
@@ -97,40 +184,34 @@
-
- - - -
+
diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index 1e5531dd82..15eba096b7 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -19,6 +19,7 @@ 'serpstack', 'serper', 'serply', + 'searchapi', 'duckduckgo', 'tavily', 'jina' @@ -182,6 +183,34 @@ bind:value={webConfig.search.serply_api_key} /> + {:else if webConfig.search.engine === 'searchapi'} +
+
+ {$i18n.t('SearchApi API Key')} +
+ + +
+
+
+ {$i18n.t('SearchApi Engine')} +
+ +
+
+ +
+
+
{:else if webConfig.search.engine === 'tavily'}
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 3b3c9cf613..133d9ab1ec 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -297,6 +297,10 @@ selectedModels = ['']; } + if ($page.url.searchParams.get('web-search') === 'true') { + webSearchEnabled = true; + } + if ($page.url.searchParams.get('q')) { prompt = $page.url.searchParams.get('q') ?? ''; selectedToolIds = ($page.url.searchParams.get('tool_ids') ?? '') @@ -859,7 +863,7 @@ model: model.id, messages: messagesBody, options: { - ...(params ?? $settings.params ?? {}), + ...{ ...($settings?.params ?? {}), ...params }, stop: (params?.stop ?? $settings?.params?.stop ?? undefined) ? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map( @@ -1016,21 +1020,6 @@ scrollToBottom(); } } - - if ($chatId == _chatId) { - if ($settings.saveChatHistory ?? true) { - chat = await updateChatById(localStorage.token, _chatId, { - messages: messages, - history: history, - models: selectedModels, - params: params, - files: chatFiles - }); - - currentChatPage.set(1); - await chats.set(await getChatList(localStorage.token, $currentChatPage)); - } - } } else { if (res !== null) { const error = await res.json(); @@ -1062,6 +1051,7 @@ messages = messages; } + await saveChatHandler(_chatId); stopResponseFlag = false; await tick(); @@ -1324,27 +1314,15 @@ document.getElementById(`speak-button-${responseMessage.id}`)?.click(); } - - if ($chatId == _chatId) { - if ($settings.saveChatHistory ?? true) { - chat = await updateChatById(localStorage.token, _chatId, { - models: selectedModels, - messages: messages, - history: history, - params: params, - files: chatFiles - }); - - currentChatPage.set(1); - await chats.set(await getChatList(localStorage.token, $currentChatPage)); - } - } } else { await handleOpenAIError(null, res, model, responseMessage); } } catch (error) { await handleOpenAIError(error, null, model, responseMessage); } + + await saveChatHandler(_chatId); + messages = messages; stopResponseFlag = false; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 76f80f3641..55daf799b8 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -504,6 +504,7 @@ diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte index 35d967b6b0..fdd8d3488f 100644 --- a/src/lib/components/chat/Messages/CodeBlock.svelte +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -217,8 +217,10 @@ __builtins__.input = input`); const drawMermaidDiagram = async () => { try { - const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code); - mermaidHtml = svg; + if (await mermaid.parse(code)) { + const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code); + mermaidHtml = svg; + } } catch (error) { console.log('Error:', error); } diff --git a/src/lib/components/chat/ModelSelector.svelte b/src/lib/components/chat/ModelSelector.svelte index cb80cd2f48..244754d307 100644 --- a/src/lib/components/chat/ModelSelector.svelte +++ b/src/lib/components/chat/ModelSelector.svelte @@ -63,6 +63,7 @@ on:click={() => { selectedModels = [...selectedModels, '']; }} + aria-label="Add Model" > { showSidebar.set(!$showSidebar); }} + aria-label="Toggle Sidebar" >
@@ -111,6 +112,7 @@ on:click={() => { showControls = !showControls; }} + aria-label="Controls" >
@@ -127,6 +129,7 @@ on:click={() => { initNewChat(); }} + aria-label="New Chat" >
{ showSidebar.set(!$showSidebar); }} + aria-label="Toggle Sidebar" >
diff --git a/src/routes/(app)/playground/+layout.svelte b/src/routes/(app)/playground/+layout.svelte index 74579665c9..f4e31c0ff0 100644 --- a/src/routes/(app)/playground/+layout.svelte +++ b/src/routes/(app)/playground/+layout.svelte @@ -29,6 +29,7 @@ on:click={() => { showSidebar.set(!$showSidebar); }} + aria-label="Toggle Sidebar" >
diff --git a/src/routes/(app)/workspace/+layout.svelte b/src/routes/(app)/workspace/+layout.svelte index f64a783d88..05ab80715c 100644 --- a/src/routes/(app)/workspace/+layout.svelte +++ b/src/routes/(app)/workspace/+layout.svelte @@ -39,6 +39,7 @@ on:click={() => { showSidebar.set(!$showSidebar); }} + aria-label="Toggle Sidebar" >
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 7ab2f2e0bb..9b209fec07 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -38,6 +38,59 @@ let loaded = false; const BREAKPOINT = 768; + const setupSocket = (websocket = true) => { + const _socket = io(`${WEBUI_BASE_URL}` || undefined, { + reconnection: true, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + randomizationFactor: 0.5, + path: '/ws/socket.io', + auth: { token: localStorage.token }, + transports: websocket ? ['websocket'] : ['polling'] + }); + + socket.set(_socket); + + _socket.on('connect_error', (err) => { + if (err.message.includes('websocket')) { + console.log('WebSocket connection failed, falling back to polling'); + _socket.close(); + setupSocket(false); + } else { + console.log('connect_error', err); + } + }); + + _socket.on('connect', () => { + console.log('connected', _socket.id); + }); + + _socket.on('reconnect_attempt', (attempt) => { + console.log('reconnect_attempt', attempt); + }); + + _socket.on('reconnect_failed', () => { + console.log('reconnect_failed'); + }); + + _socket.on('disconnect', (reason, details) => { + console.log(`Socket ${_socket.id} disconnected due to ${reason}`); + if (details) { + console.log('Additional details:', details); + } + }); + + _socket.on('user-count', (data) => { + console.log('user-count', data); + activeUserCount.set(data.count); + }); + + _socket.on('usage', (data) => { + console.log('usage', data); + USAGE_POOL.set(data['models']); + }); + }; + onMount(async () => { theme.set(localStorage.theme); @@ -80,45 +133,7 @@ await WEBUI_NAME.set(backendConfig.name); if ($config) { - const _socket = io(`${WEBUI_BASE_URL}` || undefined, { - reconnection: true, - reconnectionDelay: 1000, - reconnectionDelayMax: 5000, - randomizationFactor: 0.5, - path: '/ws/socket.io', - auth: { token: localStorage.token } - }); - - _socket.on('connect', () => { - console.log('connected'); - }); - - _socket.on('reconnect_attempt', (attempt) => { - console.log('reconnect_attempt', attempt); - }); - - _socket.on('reconnect_failed', () => { - console.log('reconnect_failed'); - }); - - _socket.on('disconnect', (reason, details) => { - console.log(`Socket ${socket.id} disconnected due to ${reason}`); - if (details) { - console.log('Additional details:', details); - } - }); - - await socket.set(_socket); - - _socket.on('user-count', (data) => { - console.log('user-count', data); - activeUserCount.set(data.count); - }); - - _socket.on('usage', (data) => { - console.log('usage', data); - USAGE_POOL.set(data['models']); - }); + setupSocket(); if (localStorage.token) { // Get Session User Info diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000000..1f53798bb4 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/uv.lock b/uv.lock index cfc4dcf5aa..60b6a580aa 100644 --- a/uv.lock +++ b/uv.lock @@ -29,7 +29,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.10.2" +version = "3.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -39,38 +39,53 @@ dependencies = [ { name = "multidict" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/11/36ba898823ab19e49e6bd791d75b9185eadef45a46fc00d3c669824df8a0/aiohttp-3.10.2.tar.gz", hash = "sha256:4d1f694b5d6e459352e5e925a42e05bac66655bfde44d81c59992463d2897014", size = 7520621 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/28/ca549838018140b92a19001a8628578b0f2a3b38c16826212cc6f706e6d4/aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691", size = 7524360 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/ef/5414e99122e15e750b1f62bca15251e277449eb1c3f9ba05965d95529c99/aiohttp-3.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87fab7f948e407444c2f57088286e00e2ed0003ceaf3d8f8cc0f60544ba61d91", size = 585345 }, - { url = "https://files.pythonhosted.org/packages/52/6c/570be295a63a47f7819a820d6f208fa7c7295fa17f5a3f601262a47b2721/aiohttp-3.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec6ad66ed660d46503243cbec7b2b3d8ddfa020f984209b3b8ef7d98ce69c3f2", size = 396134 }, - { url = "https://files.pythonhosted.org/packages/08/67/c3b44042e80c979bb49280dd567fb59a62b8f99cc0991b255ca1feca215d/aiohttp-3.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4be88807283bd96ae7b8e401abde4ca0bab597ba73b5e9a2d98f36d451e9aac", size = 387857 }, - { url = "https://files.pythonhosted.org/packages/27/f8/1a0d0c17e9aeeaa63b7d45643c4c4f05585268089abe711c475bc5e21db8/aiohttp-3.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01c98041f90927c2cbd72c22a164bb816fa3010a047d264969cf82e1d4bcf8d1", size = 1325021 }, - { url = "https://files.pythonhosted.org/packages/9d/a9/e3fa5e5f6723b19f17ea68be21eee518ebfbe30444c7fc08993fc2a25ab2/aiohttp-3.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54e36c67e1a9273ecafab18d6693da0fb5ac48fd48417e4548ac24a918c20998", size = 1363434 }, - { url = "https://files.pythonhosted.org/packages/8b/92/9df276fcc101e08378f706a4a58e6b6a346756a94198d6ec68329a2b8d02/aiohttp-3.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7de3ddb6f424af54535424082a1b5d1ae8caf8256ebd445be68c31c662354720", size = 1398791 }, - { url = "https://files.pythonhosted.org/packages/df/fe/7b4e77cd83d4e865430e9b6f923f070f66da7effe791348b56e78358b21b/aiohttp-3.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dd9c7db94b4692b827ce51dcee597d61a0e4f4661162424faf65106775b40e7", size = 1311807 }, - { url = "https://files.pythonhosted.org/packages/b4/de/571f56bad7449519ede3d0e4a0a05a056ca2db309da0bae0094a91c9a92b/aiohttp-3.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e57e21e1167705f8482ca29cc5d02702208d8bf4aff58f766d94bcd6ead838cd", size = 1270198 }, - { url = "https://files.pythonhosted.org/packages/c4/3a/a8a8f4e47248952d691d4265ed40f85ebf345c103b95f66022cdef10f58e/aiohttp-3.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a1a50e59b720060c29e2951fd9f13c01e1ea9492e5a527b92cfe04dd64453c16", size = 1290544 }, - { url = "https://files.pythonhosted.org/packages/d4/fa/c70c250c0cd8dcf96aecf783e0926db463cdcf946915406727cc3071d780/aiohttp-3.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:686c87782481fda5ee6ba572d912a5c26d9f98cc5c243ebd03f95222af3f1b0f", size = 1285178 }, - { url = "https://files.pythonhosted.org/packages/89/4f/f4005be64a97c532c7d5ab2304bbeda22ba9b506cebbb6eb09834796d088/aiohttp-3.10.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:dafb4abb257c0ed56dc36f4e928a7341b34b1379bd87e5a15ce5d883c2c90574", size = 1340738 }, - { url = "https://files.pythonhosted.org/packages/79/f2/9f3901c4c94f9218ab3fe2231294f58cb695a3404554489c102db8376dd3/aiohttp-3.10.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:494a6f77560e02bd7d1ab579fdf8192390567fc96a603f21370f6e63690b7f3d", size = 1360510 }, - { url = "https://files.pythonhosted.org/packages/54/5f/956909f87d2ede49e1fc422e811c8243d5b487d423583301a14788292d89/aiohttp-3.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6fe8503b1b917508cc68bf44dae28823ac05e9f091021e0c41f806ebbb23f92f", size = 1319760 }, - { url = "https://files.pythonhosted.org/packages/ba/cc/560d80417a8d144d5ed7b5ce92420973b68462e99245d4e2c85f68fdd8d6/aiohttp-3.10.2-cp311-cp311-win32.whl", hash = "sha256:4ddb43d06ce786221c0dfd3c91b4892c318eaa36b903f7c4278e7e2fa0dd5102", size = 358145 }, - { url = "https://files.pythonhosted.org/packages/c1/0f/b4fa4717a7c9307e246b4d886c47cc79650fadd1c4debb0086fefd85b25c/aiohttp-3.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:ca2f5abcb0a9a47e56bac173c01e9f6c6e7f27534d91451c5f22e6a35a5a2093", size = 378023 }, - { url = "https://files.pythonhosted.org/packages/ef/c3/4c11c493c69e7cc802e6aa95271b145ff17a427026b7b5cc8af091f1ce9d/aiohttp-3.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:14eb6b17f6246959fb0b035d4f4ae52caa870c4edfb6170aad14c0de5bfbf478", size = 582359 }, - { url = "https://files.pythonhosted.org/packages/2d/41/b0aefa65f9ed474ea1c9146109be67b11eca7a45b7f39b3ea059cf7428a2/aiohttp-3.10.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:465e445ec348d4e4bd349edd8b22db75f025da9d7b6dc1369c48e7935b85581e", size = 392955 }, - { url = "https://files.pythonhosted.org/packages/ed/f6/a605abb080c136291306e2253622eef25b5da140e03d9637f5a91f413a8f/aiohttp-3.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:341f8ece0276a828d95b70cd265d20e257f5132b46bf77d759d7f4e0443f2906", size = 387994 }, - { url = "https://files.pythonhosted.org/packages/c7/a4/44dfa668b7292868db9840b2c9d171576553a874ff65bd2386c04ba8b694/aiohttp-3.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01fbb87b5426381cd9418b3ddcf4fc107e296fa2d3446c18ce6c76642f340a3", size = 1331633 }, - { url = "https://files.pythonhosted.org/packages/07/ce/4187db914f041492069917fc21c0bffa9264b77003b58e91400369834e6b/aiohttp-3.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c474af073e1a6763e1c5522bbb2d85ff8318197e4c6c919b8d7886e16213345", size = 1371028 }, - { url = "https://files.pythonhosted.org/packages/dc/0e/9686ffc6fc9145cfe9483e7d518b7f1d867d65437dbf0615f28c43fbb20d/aiohttp-3.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d9076810a5621236e29b2204e67a68e1fe317c8727ee4c9abbfbb1083b442c38", size = 1413303 }, - { url = "https://files.pythonhosted.org/packages/0b/07/2b68b23ed85f4c73f12025c446127f6f55e5d9ad848bb501d6cf333cfd6c/aiohttp-3.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8f515d6859e673940e08de3922b9c4a2249653b0ac181169313bd6e4b1978ac", size = 1326993 }, - { url = "https://files.pythonhosted.org/packages/99/a6/099a861d52b22a49b22e7106d750bcbade5e12fcda2e69170f6d5fd624c4/aiohttp-3.10.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:655e583afc639bef06f3b2446972c1726007a21003cd0ef57116a123e44601bc", size = 1279018 }, - { url = "https://files.pythonhosted.org/packages/ed/f3/a5203c907dc48345d9124c3e2149a322aeb3960184bf7d65dc58dfd55ca7/aiohttp-3.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8da9449a575133828cc99985536552ea2dcd690e848f9d41b48d8853a149a959", size = 1291552 }, - { url = "https://files.pythonhosted.org/packages/1e/ca/200cd6984acf9d4293c7b2588dc59d92c2a1b5055b3b3d55a015e215ead0/aiohttp-3.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19073d57d0feb1865d12361e2a1f5a49cb764bf81a4024a3b608ab521568093a", size = 1299884 }, - { url = "https://files.pythonhosted.org/packages/f5/25/770b29e1522a71bc4a50f6268a4abc699529c88e442a593061028750cdff/aiohttp-3.10.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8e98e1845805f184d91fda6f9ab93d7c7b0dddf1c07e0255924bfdb151a8d05", size = 1338160 }, - { url = "https://files.pythonhosted.org/packages/21/79/a3f13982c363ba67f2693088717fce94a24a9c0c5d56e42df28a5ce9e3da/aiohttp-3.10.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:377220a5efde6f9497c5b74649b8c261d3cce8a84cb661be2ed8099a2196400a", size = 1365885 }, - { url = "https://files.pythonhosted.org/packages/7b/e8/da4dcf56429f9b29faf615378a4d7550140208cf755eb6f755f24113af59/aiohttp-3.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92f7f4a4dc9cdb5980973a74d43cdbb16286dacf8d1896b6c3023b8ba8436f8e", size = 1328351 }, - { url = "https://files.pythonhosted.org/packages/bd/00/11eaebbcb406eb19dbf64e6683db53b495ee15206b4b9bc58587797b9ef4/aiohttp-3.10.2-cp312-cp312-win32.whl", hash = "sha256:9bb2834a6f11d65374ce97d366d6311a9155ef92c4f0cee543b2155d06dc921f", size = 355846 }, - { url = "https://files.pythonhosted.org/packages/77/e2/aaacbc16809ee50f71e1f2856a78f64307d9b799d36878eaf77aa053053d/aiohttp-3.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:518dc3cb37365255708283d1c1c54485bbacccd84f0a0fb87ed8917ba45eda5b", size = 376527 }, + { url = "https://files.pythonhosted.org/packages/f1/90/54ccb1e4eadfb6c95deff695582453f6208584431d69bf572782e9ae542b/aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2", size = 586455 }, + { url = "https://files.pythonhosted.org/packages/c3/7a/95e88c02756e7e718f054e1bb3ec6ad5d0ee4a2ca2bb1768c5844b3de30a/aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf", size = 397255 }, + { url = "https://files.pythonhosted.org/packages/07/4f/767387b39990e1ee9aba8ce642abcc286d84d06e068dc167dab983898f18/aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e", size = 388973 }, + { url = "https://files.pythonhosted.org/packages/61/46/0df41170a4d228c07b661b1ba9d87101d99a79339dc93b8b1183d8b20545/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77", size = 1326126 }, + { url = "https://files.pythonhosted.org/packages/af/20/da0d65e07ce49d79173fed41598f487a0a722e87cfbaa8bb7e078a7c1d39/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061", size = 1364538 }, + { url = "https://files.pythonhosted.org/packages/aa/20/b59728405114e57541ba9d5b96033e69d004e811ded299537f74237629ca/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697", size = 1399896 }, + { url = "https://files.pythonhosted.org/packages/2a/92/006690c31b830acbae09d2618e41308fe4c81c0679b3b33a3af859e0b7bf/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7", size = 1312914 }, + { url = "https://files.pythonhosted.org/packages/d4/71/1a253ca215b6c867adbd503f1e142117527ea8775e65962bc09b2fad1d2c/aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0", size = 1271301 }, + { url = "https://files.pythonhosted.org/packages/0a/ab/5d1d9ff9ce6cce8fa54774d0364e64a0f3cd50e512ff09082ced8e5217a1/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5", size = 1291652 }, + { url = "https://files.pythonhosted.org/packages/75/5f/f90510ea954b9ae6e7a53d2995b97a3e5c181110fdcf469bc9238445871d/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e", size = 1286289 }, + { url = "https://files.pythonhosted.org/packages/be/9e/1f523414237798660921817c82b9225a363af436458caf584d2fa6a2eb4a/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1", size = 1341848 }, + { url = "https://files.pythonhosted.org/packages/f6/36/443472ddaa85d7d80321fda541d9535b23ecefe0bf5792cc3955ea635190/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277", size = 1361619 }, + { url = "https://files.pythonhosted.org/packages/19/f6/3ecbac0bc4359c7d7ba9e85c6b10f57e20edaf1f97751ad2f892db231ad0/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058", size = 1320869 }, + { url = "https://files.pythonhosted.org/packages/34/7e/ed74ffb36e3a0cdec1b05d8fbaa29cb532371d5a20058b3a8052fc90fe7c/aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072", size = 359271 }, + { url = "https://files.pythonhosted.org/packages/98/1b/718901f04bc8c886a742be9e83babb7b93facabf7c475cc95e2b3ab80b4d/aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff", size = 379143 }, + { url = "https://files.pythonhosted.org/packages/d9/1c/74f9dad4a2fc4107e73456896283d915937f48177b99867b63381fadac6e/aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487", size = 583468 }, + { url = "https://files.pythonhosted.org/packages/12/29/68d090551f2b58ce76c2b436ced8dd2dfd32115d41299bf0b0c308a5483c/aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a", size = 394066 }, + { url = "https://files.pythonhosted.org/packages/8f/f7/971f88b4cdcaaa4622925ba7d86de47b48ec02a9040a143514b382f78da4/aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d", size = 389098 }, + { url = "https://files.pythonhosted.org/packages/f1/5a/fe3742efdce551667b2ddf1158b27c5b8eb1edc13d5e14e996e52e301025/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75", size = 1332742 }, + { url = "https://files.pythonhosted.org/packages/1a/52/a25c0334a1845eb4967dff279151b67ca32a948145a5812ed660ed900868/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178", size = 1372134 }, + { url = "https://files.pythonhosted.org/packages/96/3d/33c1d8efc2d8ec36bff9a8eca2df9fdf8a45269c6e24a88e74f2aa4f16bd/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e", size = 1414413 }, + { url = "https://files.pythonhosted.org/packages/64/74/0f1ddaa5f0caba1d946f0dd0c31f5744116e4a029beec454ec3726d3311f/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f", size = 1328107 }, + { url = "https://files.pythonhosted.org/packages/0a/32/c10118f0ad50e4093227234f71fd0abec6982c29367f65f32ee74ed652c4/aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73", size = 1280126 }, + { url = "https://files.pythonhosted.org/packages/c6/c9/77e3d648d97c03a42acfe843d03e97be3c5ef1b4d9de52e5bd2d28eed8e7/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf", size = 1292660 }, + { url = "https://files.pythonhosted.org/packages/7e/5d/99c71f8e5c8b64295be421b4c42d472766b263a1fe32e91b64bf77005bf2/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820", size = 1300988 }, + { url = "https://files.pythonhosted.org/packages/8f/2c/76d2377dd947f52fbe8afb19b18a3b816d66c7966755c04030f93b1f7b2d/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca", size = 1339268 }, + { url = "https://files.pythonhosted.org/packages/fd/e6/3d9d935cc705d57ed524d82ec5d6b678a53ac1552720ae41282caa273584/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91", size = 1366993 }, + { url = "https://files.pythonhosted.org/packages/fe/c2/f7eed4d602f3f224600d03ab2e1a7734999b0901b1c49b94dc5891340433/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6", size = 1329459 }, + { url = "https://files.pythonhosted.org/packages/ce/8f/27f205b76531fc592abe29e1ad265a16bf934a9f609509c02d765e6a8055/aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12", size = 356968 }, + { url = "https://files.pythonhosted.org/packages/39/8c/4f6c0b2b3629f6be6c81ab84d9d577590f74f01d4412bfc4067958eaa1e1/aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc", size = 377650 }, + { url = "https://files.pythonhosted.org/packages/7b/b9/03b4327897a5b5d29338fa9b514f1c2f66a3e4fc88a4e40fad478739314d/aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092", size = 576994 }, + { url = "https://files.pythonhosted.org/packages/67/1b/20c2e159cd07b8ed6dde71c2258233902fdf415b2fe6174bd2364ba63107/aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77", size = 390684 }, + { url = "https://files.pythonhosted.org/packages/4d/6b/ff83b34f157e370431d8081c5d1741963f4fb12f9aaddb2cacbf50305225/aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385", size = 386176 }, + { url = "https://files.pythonhosted.org/packages/4d/a1/6e92817eb657de287560962df4959b7ddd22859c4b23a0309e2d3de12538/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972", size = 1303310 }, + { url = "https://files.pythonhosted.org/packages/04/29/200518dc7a39c30ae6d5bc232d7207446536e93d3d9299b8e95db6e79c54/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16", size = 1340445 }, + { url = "https://files.pythonhosted.org/packages/8e/20/53f7bba841ba7b5bb5dea580fea01c65524879ba39cb917d08c845524717/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6", size = 1385121 }, + { url = "https://files.pythonhosted.org/packages/f1/b4/d99354ad614c48dd38fb1ee880a1a54bd9ab2c3bcad3013048d4a1797d3a/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa", size = 1299669 }, + { url = "https://files.pythonhosted.org/packages/51/39/ca1de675f2a5729c71c327e52ac6344e63f036bd37281686ae5c3fb13bfb/aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689", size = 1252638 }, + { url = "https://files.pythonhosted.org/packages/54/cf/a3ae7ff43138422d477348e309ef8275779701bf305ff6054831ef98b782/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57", size = 1266889 }, + { url = "https://files.pythonhosted.org/packages/6e/7a/c6027ad70d9fb23cf254a26144de2723821dade1a624446aa22cd0b6d012/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f", size = 1266249 }, + { url = "https://files.pythonhosted.org/packages/64/fd/ed136d46bc2c7e3342fed24662b4827771d55ceb5a7687847aae977bfc17/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599", size = 1311036 }, + { url = "https://files.pythonhosted.org/packages/76/9a/43eeb0166f1119256d6f43468f900db1aed7fbe32069d2a71c82f987db4d/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5", size = 1338756 }, + { url = "https://files.pythonhosted.org/packages/d5/bc/d01ff0810b3f5e26896f76d44225ed78b088ddd33079b85cd1a23514318b/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987", size = 1299976 }, + { url = "https://files.pythonhosted.org/packages/3e/c9/50a297c4f7ab57a949f4add2d3eafe5f3e68bb42f739e933f8b32a092bda/aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04", size = 355609 }, + { url = "https://files.pythonhosted.org/packages/65/28/aee9d04fb0b3b1f90622c338a08e54af5198e704a910e20947c473298fd0/aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022", size = 375697 }, ] [[package]] @@ -216,14 +231,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.3.1" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/47/df70ecd34fbf86d69833fe4e25bb9ecbaab995c8e49df726dd416f6bb822/authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", size = 146074 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/75/47dbab150ef6f9298e227a40c93c7fed5f3ffb67c9fb62cd49f66285e46e/authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2", size = 147313 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1f/bc95e43ffb57c05b8efcc376dd55a0240bf58f47ddf5a0f92452b6457b75/Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377", size = 223827 }, + { url = "https://files.pythonhosted.org/packages/df/4c/9aa0416a403d5cc80292cb030bcd2c918cce2755e314d8c1aa18656e1e12/Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc", size = 225111 }, ] [[package]] @@ -735,15 +750,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/7d/7d/60ee3f2b16d9bfdfa [[package]] name = "duckduckgo-search" -version = "6.2.10" +version = "6.2.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "primp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/5d/e86ada69d354094786dcd34a75e66b1ed18d7b6da0fa410ad587f2c01e52/duckduckgo_search-6.2.10.tar.gz", hash = "sha256:53057368480ca496fc4e331a34648124711580cf43fbb65336eaa6fd2ee37cec", size = 33024 } +sdist = { url = "https://files.pythonhosted.org/packages/11/04/c76cd009653ace8d6a4a2a3d7851f5379911091d5d2104eef4bcb4c46c14/duckduckgo_search-6.2.11.tar.gz", hash = "sha256:6b6ef1b552c5e67f23e252025d2504caf6f9fc14f70e86c6dd512200f386c673", size = 32998 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/67/f6f2a096f331f8051bcff9a9db7cadeee683ad478b302939acf4a58b1cc9/duckduckgo_search-6.2.10-py3-none-any.whl", hash = "sha256:266c1528dcbc90931b7c800a2c1041a0cb447c83c485414d77a7e443be717ed6", size = 27451 }, + { url = "https://files.pythonhosted.org/packages/f5/67/b1eb59a859717043b605eb16d4f3afe46ec3614804ac0fbe594339667866/duckduckgo_search-6.2.11-py3-none-any.whl", hash = "sha256:6fb7069b79e8928f487001de6859034ade19201bdcd257ec198802430e374bfe", size = 27429 }, ] [[package]] @@ -923,14 +938,14 @@ wheels = [ [[package]] name = "flask-cors" -version = "4.0.1" +version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/6a/a8d56d60bcfa1ec3e4fdad81b45aafd508c3bd5c244a16526fa29139d7d4/flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4", size = 30306 } +sdist = { url = "https://files.pythonhosted.org/packages/4f/d0/d9e52b154e603b0faccc0b7c2ad36a764d8755ef4036acbf1582a67fb86b/flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", size = 30954 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/52/2aa6285f104616f73ee1ad7905a16b2b35af0143034ad0cf7b64bcba715c/Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677", size = 14290 }, + { url = "https://files.pythonhosted.org/packages/56/07/1afa0514c876282bebc1c9aee83c6bb98fe6415cf57b88d9b06e7e29bf9c/Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc", size = 14463 }, ] [[package]] @@ -1470,7 +1485,7 @@ wheels = [ [[package]] name = "langchain" -version = "0.2.14" +version = "0.2.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1484,9 +1499,9 @@ dependencies = [ { name = "sqlalchemy" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/de/61f635dcdafdb261d7bc93bc5330a21ff7c216339b95ed1c9364111c61f5/langchain-0.2.14.tar.gz", hash = "sha256:dc2aa5a58882054fb5d043c39ab8332ebd055f88f17839da68e1c7fd0a4fefe2", size = 411290 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/f6/9e1044e0b805f5f704435f9b378c26f02dfad3cd361f455e0a1129f8d7ad/langchain-0.2.15.tar.gz", hash = "sha256:f613ce7594be34f9bac687134a56f6e8274951907b798dbd037aefc95df78953", size = 414499 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/27/99ecada550317ef7bc89f439cd491ac106bbe9bae31ecdb47d2c6e03016e/langchain-0.2.14-py3-none-any.whl", hash = "sha256:eed76194ee7d9c081037a3df7868d4de90e0410b51fc1ca933a8379e464bf40c", size = 997752 }, + { url = "https://files.pythonhosted.org/packages/2a/5f/fec41e34c31265e4dc197ebe24d138c73dfe6a15832fe5db9a83c70e570c/langchain-0.2.15-py3-none-any.whl", hash = "sha256:9e6231441870aaa8523be24a5785ccccfdde759a7e27dd082b6ec80f68e49dec", size = 1001055 }, ] [[package]] @@ -1527,7 +1542,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "0.2.34" +version = "0.2.38" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1538,9 +1553,9 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/af/fe484778e17157399a66d1b66741223942674b7e0b88e461735538dcfdce/langchain_core-0.2.34.tar.gz", hash = "sha256:50048d90b175c0d5a7e28164628b3c7f8c82b0dc2cd766a663d346a18d5c9eb2", size = 313750 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/1c/c1016073a338206358b4a0dba936c109819fb4b070ec2c91456688c1a6dd/langchain_core-0.2.38.tar.gz", hash = "sha256:eb69dbedd344f2ee1f15bcea6c71a05884b867588fadc42d04632e727c1238f3", size = 316363 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/f8/595cff3ae7219faaf378392d3fb3998b1de7082c270811e9b0a7aef179ca/langchain_core-0.2.34-py3-none-any.whl", hash = "sha256:c4fd158273e28cef758b4eccc956b424b76d4bb9117ce6014ae6eb2fb985801d", size = 393863 }, + { url = "https://files.pythonhosted.org/packages/1c/e4/501fbe904530dad6ed80f03b188d7602081560dd5cc0bcf0b3c51778c314/langchain_core-0.2.38-py3-none-any.whl", hash = "sha256:8a5729bc7e68b4af089af20eff44fe4e7ca21d0e0c87ec21cef7621981fd1a4a", size = 396442 }, ] [[package]] @@ -2105,7 +2120,7 @@ wheels = [ [[package]] name = "open-webui" -version = "0.3.16" +version = "0.3.17.dev3" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -2175,28 +2190,28 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = "==3.10.2" }, + { name = "aiohttp", specifier = "==3.10.5" }, { name = "alembic", specifier = "==1.13.2" }, { name = "anthropic" }, { name = "apscheduler", specifier = "==3.10.4" }, { name = "argon2-cffi", specifier = "==23.1.0" }, - { name = "authlib", specifier = "==1.3.1" }, + { name = "authlib", specifier = "==1.3.2" }, { name = "bcrypt", specifier = "==4.2.0" }, { name = "black", specifier = "==24.8.0" }, { name = "boto3", specifier = "==1.35.0" }, { name = "chromadb", specifier = "==0.5.5" }, { name = "docker", specifier = "~=7.1.0" }, { name = "docx2txt", specifier = "==0.8" }, - { name = "duckduckgo-search", specifier = "~=6.2.1" }, + { name = "duckduckgo-search", specifier = "~=6.2.11" }, { name = "extract-msg" }, { name = "fake-useragent", specifier = "==1.5.1" }, { name = "fastapi", specifier = "==0.111.0" }, { name = "faster-whisper", specifier = "==1.0.3" }, { name = "flask", specifier = "==3.0.3" }, - { name = "flask-cors", specifier = "==4.0.1" }, + { name = "flask-cors", specifier = "==5.0.0" }, { name = "fpdf2", specifier = "==2.7.9" }, { name = "google-generativeai", specifier = "==0.7.2" }, - { name = "langchain", specifier = "==0.2.14" }, + { name = "langchain", specifier = "==0.2.15" }, { name = "langchain-chroma", specifier = "==0.1.2" }, { name = "langchain-community", specifier = "==0.2.12" }, { name = "langfuse", specifier = "==2.44.0" }, @@ -2233,7 +2248,7 @@ requires-dist = [ { name = "sentence-transformers", specifier = "==3.0.1" }, { name = "sqlalchemy", specifier = "==2.0.32" }, { name = "tiktoken" }, - { name = "unstructured", specifier = "==0.15.7" }, + { name = "unstructured", specifier = "==0.15.9" }, { name = "uvicorn", extras = ["standard"], specifier = "==0.30.6" }, { name = "validators", specifier = "==0.33.0" }, { name = "xlrd", specifier = "==2.0.1" }, @@ -3085,6 +3100,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215", size = 22299 }, ] +[[package]] +name = "python-oxmsg" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "olefile" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/d4/4ec721fd433453fe05344f41f17458775d111e9f6c668ce1a0fccec0fecd/python_oxmsg-0.0.1.tar.gz", hash = "sha256:b65c1f93d688b85a9410afa824192a1ddc39da359b04a0bd2cbd3874e84d4994", size = 34541 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/c8/fb23e1e7723ba9200b75bc121f22f67498ae098a202f1646acc4f6a54f5c/python_oxmsg-0.0.1-py3-none-any.whl", hash = "sha256:8ea7d5dda1bc161a413213da9e18ed152927c1fda2feaf5d1f02192d8ad45eea", size = 31426 }, +] + [[package]] name = "python-pptx" version = "1.0.0" @@ -3941,7 +3970,7 @@ wheels = [ [[package]] name = "unstructured" -version = "0.15.7" +version = "0.15.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, @@ -3957,6 +3986,7 @@ dependencies = [ { name = "psutil" }, { name = "python-iso639" }, { name = "python-magic" }, + { name = "python-oxmsg" }, { name = "rapidfuzz" }, { name = "requests" }, { name = "tabulate" }, @@ -3965,9 +3995,9 @@ dependencies = [ { name = "unstructured-client" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/47/57e22387f438b9fd85cb20281388403bd4ef2f91a4db37b60c4b7de778b5/unstructured-0.15.7.tar.gz", hash = "sha256:ac55bf31b1d4c19c33c0e2ec5f615d96d03a2bf49a784f23b29d5530b90d6830", size = 1856207 } +sdist = { url = "https://files.pythonhosted.org/packages/74/db/be587e728e2edf684a6c2ead46d05e02951f78b2949c571fed78266941eb/unstructured-0.15.9.tar.gz", hash = "sha256:de26d0e38bac4aa3ae2950f175d0c53a5ccae5c45806b67f55a4af8dea4c407a", size = 1858477 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/1c/cc8da380be1b489efafc8ced70bd0707aed55d3c09f7606569eed2e983ac/unstructured-0.15.7-py3-none-any.whl", hash = "sha256:9b176f18776142feed1f058f11d16046ae24d077fa96648979ae9c474819f56c", size = 2116855 }, + { url = "https://files.pythonhosted.org/packages/58/7b/93126eed91753d65d0c07e9f4c80bd715b6b6003f139483024ae00749aa2/unstructured-0.15.9-py3-none-any.whl", hash = "sha256:ddbb043461cfb9efa1d48a18e62e3b43ff4e0cec25fbf0f28bf345589c1af4d2", size = 2120717 }, ] [[package]]