grapholab / backend /models /project.py
Fabio Antonini
feat: agent projects with persistent chats and reusable documents
f9c4034
"""
GraphoLab Backend — Project and Analysis ORM models.
A Project (= "Perizia") groups one or more Documents and their Analyses.
"""
from __future__ import annotations
import enum
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.database import Base
class ProjectStatus(str, enum.Enum):
draft = "draft"
in_progress = "in_progress"
completed = "completed"
archived = "archived"
class AnalysisType(str, enum.Enum):
htr = "htr"
signature_detection = "signature_detection"
signature_verification = "signature_verification"
ner = "ner"
writer_identification = "writer_identification"
graphology = "graphology"
pipeline = "pipeline"
dating = "dating"
rag = "rag"
class Project(Base):
__tablename__ = "projects"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(256), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[ProjectStatus] = mapped_column(
Enum(ProjectStatus), default=ProjectStatus.draft, nullable=False
)
owner_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
owner: Mapped["User"] = relationship("User", back_populates="projects")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
documents: Mapped[list[Document]] = relationship(
"Document", back_populates="project", cascade="all, delete-orphan"
)
analyses: Mapped[list[Analysis]] = relationship(
"Analysis", back_populates="project", cascade="all, delete-orphan"
)
agent_chats: Mapped[list[AgentChat]] = relationship(
"AgentChat", back_populates="project", cascade="all, delete-orphan"
)
class Document(Base):
"""A file uploaded to a project (stored in MinIO)."""
__tablename__ = "documents"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
filename: Mapped[str] = mapped_column(String(512), nullable=False)
content_type: Mapped[str] = mapped_column(String(128), nullable=False)
storage_key: Mapped[str] = mapped_column(String(512), nullable=False) # MinIO object key
size_bytes: Mapped[int] = mapped_column(Integer, default=0)
project_id: Mapped[int] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
project: Mapped[Project] = relationship("Project", back_populates="documents")
uploaded_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class Analysis(Base):
"""Result of one AI analysis step on a project document."""
__tablename__ = "analyses"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
analysis_type: Mapped[AnalysisType] = mapped_column(Enum(AnalysisType), nullable=False)
result_text: Mapped[str | None] = mapped_column(Text, nullable=True)
result_storage_key: Mapped[str | None] = mapped_column(
String(512), nullable=True # MinIO key for image/PDF result
)
document_id: Mapped[int | None] = mapped_column(
ForeignKey("documents.id", ondelete="SET NULL"), nullable=True
)
project_id: Mapped[int] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
project: Mapped[Project] = relationship("Project", back_populates="analyses")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class AgentChat(Base):
"""A persistent chat session inside an agent project."""
__tablename__ = "agent_chats"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(256), nullable=False, default="Nuova chat")
project_id: Mapped[int] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
project: Mapped[Project] = relationship("Project", back_populates="agent_chats")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
messages: Mapped[list[AgentMessage]] = relationship(
"AgentMessage", back_populates="chat", cascade="all, delete-orphan",
order_by="AgentMessage.created_at"
)
class AgentMessage(Base):
"""A single message in an AgentChat."""
__tablename__ = "agent_messages"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
chat_id: Mapped[int] = mapped_column(
ForeignKey("agent_chats.id", ondelete="CASCADE"), nullable=False, index=True
)
chat: Mapped[AgentChat] = relationship("AgentChat", back_populates="messages")
role: Mapped[str] = mapped_column(String(16), nullable=False) # "user" | "assistant"
content: Mapped[str] = mapped_column(Text, nullable=False)
# JSON-encoded list of Document.id attached by the user (user messages only)
file_ids: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)