refac: prompt endpoints

This commit is contained in:
Timothy Jaeryang Baek
2026-01-24 03:08:48 +04:00
parent dff0141160
commit 5ad593e465
6 changed files with 172 additions and 114 deletions
+93 -19
View File
@@ -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.internal.db import Base, JSONField, get_db, get_db_context
from open_webui.models.groups import Groups from open_webui.models.groups import Groups
from open_webui.models.users import Users, UserResponse from open_webui.models.users import Users, UserResponse
from open_webui.models.prompt_history import PromptHistories
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
from open_webui.utils.access_control import has_access from open_webui.utils.access_control import has_access
#################### ####################
# Prompts DB Schema # Prompts DB Schema
#################### ####################
@@ -121,9 +124,6 @@ class PromptsTable:
db.refresh(result) db.refresh(result)
if result: if result:
# Create initial history entry
from open_webui.models.prompt_history import PromptHistories
snapshot = { snapshot = {
"name": form_data.name, "name": form_data.name,
"content": form_data.content, "content": form_data.content,
@@ -227,7 +227,7 @@ class PromptsTable:
command: str, command: str,
form_data: PromptForm, form_data: PromptForm,
user_id: str, user_id: str,
db: Optional[Session] = None db: Optional[Session] = None,
) -> Optional[PromptModel]: ) -> Optional[PromptModel]:
try: try:
with get_db_context(db) as db: with get_db_context(db) as db:
@@ -235,16 +235,16 @@ class PromptsTable:
if not prompt: if not prompt:
return None return None
# Get the latest history entry for parent_id latest_history = PromptHistories.get_latest_history_entry(
from open_webui.models.prompt_history import PromptHistories 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 parent_id = latest_history.id if latest_history else None
# Check if content changed to decide on history creation # Check if content changed to decide on history creation
content_changed = ( content_changed = (
prompt.name != form_data.name or prompt.name != form_data.name
prompt.content != form_data.content or or prompt.content != form_data.content
prompt.access_control != form_data.access_control or prompt.access_control != form_data.access_control
) )
# Update prompt fields # Update prompt fields
@@ -286,24 +286,86 @@ class PromptsTable:
except Exception: except Exception:
return None 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( def update_prompt_version(
self, self,
command: str, prompt_id: str,
version_id: str, version_id: str,
db: Optional[Session] = None, db: Optional[Session] = None,
) -> Optional[PromptModel]: ) -> Optional[PromptModel]:
"""Set the active version of a prompt and restore content from that version's snapshot.""" """Set the active version of a prompt and restore content from that version's snapshot."""
try: try:
with get_db_context(db) as db: 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: if not prompt:
return None return None
# Get the history entry to restore content from history_entry = PromptHistories.get_history_entry_by_id(
from open_webui.models.prompt_history import PromptHistories version_id, db=db
history_entry = PromptHistories.get_history_entry_by_id(version_id, db=db) )
if not history_entry: if not history_entry:
return None return None
@@ -333,8 +395,22 @@ class PromptsTable:
with get_db_context(db) as db: with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(command=command).first() prompt = db.query(Prompt).filter_by(command=command).first()
if prompt: if prompt:
# Delete history first (Requirement: entire history should be deleted) PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
from open_webui.models.prompt_history import PromptHistories
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) PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
prompt.is_active = False prompt.is_active = False
@@ -353,8 +429,6 @@ class PromptsTable:
with get_db_context(db) as db: with get_db_context(db) as db:
prompt = db.query(Prompt).filter_by(command=command).first() prompt = db.query(Prompt).filter_by(command=command).first()
if prompt: if prompt:
# Delete history first
from open_webui.models.prompt_history import PromptHistories
PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) PromptHistories.delete_history_by_prompt_id(prompt.id, db=db)
# Delete prompt # Delete prompt
+30 -30
View File
@@ -179,18 +179,18 @@ async def get_prompt_by_id(
############################ ############################
# UpdatePromptByCommand # UpdatePromptById
############################ ############################
@router.post("/command/{command}/update", response_model=Optional[PromptModel]) @router.post("/id/{prompt_id}/update", response_model=Optional[PromptModel])
async def update_prompt_by_command( async def update_prompt_by_id(
command: str, prompt_id: str,
form_data: PromptForm, form_data: PromptForm,
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), 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: if not prompt:
raise HTTPException( raise HTTPException(
@@ -209,9 +209,9 @@ async def update_prompt_by_command(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
) )
# Use the command from the found prompt # Use the ID from the found prompt
updated_prompt = Prompts.update_prompt_by_command( updated_prompt = Prompts.update_prompt_by_id(
prompt.command, form_data, user.id, db=db prompt.id, form_data, user.id, db=db
) )
if updated_prompt: if updated_prompt:
return 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( async def set_prompt_version(
command: str, prompt_id: str,
form_data: PromptVersionUpdateForm, form_data: PromptVersionUpdateForm,
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), 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: if not prompt:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@@ -247,7 +247,7 @@ async def set_prompt_version(
) )
updated_prompt = Prompts.update_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: if updated_prompt:
return updated_prompt return updated_prompt
@@ -259,15 +259,15 @@ async def set_prompt_version(
############################ ############################
# DeletePromptByCommand # DeletePromptById
############################ ############################
@router.delete("/command/{command}/delete", response_model=bool) @router.delete("/id/{prompt_id}/delete", response_model=bool)
async def delete_prompt_by_command( async def delete_prompt_by_id(
command: str, user=Depends(get_verified_user), db: Session = Depends(get_session) 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: if not prompt:
raise HTTPException( raise HTTPException(
@@ -285,7 +285,7 @@ async def delete_prompt_by_command(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED, 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 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( async def get_prompt_history(
command: str, prompt_id: str,
limit: int = 50, limit: int = 50,
offset: int = 0, offset: int = 0,
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ):
"""Get version history for a prompt.""" """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: if not prompt:
raise HTTPException( raise HTTPException(
@@ -329,16 +329,16 @@ async def get_prompt_history(
@router.get( @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( async def get_prompt_history_entry(
command: str, prompt_id: str,
history_id: str, history_id: str,
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ):
"""Get a specific version from history.""" """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: if not prompt:
raise HTTPException( raise HTTPException(
@@ -368,16 +368,16 @@ async def get_prompt_history_entry(
@router.delete( @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( async def delete_prompt_history_entry(
command: str, prompt_id: str,
history_id: str, history_id: str,
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ):
"""Delete a history entry. Cannot delete the active production version.""" """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: if not prompt:
raise HTTPException( raise HTTPException(
@@ -413,16 +413,16 @@ async def delete_prompt_history_entry(
return success return success
@router.get("/command/{command}/history/diff") @router.get("/id/{prompt_id}/history/diff")
async def get_prompt_diff( async def get_prompt_diff(
command: str, prompt_id: str,
from_id: str, from_id: str,
to_id: str, to_id: str,
user=Depends(get_verified_user), user=Depends(get_verified_user),
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ):
"""Get diff between two versions.""" """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: if not prompt:
raise HTTPException( raise HTTPException(
+19 -35
View File
@@ -1,6 +1,7 @@
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
type PromptItem = { type PromptItem = {
id?: string; // Prompt ID
command: string; command: string;
name: string; // Changed from title name: string; // Changed from title
content: string; content: string;
@@ -9,6 +10,7 @@ type PromptItem = {
access_control?: null | object; access_control?: null | object;
version_id?: string | null; // Active version version_id?: string | null; // Active version
commit_message?: string | null; // For history tracking commit_message?: string | null; // For history tracking
is_production?: boolean; // Whether to set new version as production
}; };
type PromptHistoryItem = { type PromptHistoryItem = {
@@ -203,22 +205,17 @@ export const getPromptById = async (token: string, promptId: string) => {
return res; return res;
}; };
export const updatePromptByCommand = async (token: string, prompt: PromptItem) => { export const updatePromptById = async (token: string, prompt: PromptItem) => {
let error = null; let error = null;
const command = prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command; const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${prompt.id}/update`, {
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/update`, {
method: 'POST', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify(prompt)
...prompt,
command: command
})
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
@@ -243,12 +240,12 @@ export const updatePromptByCommand = async (token: string, prompt: PromptItem) =
export const setProductionPromptVersion = async ( export const setProductionPromptVersion = async (
token: string, token: string,
command: string, promptId: string,
version_id: string version_id: string
) => { ) => {
let error = null; 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', method: 'POST',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
@@ -276,12 +273,10 @@ export const setProductionPromptVersion = async (
return res; return res;
}; };
export const deletePromptByCommand = async (token: string, command: string) => { export const deletePromptById = async (token: string, promptId: string) => {
let error = null; let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command; const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/delete`, {
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/delete`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
@@ -316,16 +311,14 @@ export const deletePromptByCommand = async (token: string, command: string) => {
export const getPromptHistory = async ( export const getPromptHistory = async (
token: string, token: string,
command: string, promptId: string,
limit: number = 50, limit: number = 50,
offset: number = 0 offset: number = 0
): Promise<PromptHistoryItem[]> => { ): Promise<PromptHistoryItem[]> => {
let error = null; let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command;
const res = await fetch( 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', method: 'GET',
headers: { headers: {
@@ -354,15 +347,13 @@ export const getPromptHistory = async (
export const deletePromptHistoryVersion = async ( export const deletePromptHistoryVersion = async (
token: string, token: string,
command: string, promptId: string,
historyId: string historyId: string
): Promise<boolean> => { ): Promise<boolean> => {
let error = null; let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command;
const res = await fetch( const res = await fetch(
`${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}`, `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`,
{ {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@@ -391,15 +382,13 @@ export const deletePromptHistoryVersion = async (
export const getPromptHistoryEntry = async ( export const getPromptHistoryEntry = async (
token: string, token: string,
command: string, promptId: string,
historyId: string historyId: string
): Promise<PromptHistoryItem> => { ): Promise<PromptHistoryItem> => {
let error = null; let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command;
const res = await fetch( const res = await fetch(
`${WEBUI_API_BASE_URL}/prompts/command/${command}/history/${historyId}`, `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`,
{ {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -428,16 +417,14 @@ export const getPromptHistoryEntry = async (
export const restorePromptFromHistory = async ( export const restorePromptFromHistory = async (
token: string, token: string,
command: string, promptId: string,
historyId: string, historyId: string,
commitMessage?: string commitMessage?: string
) => { ) => {
let error = null; let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command;
const res = await fetch( 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', method: 'POST',
headers: { headers: {
@@ -469,16 +456,14 @@ export const restorePromptFromHistory = async (
export const getPromptDiff = async ( export const getPromptDiff = async (
token: string, token: string,
command: string, promptId: string,
fromId: string, fromId: string,
toId: string toId: string
): Promise<PromptDiff> => { ): Promise<PromptDiff> => {
let error = null; let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command;
const res = await fetch( 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', method: 'GET',
headers: { headers: {
@@ -505,4 +490,3 @@ export const getPromptDiff = async (
return res; return res;
}; };
+2 -2
View File
@@ -9,7 +9,7 @@
import { import {
createNewPrompt, createNewPrompt,
deletePromptByCommand, deletePromptById,
getPrompts, getPrompts,
getPromptList getPromptList
} from '$lib/apis/prompts'; } from '$lib/apis/prompts';
@@ -121,7 +121,7 @@
const deleteHandler = async (prompt) => { const deleteHandler = async (prompt) => {
const command = prompt.command; 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); toast.error(err);
return null; return null;
}); });
@@ -13,7 +13,6 @@
import XMark from '$lib/components/icons/XMark.svelte'; import XMark from '$lib/components/icons/XMark.svelte';
import { import {
getPromptHistory, getPromptHistory,
updatePromptByCommand,
setProductionPromptVersion, setProductionPromptVersion,
deletePromptHistoryVersion deletePromptHistoryVersion
} from '$lib/apis/prompts'; } from '$lib/apis/prompts';
@@ -66,6 +65,7 @@
if (validateCommandString(command)) { if (validateCommandString(command)) {
await onSubmit({ await onSubmit({
id: prompt?.id,
name, name,
command, command,
content, content,
@@ -95,7 +95,7 @@
if (!prompt?.command || !edit) return; if (!prompt?.command || !edit) return;
historyLoading = true; historyLoading = true;
try { try {
history = await getPromptHistory(localStorage.token, prompt.command); history = await getPromptHistory(localStorage.token, prompt.id);
} catch (error) { } catch (error) {
console.error('Failed to load history:', error); console.error('Failed to load history:', error);
history = []; history = [];
@@ -110,7 +110,7 @@
} }
try { try {
await setProductionPromptVersion(localStorage.token, prompt.command, historyEntry.id); await setProductionPromptVersion(localStorage.token, prompt.id, historyEntry.id);
// Update local prompt object to trigger reactivity // Update local prompt object to trigger reactivity
prompt = { ...prompt, version_id: historyEntry.id }; prompt = { ...prompt, version_id: historyEntry.id };
toast.success($i18n.t('Production version updated')); toast.success($i18n.t('Production version updated'));
@@ -123,7 +123,7 @@
if (disabled) return; if (disabled) return;
try { try {
await deletePromptHistoryVersion(localStorage.token, prompt.command, historyId); await deletePromptHistoryVersion(localStorage.token, prompt.id, historyId);
toast.success($i18n.t('Version deleted')); toast.success($i18n.t('Version deleted'));
// Reload history // Reload history
await loadHistory(); await loadHistory();
@@ -6,7 +6,7 @@
const i18n = getContext('i18n'); 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 { page } from '$app/stores';
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte'; import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
@@ -19,7 +19,7 @@
const onSubmit = async (_prompt) => { const onSubmit = async (_prompt) => {
console.log(_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}`); toast.error(`${error}`);
return null; return null;
}); });