diff --git a/CHANGELOG.md b/CHANGELOG.md index d61e40c0..f9e86fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ 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.108] - 2024-03-02 + +### Added + +- **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter. +- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings. +- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes. +- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI. + +### Fixed + +- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use. +- Corrected numbered list display issue in Safari (#963). +- Restricted user ability to delete chats without proper permissions (#993). + +### Changed + +- **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added. +- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`. + ## [0.1.107] - 2024-03-01 ### Added diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index bc797f08..7965ff32 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -11,7 +11,7 @@ from pydantic import BaseModel from apps.web.models.users import Users from constants import ERROR_MESSAGES from utils.utils import decode_token, get_current_user, get_admin_user -from config import OLLAMA_API_BASE_URL, WEBUI_AUTH +from config import OLLAMA_BASE_URL, WEBUI_AUTH app = FastAPI() app.add_middleware( @@ -22,7 +22,7 @@ app.add_middleware( allow_headers=["*"], ) -app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL +app.state.OLLAMA_BASE_URL = OLLAMA_BASE_URL # TARGET_SERVER_URL = OLLAMA_API_BASE_URL @@ -32,7 +32,7 @@ REQUEST_POOL = [] @app.get("/url") async def get_ollama_api_url(user=Depends(get_admin_user)): - return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} + return {"OLLAMA_BASE_URL": app.state.OLLAMA_BASE_URL} class UrlUpdateForm(BaseModel): @@ -41,8 +41,8 @@ class UrlUpdateForm(BaseModel): @app.post("/url/update") async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): - app.state.OLLAMA_API_BASE_URL = form_data.url - return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} + app.state.OLLAMA_BASE_URL = form_data.url + return {"OLLAMA_BASE_URL": app.state.OLLAMA_BASE_URL} @app.get("/cancel/{request_id}") @@ -57,7 +57,7 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user)) @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) async def proxy(path: str, request: Request, user=Depends(get_current_user)): - target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}" + target_url = f"{app.state.OLLAMA_BASE_URL}/{path}" body = await request.body() headers = dict(request.headers) @@ -91,7 +91,13 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): def stream_content(): try: - if path in ["chat"]: + if path == "generate": + data = json.loads(body.decode("utf-8")) + + if not ("stream" in data and data["stream"] == False): + yield json.dumps({"id": request_id, "done": False}) + "\n" + + elif path == "chat": yield json.dumps({"id": request_id, "done": False}) + "\n" for chunk in r.iter_content(chunk_size=8192): @@ -103,7 +109,8 @@ async def proxy(path: str, request: Request, user=Depends(get_current_user)): finally: if hasattr(r, "close"): r.close() - REQUEST_POOL.remove(request_id) + if request_id in REQUEST_POOL: + REQUEST_POOL.remove(request_id) r = requests.request( method=request.method, diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 83c10233..2a8b2a49 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -79,6 +79,8 @@ app.state.CHUNK_SIZE = CHUNK_SIZE app.state.CHUNK_OVERLAP = CHUNK_OVERLAP app.state.RAG_TEMPLATE = RAG_TEMPLATE app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL +app.state.TOP_K = 4 + app.state.sentence_transformer_ef = ( embedding_functions.SentenceTransformerEmbeddingFunction( model_name=app.state.RAG_EMBEDDING_MODEL, @@ -210,23 +212,33 @@ async def get_rag_template(user=Depends(get_current_user)): } -class RAGTemplateForm(BaseModel): - template: str +@app.get("/query/settings") +async def get_query_settings(user=Depends(get_admin_user)): + return { + "status": True, + "template": app.state.RAG_TEMPLATE, + "k": app.state.TOP_K, + } -@app.post("/template/update") -async def update_rag_template(form_data: RAGTemplateForm, user=Depends(get_admin_user)): - # TODO: check template requirements - app.state.RAG_TEMPLATE = ( - form_data.template if form_data.template != "" else RAG_TEMPLATE - ) +class QuerySettingsForm(BaseModel): + k: Optional[int] = None + template: Optional[str] = None + + +@app.post("/query/settings/update") +async def update_query_settings( + form_data: QuerySettingsForm, user=Depends(get_admin_user) +): + app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE + app.state.TOP_K = form_data.k if form_data.k else 4 return {"status": True, "template": app.state.RAG_TEMPLATE} class QueryDocForm(BaseModel): collection_name: str query: str - k: Optional[int] = 4 + k: Optional[int] = None @app.post("/query/doc") @@ -240,7 +252,10 @@ def query_doc( name=form_data.collection_name, embedding_function=app.state.sentence_transformer_ef, ) - result = collection.query(query_texts=[form_data.query], n_results=form_data.k) + result = collection.query( + query_texts=[form_data.query], + n_results=form_data.k if form_data.k else app.state.TOP_K, + ) return result except Exception as e: print(e) @@ -253,7 +268,7 @@ def query_doc( class QueryCollectionsForm(BaseModel): collection_names: List[str] query: str - k: Optional[int] = 4 + k: Optional[int] = None def merge_and_sort_query_results(query_results, k): @@ -317,13 +332,16 @@ def query_collection( ) result = collection.query( - query_texts=[form_data.query], n_results=form_data.k + query_texts=[form_data.query], + n_results=form_data.k if form_data.k else app.state.TOP_K, ) results.append(result) except: pass - return merge_and_sort_query_results(results, form_data.k) + return merge_and_sort_query_results( + results, form_data.k if form_data.k else app.state.TOP_K + ) @app.post("/web") @@ -423,7 +441,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str): "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ] or file_ext in ["xls", "xlsx"]: loader = UnstructuredExcelLoader(file_path) - elif file_ext in known_source_ext or (file_content_type and file_content_type.find("text/") >= 0): + elif file_ext in known_source_ext or ( + file_content_type and file_content_type.find("text/") >= 0 + ): loader = TextLoader(file_path) else: loader = TextLoader(file_path) diff --git a/backend/config.py b/backend/config.py index df24b97e..cd1a2702 100644 --- a/backend/config.py +++ b/backend/config.py @@ -211,6 +211,17 @@ if ENV == "prod": if OLLAMA_API_BASE_URL == "/ollama/api": OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" + +OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") + +if OLLAMA_BASE_URL == "": + OLLAMA_BASE_URL = ( + OLLAMA_API_BASE_URL[:-4] + if OLLAMA_API_BASE_URL.endswith("/api") + else OLLAMA_API_BASE_URL + ) + + #################################### # OPENAI_API #################################### diff --git a/package.json b/package.json index 786175b0..9884e1c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.1.107", + "version": "0.1.108", "private": true, "scripts": { "dev": "vite dev --host", diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index 5fc8a5fe..0c96b2ab 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -29,7 +29,7 @@ export const getOllamaAPIUrl = async (token: string = '') => { throw error; } - return res.OLLAMA_API_BASE_URL; + return res.OLLAMA_BASE_URL; }; export const updateOllamaAPIUrl = async (token: string = '', url: string) => { @@ -64,13 +64,13 @@ export const updateOllamaAPIUrl = async (token: string = '', url: string) => { throw error; } - return res.OLLAMA_API_BASE_URL; + return res.OLLAMA_BASE_URL; }; export const getOllamaVersion = async (token: string = '') => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/version`, { method: 'GET', headers: { Accept: 'application/json', @@ -102,7 +102,7 @@ export const getOllamaVersion = async (token: string = '') => { export const getOllamaModels = async (token: string = '') => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, { method: 'GET', headers: { Accept: 'application/json', @@ -148,7 +148,7 @@ export const generateTitle = async ( console.log(template); - const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'text/event-stream', @@ -186,7 +186,7 @@ export const generatePrompt = async (token: string = '', model: string, conversa conversation = '[no existing conversation]'; } - const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'text/event-stream', @@ -217,11 +217,37 @@ export const generatePrompt = async (token: string = '', model: string, conversa return res; }; +export const generateTextCompletion = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + 'Content-Type': 'text/event-stream', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: text, + stream: true + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const generateChatCompletion = async (token: string = '', body: object) => { let controller = new AbortController(); let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, { signal: controller.signal, method: 'POST', headers: { @@ -265,7 +291,7 @@ export const cancelChatCompletion = async (token: string = '', requestId: string export const createModel = async (token: string, tagName: string, content: string) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, { method: 'POST', headers: { 'Content-Type': 'text/event-stream', @@ -290,7 +316,7 @@ export const createModel = async (token: string, tagName: string, content: strin export const deleteModel = async (token: string, tagName: string) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/delete`, { method: 'DELETE', headers: { 'Content-Type': 'text/event-stream', @@ -324,7 +350,7 @@ export const deleteModel = async (token: string, tagName: string) => { export const pullModel = async (token: string, tagName: string) => { let error = null; - const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull`, { method: 'POST', headers: { 'Content-Type': 'text/event-stream', diff --git a/src/lib/apis/rag/index.ts b/src/lib/apis/rag/index.ts index ed36f014..4e8e9b14 100644 --- a/src/lib/apis/rag/index.ts +++ b/src/lib/apis/rag/index.ts @@ -85,17 +85,49 @@ export const getRAGTemplate = async (token: string) => { return res?.template ?? ''; }; -export const updateRAGTemplate = async (token: string, template: string) => { +export const getQuerySettings = async (token: string) => { let error = null; - const res = await fetch(`${RAG_API_BASE_URL}/template/update`, { + const res = await fetch(`${RAG_API_BASE_URL}/query/settings`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type QuerySettings = { + k: number | null; + template: string | null; +}; + +export const updateQuerySettings = async (token: string, settings: QuerySettings) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/query/settings/update`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ - template: template + ...settings }) }) .then(async (res) => { @@ -183,7 +215,7 @@ export const queryDoc = async ( token: string, collection_name: string, query: string, - k: number + k: number | null = null ) => { let error = null; diff --git a/src/lib/components/chat/MessageInput/Models.svelte b/src/lib/components/chat/MessageInput/Models.svelte index 632d5650..a6b507ef 100644 --- a/src/lib/components/chat/MessageInput/Models.svelte +++ b/src/lib/components/chat/MessageInput/Models.svelte @@ -81,14 +81,18 @@ throw data; } - if (data.done == false) { - if (prompt == '' && data.response == '\n') { - continue; - } else { - prompt += data.response; - console.log(data.response); - chatInputElement.scrollTop = chatInputElement.scrollHeight; - await tick(); + if ('id' in data) { + console.log(data); + } else { + if (data.done == false) { + if (prompt == '' && data.response == '\n') { + continue; + } else { + prompt += data.response; + console.log(data.response); + chatInputElement.scrollTop = chatInputElement.scrollHeight; + await tick(); + } } } } diff --git a/src/lib/components/chat/Settings/Connections.svelte b/src/lib/components/chat/Settings/Connections.svelte index de5a95f3..3ac6f2f5 100644 --- a/src/lib/components/chat/Settings/Connections.svelte +++ b/src/lib/components/chat/Settings/Connections.svelte @@ -121,7 +121,7 @@
diff --git a/src/lib/components/documents/Settings/General.svelte b/src/lib/components/documents/Settings/General.svelte index c6662b8b..c12351f5 100644 --- a/src/lib/components/documents/Settings/General.svelte +++ b/src/lib/components/documents/Settings/General.svelte @@ -2,10 +2,10 @@ import { getDocs } from '$lib/apis/documents'; import { getChunkParams, - getRAGTemplate, + getQuerySettings, scanDocs, updateChunkParams, - updateRAGTemplate + updateQuerySettings } from '$lib/apis/rag'; import { documents } from '$lib/stores'; import { onMount, getContext } from 'svelte'; @@ -20,7 +20,10 @@ let chunkSize = 0; let chunkOverlap = 0; - let template = ''; + let querySettings = { + template: '', + k: 4 + }; const scanHandler = async () => { loading = true; @@ -35,7 +38,7 @@ const submitHandler = async () => { const res = await updateChunkParams(localStorage.token, chunkSize, chunkOverlap); - await updateRAGTemplate(localStorage.token, template); + querySettings = await updateQuerySettings(localStorage.token, querySettings); }; onMount(async () => { @@ -46,7 +49,7 @@ chunkOverlap = res.chunk_overlap; } - template = await getRAGTemplate(localStorage.token); + querySettings = await getQuerySettings(localStorage.token); }); @@ -160,10 +163,44 @@ +
Query Params
+ +
+
+
Top K
+ +
+ +
+
+ + +
+
{$i18n.t('RAG Template')}