mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-11 03:38:02 +00:00
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
"""Immutable snapshots of function configuration; the live row remains Production."""
|
|
|
|
import difflib
|
|
import time
|
|
import uuid
|
|
from copy import deepcopy
|
|
|
|
from fastapi import HTTPException
|
|
from open_webui.internal.db import Base, get_async_db_context
|
|
from pydantic import BaseModel, ConfigDict
|
|
from sqlalchemy import JSON, BigInteger, Column, Text, select, update
|
|
|
|
|
|
class FunctionHistory(Base):
|
|
__tablename__ = 'function_history'
|
|
id = Column(Text, primary_key=True)
|
|
function_id = Column(Text, nullable=False, index=True)
|
|
parent_id = Column(Text, nullable=True)
|
|
snapshot = Column(JSON, nullable=False)
|
|
user_id = Column(Text, nullable=False)
|
|
commit_message = Column(Text, nullable=True)
|
|
created_at = Column(BigInteger, nullable=False)
|
|
|
|
|
|
class FunctionHistoryResponse(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
id: str
|
|
function_id: str
|
|
parent_id: str | None = None
|
|
user_id: str
|
|
commit_message: str | None = None
|
|
created_at: int
|
|
user: dict | None = None
|
|
|
|
|
|
class FunctionHistoryModel(FunctionHistoryResponse):
|
|
snapshot: dict
|
|
|
|
|
|
class FunctionHistoryTable:
|
|
async def delete_history_entry(self, function_id, history_id, db=None):
|
|
from open_webui.models.functions import Function
|
|
|
|
async with get_async_db_context(db) as session:
|
|
try:
|
|
# Serialize with production switches on both SQLite and PostgreSQL.
|
|
await session.execute(
|
|
update(Function).where(Function.id == function_id).values(version_id=Function.version_id)
|
|
)
|
|
model = await session.get(Function, function_id, populate_existing=True)
|
|
if not model:
|
|
return False
|
|
if model.version_id == history_id:
|
|
raise HTTPException(400, 'Cannot delete the current version')
|
|
entry = (
|
|
await session.execute(select(FunctionHistory).filter_by(id=history_id, function_id=function_id))
|
|
).scalar_one_or_none()
|
|
if not entry:
|
|
return False
|
|
await session.execute(
|
|
update(FunctionHistory)
|
|
.where(FunctionHistory.function_id == function_id, FunctionHistory.parent_id == history_id)
|
|
.values(parent_id=entry.parent_id)
|
|
)
|
|
await session.delete(entry)
|
|
await session.commit()
|
|
return True
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
|
|
def new_entry(self, function_id, snapshot, user_id, parent_id=None, commit_message=None):
|
|
return FunctionHistory(
|
|
id=str(uuid.uuid4()),
|
|
function_id=function_id,
|
|
snapshot=snapshot,
|
|
user_id=user_id,
|
|
parent_id=parent_id,
|
|
commit_message=commit_message,
|
|
created_at=int(time.time()),
|
|
)
|
|
|
|
async def get_history_by_id(self, function_id, history_id, db=None):
|
|
from open_webui.models.users import User
|
|
|
|
async with get_async_db_context(db) as session:
|
|
entry = (
|
|
await session.execute(select(FunctionHistory).filter_by(function_id=function_id, id=history_id))
|
|
).scalar_one_or_none()
|
|
if not entry:
|
|
return None
|
|
result = FunctionHistoryModel.model_validate(entry)
|
|
author = (await session.execute(select(User.name).where(User.id == entry.user_id))).scalar_one_or_none()
|
|
result.user = {'name': author} if author else None
|
|
return result
|
|
|
|
async def get_history_by_function_id(self, function_id, page=1, db=None):
|
|
from open_webui.models.users import User
|
|
|
|
async with get_async_db_context(db) as session:
|
|
columns = [getattr(FunctionHistory, key) for key in FunctionHistoryResponse.model_fields if key != 'user']
|
|
rows = (
|
|
(
|
|
await session.execute(
|
|
select(*columns, User.name.label('author_name'))
|
|
.outerjoin(User, User.id == FunctionHistory.user_id)
|
|
.where(FunctionHistory.function_id == function_id)
|
|
.order_by(FunctionHistory.created_at.desc(), FunctionHistory.id.desc())
|
|
.offset((max(1, page) - 1) * 20)
|
|
.limit(20)
|
|
)
|
|
)
|
|
.mappings()
|
|
.all()
|
|
)
|
|
return [
|
|
FunctionHistoryResponse(
|
|
**{key: value for key, value in row.items() if key != 'author_name'},
|
|
user={'name': row['author_name']} if row['author_name'] else None,
|
|
)
|
|
for row in rows
|
|
]
|
|
|
|
|
|
FunctionHistories = FunctionHistoryTable()
|
|
|
|
|
|
def function_snapshot(resource):
|
|
data = (
|
|
resource if isinstance(resource, dict) else {key: getattr(resource, key) for key in ('name', 'content', 'meta')}
|
|
)
|
|
meta = data.get('meta') or {}
|
|
meta = meta.model_dump() if isinstance(meta, BaseModel) else deepcopy(meta)
|
|
meta.setdefault('description', None)
|
|
if not meta.get('i18n'):
|
|
meta.pop('i18n', None)
|
|
for key in ('manifest', 'has_user_valves', 'toggle'):
|
|
meta.pop(key, None)
|
|
return {'name': data.get('name'), 'content': data.get('content') or '', 'meta': meta}
|
|
|
|
|
|
def function_diff(before, after):
|
|
left, right = before.snapshot, after.snapshot
|
|
metadata = {
|
|
key: {'before': left.get(key), 'after': right.get(key)}
|
|
for key in ('name', 'meta')
|
|
if left.get(key) != right.get(key)
|
|
}
|
|
old, new = left.get('content') or '', right.get('content') or ''
|
|
# splitlines handles a missing final newline and CRLF without breaking the renderer.
|
|
patch = '\n'.join(
|
|
difflib.unified_diff(
|
|
old.splitlines(), new.splitlines(), fromfile='selected.py', tofile='production.py', lineterm=''
|
|
)
|
|
)
|
|
return {
|
|
'from_id': before.id,
|
|
'to_id': after.id,
|
|
'metadata': metadata,
|
|
'content_diff': patch,
|
|
'line_endings_only': old != new and not patch,
|
|
}
|