feat: chat_file table
This commit is contained in:
@@ -1606,6 +1606,7 @@ async def chat_completion(
|
||||
"user_id": user.id,
|
||||
"chat_id": form_data.pop("chat_id", None),
|
||||
"message_id": form_data.pop("id", None),
|
||||
"parent_message": form_data.pop("parent_message", None),
|
||||
"parent_message_id": form_data.pop("parent_id", None),
|
||||
"session_id": form_data.pop("session_id", None),
|
||||
"filter_ids": form_data.pop("filter_ids", []),
|
||||
@@ -1630,15 +1631,38 @@ async def chat_completion(
|
||||
},
|
||||
}
|
||||
|
||||
if metadata.get("chat_id") and (user and user.role != "admin"):
|
||||
if not metadata["chat_id"].startswith("local:"):
|
||||
if metadata.get("chat_id") and user:
|
||||
if not metadata["chat_id"].startswith(
|
||||
"local:"
|
||||
): # temporary chats are not stored
|
||||
|
||||
# Verify chat ownership
|
||||
chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id)
|
||||
if chat is None:
|
||||
if chat is None and user.role != "admin": # admins can access any chat
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
# Insert chat files from parent message if any
|
||||
parent_message = metadata.get("parent_message", {})
|
||||
parent_message_files = parent_message.get("files", [])
|
||||
if parent_message_files:
|
||||
try:
|
||||
Chats.insert_chat_files(
|
||||
metadata["chat_id"],
|
||||
parent_message.get("id"),
|
||||
[
|
||||
file_item.get("id")
|
||||
for file_item in parent_message_files
|
||||
if file_item.get("type") == "file"
|
||||
],
|
||||
user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(f"Error inserting chat files: {e}")
|
||||
pass
|
||||
|
||||
request.state.metadata = metadata
|
||||
form_data["metadata"] = metadata
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Add chat_file table
|
||||
|
||||
Revision ID: c440947495f3
|
||||
Revises: 81cc2ce44d79
|
||||
Create Date: 2025-12-21 20:27:41.694897
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c440947495f3"
|
||||
down_revision: Union[str, None] = "81cc2ce44d79"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"chat_file",
|
||||
sa.Column("id", sa.Text(), primary_key=True),
|
||||
sa.Column("user_id", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"chat_id",
|
||||
sa.Text(),
|
||||
sa.ForeignKey("chat.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"file_id",
|
||||
sa.Text(),
|
||||
sa.ForeignKey("file.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("message_id", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.BigInteger(), nullable=False),
|
||||
sa.Column("updated_at", sa.BigInteger(), nullable=False),
|
||||
# indexes
|
||||
sa.Index("ix_chat_file_chat_id", "chat_id"),
|
||||
sa.Index("ix_chat_file_file_id", "file_id"),
|
||||
sa.Index("ix_chat_file_message_id", "message_id"),
|
||||
sa.Index("ix_chat_file_user_id", "user_id"),
|
||||
# unique constraints
|
||||
sa.UniqueConstraint(
|
||||
"chat_id", "file_id", name="uq_chat_file_chat_file"
|
||||
), # prevent duplicate entries
|
||||
)
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("chat_file")
|
||||
pass
|
||||
@@ -10,7 +10,17 @@ from open_webui.models.folders import Folders
|
||||
from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, Index
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
Column,
|
||||
ForeignKey,
|
||||
String,
|
||||
Text,
|
||||
JSON,
|
||||
Index,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy import or_, func, select, and_, text
|
||||
from sqlalchemy.sql import exists
|
||||
from sqlalchemy.sql.expression import bindparam
|
||||
@@ -74,6 +84,38 @@ class ChatModel(BaseModel):
|
||||
folder_id: Optional[str] = None
|
||||
|
||||
|
||||
class ChatFile(Base):
|
||||
__tablename__ = "chat_file"
|
||||
|
||||
id = Column(Text, unique=True, primary_key=True)
|
||||
user_id = Column(Text, nullable=False)
|
||||
|
||||
chat_id = Column(Text, ForeignKey("chat.id", ondelete="CASCADE"), nullable=False)
|
||||
message_id = Column(Text, nullable=True)
|
||||
file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
created_at = Column(BigInteger, nullable=False)
|
||||
updated_at = Column(BigInteger, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("chat_id", "file_id", name="uq_chat_file_chat_file"),
|
||||
)
|
||||
|
||||
|
||||
class ChatFileModel(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
|
||||
chat_id: str
|
||||
message_id: Optional[str] = None
|
||||
file_id: str
|
||||
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
####################
|
||||
# Forms
|
||||
####################
|
||||
@@ -1219,5 +1261,81 @@ class ChatTable:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def insert_chat_files(
|
||||
self, chat_id: str, message_id: str, file_ids: list[str], user_id: str
|
||||
) -> Optional[list[ChatFileModel]]:
|
||||
if not file_ids:
|
||||
return None
|
||||
|
||||
chat_message_file_ids = [
|
||||
item.id
|
||||
for item in self.get_chat_files_by_chat_id_and_message_id(
|
||||
chat_id, message_id
|
||||
)
|
||||
]
|
||||
# Remove duplicates and existing file_ids
|
||||
file_ids = list(
|
||||
set(
|
||||
[
|
||||
file_id
|
||||
for file_id in file_ids
|
||||
if file_id and file_id not in chat_message_file_ids
|
||||
]
|
||||
)
|
||||
)
|
||||
if not file_ids:
|
||||
return None
|
||||
|
||||
try:
|
||||
with get_db() as db:
|
||||
now = int(time.time())
|
||||
|
||||
chat_files = [
|
||||
ChatFileModel(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
file_id=file_id,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
for file_id in file_ids
|
||||
]
|
||||
|
||||
results = [
|
||||
ChatFile(**chat_file.model_dump()) for chat_file in chat_files
|
||||
]
|
||||
|
||||
db.add_all(results)
|
||||
db.commit()
|
||||
|
||||
return chat_files
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chat_files_by_chat_id_and_message_id(
|
||||
self, chat_id: str, message_id: str
|
||||
) -> list[ChatFileModel]:
|
||||
with get_db() as db:
|
||||
all_chat_files = (
|
||||
db.query(ChatFile)
|
||||
.filter_by(chat_id=chat_id, message_id=message_id)
|
||||
.order_by(ChatFile.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
ChatFileModel.model_validate(chat_file) for chat_file in all_chat_files
|
||||
]
|
||||
|
||||
def delete_chat_file(self, chat_id: str, file_id: str) -> bool:
|
||||
try:
|
||||
with get_db() as db:
|
||||
db.query(ChatFile).filter_by(chat_id=chat_id, file_id=file_id).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
Chats = ChatTable()
|
||||
|
||||
@@ -22,11 +22,43 @@ import base64
|
||||
import io
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
BASE64_IMAGE_URL_PREFIX = re.compile(r"data:image/\w+;base64,", re.IGNORECASE)
|
||||
MARKDOWN_IMAGE_URL_PATTERN = re.compile(r"!\[(.*?)\]\((.+?)\)", re.IGNORECASE)
|
||||
|
||||
|
||||
def get_image_base64_from_url(url: str) -> Optional[str]:
|
||||
try:
|
||||
if url.startswith("http"):
|
||||
# Download the image from the URL
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
image_data = response.content
|
||||
encoded_string = base64.b64encode(image_data).decode("utf-8")
|
||||
content_type = response.headers.get("Content-Type", "image/png")
|
||||
return f"data:{content_type};base64,{encoded_string}"
|
||||
else:
|
||||
file = Files.get_file_by_id(url)
|
||||
|
||||
if not file:
|
||||
return None
|
||||
|
||||
file_path = Storage.get_file(file.path)
|
||||
file_path = Path(file_path)
|
||||
|
||||
if file_path.is_file():
|
||||
with open(file_path, "rb") as image_file:
|
||||
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
||||
content_type, _ = mimetypes.guess_type(file_path.name)
|
||||
return f"data:{content_type};base64,{encoded_string}"
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def get_image_url_from_base64(request, base64_image_string, metadata, user):
|
||||
if BASE64_IMAGE_URL_PREFIX.match(base64_image_string):
|
||||
image_url = ""
|
||||
|
||||
@@ -60,6 +60,7 @@ from open_webui.utils.webhook import post_webhook
|
||||
from open_webui.utils.files import (
|
||||
convert_markdown_base64_images,
|
||||
get_file_url_from_base64,
|
||||
get_image_base64_from_url,
|
||||
get_image_url_from_base64,
|
||||
)
|
||||
|
||||
@@ -1108,6 +1109,45 @@ def apply_params_to_form_data(form_data, model):
|
||||
return form_data
|
||||
|
||||
|
||||
async def convert_url_images_to_base64(form_data):
|
||||
messages = form_data.get("messages", [])
|
||||
|
||||
for message in messages:
|
||||
content = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
|
||||
new_content = []
|
||||
|
||||
for item in content:
|
||||
if not isinstance(item, dict) or item.get("type") != "image_url":
|
||||
new_content.append(item)
|
||||
continue
|
||||
|
||||
image_url = item.get("image_url", {}).get("url", "")
|
||||
if image_url.startswith("data:image/"):
|
||||
new_content.append(item)
|
||||
continue
|
||||
|
||||
try:
|
||||
base64_data = await asyncio.to_thread(
|
||||
get_image_base64_from_url, image_url
|
||||
)
|
||||
new_content.append(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": base64_data},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(f"Error converting image URL to base64: {e}")
|
||||
new_content.append(item)
|
||||
|
||||
message["content"] = new_content
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
# Pipeline Inlet -> Filter Inlet -> Chat Memory -> Chat Web Search -> Chat Image Generation
|
||||
# -> Chat Code Interpreter (Form Data Update) -> (Default) Chat Tools Function Calling
|
||||
@@ -1125,6 +1165,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
except:
|
||||
pass
|
||||
|
||||
form_data = await convert_url_images_to_base64(form_data)
|
||||
|
||||
event_emitter = get_event_emitter(metadata)
|
||||
event_caller = get_event_call(metadata)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user