diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index 22ad884bb..537d328c3 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -6,12 +6,15 @@ from sqlalchemy.orm import Session from open_webui.internal.db import Base, JSONField, get_db, get_db_context from open_webui.models.groups import Groups from open_webui.models.users import Users, UserResponse +from open_webui.models.prompt_history import PromptHistories + from pydantic import BaseModel, ConfigDict from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON from open_webui.utils.access_control import has_access + #################### # Prompts DB Schema #################### @@ -63,7 +66,7 @@ class PromptModel(BaseModel): created_at: Optional[int] = None updated_at: Optional[int] = None access_control: Optional[dict] = None - + model_config = ConfigDict(from_attributes=True) @@ -98,7 +101,7 @@ class PromptsTable: ) -> Optional[PromptModel]: now = int(time.time()) prompt_id = str(uuid.uuid4()) - + prompt = PromptModel( id=prompt_id, user_id=user_id, @@ -119,11 +122,8 @@ class PromptsTable: db.add(result) db.commit() db.refresh(result) - + if result: - # Create initial history entry - from open_webui.models.prompt_history import PromptHistories - snapshot = { "name": form_data.name, "content": form_data.content, @@ -132,7 +132,7 @@ class PromptsTable: "meta": form_data.meta or {}, "access_control": form_data.access_control, } - + history_entry = PromptHistories.create_history_entry( prompt_id=prompt_id, snapshot=snapshot, @@ -141,13 +141,13 @@ class PromptsTable: commit_message=form_data.commit_message or "Initial version", db=db, ) - + # Set the initial version as the production version if history_entry: result.version_id = history_entry.id db.commit() db.refresh(result) - + return PromptModel.model_validate(result) else: return None @@ -223,28 +223,28 @@ class PromptsTable: ] def update_prompt_by_command( - self, - command: str, - form_data: PromptForm, + self, + command: str, + form_data: PromptForm, user_id: str, - db: Optional[Session] = None + db: Optional[Session] = None, ) -> Optional[PromptModel]: try: with get_db_context(db) as db: prompt = db.query(Prompt).filter_by(command=command).first() if not prompt: return None - - # Get the latest history entry for parent_id - from open_webui.models.prompt_history import PromptHistories - latest_history = PromptHistories.get_latest_history_entry(prompt.id, db=db) + + latest_history = PromptHistories.get_latest_history_entry( + prompt.id, db=db + ) parent_id = latest_history.id if latest_history else None - + # Check if content changed to decide on history creation content_changed = ( - prompt.name != form_data.name or - prompt.content != form_data.content or - prompt.access_control != form_data.access_control + prompt.name != form_data.name + or prompt.content != form_data.content + or prompt.access_control != form_data.access_control ) # Update prompt fields @@ -254,9 +254,9 @@ class PromptsTable: prompt.meta = form_data.meta or prompt.meta prompt.access_control = form_data.access_control prompt.updated_at = int(time.time()) - + db.commit() - + # Create history entry only if content changed if content_changed: snapshot = { @@ -267,7 +267,7 @@ class PromptsTable: "meta": form_data.meta or {}, "access_control": form_data.access_control, } - + history_entry = PromptHistories.create_history_entry( prompt_id=prompt.id, snapshot=snapshot, @@ -276,38 +276,100 @@ class PromptsTable: commit_message=form_data.commit_message, db=db, ) - + # Set as production if flag is True (default) if form_data.is_production and history_entry: prompt.version_id = history_entry.id db.commit() - + return PromptModel.model_validate(prompt) except Exception: return None + def update_prompt_by_id( + self, + prompt_id: str, + form_data: PromptForm, + user_id: str, + db: Optional[Session] = None, + ) -> Optional[PromptModel]: + try: + with get_db_context(db) as db: + prompt = db.query(Prompt).filter_by(id=prompt_id).first() + if not prompt: + return None + latest_history = PromptHistories.get_latest_history_entry( + prompt.id, db=db + ) + parent_id = latest_history.id if latest_history else None + + # Check if content changed to decide on history creation + content_changed = ( + prompt.name != form_data.name + or prompt.content != form_data.content + or prompt.access_control != form_data.access_control + ) + + # Update prompt fields + prompt.name = form_data.name + prompt.content = form_data.content + prompt.data = form_data.data or prompt.data + prompt.meta = form_data.meta or prompt.meta + prompt.access_control = form_data.access_control + prompt.updated_at = int(time.time()) + + db.commit() + + # Create history entry only if content changed + if content_changed: + snapshot = { + "name": form_data.name, + "content": form_data.content, + "command": prompt.command, + "data": form_data.data or {}, + "meta": form_data.meta or {}, + "access_control": form_data.access_control, + } + + history_entry = PromptHistories.create_history_entry( + prompt_id=prompt.id, + snapshot=snapshot, + user_id=user_id, + parent_id=parent_id, + commit_message=form_data.commit_message, + db=db, + ) + + # Set as production if flag is True (default) + if form_data.is_production and history_entry: + prompt.version_id = history_entry.id + db.commit() + + return PromptModel.model_validate(prompt) + except Exception: + return None def update_prompt_version( self, - command: str, + prompt_id: str, version_id: str, db: Optional[Session] = None, ) -> Optional[PromptModel]: """Set the active version of a prompt and restore content from that version's snapshot.""" try: with get_db_context(db) as db: - prompt = db.query(Prompt).filter_by(command=command).first() + prompt = db.query(Prompt).filter_by(id=prompt_id).first() if not prompt: return None - - # Get the history entry to restore content from - from open_webui.models.prompt_history import PromptHistories - history_entry = PromptHistories.get_history_entry_by_id(version_id, db=db) - + + history_entry = PromptHistories.get_history_entry_by_id( + version_id, db=db + ) + if not history_entry: return None - + # Restore prompt content from the snapshot snapshot = history_entry.snapshot if snapshot: @@ -316,11 +378,11 @@ class PromptsTable: prompt.data = snapshot.get("data", prompt.data) prompt.meta = snapshot.get("meta", prompt.meta) # Note: command and access_control are not restored from snapshot - + prompt.version_id = version_id prompt.updated_at = int(time.time()) db.commit() - + return PromptModel.model_validate(prompt) except Exception: return None @@ -333,8 +395,22 @@ class PromptsTable: with get_db_context(db) as db: prompt = db.query(Prompt).filter_by(command=command).first() if prompt: - # Delete history first (Requirement: entire history should be deleted) - from open_webui.models.prompt_history import PromptHistories + PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) + + prompt.is_active = False + prompt.updated_at = int(time.time()) + db.commit() + return True + return False + except Exception: + return False + + def delete_prompt_by_id(self, prompt_id: str, db: Optional[Session] = None) -> bool: + """Soft delete a prompt by setting is_active to False.""" + try: + with get_db_context(db) as db: + prompt = db.query(Prompt).filter_by(id=prompt_id).first() + if prompt: PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) prompt.is_active = False @@ -353,10 +429,8 @@ class PromptsTable: with get_db_context(db) as db: prompt = db.query(Prompt).filter_by(command=command).first() if prompt: - # Delete history first - from open_webui.models.prompt_history import PromptHistories PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) - + # Delete prompt db.query(Prompt).filter_by(command=command).delete() db.commit() diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index d243b1655..6047ba0d8 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -179,18 +179,18 @@ async def get_prompt_by_id( ############################ -# UpdatePromptByCommand +# UpdatePromptById ############################ -@router.post("/command/{command}/update", response_model=Optional[PromptModel]) -async def update_prompt_by_command( - command: str, +@router.post("/id/{prompt_id}/update", response_model=Optional[PromptModel]) +async def update_prompt_by_id( + prompt_id: str, form_data: PromptForm, user=Depends(get_verified_user), db: Session = Depends(get_session), ): - prompt = Prompts.get_prompt_by_command(command, db=db) + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: raise HTTPException( @@ -209,9 +209,9 @@ async def update_prompt_by_command( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Use the command from the found prompt - updated_prompt = Prompts.update_prompt_by_command( - prompt.command, form_data, user.id, db=db + # Use the ID from the found prompt + updated_prompt = Prompts.update_prompt_by_id( + prompt.id, form_data, user.id, db=db ) if updated_prompt: return updated_prompt @@ -222,14 +222,14 @@ async def update_prompt_by_command( ) -@router.post("/command/{command}/set/version", response_model=Optional[PromptModel]) +@router.post("/id/{prompt_id}/set/version", response_model=Optional[PromptModel]) async def set_prompt_version( - command: str, + prompt_id: str, form_data: PromptVersionUpdateForm, user=Depends(get_verified_user), db: Session = Depends(get_session), ): - prompt = Prompts.get_prompt_by_command(command, db=db) + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -247,7 +247,7 @@ async def set_prompt_version( ) updated_prompt = Prompts.update_prompt_version( - prompt.command, form_data.version_id, db=db + prompt.id, form_data.version_id, db=db ) if updated_prompt: return updated_prompt @@ -259,15 +259,15 @@ async def set_prompt_version( ############################ -# DeletePromptByCommand +# DeletePromptById ############################ -@router.delete("/command/{command}/delete", response_model=bool) -async def delete_prompt_by_command( - command: str, user=Depends(get_verified_user), db: Session = Depends(get_session) +@router.delete("/id/{prompt_id}/delete", response_model=bool) +async def delete_prompt_by_id( + prompt_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) ): - prompt = Prompts.get_prompt_by_command(command, db=db) + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: raise HTTPException( @@ -285,7 +285,7 @@ async def delete_prompt_by_command( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - result = Prompts.delete_prompt_by_command(prompt.command, db=db) + result = Prompts.delete_prompt_by_id(prompt.id, db=db) return result @@ -294,16 +294,16 @@ async def delete_prompt_by_command( ############################ -@router.get("/command/{command}/history", response_model=list[PromptHistoryResponse]) +@router.get("/id/{prompt_id}/history", response_model=list[PromptHistoryResponse]) async def get_prompt_history( - command: str, + prompt_id: str, limit: int = 50, offset: int = 0, user=Depends(get_verified_user), db: Session = Depends(get_session), ): """Get version history for a prompt.""" - prompt = Prompts.get_prompt_by_command(command, db=db) + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: raise HTTPException( @@ -329,16 +329,16 @@ async def get_prompt_history( @router.get( - "/command/{command}/history/{history_id}", response_model=PromptHistoryModel + "/id/{prompt_id}/history/{history_id}", response_model=PromptHistoryModel ) async def get_prompt_history_entry( - command: str, + prompt_id: str, history_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session), ): """Get a specific version from history.""" - prompt = Prompts.get_prompt_by_command(command, db=db) + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: raise HTTPException( @@ -368,16 +368,16 @@ async def get_prompt_history_entry( @router.delete( - "/command/{command}/history/{history_id}", response_model=bool + "/id/{prompt_id}/history/{history_id}", response_model=bool ) async def delete_prompt_history_entry( - command: str, + prompt_id: str, history_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session), ): """Delete a history entry. Cannot delete the active production version.""" - prompt = Prompts.get_prompt_by_command(command, db=db) + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: raise HTTPException( @@ -413,16 +413,16 @@ async def delete_prompt_history_entry( return success -@router.get("/command/{command}/history/diff") +@router.get("/id/{prompt_id}/history/diff") async def get_prompt_diff( - command: str, + prompt_id: str, from_id: str, to_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session), ): """Get diff between two versions.""" - prompt = Prompts.get_prompt_by_command(command, db=db) + prompt = Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: raise HTTPException( diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts index 2e92ff30b..d058b677c 100644 --- a/src/lib/apis/prompts/index.ts +++ b/src/lib/apis/prompts/index.ts @@ -1,6 +1,7 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; type PromptItem = { + id?: string; // Prompt ID command: string; name: string; // Changed from title content: string; @@ -9,6 +10,7 @@ type PromptItem = { access_control?: null | object; version_id?: string | null; // Active version commit_message?: string | null; // For history tracking + is_production?: boolean; // Whether to set new version as production }; type PromptHistoryItem = { @@ -203,22 +205,17 @@ export const getPromptById = async (token: string, promptId: string) => { return res; }; -export const updatePromptByCommand = async (token: string, prompt: PromptItem) => { +export const updatePromptById = async (token: string, prompt: PromptItem) => { let error = null; - const command = prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command; - - const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/update`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${prompt.id}/update`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', authorization: `Bearer ${token}` }, - body: JSON.stringify({ - ...prompt, - command: command - }) + body: JSON.stringify(prompt) }) .then(async (res) => { if (!res.ok) throw await res.json(); @@ -243,12 +240,12 @@ export const updatePromptByCommand = async (token: string, prompt: PromptItem) = export const setProductionPromptVersion = async ( token: string, - command: string, + promptId: string, version_id: string ) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/set/version`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/set/version`, { method: 'POST', headers: { Accept: 'application/json', @@ -276,12 +273,10 @@ export const setProductionPromptVersion = async ( return res; }; -export const deletePromptByCommand = async (token: string, command: string) => { +export const deletePromptById = async (token: string, promptId: string) => { let error = null; - command = command.charAt(0) === '/' ? command.slice(1) : command; - - const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/delete`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/delete`, { method: 'DELETE', headers: { Accept: 'application/json', @@ -316,16 +311,14 @@ export const deletePromptByCommand = async (token: string, command: string) => { export const getPromptHistory = async ( token: string, - command: string, + promptId: string, limit: number = 50, offset: number = 0 ): Promise => { let error = null; - command = command.charAt(0) === '/' ? command.slice(1) : command; - const res = await fetch( - `${WEBUI_API_BASE_URL}/prompts/command/${command}/history?limit=${limit}&offset=${offset}`, + `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history?limit=${limit}&offset=${offset}`, { method: 'GET', headers: { @@ -354,15 +347,13 @@ export const getPromptHistory = async ( export const deletePromptHistoryVersion = async ( token: string, - command: string, + promptId: string, historyId: string ): Promise => { let error = null; - command = command.charAt(0) === '/' ? command.slice(1) : command; - const res = await fetch( - `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}`, + `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, { method: 'DELETE', headers: { @@ -391,15 +382,13 @@ export const deletePromptHistoryVersion = async ( export const getPromptHistoryEntry = async ( token: string, - command: string, + promptId: string, historyId: string ): Promise => { let error = null; - command = command.charAt(0) === '/' ? command.slice(1) : command; - const res = await fetch( - `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}`, + `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, { method: 'GET', headers: { @@ -428,16 +417,14 @@ export const getPromptHistoryEntry = async ( export const restorePromptFromHistory = async ( token: string, - command: string, + promptId: string, historyId: string, commitMessage?: string ) => { let error = null; - command = command.charAt(0) === '/' ? command.slice(1) : command; - const res = await fetch( - `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}/restore`, + `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}/restore`, { method: 'POST', headers: { @@ -469,16 +456,14 @@ export const restorePromptFromHistory = async ( export const getPromptDiff = async ( token: string, - command: string, + promptId: string, fromId: string, toId: string ): Promise => { let error = null; - command = command.charAt(0) === '/' ? command.slice(1) : command; - const res = await fetch( - `${WEBUI_API_BASE_URL}/prompts/command/${command}/history/diff?from_id=${fromId}&to_id=${toId}`, + `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/diff?from_id=${fromId}&to_id=${toId}`, { method: 'GET', headers: { @@ -505,4 +490,3 @@ export const getPromptDiff = async ( return res; }; - diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte index 495a741f0..585b12cb4 100644 --- a/src/lib/components/workspace/Prompts.svelte +++ b/src/lib/components/workspace/Prompts.svelte @@ -9,7 +9,7 @@ import { createNewPrompt, - deletePromptByCommand, + deletePromptById, getPrompts, getPromptList } from '$lib/apis/prompts'; @@ -121,7 +121,7 @@ const deleteHandler = async (prompt) => { const command = prompt.command; - const res = await deletePromptByCommand(localStorage.token, command).catch((err) => { + const res = await deletePromptById(localStorage.token, prompt.id).catch((err) => { toast.error(err); return null; }); diff --git a/src/lib/components/workspace/Prompts/PromptEditor.svelte b/src/lib/components/workspace/Prompts/PromptEditor.svelte index dbc3997d8..732825524 100644 --- a/src/lib/components/workspace/Prompts/PromptEditor.svelte +++ b/src/lib/components/workspace/Prompts/PromptEditor.svelte @@ -13,7 +13,6 @@ import XMark from '$lib/components/icons/XMark.svelte'; import { getPromptHistory, - updatePromptByCommand, setProductionPromptVersion, deletePromptHistoryVersion } from '$lib/apis/prompts'; @@ -66,6 +65,7 @@ if (validateCommandString(command)) { await onSubmit({ + id: prompt?.id, name, command, content, @@ -95,7 +95,7 @@ if (!prompt?.command || !edit) return; historyLoading = true; try { - history = await getPromptHistory(localStorage.token, prompt.command); + history = await getPromptHistory(localStorage.token, prompt.id); } catch (error) { console.error('Failed to load history:', error); history = []; @@ -110,7 +110,7 @@ } try { - await setProductionPromptVersion(localStorage.token, prompt.command, historyEntry.id); + await setProductionPromptVersion(localStorage.token, prompt.id, historyEntry.id); // Update local prompt object to trigger reactivity prompt = { ...prompt, version_id: historyEntry.id }; toast.success($i18n.t('Production version updated')); @@ -123,7 +123,7 @@ if (disabled) return; try { - await deletePromptHistoryVersion(localStorage.token, prompt.command, historyId); + await deletePromptHistoryVersion(localStorage.token, prompt.id, historyId); toast.success($i18n.t('Version deleted')); // Reload history await loadHistory(); diff --git a/src/routes/(app)/workspace/prompts/[id]/+page.svelte b/src/routes/(app)/workspace/prompts/[id]/+page.svelte index 2d989be49..5e75b0099 100644 --- a/src/routes/(app)/workspace/prompts/[id]/+page.svelte +++ b/src/routes/(app)/workspace/prompts/[id]/+page.svelte @@ -6,7 +6,7 @@ const i18n = getContext('i18n'); - import { getPromptById, getPrompts, updatePromptByCommand } from '$lib/apis/prompts'; + import { getPromptById, getPrompts, updatePromptById } from '$lib/apis/prompts'; import { page } from '$app/stores'; import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte'; @@ -19,7 +19,7 @@ const onSubmit = async (_prompt) => { console.log(_prompt); - const updatedPrompt = await updatePromptByCommand(localStorage.token, _prompt).catch((error) => { + const updatedPrompt = await updatePromptById(localStorage.token, _prompt).catch((error) => { toast.error(`${error}`); return null; });