feat: chat folders, attachments, treeview backend - migration, models, routes

This commit is contained in:
Agent Zero
2026-07-17 09:15:39 +02:00
parent 50650f5b17
commit 9a29206190
6 changed files with 343 additions and 3 deletions
+194 -2
View File
@@ -6,8 +6,8 @@ from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse, StreamingResponse
from sqlalchemy import select
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.plugins.builtins.ai_assistant.models import (
AIAgent,
AIChatAttachment,
AIChatFolder,
AIChatSession,
AIModel,
AIPreset,
@@ -29,12 +31,18 @@ from app.plugins.builtins.ai_assistant.schemas import (
AIPresetUpdate,
AIProviderCreate,
AIProviderUpdate,
ChatAttachmentResponse,
ChatFolderCreate,
ChatFolderUpdate,
ChatFolderResponse,
ChatSendRequest,
ChatSessionCreate,
ChatSessionUpdate,
)
from app.plugins.builtins.ai_assistant.services import (
agent_to_response,
attachment_to_response,
folder_to_response,
get_agent_by_id,
get_default_agent,
get_preset_by_id,
@@ -572,3 +580,187 @@ async def chat_stream(
"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,
)