Add session export/import as JSON
Sessions can now be downloaded as a JSON file containing the session
metadata and every message with its tags, and re-imported as a new
session from that same format.
GET /chat/sessions/{id}/export streams the session as a file download.
POST /chat/sessions/import validates the roles, then recreates the
session under a fresh UUID so importing never collides with an existing
session. The sidebar gets a per-row download button and an import button
that feeds a hidden file input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+88
-2
@@ -2,12 +2,12 @@ import asyncio
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session as DBSession
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import Session, Message
|
||||
from app.db.models import Session, Message, MessageTag
|
||||
from app.core.memory_kb import memory as mem
|
||||
from app.core import llm
|
||||
from app.core.memory_kb import knowledge
|
||||
@@ -47,6 +47,25 @@ class MessageResponse(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ImportMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
created_at: datetime | None = None
|
||||
tags: list[str] = []
|
||||
|
||||
|
||||
class ImportSessionMeta(BaseModel):
|
||||
title: str | None = None
|
||||
model: str | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class ImportRequest(BaseModel):
|
||||
session: ImportSessionMeta = ImportSessionMeta()
|
||||
messages: list[ImportMessage]
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=list[SessionResponse])
|
||||
def list_sessions(db: DBSession = Depends(get_db)):
|
||||
return db.query(Session).order_by(Session.updated_at.desc()).all()
|
||||
@@ -69,6 +88,73 @@ def get_messages(session_id: str, db: DBSession = Depends(get_db)):
|
||||
]
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/export")
|
||||
def export_session(session_id: str, db: DBSession = Depends(get_db)):
|
||||
"""セッションのメタデータと全メッセージをJSONファイルとしてダウンロードする。"""
|
||||
session = db.get(Session, session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
data = {
|
||||
"session": {
|
||||
"id": session.id,
|
||||
"title": session.title,
|
||||
"model": session.model,
|
||||
"created_at": session.created_at.isoformat(),
|
||||
"updated_at": session.updated_at.isoformat(),
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"id": m.id,
|
||||
"role": m.role,
|
||||
"content": m.content,
|
||||
"created_at": m.created_at.isoformat(),
|
||||
"tags": [t.tag for t in m.tags],
|
||||
}
|
||||
for m in session.messages
|
||||
],
|
||||
}
|
||||
return JSONResponse(
|
||||
content=data,
|
||||
headers={"Content-Disposition": f'attachment; filename="session_{session.id}.json"'},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/sessions/import", response_model=SessionResponse, status_code=201)
|
||||
def import_session(req: ImportRequest, db: DBSession = Depends(get_db)):
|
||||
"""export で出力したJSON(またはその形式に沿ったJSON)から新規セッションを作成する。"""
|
||||
if not req.messages:
|
||||
raise HTTPException(status_code=400, detail="messages is empty")
|
||||
for m in req.messages:
|
||||
if m.role not in ("user", "assistant", "system"):
|
||||
raise HTTPException(status_code=400, detail=f"invalid role: {m.role}")
|
||||
|
||||
title = req.session.title or req.messages[0].content[:40]
|
||||
session = Session(
|
||||
id=str(uuid.uuid4()),
|
||||
title=title,
|
||||
model=req.session.model or DEFAULT_MODEL,
|
||||
)
|
||||
if req.session.created_at:
|
||||
session.created_at = req.session.created_at
|
||||
session.updated_at = req.session.updated_at or datetime.now(timezone.utc)
|
||||
db.add(session)
|
||||
db.flush()
|
||||
|
||||
for m in req.messages:
|
||||
msg = Message(session_id=session.id, role=m.role, content=m.content)
|
||||
if m.created_at:
|
||||
msg.created_at = m.created_at
|
||||
db.add(msg)
|
||||
db.flush()
|
||||
for tag in m.tags:
|
||||
db.add(MessageTag(message_id=msg.id, tag=tag))
|
||||
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
return session
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}", status_code=204)
|
||||
def delete_session(session_id: str, db: DBSession = Depends(get_db)):
|
||||
session = db.get(Session, session_id)
|
||||
|
||||
Reference in New Issue
Block a user