diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e11228706..da4046e73f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,19 @@ 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.5.19] - 2024-03-04 +## [0.5.20] - 2025-03-05 + +### Added + +- **⚡ Toggle Code Execution On/Off**: You can now enable or disable code execution, providing more control over security, ensuring a safer and more customizable experience. + +### Fixed + +- **📜 Pinyin Keyboard Enter Key Now Works Properly**: Resolved an issue where the Enter key for Pinyin keyboards was not functioning as expected, ensuring seamless input for Chinese users. +- **🖼️ Web Manifest Loading Issue Fixed**: Addressed inconsistencies with 'site.webmanifest', guaranteeing proper loading and representation of the app across different browsers and devices. +- **📦 Non-Root Container Issue Resolved**: Fixed a critical issue where the UI failed to load correctly in non-root containers, ensuring reliable deployment in various environments. + +## [0.5.19] - 2025-03-04 ### Added diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 583b80581a..bd4ffcbf10 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -593,7 +593,10 @@ for file_path in (FRONTEND_BUILD_DIR / "static").glob("**/*"): (FRONTEND_BUILD_DIR / "static") ) target_path.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(file_path, target_path) + try: + shutil.copyfile(file_path, target_path) + except Exception as e: + logging.error(f"An error occurred: {e}") frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png" @@ -1377,6 +1380,11 @@ Responses from models: {{responses}}""" # Code Interpreter #################################### +ENABLE_CODE_EXECUTION = PersistentConfig( + "ENABLE_CODE_EXECUTION", + "code_execution.enable", + os.environ.get("ENABLE_CODE_EXECUTION", "True").lower() == "true", +) CODE_EXECUTION_ENGINE = PersistentConfig( "CODE_EXECUTION_ENGINE", @@ -1553,7 +1561,9 @@ ELASTICSEARCH_USERNAME = os.environ.get("ELASTICSEARCH_USERNAME", None) ELASTICSEARCH_PASSWORD = os.environ.get("ELASTICSEARCH_PASSWORD", None) ELASTICSEARCH_CLOUD_ID = os.environ.get("ELASTICSEARCH_CLOUD_ID", None) SSL_ASSERT_FINGERPRINT = os.environ.get("SSL_ASSERT_FINGERPRINT", None) -ELASTICSEARCH_INDEX_PREFIX = os.environ.get("ELASTICSEARCH_INDEX_PREFIX", "open_webui_collections") +ELASTICSEARCH_INDEX_PREFIX = os.environ.get( + "ELASTICSEARCH_INDEX_PREFIX", "open_webui_collections" +) # Pgvector PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL) if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"): diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 1b1028120c..0ace155ebe 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -105,6 +105,7 @@ from open_webui.config import ( # Direct Connections ENABLE_DIRECT_CONNECTIONS, # Code Execution + ENABLE_CODE_EXECUTION, CODE_EXECUTION_ENGINE, CODE_EXECUTION_JUPYTER_URL, CODE_EXECUTION_JUPYTER_AUTH, @@ -662,6 +663,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( # ######################################## +app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH @@ -1175,6 +1177,7 @@ async def get_app_config(request: Request): "enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS, "enable_channels": app.state.config.ENABLE_CHANNELS, "enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH, + "enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION, "enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER, "enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION, "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, diff --git a/backend/open_webui/retrieval/vector/dbs/elasticsearch.py b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py index a558e1fb0c..c896284946 100644 --- a/backend/open_webui/retrieval/vector/dbs/elasticsearch.py +++ b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py @@ -1,30 +1,28 @@ from elasticsearch import Elasticsearch, BadRequestError from typing import Optional import ssl -from elasticsearch.helpers import bulk,scan +from elasticsearch.helpers import bulk, scan from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult from open_webui.config import ( ELASTICSEARCH_URL, - ELASTICSEARCH_CA_CERTS, + ELASTICSEARCH_CA_CERTS, ELASTICSEARCH_API_KEY, ELASTICSEARCH_USERNAME, - ELASTICSEARCH_PASSWORD, + ELASTICSEARCH_PASSWORD, ELASTICSEARCH_CLOUD_ID, ELASTICSEARCH_INDEX_PREFIX, SSL_ASSERT_FINGERPRINT, - ) - - class ElasticsearchClient: """ Important: - in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating - an index for each file but store it as a text field, while seperating to different index + in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating + an index for each file but store it as a text field, while seperating to different index baesd on the embedding length. """ + def __init__(self): self.index_prefix = ELASTICSEARCH_INDEX_PREFIX self.client = Elasticsearch( @@ -32,15 +30,19 @@ class ElasticsearchClient: ca_certs=ELASTICSEARCH_CA_CERTS, api_key=ELASTICSEARCH_API_KEY, cloud_id=ELASTICSEARCH_CLOUD_ID, - basic_auth=(ELASTICSEARCH_USERNAME,ELASTICSEARCH_PASSWORD) if ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD else None, - ssl_assert_fingerprint=SSL_ASSERT_FINGERPRINT - + basic_auth=( + (ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) + if ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD + else None + ), + ssl_assert_fingerprint=SSL_ASSERT_FINGERPRINT, ) - #Status: works - def _get_index_name(self,dimension:int)->str: + + # Status: works + def _get_index_name(self, dimension: int) -> str: return f"{self.index_prefix}_d{str(dimension)}" - - #Status: works + + # Status: works def _scan_result_to_get_result(self, result) -> GetResult: if not result: return None @@ -55,7 +57,7 @@ class ElasticsearchClient: return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) - #Status: works + # Status: works def _result_to_get_result(self, result) -> GetResult: if not result["hits"]["hits"]: return None @@ -70,7 +72,7 @@ class ElasticsearchClient: return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) - #Status: works + # Status: works def _result_to_search_result(self, result) -> SearchResult: ids = [] distances = [] @@ -84,19 +86,21 @@ class ElasticsearchClient: metadatas.append(hit["_source"].get("metadata")) return SearchResult( - ids=[ids], distances=[distances], documents=[documents], metadatas=[metadatas] + ids=[ids], + distances=[distances], + documents=[documents], + metadatas=[metadatas], ) - #Status: works + + # Status: works def _create_index(self, dimension: int): body = { "mappings": { "dynamic_templates": [ { - "strings": { - "match_mapping_type": "string", - "mapping": { - "type": "keyword" - } + "strings": { + "match_mapping_type": "string", + "mapping": {"type": "keyword"}, } } ], @@ -111,68 +115,52 @@ class ElasticsearchClient: }, "text": {"type": "text"}, "metadata": {"type": "object"}, - } + }, } } self.client.indices.create(index=self._get_index_name(dimension), body=body) - #Status: works + + # Status: works def _create_batches(self, items: list[VectorItem], batch_size=100): for i in range(0, len(items), batch_size): - yield items[i : min(i + batch_size,len(items))] + yield items[i : min(i + batch_size, len(items))] - #Status: works - def has_collection(self,collection_name) -> bool: + # Status: works + def has_collection(self, collection_name) -> bool: query_body = {"query": {"bool": {"filter": []}}} - query_body["query"]["bool"]["filter"].append({"term": {"collection": collection_name}}) + query_body["query"]["bool"]["filter"].append( + {"term": {"collection": collection_name}} + ) try: - result = self.client.count( - index=f"{self.index_prefix}*", - body=query_body - ) - - return result.body["count"]>0 + result = self.client.count(index=f"{self.index_prefix}*", body=query_body) + + return result.body["count"] > 0 except Exception as e: return None - - def delete_collection(self, collection_name: str): - query = { - "query": { - "term": {"collection": collection_name} - } - } + query = {"query": {"term": {"collection": collection_name}}} self.client.delete_by_query(index=f"{self.index_prefix}*", body=query) - #Status: works + + # Status: works def search( self, collection_name: str, vectors: list[list[float]], limit: int ) -> Optional[SearchResult]: query = { "size": limit, - "_source": [ - "text", - "metadata" - ], + "_source": ["text", "metadata"], "query": { "script_score": { "query": { - "bool": { - "filter": [ - { - "term": { - "collection": collection_name - } - } - ] - } + "bool": {"filter": [{"term": {"collection": collection_name}}]} }, "script": { "source": "cosineSimilarity(params.vector, 'vector') + 1.0", "params": { "vector": vectors[0] - }, # Assuming single query vector + }, # Assuming single query vector }, } }, @@ -183,7 +171,8 @@ class ElasticsearchClient: ) return self._result_to_search_result(result) - #Status: only tested halfwat + + # Status: only tested halfwat def query( self, collection_name: str, filter: dict, limit: Optional[int] = None ) -> Optional[GetResult]: @@ -197,7 +186,9 @@ class ElasticsearchClient: for field, value in filter.items(): query_body["query"]["bool"]["filter"].append({"term": {field: value}}) - query_body["query"]["bool"]["filter"].append({"term": {"collection": collection_name}}) + query_body["query"]["bool"]["filter"].append( + {"term": {"collection": collection_name}} + ) size = limit if limit else 10 try: @@ -206,59 +197,53 @@ class ElasticsearchClient: body=query_body, size=size, ) - + return self._result_to_get_result(result) except Exception as e: return None - #Status: works - def _has_index(self,dimension:int): - return self.client.indices.exists(index=self._get_index_name(dimension=dimension)) + # Status: works + def _has_index(self, dimension: int): + return self.client.indices.exists( + index=self._get_index_name(dimension=dimension) + ) def get_or_create_index(self, dimension: int): if not self._has_index(dimension=dimension): self._create_index(dimension=dimension) - #Status: works + + # Status: works def get(self, collection_name: str) -> Optional[GetResult]: # Get all the items in the collection. query = { - "query": { - "bool": { - "filter": [ - { - "term": { - "collection": collection_name - } - } - ] - } - }, "_source": ["text", "metadata"]} + "query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}}, + "_source": ["text", "metadata"], + } results = list(scan(self.client, index=f"{self.index_prefix}*", query=query)) - + return self._scan_result_to_get_result(results) - #Status: works + # Status: works def insert(self, collection_name: str, items: list[VectorItem]): if not self._has_index(dimension=len(items[0]["vector"])): self._create_index(dimension=len(items[0]["vector"])) - for batch in self._create_batches(items): actions = [ - { - "_index":self._get_index_name(dimension=len(items[0]["vector"])), - "_id": item["id"], - "_source": { - "collection": collection_name, - "vector": item["vector"], - "text": item["text"], - "metadata": item["metadata"], - }, - } + { + "_index": self._get_index_name(dimension=len(items[0]["vector"])), + "_id": item["id"], + "_source": { + "collection": collection_name, + "vector": item["vector"], + "text": item["text"], + "metadata": item["metadata"], + }, + } for item in batch ] - bulk(self.client,actions) + bulk(self.client, actions) # Upsert documents using the update API with doc_as_upsert=True. def upsert(self, collection_name: str, items: list[VectorItem]): @@ -280,8 +265,7 @@ class ElasticsearchClient: } for item in batch ] - bulk(self.client,actions) - + bulk(self.client, actions) # Delete specific documents from a collection by filtering on both collection and document IDs. def delete( @@ -292,21 +276,16 @@ class ElasticsearchClient: ): query = { - "query": { - "bool": { - "filter": [ - {"term": {"collection": collection_name}} - ] - } - } + "query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}} } - #logic based on chromaDB + # logic based on chromaDB if ids: query["query"]["bool"]["filter"].append({"terms": {"_id": ids}}) elif filter: for field, value in filter.items(): - query["query"]["bool"]["filter"].append({"term": {f"metadata.{field}": value}}) - + query["query"]["bool"]["filter"].append( + {"term": {f"metadata.{field}": value}} + ) self.client.delete_by_query(index=f"{self.index_prefix}*", body=query) diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 388c44f9c6..2a4c651f2a 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -70,6 +70,7 @@ async def set_direct_connections_config( # CodeInterpreterConfig ############################ class CodeInterpreterConfigForm(BaseModel): + ENABLE_CODE_EXECUTION: bool CODE_EXECUTION_ENGINE: str CODE_EXECUTION_JUPYTER_URL: Optional[str] CODE_EXECUTION_JUPYTER_AUTH: Optional[str] @@ -89,6 +90,7 @@ class CodeInterpreterConfigForm(BaseModel): @router.get("/code_execution", response_model=CodeInterpreterConfigForm) async def get_code_execution_config(request: Request, user=Depends(get_admin_user)): return { + "ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION, "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE, "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL, "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, @@ -111,6 +113,8 @@ async def set_code_execution_config( request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) ): + request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION + request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE request.app.state.config.CODE_EXECUTION_JUPYTER_URL = ( form_data.CODE_EXECUTION_JUPYTER_URL @@ -153,6 +157,7 @@ async def set_code_execution_config( ) return { + "ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION, "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE, "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL, "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, diff --git a/backend/open_webui/static/site.webmanifest b/backend/open_webui/static/site.webmanifest index 738552549f..95915ae2bc 100644 --- a/backend/open_webui/static/site.webmanifest +++ b/backend/open_webui/static/site.webmanifest @@ -1,21 +1,21 @@ { - "name": "Open WebUI", - "short_name": "WebUI", - "icons": [ - { - "src": "/static/web-app-manifest-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/static/web-app-manifest-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" - } \ No newline at end of file + "name": "Open WebUI", + "short_name": "WebUI", + "icons": [ + { + "src": "/static/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/static/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index d0c02a569c..6dd3234b06 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -72,7 +72,7 @@ def get_license_data(app, key): if key: try: res = requests.post( - "https://api.openwebui.com/api/v1/license", + "https://api.openwebui.com/api/v1/license/", json={"key": key, "version": "1"}, timeout=5, ) diff --git a/package-lock.json b/package-lock.json index 719e8718d7..e98b0968cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.5.19", + "version": "0.5.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.5.19", + "version": "0.5.20", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index 63d7a49c9d..2e4b905b19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.5.19", + "version": "0.5.20", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/src/lib/components/admin/Settings/CodeExecution.svelte b/src/lib/components/admin/Settings/CodeExecution.svelte index c835374551..6050fb26bb 100644 --- a/src/lib/components/admin/Settings/CodeExecution.svelte +++ b/src/lib/components/admin/Settings/CodeExecution.svelte @@ -45,6 +45,16 @@