forked from open-webui/open-webui
		
	main #2
					 13 changed files with 368 additions and 172 deletions
				
			
		
							
								
								
									
										18
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -5,6 +5,24 @@ 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/), | 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). | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||||
| 
 | 
 | ||||||
|  | ## [0.1.111] - 2024-03-10 | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | 
 | ||||||
|  | - 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role. | ||||||
|  | - 🔄 **Update All Models**: Added a convenient button to update all models at once. | ||||||
|  | - 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance. | ||||||
|  | - 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111. | ||||||
|  | - 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow. | ||||||
|  | 
 | ||||||
|  | ### Fixed | ||||||
|  | 
 | ||||||
|  | - 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094). | ||||||
|  | - 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104). | ||||||
|  | - 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105). | ||||||
|  | - 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098). | ||||||
|  | - 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue. | ||||||
|  | 
 | ||||||
| ## [0.1.110] - 2024-03-06 | ## [0.1.110] - 2024-03-06 | ||||||
| 
 | 
 | ||||||
| ### Added | ### Added | ||||||
|  |  | ||||||
|  | @ -179,12 +179,18 @@ def merge_models_lists(model_lists): | ||||||
| 
 | 
 | ||||||
| async def get_all_models(): | async def get_all_models(): | ||||||
|     print("get_all_models") |     print("get_all_models") | ||||||
|  | 
 | ||||||
|  |     if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "": | ||||||
|  |         models = {"data": []} | ||||||
|  |     else: | ||||||
|         tasks = [ |         tasks = [ | ||||||
|             fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx]) |             fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx]) | ||||||
|             for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS) |             for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS) | ||||||
|         ] |         ] | ||||||
|         responses = await asyncio.gather(*tasks) |         responses = await asyncio.gather(*tasks) | ||||||
|     responses = list(filter(lambda x: x is not None and "error" not in x, responses)) |         responses = list( | ||||||
|  |             filter(lambda x: x is not None and "error" not in x, responses) | ||||||
|  |         ) | ||||||
|         models = { |         models = { | ||||||
|             "data": merge_models_lists( |             "data": merge_models_lists( | ||||||
|                 list(map(lambda response: response["data"], responses)) |                 list(map(lambda response: response["data"], responses)) | ||||||
|  |  | ||||||
|  | @ -77,6 +77,7 @@ from constants import ERROR_MESSAGES | ||||||
| 
 | 
 | ||||||
| app = FastAPI() | app = FastAPI() | ||||||
| 
 | 
 | ||||||
|  | app.state.PDF_EXTRACT_IMAGES = False | ||||||
| app.state.CHUNK_SIZE = CHUNK_SIZE | app.state.CHUNK_SIZE = CHUNK_SIZE | ||||||
| app.state.CHUNK_OVERLAP = CHUNK_OVERLAP | app.state.CHUNK_OVERLAP = CHUNK_OVERLAP | ||||||
| app.state.RAG_TEMPLATE = RAG_TEMPLATE | app.state.RAG_TEMPLATE = RAG_TEMPLATE | ||||||
|  | @ -184,12 +185,15 @@ async def update_embedding_model( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.get("/chunk") | @app.get("/config") | ||||||
| async def get_chunk_params(user=Depends(get_admin_user)): | async def get_rag_config(user=Depends(get_admin_user)): | ||||||
|     return { |     return { | ||||||
|         "status": True, |         "status": True, | ||||||
|  |         "pdf_extract_images": app.state.PDF_EXTRACT_IMAGES, | ||||||
|  |         "chunk": { | ||||||
|             "chunk_size": app.state.CHUNK_SIZE, |             "chunk_size": app.state.CHUNK_SIZE, | ||||||
|             "chunk_overlap": app.state.CHUNK_OVERLAP, |             "chunk_overlap": app.state.CHUNK_OVERLAP, | ||||||
|  |         }, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -198,17 +202,24 @@ class ChunkParamUpdateForm(BaseModel): | ||||||
|     chunk_overlap: int |     chunk_overlap: int | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.post("/chunk/update") | class ConfigUpdateForm(BaseModel): | ||||||
| async def update_chunk_params( |     pdf_extract_images: bool | ||||||
|     form_data: ChunkParamUpdateForm, user=Depends(get_admin_user) |     chunk: ChunkParamUpdateForm | ||||||
| ): | 
 | ||||||
|     app.state.CHUNK_SIZE = form_data.chunk_size | 
 | ||||||
|     app.state.CHUNK_OVERLAP = form_data.chunk_overlap | @app.post("/config/update") | ||||||
|  | async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)): | ||||||
|  |     app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images | ||||||
|  |     app.state.CHUNK_SIZE = form_data.chunk.chunk_size | ||||||
|  |     app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         "status": True, |         "status": True, | ||||||
|  |         "pdf_extract_images": app.state.PDF_EXTRACT_IMAGES, | ||||||
|  |         "chunk": { | ||||||
|             "chunk_size": app.state.CHUNK_SIZE, |             "chunk_size": app.state.CHUNK_SIZE, | ||||||
|             "chunk_overlap": app.state.CHUNK_OVERLAP, |             "chunk_overlap": app.state.CHUNK_OVERLAP, | ||||||
|  |         }, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -364,7 +375,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str): | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     if file_ext == "pdf": |     if file_ext == "pdf": | ||||||
|         loader = PyPDFLoader(file_path, extract_images=True) |         loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES) | ||||||
|     elif file_ext == "csv": |     elif file_ext == "csv": | ||||||
|         loader = CSVLoader(file_path) |         loader = CSVLoader(file_path) | ||||||
|     elif file_ext == "rst": |     elif file_ext == "rst": | ||||||
|  |  | ||||||
|  | @ -95,3 +95,89 @@ def rag_template(template: str, context: str, query: str): | ||||||
|     template = re.sub(r"\[query\]", query, template) |     template = re.sub(r"\[query\]", query, template) | ||||||
| 
 | 
 | ||||||
|     return template |     return template | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def rag_messages(docs, messages, template, k, embedding_function): | ||||||
|  |     print(docs) | ||||||
|  | 
 | ||||||
|  |     last_user_message_idx = None | ||||||
|  |     for i in range(len(messages) - 1, -1, -1): | ||||||
|  |         if messages[i]["role"] == "user": | ||||||
|  |             last_user_message_idx = i | ||||||
|  |             break | ||||||
|  | 
 | ||||||
|  |     user_message = messages[last_user_message_idx] | ||||||
|  | 
 | ||||||
|  |     if isinstance(user_message["content"], list): | ||||||
|  |         # Handle list content input | ||||||
|  |         content_type = "list" | ||||||
|  |         query = "" | ||||||
|  |         for content_item in user_message["content"]: | ||||||
|  |             if content_item["type"] == "text": | ||||||
|  |                 query = content_item["text"] | ||||||
|  |                 break | ||||||
|  |     elif isinstance(user_message["content"], str): | ||||||
|  |         # Handle text content input | ||||||
|  |         content_type = "text" | ||||||
|  |         query = user_message["content"] | ||||||
|  |     else: | ||||||
|  |         # Fallback in case the input does not match expected types | ||||||
|  |         content_type = None | ||||||
|  |         query = "" | ||||||
|  | 
 | ||||||
|  |     relevant_contexts = [] | ||||||
|  | 
 | ||||||
|  |     for doc in docs: | ||||||
|  |         context = None | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             if doc["type"] == "collection": | ||||||
|  |                 context = query_collection( | ||||||
|  |                     collection_names=doc["collection_names"], | ||||||
|  |                     query=query, | ||||||
|  |                     k=k, | ||||||
|  |                     embedding_function=embedding_function, | ||||||
|  |                 ) | ||||||
|  |             else: | ||||||
|  |                 context = query_doc( | ||||||
|  |                     collection_name=doc["collection_name"], | ||||||
|  |                     query=query, | ||||||
|  |                     k=k, | ||||||
|  |                     embedding_function=embedding_function, | ||||||
|  |                 ) | ||||||
|  |         except Exception as e: | ||||||
|  |             print(e) | ||||||
|  |             context = None | ||||||
|  | 
 | ||||||
|  |         relevant_contexts.append(context) | ||||||
|  | 
 | ||||||
|  |     context_string = "" | ||||||
|  |     for context in relevant_contexts: | ||||||
|  |         if context: | ||||||
|  |             context_string += " ".join(context["documents"][0]) + "\n" | ||||||
|  | 
 | ||||||
|  |     ra_content = rag_template( | ||||||
|  |         template=template, | ||||||
|  |         context=context_string, | ||||||
|  |         query=query, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     if content_type == "list": | ||||||
|  |         new_content = [] | ||||||
|  |         for content_item in user_message["content"]: | ||||||
|  |             if content_item["type"] == "text": | ||||||
|  |                 # Update the text item's content with ra_content | ||||||
|  |                 new_content.append({"type": "text", "text": ra_content}) | ||||||
|  |             else: | ||||||
|  |                 # Keep other types of content as they are | ||||||
|  |                 new_content.append(content_item) | ||||||
|  |         new_user_message = {**user_message, "content": new_content} | ||||||
|  |     else: | ||||||
|  |         new_user_message = { | ||||||
|  |             **user_message, | ||||||
|  |             "content": ra_content, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     messages[last_user_message_idx] = new_user_message | ||||||
|  | 
 | ||||||
|  |     return messages | ||||||
|  |  | ||||||
|  | @ -209,10 +209,6 @@ OLLAMA_API_BASE_URL = os.environ.get( | ||||||
| 
 | 
 | ||||||
| OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | ||||||
| 
 | 
 | ||||||
| if ENV == "prod": |  | ||||||
|     if OLLAMA_BASE_URL == "/ollama": |  | ||||||
|         OLLAMA_BASE_URL = "http://host.docker.internal:11434" |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | ||||||
|     OLLAMA_BASE_URL = ( |     OLLAMA_BASE_URL = ( | ||||||
|  | @ -221,6 +217,11 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | ||||||
|         else OLLAMA_API_BASE_URL |         else OLLAMA_API_BASE_URL | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  | if ENV == "prod": | ||||||
|  |     if OLLAMA_BASE_URL == "/ollama": | ||||||
|  |         OLLAMA_BASE_URL = "http://host.docker.internal:11434" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") | OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") | ||||||
| OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL | OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL | ||||||
| 
 | 
 | ||||||
|  | @ -234,8 +235,6 @@ OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")] | ||||||
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | ||||||
| OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") | OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") | ||||||
| 
 | 
 | ||||||
| if OPENAI_API_KEY == "": |  | ||||||
|     OPENAI_API_KEY = "none" |  | ||||||
| 
 | 
 | ||||||
| if OPENAI_API_BASE_URL == "": | if OPENAI_API_BASE_URL == "": | ||||||
|     OPENAI_API_BASE_URL = "https://api.openai.com/v1" |     OPENAI_API_BASE_URL = "https://api.openai.com/v1" | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| { | { | ||||||
|  |     "version": "0.0.1", | ||||||
|     "ui": { |     "ui": { | ||||||
|         "prompt_suggestions": [ |         "prompt_suggestions": [ | ||||||
|             { |             { | ||||||
|  |  | ||||||
							
								
								
									
										132
									
								
								backend/main.py
									
										
									
									
									
								
							
							
						
						
									
										132
									
								
								backend/main.py
									
										
									
									
									
								
							|  | @ -28,7 +28,7 @@ from typing import List | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| from utils.utils import get_admin_user | from utils.utils import get_admin_user | ||||||
| from apps.rag.utils import query_doc, query_collection, rag_template | from apps.rag.utils import rag_messages | ||||||
| 
 | 
 | ||||||
| from config import ( | from config import ( | ||||||
|     WEBUI_NAME, |     WEBUI_NAME, | ||||||
|  | @ -60,19 +60,6 @@ app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST | ||||||
| 
 | 
 | ||||||
| origins = ["*"] | origins = ["*"] | ||||||
| 
 | 
 | ||||||
| app.add_middleware( |  | ||||||
|     CORSMiddleware, |  | ||||||
|     allow_origins=origins, |  | ||||||
|     allow_credentials=True, |  | ||||||
|     allow_methods=["*"], |  | ||||||
|     allow_headers=["*"], |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @app.on_event("startup") |  | ||||||
| async def on_startup(): |  | ||||||
|     await litellm_app_startup() |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class RAGMiddleware(BaseHTTPMiddleware): | class RAGMiddleware(BaseHTTPMiddleware): | ||||||
|     async def dispatch(self, request: Request, call_next): |     async def dispatch(self, request: Request, call_next): | ||||||
|  | @ -91,98 +78,33 @@ class RAGMiddleware(BaseHTTPMiddleware): | ||||||
|             # Example: Add a new key-value pair or modify existing ones |             # Example: Add a new key-value pair or modify existing ones | ||||||
|             # data["modified"] = True  # Example modification |             # data["modified"] = True  # Example modification | ||||||
|             if "docs" in data: |             if "docs" in data: | ||||||
|                 docs = data["docs"] |  | ||||||
|                 print(docs) |  | ||||||
| 
 | 
 | ||||||
|                 last_user_message_idx = None |                 data = {**data} | ||||||
|                 for i in range(len(data["messages"]) - 1, -1, -1): |                 data["messages"] = rag_messages( | ||||||
|                     if data["messages"][i]["role"] == "user": |                     data["docs"], | ||||||
|                         last_user_message_idx = i |                     data["messages"], | ||||||
|                         break |                     rag_app.state.RAG_TEMPLATE, | ||||||
| 
 |                     rag_app.state.TOP_K, | ||||||
|                 user_message = data["messages"][last_user_message_idx] |                     rag_app.state.sentence_transformer_ef, | ||||||
| 
 |  | ||||||
|                 if isinstance(user_message["content"], list): |  | ||||||
|                     # Handle list content input |  | ||||||
|                     content_type = "list" |  | ||||||
|                     query = "" |  | ||||||
|                     for content_item in user_message["content"]: |  | ||||||
|                         if content_item["type"] == "text": |  | ||||||
|                             query = content_item["text"] |  | ||||||
|                             break |  | ||||||
|                 elif isinstance(user_message["content"], str): |  | ||||||
|                     # Handle text content input |  | ||||||
|                     content_type = "text" |  | ||||||
|                     query = user_message["content"] |  | ||||||
|                 else: |  | ||||||
|                     # Fallback in case the input does not match expected types |  | ||||||
|                     content_type = None |  | ||||||
|                     query = "" |  | ||||||
| 
 |  | ||||||
|                 relevant_contexts = [] |  | ||||||
| 
 |  | ||||||
|                 for doc in docs: |  | ||||||
|                     context = None |  | ||||||
| 
 |  | ||||||
|                     try: |  | ||||||
|                         if doc["type"] == "collection": |  | ||||||
|                             context = query_collection( |  | ||||||
|                                 collection_names=doc["collection_names"], |  | ||||||
|                                 query=query, |  | ||||||
|                                 k=rag_app.state.TOP_K, |  | ||||||
|                                 embedding_function=rag_app.state.sentence_transformer_ef, |  | ||||||
|                 ) |                 ) | ||||||
|                         else: |  | ||||||
|                             context = query_doc( |  | ||||||
|                                 collection_name=doc["collection_name"], |  | ||||||
|                                 query=query, |  | ||||||
|                                 k=rag_app.state.TOP_K, |  | ||||||
|                                 embedding_function=rag_app.state.sentence_transformer_ef, |  | ||||||
|                             ) |  | ||||||
|                     except Exception as e: |  | ||||||
|                         print(e) |  | ||||||
|                         context = None |  | ||||||
| 
 |  | ||||||
|                     relevant_contexts.append(context) |  | ||||||
| 
 |  | ||||||
|                 context_string = "" |  | ||||||
|                 for context in relevant_contexts: |  | ||||||
|                     if context: |  | ||||||
|                         context_string += " ".join(context["documents"][0]) + "\n" |  | ||||||
| 
 |  | ||||||
|                 ra_content = rag_template( |  | ||||||
|                     template=rag_app.state.RAG_TEMPLATE, |  | ||||||
|                     context=context_string, |  | ||||||
|                     query=query, |  | ||||||
|                 ) |  | ||||||
| 
 |  | ||||||
|                 if content_type == "list": |  | ||||||
|                     new_content = [] |  | ||||||
|                     for content_item in user_message["content"]: |  | ||||||
|                         if content_item["type"] == "text": |  | ||||||
|                             # Update the text item's content with ra_content |  | ||||||
|                             new_content.append({"type": "text", "text": ra_content}) |  | ||||||
|                         else: |  | ||||||
|                             # Keep other types of content as they are |  | ||||||
|                             new_content.append(content_item) |  | ||||||
|                     new_user_message = {**user_message, "content": new_content} |  | ||||||
|                 else: |  | ||||||
|                     new_user_message = { |  | ||||||
|                         **user_message, |  | ||||||
|                         "content": ra_content, |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                 data["messages"][last_user_message_idx] = new_user_message |  | ||||||
|                 del data["docs"] |                 del data["docs"] | ||||||
| 
 | 
 | ||||||
|                 print(data["messages"]) |                 print(data["messages"]) | ||||||
| 
 | 
 | ||||||
|             modified_body_bytes = json.dumps(data).encode("utf-8") |             modified_body_bytes = json.dumps(data).encode("utf-8") | ||||||
| 
 | 
 | ||||||
|             # Create a new request with the modified body |             # Replace the request body with the modified one | ||||||
|             scope = request.scope |             request._body = modified_body_bytes | ||||||
|             scope["body"] = modified_body_bytes | 
 | ||||||
|             request = Request(scope, receive=lambda: self._receive(modified_body_bytes)) |             # Set custom header to ensure content-length matches new body length | ||||||
|  |             request.headers.__dict__["_list"] = [ | ||||||
|  |                 (b"content-length", str(len(modified_body_bytes)).encode("utf-8")), | ||||||
|  |                 *[ | ||||||
|  |                     (k, v) | ||||||
|  |                     for k, v in request.headers.raw | ||||||
|  |                     if k.lower() != b"content-length" | ||||||
|  |                 ], | ||||||
|  |             ] | ||||||
| 
 | 
 | ||||||
|         response = await call_next(request) |         response = await call_next(request) | ||||||
|         return response |         return response | ||||||
|  | @ -194,6 +116,15 @@ class RAGMiddleware(BaseHTTPMiddleware): | ||||||
| app.add_middleware(RAGMiddleware) | app.add_middleware(RAGMiddleware) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | app.add_middleware( | ||||||
|  |     CORSMiddleware, | ||||||
|  |     allow_origins=origins, | ||||||
|  |     allow_credentials=True, | ||||||
|  |     allow_methods=["*"], | ||||||
|  |     allow_headers=["*"], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @app.middleware("http") | @app.middleware("http") | ||||||
| async def check_url(request: Request, call_next): | async def check_url(request: Request, call_next): | ||||||
|     start_time = int(time.time()) |     start_time = int(time.time()) | ||||||
|  | @ -204,6 +135,11 @@ async def check_url(request: Request, call_next): | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @app.on_event("startup") | ||||||
|  | async def on_startup(): | ||||||
|  |     await litellm_app_startup() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| app.mount("/api/v1", webui_app) | app.mount("/api/v1", webui_app) | ||||||
| app.mount("/litellm/api", litellm_app) | app.mount("/litellm/api", litellm_app) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
| 	"name": "open-webui", | 	"name": "open-webui", | ||||||
| 	"version": "0.1.110", | 	"version": "0.1.111", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"dev": "vite dev --host", | 		"dev": "vite dev --host", | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { RAG_API_BASE_URL } from '$lib/constants'; | import { RAG_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
| export const getChunkParams = async (token: string) => { | export const getRAGConfig = async (token: string) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${RAG_API_BASE_URL}/chunk`, { | 	const res = await fetch(`${RAG_API_BASE_URL}/config`, { | ||||||
| 		method: 'GET', | 		method: 'GET', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			'Content-Type': 'application/json', | 			'Content-Type': 'application/json', | ||||||
|  | @ -27,18 +27,27 @@ export const getChunkParams = async (token: string) => { | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const updateChunkParams = async (token: string, size: number, overlap: number) => { | type ChunkConfigForm = { | ||||||
|  | 	chunk_size: number; | ||||||
|  | 	chunk_overlap: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type RAGConfigForm = { | ||||||
|  | 	pdf_extract_images: boolean; | ||||||
|  | 	chunk: ChunkConfigForm; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${RAG_API_BASE_URL}/chunk/update`, { | 	const res = await fetch(`${RAG_API_BASE_URL}/config/update`, { | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			'Content-Type': 'application/json', | 			'Content-Type': 'application/json', | ||||||
| 			Authorization: `Bearer ${token}` | 			Authorization: `Bearer ${token}` | ||||||
| 		}, | 		}, | ||||||
| 		body: JSON.stringify({ | 		body: JSON.stringify({ | ||||||
| 			chunk_size: size, | 			...payload | ||||||
| 			chunk_overlap: overlap |  | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
| 		.then(async (res) => { | 		.then(async (res) => { | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ | ||||||
| 	import { splitStream } from '$lib/utils'; | 	import { splitStream } from '$lib/utils'; | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
| 	import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm'; | 	import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm'; | ||||||
|  | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
| 
 | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
|  | @ -39,6 +40,10 @@ | ||||||
| 
 | 
 | ||||||
| 	let OLLAMA_URLS = []; | 	let OLLAMA_URLS = []; | ||||||
| 	let selectedOllamaUrlIdx: string | null = null; | 	let selectedOllamaUrlIdx: string | null = null; | ||||||
|  | 
 | ||||||
|  | 	let updateModelId = null; | ||||||
|  | 	let updateProgress = null; | ||||||
|  | 
 | ||||||
| 	let showExperimentalOllama = false; | 	let showExperimentalOllama = false; | ||||||
| 	let ollamaVersion = ''; | 	let ollamaVersion = ''; | ||||||
| 	const MAX_PARALLEL_DOWNLOADS = 3; | 	const MAX_PARALLEL_DOWNLOADS = 3; | ||||||
|  | @ -63,6 +68,71 @@ | ||||||
| 
 | 
 | ||||||
| 	let deleteModelTag = ''; | 	let deleteModelTag = ''; | ||||||
| 
 | 
 | ||||||
|  | 	const updateModelsHandler = async () => { | ||||||
|  | 		for (const model of $models.filter( | ||||||
|  | 			(m) => | ||||||
|  | 				m.size != null && | ||||||
|  | 				(selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx)) | ||||||
|  | 		)) { | ||||||
|  | 			console.log(model); | ||||||
|  | 
 | ||||||
|  | 			updateModelId = model.id; | ||||||
|  | 			const res = await pullModel(localStorage.token, model.id, selectedOllamaUrlIdx).catch( | ||||||
|  | 				(error) => { | ||||||
|  | 					toast.error(error); | ||||||
|  | 					return null; | ||||||
|  | 				} | ||||||
|  | 			); | ||||||
|  | 
 | ||||||
|  | 			if (res) { | ||||||
|  | 				const reader = res.body | ||||||
|  | 					.pipeThrough(new TextDecoderStream()) | ||||||
|  | 					.pipeThrough(splitStream('\n')) | ||||||
|  | 					.getReader(); | ||||||
|  | 
 | ||||||
|  | 				while (true) { | ||||||
|  | 					try { | ||||||
|  | 						const { value, done } = await reader.read(); | ||||||
|  | 						if (done) break; | ||||||
|  | 
 | ||||||
|  | 						let lines = value.split('\n'); | ||||||
|  | 
 | ||||||
|  | 						for (const line of lines) { | ||||||
|  | 							if (line !== '') { | ||||||
|  | 								let data = JSON.parse(line); | ||||||
|  | 
 | ||||||
|  | 								console.log(data); | ||||||
|  | 								if (data.error) { | ||||||
|  | 									throw data.error; | ||||||
|  | 								} | ||||||
|  | 								if (data.detail) { | ||||||
|  | 									throw data.detail; | ||||||
|  | 								} | ||||||
|  | 								if (data.status) { | ||||||
|  | 									if (data.digest) { | ||||||
|  | 										updateProgress = 0; | ||||||
|  | 										if (data.completed) { | ||||||
|  | 											updateProgress = Math.round((data.completed / data.total) * 1000) / 10; | ||||||
|  | 										} else { | ||||||
|  | 											updateProgress = 100; | ||||||
|  | 										} | ||||||
|  | 									} else { | ||||||
|  | 										toast.success(data.status); | ||||||
|  | 									} | ||||||
|  | 								} | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} catch (error) { | ||||||
|  | 						console.log(error); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		updateModelId = null; | ||||||
|  | 		updateProgress = null; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	const pullModelHandler = async () => { | 	const pullModelHandler = async () => { | ||||||
| 		const sanitizedModelTag = modelTag.trim(); | 		const sanitizedModelTag = modelTag.trim(); | ||||||
| 		if (modelDownloadStatus[sanitizedModelTag]) { | 		if (modelDownloadStatus[sanitizedModelTag]) { | ||||||
|  | @ -389,7 +459,7 @@ | ||||||
| 			return []; | 			return []; | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		if (OLLAMA_URLS.length > 1) { | 		if (OLLAMA_URLS.length > 0) { | ||||||
| 			selectedOllamaUrlIdx = 0; | 			selectedOllamaUrlIdx = 0; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -404,18 +474,51 @@ | ||||||
| 			<div class="space-y-2 pr-1.5"> | 			<div class="space-y-2 pr-1.5"> | ||||||
| 				<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> | 				<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> | ||||||
| 
 | 
 | ||||||
| 				{#if OLLAMA_URLS.length > 1} | 				{#if OLLAMA_URLS.length > 0} | ||||||
|  | 					<div class="flex gap-2"> | ||||||
| 						<div class="flex-1 pb-1"> | 						<div class="flex-1 pb-1"> | ||||||
| 							<select | 							<select | ||||||
| 								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | 								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 								bind:value={selectedOllamaUrlIdx} | 								bind:value={selectedOllamaUrlIdx} | ||||||
| 							placeholder={$i18n.t('Select an Ollama instance')} | 								placeholder="Select an Ollama instance" | ||||||
| 							> | 							> | ||||||
| 								{#each OLLAMA_URLS as url, idx} | 								{#each OLLAMA_URLS as url, idx} | ||||||
| 									<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option> | 									<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option> | ||||||
| 								{/each} | 								{/each} | ||||||
| 							</select> | 							</select> | ||||||
| 						</div> | 						</div> | ||||||
|  | 
 | ||||||
|  | 						<div> | ||||||
|  | 							<div class="flex w-full justify-end"> | ||||||
|  | 								<Tooltip content="Update All Models" placement="top"> | ||||||
|  | 									<button | ||||||
|  | 										class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" | ||||||
|  | 										on:click={() => { | ||||||
|  | 											updateModelsHandler(); | ||||||
|  | 										}} | ||||||
|  | 									> | ||||||
|  | 										<svg | ||||||
|  | 											xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 											viewBox="0 0 16 16" | ||||||
|  | 											fill="currentColor" | ||||||
|  | 											class="w-4 h-4" | ||||||
|  | 										> | ||||||
|  | 											<path | ||||||
|  | 												d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z" | ||||||
|  | 											/> | ||||||
|  | 											<path | ||||||
|  | 												d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z" | ||||||
|  | 											/> | ||||||
|  | 										</svg> | ||||||
|  | 									</button> | ||||||
|  | 								</Tooltip> | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
|  | 					{#if updateModelId} | ||||||
|  | 						Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''} | ||||||
|  | 					{/if} | ||||||
| 				{/if} | 				{/if} | ||||||
| 
 | 
 | ||||||
| 				<div class="space-y-2"> | 				<div class="space-y-2"> | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { getDocs } from '$lib/apis/documents'; | 	import { getDocs } from '$lib/apis/documents'; | ||||||
| 	import { | 	import { | ||||||
| 		getChunkParams, | 		getRAGConfig, | ||||||
|  | 		updateRAGConfig, | ||||||
| 		getQuerySettings, | 		getQuerySettings, | ||||||
| 		scanDocs, | 		scanDocs, | ||||||
| 		updateChunkParams, |  | ||||||
| 		updateQuerySettings | 		updateQuerySettings | ||||||
| 	} from '$lib/apis/rag'; | 	} from '$lib/apis/rag'; | ||||||
| 	import { documents } from '$lib/stores'; | 	import { documents } from '$lib/stores'; | ||||||
|  | @ -19,6 +19,7 @@ | ||||||
| 
 | 
 | ||||||
| 	let chunkSize = 0; | 	let chunkSize = 0; | ||||||
| 	let chunkOverlap = 0; | 	let chunkOverlap = 0; | ||||||
|  | 	let pdfExtractImages = true; | ||||||
| 
 | 
 | ||||||
| 	let querySettings = { | 	let querySettings = { | ||||||
| 		template: '', | 		template: '', | ||||||
|  | @ -37,16 +38,24 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const submitHandler = async () => { | 	const submitHandler = async () => { | ||||||
| 		const res = await updateChunkParams(localStorage.token, chunkSize, chunkOverlap); | 		const res = await updateRAGConfig(localStorage.token, { | ||||||
|  | 			pdf_extract_images: pdfExtractImages, | ||||||
|  | 			chunk: { | ||||||
|  | 				chunk_overlap: chunkOverlap, | ||||||
|  | 				chunk_size: chunkSize | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
| 		querySettings = await updateQuerySettings(localStorage.token, querySettings); | 		querySettings = await updateQuerySettings(localStorage.token, querySettings); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		const res = await getChunkParams(localStorage.token); | 		const res = await getRAGConfig(localStorage.token); | ||||||
| 
 | 
 | ||||||
| 		if (res) { | 		if (res) { | ||||||
| 			chunkSize = res.chunk_size; | 			pdfExtractImages = res.pdf_extract_images; | ||||||
| 			chunkOverlap = res.chunk_overlap; | 
 | ||||||
|  | 			chunkSize = res.chunk.chunk_size; | ||||||
|  | 			chunkOverlap = res.chunk.chunk_overlap; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		querySettings = await getQuerySettings(localStorage.token); | 		querySettings = await getQuerySettings(localStorage.token); | ||||||
|  | @ -163,6 +172,22 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
|  | 			<div> | ||||||
|  | 				<div class="flex justify-between items-center text-xs"> | ||||||
|  | 					<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div> | ||||||
|  | 
 | ||||||
|  | 					<button | ||||||
|  | 						class=" text-xs font-medium text-gray-500" | ||||||
|  | 						type="button" | ||||||
|  | 						on:click={() => { | ||||||
|  | 							pdfExtractImages = !pdfExtractImages; | ||||||
|  | 						}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button | ||||||
|  | 					> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 
 | ||||||
|  | 		<div> | ||||||
| 			<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> | 			<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> | ||||||
| 
 | 
 | ||||||
| 			<div class=" flex"> | 			<div class=" flex"> | ||||||
|  |  | ||||||
|  | @ -236,7 +236,7 @@ | ||||||
| 	"Prompts": "Prompts", | 	"Prompts": "Prompts", | ||||||
| 	"Pull a model from Ollama.com": "Ein Modell von Ollama.com abrufen", | 	"Pull a model from Ollama.com": "Ein Modell von Ollama.com abrufen", | ||||||
| 	"Pull Progress": "Fortschritt abrufen", | 	"Pull Progress": "Fortschritt abrufen", | ||||||
| 	"Query Params": "", | 	"Query Params": "Query Parameter", | ||||||
| 	"RAG Template": "RAG-Vorlage", | 	"RAG Template": "RAG-Vorlage", | ||||||
| 	"Raw Format": "Rohformat", | 	"Raw Format": "Rohformat", | ||||||
| 	"Record voice": "Stimme aufnehmen", | 	"Record voice": "Stimme aufnehmen", | ||||||
|  | @ -346,5 +346,6 @@ | ||||||
| 	"Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.", | 	"Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.", | ||||||
| 	"You": "Du", | 	"You": "Du", | ||||||
| 	"You're a helpful assistant.": "Du bist ein hilfreicher Assistent.", | 	"You're a helpful assistant.": "Du bist ein hilfreicher Assistent.", | ||||||
| 	"You're now logged in.": "Du bist nun eingeloggt." | 	"You're now logged in.": "Du bist nun eingeloggt.", | ||||||
|  | 	"PDF Extract Images (OCR)": "Text von Bilder aus PDFs extrahieren (OCR)" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -236,7 +236,7 @@ | ||||||
| 	"Prompts": "Prompts", | 	"Prompts": "Prompts", | ||||||
| 	"Pull a model from Ollama.com": "Pull a model from Ollama.com", | 	"Pull a model from Ollama.com": "Pull a model from Ollama.com", | ||||||
| 	"Pull Progress": "Pull Progress", | 	"Pull Progress": "Pull Progress", | ||||||
| 	"Query Params": "", | 	"Query Params": "Query Params", | ||||||
| 	"RAG Template": "RAG Template", | 	"RAG Template": "RAG Template", | ||||||
| 	"Raw Format": "Raw Format", | 	"Raw Format": "Raw Format", | ||||||
| 	"Record voice": "Record voice", | 	"Record voice": "Record voice", | ||||||
|  | @ -346,5 +346,6 @@ | ||||||
| 	"Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword].", | 	"Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword].", | ||||||
| 	"You": "You", | 	"You": "You", | ||||||
| 	"You're a helpful assistant.": "You're a helpful assistant.", | 	"You're a helpful assistant.": "You're a helpful assistant.", | ||||||
| 	"You're now logged in.": "You're now logged in." | 	"You're now logged in.": "You're now logged in.", | ||||||
|  | 	"PDF Extract Images (OCR)": "PDF Extract Images (OCR)" | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue