Files
ai_agent/backend/app/db/models/conversation.py
T
furyhawk 8351e73d39 feat: add Zustand stores for conversation, file preview, sidebar, theme, and knowledge base selection
- Implemented `conversation-store` for managing conversations and messages.
- Created `file-preview-store` to handle file preview state.
- Added `sidebar-store` for sidebar visibility management.
- Developed `theme-store` for theme persistence and management.
- Introduced `kb-selection-store` for managing active knowledge base selections with persistence.

chore: define API and chat types

- Added types for API responses, authentication, chat messages, conversations, and projects.
- Defined interfaces for various entities including users, sessions, and message ratings.

build: configure TypeScript and testing setup

- Set up `tsconfig.json` for TypeScript configuration.
- Created `vitest.config.ts` for testing configuration with Vitest.
- Added `vitest.setup.ts` for global test setup including mocks for Next.js router and media queries.
- Configured Vercel deployment settings in `vercel.json`.
2026-06-11 16:54:43 +08:00

139 lines
5.2 KiB
Python

"""Conversation and message models for AI chat persistence."""
import uuid
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base, TimestampMixin
if TYPE_CHECKING:
from app.db.models.chat_file import ChatFile
class Conversation(Base, TimestampMixin):
"""Conversation model - groups messages in a chat session.
Attributes:
id: Unique conversation identifier
user_id: Optional user who owns this conversation (if auth enabled)
project_id: Optional project this conversation belongs to (if pydantic_deep)
title: Auto-generated or user-defined title
is_archived: Whether the conversation is archived
messages: List of messages in this conversation
"""
__tablename__ = "conversations"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=True,
index=True,
)
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
is_archived: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# Relationships
messages: Mapped[list["Message"]] = relationship(
"Message",
back_populates="conversation",
cascade="all, delete-orphan",
order_by="Message.created_at",
)
def __repr__(self) -> str:
return f"<Conversation(id={self.id}, title={self.title})>"
class Message(Base, TimestampMixin):
"""Message model - individual message in a conversation.
Attributes:
id: Unique message identifier
conversation_id: The conversation this message belongs to
role: Message role (user, assistant, system)
content: Message text content
model_name: AI model used (for assistant messages)
tokens_used: Token count for this message
tool_calls: List of tool calls made in this message
"""
__tablename__ = "messages"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
conversation_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("conversations.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
role: Mapped[str] = mapped_column(String(20), nullable=False) # user, assistant, system
content: Mapped[str] = mapped_column(Text, nullable=False)
model_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
tokens_used: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Relationships
conversation: Mapped["Conversation"] = relationship("Conversation", back_populates="messages")
tool_calls: Mapped[list["ToolCall"]] = relationship(
"ToolCall",
back_populates="message",
cascade="all, delete-orphan",
order_by="ToolCall.started_at",
)
files: Mapped[list["ChatFile"]] = relationship(
"ChatFile",
foreign_keys="ChatFile.message_id",
cascade="all, delete-orphan",
)
def __repr__(self) -> str:
return f"<Message(id={self.id}, role={self.role})>"
class ToolCall(Base):
"""ToolCall model - record of a tool invocation.
Attributes:
id: Unique tool call identifier
message_id: The assistant message that triggered this call
tool_call_id: External ID from PydanticAI
tool_name: Name of the tool that was called
args: JSON arguments passed to the tool
result: Result returned by the tool
status: Current status (pending, running, completed, failed)
started_at: When the tool call started
completed_at: When the tool call completed
duration_ms: Execution time in milliseconds
"""
__tablename__ = "tool_calls"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
message_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("messages.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
tool_call_id: Mapped[str] = mapped_column(String(100), nullable=False)
tool_name: Mapped[str] = mapped_column(String(100), nullable=False)
args: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False, default=dict)
result: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="pending"
) # pending, running, completed, failed
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Relationships
message: Mapped["Message"] = relationship("Message", back_populates="tool_calls")
def __repr__(self) -> str:
return f"<ToolCall(id={self.id}, tool_name={self.tool_name}, status={self.status})>"