feat: chat folders, attachments, treeview backend - migration, models, routes
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
-- AI Assistant plugin migration 0002: chat folders + attachments
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_chat_folders (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
parent_id UUID REFERENCES ai_chat_folders(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_ai_folders_user ON ai_chat_folders(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_ai_folders_tenant ON ai_chat_folders(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_ai_folders_parent ON ai_chat_folders(parent_id);
|
||||||
|
|
||||||
|
ALTER TABLE ai_chat_sessions ADD COLUMN IF NOT EXISTS folder_id UUID REFERENCES ai_chat_folders(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_chat_attachments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
message_id UUID REFERENCES ai_chat_messages(id) ON DELETE CASCADE,
|
||||||
|
session_id UUID NOT NULL REFERENCES ai_chat_sessions(id) ON DELETE CASCADE,
|
||||||
|
filename VARCHAR(255) NOT NULL,
|
||||||
|
mime_type VARCHAR(255) NOT NULL DEFAULT 'application/octet-stream',
|
||||||
|
size_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
storage_path VARCHAR(1024) NOT NULL,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_ai_attachments_message ON ai_chat_attachments(message_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_ai_attachments_session ON ai_chat_attachments(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_ai_attachments_tenant ON ai_chat_attachments(tenant_id);
|
||||||
@@ -176,3 +176,58 @@ class AIChatMessage(Base, TenantMixin):
|
|||||||
tool_results: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
tool_results: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||||
tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
model_used: Mapped[str] = mapped_column(String(200), nullable=False, default="")
|
model_used: Mapped[str] = mapped_column(String(200), nullable=False, default="")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Chat Folders ---
|
||||||
|
|
||||||
|
class AIChatFolder(Base, TenantMixin):
|
||||||
|
"""Folder for organizing chat sessions."""
|
||||||
|
|
||||||
|
__tablename__ = "ai_chat_folders"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_ai_folders_user", "user_id"),
|
||||||
|
Index("ix_ai_folders_tenant", "tenant_id"),
|
||||||
|
Index("ix_ai_folders_parent", "parent_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
parent_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
PGUUID(as_uuid=True),
|
||||||
|
ForeignKey("ai_chat_folders.id", ondelete="CASCADE"),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Chat Attachments ---
|
||||||
|
|
||||||
|
class AIChatAttachment(Base, TenantMixin):
|
||||||
|
"""File attached to a chat message."""
|
||||||
|
|
||||||
|
__tablename__ = "ai_chat_attachments"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_ai_attachments_message", "message_id"),
|
||||||
|
Index("ix_ai_attachments_session", "session_id"),
|
||||||
|
Index("ix_ai_attachments_tenant", "tenant_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
message_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
PGUUID(as_uuid=True),
|
||||||
|
ForeignKey("ai_chat_messages.id", ondelete="CASCADE"),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
session_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
PGUUID(as_uuid=True),
|
||||||
|
ForeignKey("ai_chat_sessions.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
filename: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
mime_type: Mapped[str] = mapped_column(String(255), nullable=False, default="application/octet-stream")
|
||||||
|
size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
storage_path: Mapped[str] = mapped_column(String(1024), nullable=False)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class AIAssistantPlugin(BasePlugin):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
events=[],
|
events=[],
|
||||||
migrations=["0001_initial.sql"],
|
migrations=["0001_initial.sql", "0002_folders_attachments.sql"],
|
||||||
permissions=[
|
permissions=[
|
||||||
"ai:read",
|
"ai:read",
|
||||||
"ai:write",
|
"ai:write",
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ from app.core.db import get_db, set_tenant_context
|
|||||||
from app.deps import get_current_user, require_permission
|
from app.deps import get_current_user, require_permission
|
||||||
from app.plugins.builtins.ai_assistant.models import (
|
from app.plugins.builtins.ai_assistant.models import (
|
||||||
AIAgent,
|
AIAgent,
|
||||||
|
AIChatAttachment,
|
||||||
|
AIChatFolder,
|
||||||
AIChatSession,
|
AIChatSession,
|
||||||
AIModel,
|
AIModel,
|
||||||
AIPreset,
|
AIPreset,
|
||||||
@@ -29,12 +31,18 @@ from app.plugins.builtins.ai_assistant.schemas import (
|
|||||||
AIPresetUpdate,
|
AIPresetUpdate,
|
||||||
AIProviderCreate,
|
AIProviderCreate,
|
||||||
AIProviderUpdate,
|
AIProviderUpdate,
|
||||||
|
ChatAttachmentResponse,
|
||||||
|
ChatFolderCreate,
|
||||||
|
ChatFolderUpdate,
|
||||||
|
ChatFolderResponse,
|
||||||
ChatSendRequest,
|
ChatSendRequest,
|
||||||
ChatSessionCreate,
|
ChatSessionCreate,
|
||||||
ChatSessionUpdate,
|
ChatSessionUpdate,
|
||||||
)
|
)
|
||||||
from app.plugins.builtins.ai_assistant.services import (
|
from app.plugins.builtins.ai_assistant.services import (
|
||||||
agent_to_response,
|
agent_to_response,
|
||||||
|
attachment_to_response,
|
||||||
|
folder_to_response,
|
||||||
get_agent_by_id,
|
get_agent_by_id,
|
||||||
get_default_agent,
|
get_default_agent,
|
||||||
get_preset_by_id,
|
get_preset_by_id,
|
||||||
@@ -572,3 +580,187 @@ async def chat_stream(
|
|||||||
"X-Accel-Buffering": "no",
|
"X-Accel-Buffering": "no",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Chat Folders ───
|
||||||
|
|
||||||
|
@router.get("/folders", dependencies=[Depends(require_permission("ai:read"))])
|
||||||
|
async def list_folders(
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
result = await db.execute(
|
||||||
|
select(AIChatFolder)
|
||||||
|
.where(AIChatFolder.tenant_id == tenant_id)
|
||||||
|
.where(AIChatFolder.user_id == user_id)
|
||||||
|
.order_by(AIChatFolder.name.asc())
|
||||||
|
)
|
||||||
|
folders = list(result.scalars().all())
|
||||||
|
return [folder_to_response(f) for f in folders]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/folders", dependencies=[Depends(require_permission("ai:write"))])
|
||||||
|
async def create_folder(
|
||||||
|
data: ChatFolderCreate,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
await set_tenant_context(db, tenant_id)
|
||||||
|
|
||||||
|
folder = AIChatFolder(
|
||||||
|
name=data.name,
|
||||||
|
parent_id=uuid.UUID(data.parent_id) if data.parent_id else None,
|
||||||
|
user_id=user_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
)
|
||||||
|
db.add(folder)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(folder)
|
||||||
|
return folder_to_response(folder)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/folders/{folder_id}", dependencies=[Depends(require_permission("ai:write"))])
|
||||||
|
async def update_folder(
|
||||||
|
folder_id: str,
|
||||||
|
data: ChatFolderUpdate,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
result = await db.execute(
|
||||||
|
select(AIChatFolder)
|
||||||
|
.where(AIChatFolder.id == uuid.UUID(folder_id))
|
||||||
|
.where(AIChatFolder.tenant_id == tenant_id)
|
||||||
|
.where(AIChatFolder.user_id == user_id)
|
||||||
|
)
|
||||||
|
folder = result.scalar_one_or_none()
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(status_code=404, detail="Folder not found")
|
||||||
|
|
||||||
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
|
if "parent_id" in update_data and update_data["parent_id"]:
|
||||||
|
update_data["parent_id"] = uuid.UUID(update_data["parent_id"])
|
||||||
|
for field, val in update_data.items():
|
||||||
|
setattr(folder, field, val)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(folder)
|
||||||
|
return folder_to_response(folder)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/folders/{folder_id}", dependencies=[Depends(require_permission("ai:write"))])
|
||||||
|
async def delete_folder(
|
||||||
|
folder_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
result = await db.execute(
|
||||||
|
select(AIChatFolder)
|
||||||
|
.where(AIChatFolder.id == uuid.UUID(folder_id))
|
||||||
|
.where(AIChatFolder.tenant_id == tenant_id)
|
||||||
|
.where(AIChatFolder.user_id == user_id)
|
||||||
|
)
|
||||||
|
folder = result.scalar_one_or_none()
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(status_code=404, detail="Folder not found")
|
||||||
|
await db.delete(folder)
|
||||||
|
await db.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Attachments ───
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ATTACHMENT_DIR = Path(os.environ.get("STORAGE_PATH", "/data/storage")) / "ai_attachments"
|
||||||
|
MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024 # 25MB
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sessions/{session_id}/attachments", dependencies=[Depends(require_permission("ai:write"))])
|
||||||
|
async def upload_attachment(
|
||||||
|
session_id: str,
|
||||||
|
file: UploadFile,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
session = await get_session_by_id(db, uuid.UUID(session_id), user_id, tenant_id)
|
||||||
|
if not session:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
content = await file.read()
|
||||||
|
if len(content) > MAX_ATTACHMENT_SIZE:
|
||||||
|
raise HTTPException(status_code=413, detail="File too large (max 25MB)")
|
||||||
|
|
||||||
|
ATTACHMENT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
file_id = str(uuid.uuid4())
|
||||||
|
safe_filename = file.filename or "unnamed"
|
||||||
|
storage_path = str(ATTACHMENT_DIR / f"{file_id}_{safe_filename}")
|
||||||
|
with open(storage_path, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
attachment = AIChatAttachment(
|
||||||
|
session_id=session.id,
|
||||||
|
filename=safe_filename,
|
||||||
|
mime_type=file.content_type or "application/octet-stream",
|
||||||
|
size_bytes=len(content),
|
||||||
|
storage_path=storage_path,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
)
|
||||||
|
db.add(attachment)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(attachment)
|
||||||
|
return attachment_to_response(attachment)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessions/{session_id}/attachments", dependencies=[Depends(require_permission("ai:read"))])
|
||||||
|
async def list_attachments(
|
||||||
|
session_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
session = await get_session_by_id(db, uuid.UUID(session_id), user_id, tenant_id)
|
||||||
|
if not session:
|
||||||
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(AIChatAttachment)
|
||||||
|
.where(AIChatAttachment.session_id == session.id)
|
||||||
|
.where(AIChatAttachment.tenant_id == tenant_id)
|
||||||
|
.order_by(AIChatAttachment.created_at.asc())
|
||||||
|
)
|
||||||
|
attachments = list(result.scalars().all())
|
||||||
|
return [attachment_to_response(a) for a in attachments]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/attachments/{attachment_id}/download", dependencies=[Depends(require_permission("ai:read"))])
|
||||||
|
async def download_attachment(
|
||||||
|
attachment_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
result = await db.execute(
|
||||||
|
select(AIChatAttachment)
|
||||||
|
.where(AIChatAttachment.id == uuid.UUID(attachment_id))
|
||||||
|
.where(AIChatAttachment.tenant_id == tenant_id)
|
||||||
|
)
|
||||||
|
attachment = result.scalar_one_or_none()
|
||||||
|
if not attachment:
|
||||||
|
raise HTTPException(status_code=404, detail="Attachment not found")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
attachment.storage_path,
|
||||||
|
filename=attachment.filename,
|
||||||
|
media_type=attachment.mime_type,
|
||||||
|
)
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ class ChatSessionUpdate(BaseModel):
|
|||||||
title: str | None = Field(None, max_length=255)
|
title: str | None = Field(None, max_length=255)
|
||||||
is_pinned: bool | None = None
|
is_pinned: bool | None = None
|
||||||
agent_id: str | None = None
|
agent_id: str | None = None
|
||||||
|
folder_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ChatSessionResponse(BaseModel):
|
class ChatSessionResponse(BaseModel):
|
||||||
@@ -179,6 +180,7 @@ class ChatSessionResponse(BaseModel):
|
|||||||
title: str
|
title: str
|
||||||
is_pinned: bool
|
is_pinned: bool
|
||||||
is_sidebar: bool
|
is_sidebar: bool
|
||||||
|
folder_id: str | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
updated_at: datetime | None = None
|
updated_at: datetime | None = None
|
||||||
|
|
||||||
@@ -203,6 +205,39 @@ class ChatSendRequest(BaseModel):
|
|||||||
agent_id: str | None = None # override session agent
|
agent_id: str | None = None # override session agent
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Folders ───
|
||||||
|
|
||||||
|
class ChatFolderCreate(BaseModel):
|
||||||
|
name: str = Field(..., min_length=1, max_length=255)
|
||||||
|
parent_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ChatFolderUpdate(BaseModel):
|
||||||
|
name: str | None = Field(None, min_length=1, max_length=255)
|
||||||
|
parent_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ChatFolderResponse(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
parent_id: str | None = None
|
||||||
|
user_id: str
|
||||||
|
created_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Attachments ───
|
||||||
|
|
||||||
|
class ChatAttachmentResponse(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
id: str
|
||||||
|
message_id: str | None = None
|
||||||
|
session_id: str
|
||||||
|
filename: str
|
||||||
|
mime_type: str
|
||||||
|
size_bytes: int
|
||||||
|
|
||||||
|
|
||||||
# ─── Tools ───
|
# ─── Tools ───
|
||||||
|
|
||||||
class AIToolResponse(BaseModel):
|
class AIToolResponse(BaseModel):
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.core.permissions import check_permission
|
from app.core.permissions import check_permission
|
||||||
from app.plugins.builtins.ai_assistant.models import (
|
from app.plugins.builtins.ai_assistant.models import (
|
||||||
AIAgent,
|
AIAgent,
|
||||||
|
AIChatAttachment,
|
||||||
|
AIChatFolder,
|
||||||
AIChatMessage,
|
AIChatMessage,
|
||||||
AIChatSession,
|
AIChatSession,
|
||||||
AIModel,
|
AIModel,
|
||||||
@@ -113,6 +115,7 @@ def session_to_response(session: AIChatSession) -> dict[str, Any]:
|
|||||||
"title": session.title,
|
"title": session.title,
|
||||||
"is_pinned": session.is_pinned,
|
"is_pinned": session.is_pinned,
|
||||||
"is_sidebar": session.is_sidebar,
|
"is_sidebar": session.is_sidebar,
|
||||||
|
"folder_id": str(session.folder_id) if hasattr(session, 'folder_id') and session.folder_id else None,
|
||||||
"created_at": session.created_at.isoformat() if session.created_at else None,
|
"created_at": session.created_at.isoformat() if session.created_at else None,
|
||||||
"updated_at": session.updated_at.isoformat() if session.updated_at else None,
|
"updated_at": session.updated_at.isoformat() if session.updated_at else None,
|
||||||
}
|
}
|
||||||
@@ -132,6 +135,27 @@ def message_to_response(msg: AIChatMessage) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def folder_to_response(folder: AIChatFolder) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": str(folder.id),
|
||||||
|
"name": folder.name,
|
||||||
|
"parent_id": str(folder.parent_id) if folder.parent_id else None,
|
||||||
|
"user_id": str(folder.user_id),
|
||||||
|
"created_at": folder.created_at.isoformat() if folder.created_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def attachment_to_response(att: AIChatAttachment) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": str(att.id),
|
||||||
|
"message_id": str(att.message_id) if att.message_id else None,
|
||||||
|
"session_id": str(att.session_id),
|
||||||
|
"filename": att.filename,
|
||||||
|
"mime_type": att.mime_type,
|
||||||
|
"size_bytes": att.size_bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ─── Provider/Model/Preset/Agent CRUD ───
|
# ─── Provider/Model/Preset/Agent CRUD ───
|
||||||
|
|
||||||
async def get_default_provider(db: AsyncSession, tenant_id: uuid.UUID) -> AIProvider | None:
|
async def get_default_provider(db: AsyncSession, tenant_id: uuid.UUID) -> AIProvider | None:
|
||||||
|
|||||||
Reference in New Issue
Block a user