diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c4a3bb1fa..0a36356f3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,17 @@ # Pull Request Checklist -- [ ] **Target branch:** Pull requests should target the `dev` branch. -- [ ] **Description:** Briefly describe the changes in this pull request. +### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request. + +**Before submitting, make sure you've checked the following:** + +- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch. +- [ ] **Description:** Provide a concise description of the changes made in this pull request. - [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description. - [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources? - [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation? -- [ ] **Testing:** Have you written and run sufficient tests for the changes? -- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues? -- [ ] **Label title:** Ensure the pull request title is labeled properly using one of the following: +- [ ] **Testing:** Have you written and run sufficient tests for validating the changes? +- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards? +- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following: - **BREAKING CHANGE**: Significant changes that may affect compatibility - **build**: Changes that affect the build system or external dependencies - **ci**: Changes to our continuous integration processes or workflows @@ -26,7 +30,7 @@ ### Description -- [Briefly describe the changes made in this pull request, including any relevant motivation and impact.] +- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)] ### Added @@ -62,3 +66,7 @@ - [Insert any additional context, notes, or explanations for the changes] - [Reference any related issues, commits, or other relevant information] + +### Screenshots or Videos + +- [Attach any relevant screenshots or videos demonstrating the changes] diff --git a/.gitignore b/.gitignore index 55209604a..a54fef595 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ __pycache__/ # C extensions *.so +# Pyodide distribution +static/pyodide/* +!static/pyodide/pyodide-lock.json + # Distribution / packaging .Python build/ diff --git a/.prettierignore b/.prettierignore index bdcce08cc..82c491257 100644 --- a/.prettierignore +++ b/.prettierignore @@ -310,3 +310,7 @@ dist # cypress artifacts cypress/videos cypress/screenshots + + + +/static/* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2047cc3fb..98656a309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ 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.1.125] - 2024-05-19 + +### Added + +- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI. +- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access. +- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience. +- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'. +- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory. +- **💾 Persistent Settings**: Settings now saved as config.json for convenience. +- **🩺 Health Check Endpoint**: Added for Docker deployment. +- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction. +- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents. +- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added. + +### Changed + +- **👤 Shared Chat Update**: Shared chat now includes creator user information. + ## [0.1.124] - 2024-05-08 ### Added diff --git a/Dockerfile b/Dockerfile index d874c8561..dee049fb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,9 @@ ARG USE_CUDA_VER=cu121 # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 ARG USE_RERANKING_MODEL="" +# Override at your own risk - non-root configurations are untested +ARG UID=0 +ARG GID=0 ######## WebUI frontend ######## FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build @@ -32,6 +35,8 @@ ARG USE_OLLAMA ARG USE_CUDA_VER ARG USE_EMBEDDING_MODEL ARG USE_RERANKING_MODEL +ARG UID +ARG GID ## Basis ## ENV ENV=prod \ @@ -76,9 +81,20 @@ ENV HF_HOME="/app/backend/data/cache/embedding/models" WORKDIR /app/backend ENV HOME /root +# Create user and group if not root +RUN if [ $UID -ne 0 ]; then \ + if [ $GID -ne 0 ]; then \ + addgroup --gid $GID app; \ + fi; \ + adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \ + fi + RUN mkdir -p $HOME/.cache/chroma RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id +# Make sure the user has access to the app and root directory +RUN chown -R $UID:$GID /app $HOME + RUN if [ "$USE_OLLAMA" = "true" ]; then \ apt-get update && \ # Install pandoc and netcat @@ -94,7 +110,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \ else \ apt-get update && \ # Install pandoc and netcat - apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \ + apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \ # for RAG OCR apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \ # cleanup @@ -102,7 +118,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \ fi # install python dependencies -COPY ./backend/requirements.txt ./requirements.txt +COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt RUN pip3 install uv && \ if [ "$USE_CUDA" = "true" ]; then \ @@ -125,16 +141,17 @@ RUN pip3 install uv && \ # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx # copy built frontend files -COPY --from=build /app/build /app/build -COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md -COPY --from=build /app/package.json /app/package.json +COPY --chown=$UID:$GID --from=build /app/build /app/build +COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md +COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json # copy backend files -COPY ./backend . +COPY --chown=$UID:$GID ./backend . EXPOSE 8080 HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1 +USER $UID:$GID CMD [ "bash", "start.sh"] diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py index 1c309439d..4419ccf19 100644 --- a/backend/apps/images/main.py +++ b/backend/apps/images/main.py @@ -397,7 +397,7 @@ def generate_image( user=Depends(get_current_user), ): - width, height = tuple(map(int, app.state.config.IMAGE_SIZE).split("x")) + width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x"))) r = None try: diff --git a/backend/apps/litellm/main.py b/backend/apps/litellm/main.py index 6db426439..6a355038b 100644 --- a/backend/apps/litellm/main.py +++ b/backend/apps/litellm/main.py @@ -75,6 +75,10 @@ with open(LITELLM_CONFIG_DIR, "r") as file: litellm_config = yaml.safe_load(file) +app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value +app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value + + app.state.ENABLE = ENABLE_LITELLM app.state.CONFIG = litellm_config @@ -151,10 +155,6 @@ async def shutdown_litellm_background(): background_process = None -app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST - - @app.get("/") async def get_status(): return {"status": True} diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index cb80eeed2..df268067f 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -64,8 +64,8 @@ app.add_middleware( app.state.config = AppConfig() -app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST +app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER +app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS app.state.MODELS = {} @@ -124,8 +124,9 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user)) async def fetch_url(url): + timeout = aiohttp.ClientTimeout(total=5) try: - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get(url) as response: return await response.json() except Exception as e: @@ -177,11 +178,12 @@ async def get_ollama_tags( if url_idx == None: models = await get_all_models() - if app.state.ENABLE_MODEL_FILTER: + if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user": models["models"] = list( filter( - lambda model: model["name"] in app.state.MODEL_FILTER_LIST, + lambda model: model["name"] + in app.state.config.MODEL_FILTER_LIST, models["models"], ) ) @@ -1045,11 +1047,12 @@ async def get_openai_models( if url_idx == None: models = await get_all_models() - if app.state.ENABLE_MODEL_FILTER: + if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user": models["models"] = list( filter( - lambda model: model["name"] in app.state.MODEL_FILTER_LIST, + lambda model: model["name"] + in app.state.config.MODEL_FILTER_LIST, models["models"], ) ) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index 65ed25f1c..85ee531f1 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -21,6 +21,7 @@ from utils.utils import ( ) from config import ( SRC_LOG_LEVELS, + ENABLE_OPENAI_API, OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR, @@ -46,11 +47,14 @@ app.add_middleware( allow_headers=["*"], ) + app.state.config = AppConfig() -app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST +app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER +app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST + +app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS @@ -68,6 +72,21 @@ async def check_url(request: Request, call_next): return response +@app.get("/config") +async def get_config(user=Depends(get_admin_user)): + return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API} + + +class OpenAIConfigForm(BaseModel): + enable_openai_api: Optional[bool] = None + + +@app.post("/config/update") +async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)): + app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api + return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API} + + class UrlsUpdateForm(BaseModel): urls: List[str] @@ -164,11 +183,15 @@ async def speech(request: Request, user=Depends(get_verified_user)): async def fetch_url(url, key): + timeout = aiohttp.ClientTimeout(total=5) try: - headers = {"Authorization": f"Bearer {key}"} - async with aiohttp.ClientSession() as session: - async with session.get(url, headers=headers) as response: - return await response.json() + if key != "": + headers = {"Authorization": f"Bearer {key}"} + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url, headers=headers) as response: + return await response.json() + else: + return None except Exception as e: # Handle connection error here log.error(f"Connection error: {e}") @@ -200,7 +223,7 @@ async def get_all_models(): if ( len(app.state.config.OPENAI_API_KEYS) == 1 and app.state.config.OPENAI_API_KEYS[0] == "" - ): + ) or not app.state.config.ENABLE_OPENAI_API: models = {"data": []} else: tasks = [ @@ -237,11 +260,11 @@ async def get_all_models(): async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)): if url_idx == None: models = await get_all_models() - if app.state.ENABLE_MODEL_FILTER: + if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user": models["data"] = list( filter( - lambda model: model["id"] in app.state.MODEL_FILTER_LIST, + lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, models["data"], ) ) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 34b88f30e..1e354c874 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -70,6 +70,7 @@ from utils.misc import ( from utils.utils import get_current_user, get_admin_user from config import ( + ENV, SRC_LOG_LEVELS, UPLOAD_DIR, DOCS_DIR, @@ -266,7 +267,7 @@ async def update_embedding_config( app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url app.state.config.OPENAI_API_KEY = form_data.openai_config.key - update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL), True + update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL) app.state.EMBEDDING_FUNCTION = get_embedding_function( app.state.config.RAG_EMBEDDING_ENGINE, @@ -439,12 +440,12 @@ async def update_query_settings( form_data: QuerySettingsForm, user=Depends(get_admin_user) ): app.state.config.RAG_TEMPLATE = ( - form_data.template if form_data.template else RAG_TEMPLATE, + form_data.template if form_data.template else RAG_TEMPLATE ) app.state.config.TOP_K = form_data.k if form_data.k else 4 app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0 app.state.config.ENABLE_RAG_HYBRID_SEARCH = ( - form_data.hybrid if form_data.hybrid else False, + form_data.hybrid if form_data.hybrid else False ) return { "status": True, @@ -1006,3 +1007,14 @@ def reset(user=Depends(get_admin_user)) -> bool: log.exception(e) return True + + +if ENV == "dev": + + @app.get("/ef") + async def get_embeddings(): + return {"result": app.state.EMBEDDING_FUNCTION("hello world")} + + @app.get("/ef/{text}") + async def get_embeddings_text(text: str): + return {"result": app.state.EMBEDDING_FUNCTION(text)} diff --git a/backend/apps/web/internal/migrations/008_add_memory.py b/backend/apps/web/internal/migrations/008_add_memory.py new file mode 100644 index 000000000..9307aa4d5 --- /dev/null +++ b/backend/apps/web/internal/migrations/008_add_memory.py @@ -0,0 +1,53 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + @migrator.create_model + class Memory(pw.Model): + id = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + content = pw.TextField(null=False) + updated_at = pw.BigIntegerField(null=False) + created_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = "memory" + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model("memory") diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 755e3911b..2b6966381 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -9,6 +9,7 @@ from apps.web.routers import ( modelfiles, prompts, configs, + memories, utils, ) from config import ( @@ -41,6 +42,7 @@ app.state.config.USER_PERMISSIONS = USER_PERMISSIONS app.state.config.WEBHOOK_URL = WEBHOOK_URL app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER + app.add_middleware( CORSMiddleware, allow_origins=origins, @@ -52,9 +54,12 @@ app.add_middleware( app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(chats.router, prefix="/chats", tags=["chats"]) + app.include_router(documents.router, prefix="/documents", tags=["documents"]) app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"]) app.include_router(prompts.router, prefix="/prompts", tags=["prompts"]) +app.include_router(memories.router, prefix="/memories", tags=["memories"]) + app.include_router(configs.router, prefix="/configs", tags=["configs"]) app.include_router(utils.router, prefix="/utils", tags=["utils"]) diff --git a/backend/apps/web/models/memories.py b/backend/apps/web/models/memories.py new file mode 100644 index 000000000..8382b3e52 --- /dev/null +++ b/backend/apps/web/models/memories.py @@ -0,0 +1,118 @@ +from pydantic import BaseModel +from peewee import * +from playhouse.shortcuts import model_to_dict +from typing import List, Union, Optional + +from apps.web.internal.db import DB +from apps.web.models.chats import Chats + +import time +import uuid + +#################### +# Memory DB Schema +#################### + + +class Memory(Model): + id = CharField(unique=True) + user_id = CharField() + content = TextField() + updated_at = BigIntegerField() + created_at = BigIntegerField() + + class Meta: + database = DB + + +class MemoryModel(BaseModel): + id: str + user_id: str + content: str + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class MemoriesTable: + def __init__(self, db): + self.db = db + self.db.create_tables([Memory]) + + def insert_new_memory( + self, + user_id: str, + content: str, + ) -> Optional[MemoryModel]: + id = str(uuid.uuid4()) + + memory = MemoryModel( + **{ + "id": id, + "user_id": user_id, + "content": content, + "created_at": int(time.time()), + "updated_at": int(time.time()), + } + ) + result = Memory.create(**memory.model_dump()) + if result: + return memory + else: + return None + + def get_memories(self) -> List[MemoryModel]: + try: + memories = Memory.select() + return [MemoryModel(**model_to_dict(memory)) for memory in memories] + except: + return None + + def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]: + try: + memories = Memory.select().where(Memory.user_id == user_id) + return [MemoryModel(**model_to_dict(memory)) for memory in memories] + except: + return None + + def get_memory_by_id(self, id) -> Optional[MemoryModel]: + try: + memory = Memory.get(Memory.id == id) + return MemoryModel(**model_to_dict(memory)) + except: + return None + + def delete_memory_by_id(self, id: str) -> bool: + try: + query = Memory.delete().where(Memory.id == id) + query.execute() # Remove the rows, return number of rows removed. + + return True + + except: + return False + + def delete_memories_by_user_id(self, user_id: str) -> bool: + try: + query = Memory.delete().where(Memory.user_id == user_id) + query.execute() + + return True + except: + return False + + def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool: + try: + query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id) + query.execute() + + return True + except: + return False + + +Memories = MemoriesTable(DB) diff --git a/backend/apps/web/routers/memories.py b/backend/apps/web/routers/memories.py new file mode 100644 index 000000000..f20e02601 --- /dev/null +++ b/backend/apps/web/routers/memories.py @@ -0,0 +1,145 @@ +from fastapi import Response, Request +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import logging + +from apps.web.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 + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +router = APIRouter() + + +@router.get("/ef") +async def get_embeddings(request: Request): + return {"result": request.app.state.EMBEDDING_FUNCTION("hello world")} + + +############################ +# GetMemories +############################ + + +@router.get("/", response_model=List[MemoryModel]) +async def get_memories(user=Depends(get_verified_user)): + return Memories.get_memories_by_user_id(user.id) + + +############################ +# AddMemory +############################ + + +class AddMemoryForm(BaseModel): + content: str + + +@router.post("/add", response_model=Optional[MemoryModel]) +async def add_memory( + request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user) +): + memory = Memories.insert_new_memory(user.id, form_data.content) + memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content) + + collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}") + collection.upsert( + documents=[memory.content], + ids=[memory.id], + embeddings=[memory_embedding], + metadatas=[{"created_at": memory.created_at}], + ) + + return memory + + +############################ +# QueryMemory +############################ + + +class QueryMemoryForm(BaseModel): + content: str + + +@router.post("/query") +async def query_memory( + request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user) +): + query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content) + collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}") + + results = collection.query( + query_embeddings=[query_embedding], + n_results=1, # how many results to return + ) + + return results + + +############################ +# ResetMemoryFromVectorDB +############################ +@router.get("/reset", response_model=bool) +async def reset_memory_from_vector_db( + request: Request, user=Depends(get_verified_user) +): + CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}") + collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}") + + memories = Memories.get_memories_by_user_id(user.id) + for memory in memories: + memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content) + collection.upsert( + documents=[memory.content], + ids=[memory.id], + embeddings=[memory_embedding], + ) + return True + + +############################ +# DeleteMemoriesByUserId +############################ + + +@router.delete("/user", response_model=bool) +async def delete_memory_by_user_id(user=Depends(get_verified_user)): + result = Memories.delete_memories_by_user_id(user.id) + + if result: + try: + CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}") + except Exception as e: + log.error(e) + return True + + return False + + +############################ +# DeleteMemoryById +############################ + + +@router.delete("/{memory_id}", response_model=bool) +async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)): + result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id) + + if result: + collection = CHROMA_CLIENT.get_or_create_collection( + name=f"user-memory-{user.id}" + ) + collection.delete(ids=[memory_id]) + return True + + return False diff --git a/backend/apps/web/routers/users.py b/backend/apps/web/routers/users.py index d87854e89..d77475d8d 100644 --- a/backend/apps/web/routers/users.py +++ b/backend/apps/web/routers/users.py @@ -11,8 +11,9 @@ import logging from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users from apps.web.models.auths import Auths +from apps.web.models.chats import Chats -from utils.utils import get_current_user, get_password_hash, get_admin_user +from utils.utils import get_verified_user, get_password_hash, get_admin_user from constants import ERROR_MESSAGES from config import SRC_LOG_LEVELS @@ -67,6 +68,41 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin ) +############################ +# GetUserById +############################ + + +class UserResponse(BaseModel): + name: str + profile_image_url: str + + +@router.get("/{user_id}", response_model=UserResponse) +async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): + + if user_id.startswith("shared-"): + chat_id = user_id.replace("shared-", "") + chat = Chats.get_chat_by_id(chat_id) + if chat: + user_id = chat.user_id + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + user = Users.get_user_by_id(user_id) + + if user: + return UserResponse(name=user.name, profile_image_url=user.profile_image_url) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + ############################ # UpdateUserById ############################ diff --git a/backend/config.py b/backend/config.py index e847593fb..41f0c09a3 100644 --- a/backend/config.py +++ b/backend/config.py @@ -417,6 +417,14 @@ OLLAMA_BASE_URLS = PersistentConfig( # OPENAI_API #################################### + +ENABLE_OPENAI_API = PersistentConfig( + "ENABLE_OPENAI_API", + "openai.enable", + os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true", +) + + OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") diff --git a/backend/main.py b/backend/main.py index 5602e33ee..28207743f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -118,6 +118,18 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL origins = ["*"] +# Custom middleware to add security headers +# class SecurityHeadersMiddleware(BaseHTTPMiddleware): +# async def dispatch(self, request: Request, call_next): +# response: Response = await call_next(request) +# response.headers["Cross-Origin-Opener-Policy"] = "same-origin" +# response.headers["Cross-Origin-Embedder-Policy"] = "require-corp" +# return response + + +# app.add_middleware(SecurityHeadersMiddleware) + + class RAGMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): return_citations = False @@ -227,9 +239,15 @@ async def check_url(request: Request, call_next): return response -app.mount("/api/v1", webui_app) -app.mount("/litellm/api", litellm_app) +@app.middleware("http") +async def update_embedding_function(request: Request, call_next): + response = await call_next(request) + if "/embedding/update" in request.url.path: + webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION + return response + +app.mount("/litellm/api", litellm_app) app.mount("/ollama", ollama_app) app.mount("/openai/api", openai_app) @@ -237,6 +255,10 @@ app.mount("/images/api/v1", images_app) app.mount("/audio/api/v1", audio_app) app.mount("/rag/api/v1", rag_app) +app.mount("/api/v1", webui_app) + +webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION + @app.get("/api/config") async def get_app_config(): @@ -279,14 +301,14 @@ class ModelFilterConfigForm(BaseModel): async def update_model_filter_config( form_data: ModelFilterConfigForm, user=Depends(get_admin_user) ): - app.state.config.ENABLE_MODEL_FILTER, form_data.enabled - app.state.config.MODEL_FILTER_LIST, form_data.models + app.state.config.ENABLE_MODEL_FILTER = form_data.enabled + app.state.config.MODEL_FILTER_LIST = form_data.models - ollama_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER - ollama_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST + ollama_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER + ollama_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST - openai_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER - openai_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST + openai_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER + openai_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST litellm_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER litellm_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST diff --git a/cypress/e2e/chat.cy.ts b/cypress/e2e/chat.cy.ts index 6f5fa36c9..ced998104 100644 --- a/cypress/e2e/chat.cy.ts +++ b/cypress/e2e/chat.cy.ts @@ -62,18 +62,17 @@ describe('Settings', () => { .should('exist'); // spy on requests const spy = cy.spy(); - cy.intercept("GET", "/api/v1/chats/*", spy); + cy.intercept('GET', '/api/v1/chats/*', spy); // Open context menu cy.get('#chat-context-menu-button').click(); // Click share button cy.get('#chat-share-button').click(); // Check if the share dialog is visible cy.get('#copy-and-share-chat-button').should('exist'); - cy.wrap({}, { timeout: 5000 }) - .should(() => { - // Check if the request was made twice (once for to replace chat object and once more due to change event) - expect(spy).to.be.callCount(2); - }); + cy.wrap({}, { timeout: 5000 }).should(() => { + // Check if the request was made twice (once for to replace chat object and once more due to change event) + expect(spy).to.be.callCount(2); + }); }); }); }); diff --git a/package-lock.json b/package-lock.json index 8f34cddf3..5f98d38f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { "name": "open-webui", - "version": "0.1.124", + "version": "0.1.125", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.1.124", + "version": "0.1.125", "dependencies": { + "@pyscript/core": "^0.4.32", "@sveltejs/adapter-node": "^1.3.1", "async": "^3.2.5", "bits-ui": "^0.19.7", @@ -22,6 +23,7 @@ "js-sha256": "^0.10.1", "katex": "^0.16.9", "marked": "^9.1.0", + "pyodide": "^0.26.0-alpha.4", "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "uuid": "^9.0.1" @@ -890,6 +892,19 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@pyscript/core": { + "version": "0.4.32", + "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.32.tgz", + "integrity": "sha512-WQATzPp1ggf871+PukCmTypzScXkEB1EWD/vg5GNxpM96N6rDPqQ13msuA5XvwU01ZVhL8HHSFDLk4IfaXNGWg==", + "dependencies": { + "@ungap/with-resolvers": "^0.1.0", + "basic-devtools": "^0.1.6", + "polyscript": "^0.12.8", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1", + "type-checked-collections": "^0.1.7" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.7", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", @@ -1605,8 +1620,12 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@ungap/with-resolvers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz", + "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" }, "node_modules/@vitest/expect": { "version": "1.6.0", @@ -1713,6 +1732,11 @@ "@types/estree": "^1.0.0" } }, + "node_modules/@webreflection/fetch": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", + "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==" + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -2027,6 +2051,11 @@ "dev": true, "optional": true }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2047,6 +2076,11 @@ } ] }, + "node_modules/basic-devtools": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz", + "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==" + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2661,6 +2695,28 @@ "@types/estree": "^1.0.0" } }, + "node_modules/codedent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz", + "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==", + "dependencies": { + "plain-tag": "^0.1.3" + } + }, + "node_modules/coincident": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", + "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "gc-hook": "^0.3.1", + "proxy-target": "^3.0.2" + }, + "optionalDependencies": { + "ws": "^8.16.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4001,6 +4057,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gc-hook": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", + "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -4328,6 +4389,11 @@ "node": ">=12.0.0" } }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -5838,6 +5904,29 @@ "pathe": "^1.1.2" } }, + "node_modules/plain-tag": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz", + "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==" + }, + "node_modules/polyscript": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.12.8.tgz", + "integrity": "sha512-kcG3W9jU/s1sYjWOTAa2jAh5D2jm3zJRi+glSTsC+lA3D1b/Sd67pEIGpyL9bWNKYSimqAx4se6jAhQjJZ7+jQ==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "@webreflection/fetch": "^0.1.5", + "basic-devtools": "^0.1.6", + "codedent": "^0.1.2", + "coincident": "^1.2.3", + "gc-hook": "^0.3.1", + "html-escaper": "^3.0.3", + "proxy-target": "^3.0.2", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1" + } + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -6151,6 +6240,11 @@ "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", "dev": true }, + "node_modules/proxy-target": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz", + "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -6176,6 +6270,18 @@ "node": ">=6" } }, + "node_modules/pyodide": { + "version": "0.26.0-alpha.4", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.0-alpha.4.tgz", + "integrity": "sha512-Ixuczq99DwhQlE+Bt0RaS6Ln9MHSZOkbU6iN8azwaeorjHtr7ukaxh+FeTxViFrp2y+ITyKgmcobY+JnBPcULw==", + "dependencies": { + "base-64": "^1.0.0", + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/qs": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", @@ -6858,6 +6964,11 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, + "node_modules/sticky-module": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz", + "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==" + }, "node_modules/stream-composer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", @@ -7520,6 +7631,11 @@ "node": ">=14.14" } }, + "node_modules/to-json-callback": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz", + "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7629,6 +7745,11 @@ "node": ">= 0.8.0" } }, + "node_modules/type-checked-collections": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz", + "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8883,6 +9004,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2b6abdd74..2b412e310 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "open-webui", - "version": "0.1.124", + "version": "0.1.125", "private": true, "scripts": { - "dev": "vite dev --host", - "build": "vite build", + "dev": "npm run pyodide:fetch && vite dev --host", + "build": "npm run pyodide:fetch && vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", @@ -16,7 +16,8 @@ "format:backend": "black . --exclude \"/venv/\"", "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"", "cy:open": "cypress open", - "test:frontend": "vitest" + "test:frontend": "vitest", + "pyodide:fetch": "node scripts/prepare-pyodide.js" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", @@ -47,6 +48,7 @@ }, "type": "module", "dependencies": { + "@pyscript/core": "^0.4.32", "@sveltejs/adapter-node": "^1.3.1", "async": "^3.2.5", "bits-ui": "^0.19.7", @@ -61,6 +63,7 @@ "js-sha256": "^0.10.1", "katex": "^0.16.9", "marked": "^9.1.0", + "pyodide": "^0.26.0-alpha.4", "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "uuid": "^9.0.1" diff --git a/scripts/prepare-pyodide.js b/scripts/prepare-pyodide.js new file mode 100644 index 000000000..c14a5bf1b --- /dev/null +++ b/scripts/prepare-pyodide.js @@ -0,0 +1,39 @@ +const packages = [ + 'requests', + 'beautifulsoup4', + 'numpy', + 'pandas', + 'matplotlib', + 'scikit-learn', + 'scipy', + 'regex', + 'seaborn' +]; + +import { loadPyodide } from 'pyodide'; +import { writeFile, copyFile, readdir } from 'fs/promises'; + +async function downloadPackages() { + console.log('Setting up pyodide + micropip'); + const pyodide = await loadPyodide({ + packageCacheDir: 'static/pyodide' + }); + await pyodide.loadPackage('micropip'); + const micropip = pyodide.pyimport('micropip'); + console.log('Downloading Pyodide packages:', packages); + await micropip.install(packages); + console.log('Pyodide packages downloaded, freezing into lock file'); + const lockFile = await micropip.freeze(); + await writeFile('static/pyodide/pyodide-lock.json', lockFile); +} + +async function copyPyodide() { + console.log('Copying Pyodide files into static directory'); + // Copy all files from node_modules/pyodide to static/pyodide + for await (const entry of await readdir('node_modules/pyodide')) { + await copyFile(`node_modules/pyodide/${entry}`, `static/pyodide/${entry}`); + } +} + +await downloadPackages(); +await copyPyodide(); diff --git a/src/app.html b/src/app.html index 1616cc668..138fb2829 100644 --- a/src/app.html +++ b/src/app.html @@ -12,6 +12,7 @@ title="Open WebUI" href="/opensearch.xml" /> + {#if code} -
{@html highlightedCode || code}
+
+
+
+ {#if executing}
+