From 2f75eef499cc5cb48deb639cebebf86c405640bd Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 17 Feb 2025 16:25:50 -0800 Subject: [PATCH] enh: code execution settings --- backend/open_webui/config.py | 52 +++- backend/open_webui/main.py | 20 +- backend/open_webui/routers/configs.py | 38 ++- backend/open_webui/routers/utils.py | 52 +++- src/lib/apis/configs/index.ts | 8 +- src/lib/apis/utils/index.ts | 56 +++- src/lib/components/admin/Settings.svelte | 12 +- .../admin/Settings/CodeExecution.svelte | 257 ++++++++++++++++++ .../admin/Settings/CodeInterpreter.svelte | 166 ----------- .../components/admin/Settings/General.svelte | 4 +- .../components/chat/Messages/CodeBlock.svelte | 18 +- .../components/chat/Settings/Account.svelte | 2 +- src/lib/components/common/CodeEditor.svelte | 2 +- src/lib/components/layout/Navbar/Menu.svelte | 2 +- .../components/layout/Sidebar/ChatMenu.svelte | 2 +- 15 files changed, 478 insertions(+), 213 deletions(-) create mode 100644 src/lib/components/admin/Settings/CodeExecution.svelte delete mode 100644 src/lib/components/admin/Settings/CodeInterpreter.svelte diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index d62ac3114..f34c78dd8 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1377,6 +1377,39 @@ Responses from models: {{responses}}""" # Code Interpreter #################################### + +CODE_EXECUTION_ENGINE = PersistentConfig( + "CODE_EXECUTION_ENGINE", + "code_execution.engine", + os.environ.get("CODE_EXECUTION_ENGINE", "pyodide"), +) + +CODE_EXECUTION_JUPYTER_URL = PersistentConfig( + "CODE_EXECUTION_JUPYTER_URL", + "code_execution.jupyter.url", + os.environ.get("CODE_EXECUTION_JUPYTER_URL", ""), +) + +CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig( + "CODE_EXECUTION_JUPYTER_AUTH", + "code_execution.jupyter.auth", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""), +) + +CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig( + "CODE_EXECUTION_JUPYTER_AUTH_TOKEN", + "code_execution.jupyter.auth_token", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""), +) + + +CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig( + "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", + "code_execution.jupyter.auth_password", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""), +) + + ENABLE_CODE_INTERPRETER = PersistentConfig( "ENABLE_CODE_INTERPRETER", "code_interpreter.enable", @@ -1398,26 +1431,37 @@ CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig( CODE_INTERPRETER_JUPYTER_URL = PersistentConfig( "CODE_INTERPRETER_JUPYTER_URL", "code_interpreter.jupyter.url", - os.environ.get("CODE_INTERPRETER_JUPYTER_URL", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_URL", os.environ.get("CODE_EXECUTION_JUPYTER_URL", "") + ), ) CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig( "CODE_INTERPRETER_JUPYTER_AUTH", "code_interpreter.jupyter.auth", - os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_AUTH", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""), + ), ) CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig( "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", "code_interpreter.jupyter.auth_token", - os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""), + ), ) CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig( "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", "code_interpreter.jupyter.auth_password", - os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""), + ), ) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index a6fa6bd8c..19ed89880 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -100,7 +100,12 @@ from open_webui.config import ( OPENAI_API_CONFIGS, # Direct Connections ENABLE_DIRECT_CONNECTIONS, - # Code Interpreter + # Code Execution + CODE_EXECUTION_ENGINE, + CODE_EXECUTION_JUPYTER_URL, + CODE_EXECUTION_JUPYTER_AUTH, + CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, ENABLE_CODE_INTERPRETER, CODE_INTERPRETER_ENGINE, CODE_INTERPRETER_PROMPT_TEMPLATE, @@ -613,10 +618,18 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( ######################################## # -# CODE INTERPRETER +# 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 +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD +) + app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE @@ -1120,6 +1133,9 @@ async def get_app_config(request: Request): { "default_models": app.state.config.DEFAULT_MODELS, "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + "code": { + "engine": app.state.config.CODE_EXECUTION_ENGINE, + }, "audio": { "tts": { "engine": app.state.config.TTS_ENGINE, diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 016075234..d460ae670 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -70,6 +70,11 @@ async def set_direct_connections_config( # CodeInterpreterConfig ############################ class CodeInterpreterConfigForm(BaseModel): + CODE_EXECUTION_ENGINE: str + CODE_EXECUTION_JUPYTER_URL: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str] ENABLE_CODE_INTERPRETER: bool CODE_INTERPRETER_ENGINE: str CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str] @@ -79,9 +84,14 @@ class CodeInterpreterConfigForm(BaseModel): CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str] -@router.get("/code_interpreter", response_model=CodeInterpreterConfigForm) -async def get_code_interpreter_config(request: Request, user=Depends(get_admin_user)): +@router.get("/code_execution", response_model=CodeInterpreterConfigForm) +async def get_code_execution_config(request: Request, user=Depends(get_admin_user)): return { + "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, + "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, @@ -92,10 +102,25 @@ async def get_code_interpreter_config(request: Request, user=Depends(get_admin_u } -@router.post("/code_interpreter", response_model=CodeInterpreterConfigForm) -async def set_code_interpreter_config( +@router.post("/code_execution", response_model=CodeInterpreterConfigForm) +async def set_code_execution_config( request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) ): + + 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 + ) + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = ( + form_data.CODE_EXECUTION_JUPYTER_AUTH + ) + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = ( + form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN + ) + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( + form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD + ) + request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = ( @@ -118,6 +143,11 @@ async def set_code_interpreter_config( ) return { + "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, + "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py index ea73e9759..61863bda5 100644 --- a/backend/open_webui/routers/utils.py +++ b/backend/open_webui/routers/utils.py @@ -4,45 +4,75 @@ import markdown from open_webui.models.chats import ChatTitleMessagesForm from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Response, status +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from pydantic import BaseModel from starlette.responses import FileResponse + + from open_webui.utils.misc import get_gravatar_url from open_webui.utils.pdf_generator import PDFGenerator -from open_webui.utils.auth import get_admin_user +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.code_interpreter import execute_code_jupyter + router = APIRouter() @router.get("/gravatar") -async def get_gravatar( - email: str, -): +async def get_gravatar(email: str, user=Depends(get_verified_user)): return get_gravatar_url(email) -class CodeFormatRequest(BaseModel): +class CodeForm(BaseModel): code: str @router.post("/code/format") -async def format_code(request: CodeFormatRequest): +async def format_code(form_data: CodeForm, user=Depends(get_verified_user)): try: - formatted_code = black.format_str(request.code, mode=black.Mode()) + formatted_code = black.format_str(form_data.code, mode=black.Mode()) return {"code": formatted_code} except black.NothingChanged: - return {"code": request.code} + return {"code": form_data.code} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) +@router.post("/code/execute") +async def execute_code( + request: Request, form_data: CodeForm, user=Depends(get_verified_user) +): + if request.app.state.config.CODE_EXECUTION_ENGINE == "jupyter": + output = await execute_code_jupyter( + request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + form_data.code, + ( + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "token" + else None + ), + ( + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password" + else None + ), + ) + + return output + else: + raise HTTPException( + status_code=400, + detail="Code execution engine not supported", + ) + + class MarkdownForm(BaseModel): md: str @router.post("/markdown") async def get_html_from_markdown( - form_data: MarkdownForm, + form_data: MarkdownForm, user=Depends(get_verified_user) ): return {"html": markdown.markdown(form_data.md)} @@ -54,7 +84,7 @@ class ChatForm(BaseModel): @router.post("/pdf") async def download_chat_as_pdf( - form_data: ChatTitleMessagesForm, + form_data: ChatTitleMessagesForm, user=Depends(get_verified_user) ): try: pdf_bytes = PDFGenerator(form_data).generate_chat_pdf() diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index d7f02564c..f7f02c740 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -115,10 +115,10 @@ export const setDirectConnectionsConfig = async (token: string, config: object) return res; }; -export const getCodeInterpreterConfig = async (token: string) => { +export const getCodeExecutionConfig = async (token: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -142,10 +142,10 @@ export const getCodeInterpreterConfig = async (token: string) => { return res; }; -export const setCodeInterpreterConfig = async (token: string, config: object) => { +export const setCodeExecutionConfig = async (token: string, config: object) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/lib/apis/utils/index.ts b/src/lib/apis/utils/index.ts index 40fdbfcfa..64db56124 100644 --- a/src/lib/apis/utils/index.ts +++ b/src/lib/apis/utils/index.ts @@ -1,12 +1,13 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; -export const getGravatarUrl = async (email: string) => { +export const getGravatarUrl = async (token: string, email: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, { method: 'GET', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` } }) .then(async (res) => { @@ -22,13 +23,14 @@ export const getGravatarUrl = async (email: string) => { return res; }; -export const formatPythonCode = async (code: string) => { +export const executeCode = async (token: string, code: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/execute`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` }, body: JSON.stringify({ code: code @@ -55,13 +57,48 @@ export const formatPythonCode = async (code: string) => { return res; }; -export const downloadChatAsPDF = async (title: string, messages: object[]) => { +export const formatPythonCode = async (token: string, code: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + code: code + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + + error = err; + if (err.detail) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const downloadChatAsPDF = async (token: string, title: string, messages: object[]) => { let error = null; const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` }, body: JSON.stringify({ title: title, @@ -81,13 +118,14 @@ export const downloadChatAsPDF = async (title: string, messages: object[]) => { return blob; }; -export const getHTMLFromMarkdown = async (md: string) => { +export const getHTMLFromMarkdown = async (token: string, md: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` }, body: JSON.stringify({ md: md diff --git a/src/lib/components/admin/Settings.svelte b/src/lib/components/admin/Settings.svelte index 415e4377a..60edbd25a 100644 --- a/src/lib/components/admin/Settings.svelte +++ b/src/lib/components/admin/Settings.svelte @@ -19,7 +19,7 @@ import ChartBar from '../icons/ChartBar.svelte'; import DocumentChartBar from '../icons/DocumentChartBar.svelte'; import Evaluations from './Settings/Evaluations.svelte'; - import CodeInterpreter from './Settings/CodeInterpreter.svelte'; + import CodeExecution from './Settings/CodeExecution.svelte'; const i18n = getContext('i18n'); @@ -191,11 +191,11 @@