chore: remove legacy Chat/Message/TaskAction, clean imports
Some checks failed
Deploy Tracker / deploy (push) Failing after 3s
Some checks failed
Deploy Tracker / deploy (push) Failing after 3s
This commit is contained in:
parent
8b3a9b2148
commit
c5243fad83
@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..models import Member
|
from ..models import Member
|
||||||
from ..models.chat import Attachment, Message # Legacy imports
|
from ..models.event import EventAttachment
|
||||||
from .schemas import UploadOut
|
from .schemas import UploadOut
|
||||||
|
|
||||||
router = APIRouter(tags=["attachments"])
|
router = APIRouter(tags=["attachments"])
|
||||||
@ -69,7 +69,7 @@ async def download_attachment(
|
|||||||
):
|
):
|
||||||
"""Download an attachment by ID."""
|
"""Download an attachment by ID."""
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Attachment).where(Attachment.id == uuid.UUID(attachment_id))
|
select(EventAttachment).where(EventAttachment.id == uuid.UUID(attachment_id))
|
||||||
)
|
)
|
||||||
att = result.scalar_one_or_none()
|
att = result.scalar_one_or_none()
|
||||||
if not att:
|
if not att:
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"""ORM → Pydantic converters. Single place for all model-to-schema transformations."""
|
"""ORM → Pydantic converters. Single place for all model-to-schema transformations."""
|
||||||
|
|
||||||
from ..models import Event, EventAttachment, Member, ProjectFile, Step, Task
|
from ..models import Event, EventAttachment, Member, ProjectFile, Step, Task
|
||||||
from ..models.chat import Message, Attachment # Legacy for backward compatibility
|
|
||||||
from ..enums import MemberType
|
from ..enums import MemberType
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
AgentConfigOut,
|
AgentConfigOut,
|
||||||
@ -9,7 +8,6 @@ from .schemas import (
|
|||||||
EventOut,
|
EventOut,
|
||||||
MemberBrief,
|
MemberBrief,
|
||||||
MemberOut,
|
MemberOut,
|
||||||
MessageOut,
|
|
||||||
ProjectFileOut,
|
ProjectFileOut,
|
||||||
StepOut,
|
StepOut,
|
||||||
SubtaskBrief,
|
SubtaskBrief,
|
||||||
@ -17,13 +15,6 @@ from .schemas import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_mentions(m: Message) -> list[MemberBrief]:
|
|
||||||
"""Convert stored mention slugs to MemberBrief objects.
|
|
||||||
Uses pre-loaded relationships if available, otherwise returns slug-only briefs."""
|
|
||||||
if not m.mentions:
|
|
||||||
return []
|
|
||||||
# For now: return slug-only briefs (id will be empty until we store mention IDs)
|
|
||||||
return [MemberBrief(id="", slug=slug, name=slug) for slug in m.mentions]
|
|
||||||
|
|
||||||
|
|
||||||
def member_brief(m: Member | None) -> MemberBrief | None:
|
def member_brief(m: Member | None) -> MemberBrief | None:
|
||||||
@ -58,7 +49,7 @@ def member_out(m: Member) -> MemberOut:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def attachment_out(a: Attachment | EventAttachment) -> AttachmentOut:
|
def attachment_out(a: EventAttachment) -> AttachmentOut:
|
||||||
return AttachmentOut(
|
return AttachmentOut(
|
||||||
id=str(a.id),
|
id=str(a.id),
|
||||||
filename=a.filename,
|
filename=a.filename,
|
||||||
@ -78,24 +69,7 @@ def step_out(s: Step) -> StepOut:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def message_out(m: Message) -> MessageOut:
|
|
||||||
return MessageOut(
|
|
||||||
id=str(m.id),
|
|
||||||
chat_id=str(m.chat_id) if m.chat_id else None,
|
|
||||||
task_id=str(m.task_id) if m.task_id else None,
|
|
||||||
parent_id=str(m.parent_id) if m.parent_id else None,
|
|
||||||
author_type=m.author_type,
|
|
||||||
author_id=str(m.author_id) if m.author_id else None,
|
|
||||||
author=member_brief(m.author) if m.author else None,
|
|
||||||
content=m.content,
|
|
||||||
thinking=m.thinking,
|
|
||||||
tool_log=m.tool_log,
|
|
||||||
mentions=_resolve_mentions(m),
|
|
||||||
actor=member_brief(m.actor) if hasattr(m, 'actor') and m.actor else None,
|
|
||||||
voice_url=m.voice_url,
|
|
||||||
attachments=[attachment_out(a) for a in (m.attachments or [])],
|
|
||||||
created_at=m.created_at.isoformat() if m.created_at else "",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def task_out(t: Task, project_slug: str = "") -> TaskOut:
|
def task_out(t: Task, project_slug: str = "") -> TaskOut:
|
||||||
|
|||||||
@ -1,173 +0,0 @@
|
|||||||
"""Messages API — unified messages for chats and task comments."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from ..database import get_db
|
|
||||||
from ..enums import AuthorType, ChatKind
|
|
||||||
from ..models.chat import Message, Chat, Attachment # Legacy imports
|
|
||||||
from .schemas import MessageOut
|
|
||||||
from .converters import message_out
|
|
||||||
|
|
||||||
router = APIRouter(tags=["messages"])
|
|
||||||
|
|
||||||
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/data/uploads")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Schemas ---
|
|
||||||
|
|
||||||
class AttachmentInput(BaseModel):
|
|
||||||
"""Uploaded file info from /upload endpoint."""
|
|
||||||
file_id: str # UUID from upload
|
|
||||||
filename: str
|
|
||||||
mime_type: str | None = None
|
|
||||||
size: int = 0
|
|
||||||
storage_name: str # filename on disk
|
|
||||||
|
|
||||||
|
|
||||||
class MessageCreate(BaseModel):
|
|
||||||
chat_id: str | None = None
|
|
||||||
task_id: str | None = None
|
|
||||||
parent_id: str | None = None
|
|
||||||
content: str
|
|
||||||
thinking: str | None = None
|
|
||||||
mentions: list[str] = []
|
|
||||||
voice_url: str | None = None
|
|
||||||
attachments: list[AttachmentInput] = []
|
|
||||||
|
|
||||||
|
|
||||||
# --- Endpoints ---
|
|
||||||
|
|
||||||
@router.get("/messages", response_model=list[MessageOut])
|
|
||||||
async def list_messages(
|
|
||||||
chat_id: Optional[str] = Query(None),
|
|
||||||
task_id: Optional[str] = Query(None),
|
|
||||||
parent_id: Optional[str] = Query(None),
|
|
||||||
limit: int = Query(50, le=200),
|
|
||||||
offset: int = Query(0),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
):
|
|
||||||
q = select(Message).options(selectinload(Message.attachments), selectinload(Message.author), selectinload(Message.actor))
|
|
||||||
|
|
||||||
if chat_id:
|
|
||||||
q = q.where(Message.chat_id == uuid.UUID(chat_id))
|
|
||||||
if task_id:
|
|
||||||
q = q.where(Message.task_id == uuid.UUID(task_id))
|
|
||||||
if parent_id:
|
|
||||||
q = q.where(Message.parent_id == uuid.UUID(parent_id))
|
|
||||||
|
|
||||||
# For top-level messages only (no threads), if no parent_id filter
|
|
||||||
if not parent_id and (chat_id or task_id):
|
|
||||||
q = q.where(Message.parent_id.is_(None))
|
|
||||||
|
|
||||||
# Get newest N messages (DESC), then reverse to chronological order
|
|
||||||
q = q.order_by(Message.created_at.desc()).offset(offset).limit(limit)
|
|
||||||
result = await db.execute(q)
|
|
||||||
messages = [message_out(m).model_dump() for m in result.scalars()]
|
|
||||||
messages.reverse()
|
|
||||||
return messages
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/messages", response_model=MessageOut)
|
|
||||||
async def create_message(req: MessageCreate, request: Request, db: AsyncSession = Depends(get_db)):
|
|
||||||
if not req.chat_id and not req.task_id:
|
|
||||||
raise HTTPException(400, "Either chat_id or task_id must be provided")
|
|
||||||
|
|
||||||
# Resolve author from auth — never trust client-provided author fields
|
|
||||||
member = getattr(request.state, "member", None)
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(401, "Not authenticated")
|
|
||||||
author_id = member.id
|
|
||||||
author_type = member.type
|
|
||||||
|
|
||||||
msg = Message(
|
|
||||||
chat_id=uuid.UUID(req.chat_id) if req.chat_id else None,
|
|
||||||
task_id=uuid.UUID(req.task_id) if req.task_id else None,
|
|
||||||
parent_id=uuid.UUID(req.parent_id) if req.parent_id else None,
|
|
||||||
author_type=author_type,
|
|
||||||
author_id=author_id,
|
|
||||||
content=req.content,
|
|
||||||
thinking=req.thinking,
|
|
||||||
mentions=req.mentions,
|
|
||||||
voice_url=req.voice_url,
|
|
||||||
)
|
|
||||||
db.add(msg)
|
|
||||||
await db.flush() # get msg.id
|
|
||||||
|
|
||||||
# Create attachment records
|
|
||||||
for att_in in req.attachments:
|
|
||||||
att = Attachment(
|
|
||||||
message_id=msg.id,
|
|
||||||
filename=att_in.filename,
|
|
||||||
mime_type=att_in.mime_type,
|
|
||||||
size=att_in.size,
|
|
||||||
storage_path=att_in.storage_name,
|
|
||||||
)
|
|
||||||
db.add(att)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
result2 = await db.execute(
|
|
||||||
select(Message).where(Message.id == msg.id).options(
|
|
||||||
selectinload(Message.attachments),
|
|
||||||
selectinload(Message.author)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
msg = result2.scalar_one()
|
|
||||||
|
|
||||||
author_id = str(msg.author_id) if msg.author_id else None
|
|
||||||
|
|
||||||
# Build response using shared converter
|
|
||||||
msg_data = message_out(msg).model_dump()
|
|
||||||
|
|
||||||
# Broadcast via WebSocket
|
|
||||||
from ..ws.manager import manager
|
|
||||||
from ..models.task import Task
|
|
||||||
|
|
||||||
project_id = None
|
|
||||||
if req.chat_id:
|
|
||||||
chat_result = await db.execute(select(Chat).where(Chat.id == uuid.UUID(req.chat_id)))
|
|
||||||
chat = chat_result.scalar_one_or_none()
|
|
||||||
if chat and chat.project_id:
|
|
||||||
project_id = str(chat.project_id)
|
|
||||||
elif chat and chat.kind == ChatKind.LOBBY:
|
|
||||||
await manager.broadcast_all(
|
|
||||||
{"type": "message.new", "data": msg_data},
|
|
||||||
exclude_member_id=author_id,
|
|
||||||
)
|
|
||||||
return message_out(msg)
|
|
||||||
|
|
||||||
# If no chat_id but task_id — resolve project from task
|
|
||||||
if not project_id and req.task_id:
|
|
||||||
task_result = await db.execute(select(Task).where(Task.id == uuid.UUID(req.task_id)))
|
|
||||||
task = task_result.scalar_one_or_none()
|
|
||||||
if task and task.project_id:
|
|
||||||
project_id = str(task.project_id)
|
|
||||||
|
|
||||||
if project_id:
|
|
||||||
await manager.broadcast_message(project_id, msg_data, author_id=author_id)
|
|
||||||
else:
|
|
||||||
await manager.broadcast_all(
|
|
||||||
{"type": "message.new", "data": msg_data},
|
|
||||||
exclude_member_id=author_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return message_out(msg)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/messages/{message_id}/replies", response_model=list[MessageOut])
|
|
||||||
async def list_replies(message_id: str, db: AsyncSession = Depends(get_db)):
|
|
||||||
"""Get thread replies for a message."""
|
|
||||||
result = await db.execute(
|
|
||||||
select(Message)
|
|
||||||
.where(Message.parent_id == uuid.UUID(message_id))
|
|
||||||
.options(selectinload(Message.attachments), selectinload(Message.author))
|
|
||||||
.order_by(Message.created_at)
|
|
||||||
)
|
|
||||||
return [message_out(m) for m in result.scalars()]
|
|
||||||
@ -9,9 +9,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..enums import ChatKind, MemberRole, ProjectStatus
|
from ..enums import MemberRole, ProjectStatus
|
||||||
from ..models import Project, Member, ProjectMember
|
from ..models import Project, Member, ProjectMember
|
||||||
from ..models.chat import Chat # Legacy import
|
|
||||||
from ..ws.manager import manager
|
from ..ws.manager import manager
|
||||||
from .schemas import ProjectOut
|
from .schemas import ProjectOut
|
||||||
|
|
||||||
@ -60,10 +59,6 @@ async def _get_project(project_id: str, db: AsyncSession) -> Project:
|
|||||||
|
|
||||||
|
|
||||||
async def _project_out(p: Project, db: AsyncSession) -> ProjectOut:
|
async def _project_out(p: Project, db: AsyncSession) -> ProjectOut:
|
||||||
chat_result = await db.execute(
|
|
||||||
select(Chat).where(Chat.project_id == p.id, Chat.kind == ChatKind.PROJECT)
|
|
||||||
)
|
|
||||||
chat = chat_result.scalar_one_or_none()
|
|
||||||
return ProjectOut(
|
return ProjectOut(
|
||||||
id=str(p.id),
|
id=str(p.id),
|
||||||
name=p.name,
|
name=p.name,
|
||||||
@ -72,7 +67,6 @@ async def _project_out(p: Project, db: AsyncSession) -> ProjectOut:
|
|||||||
repo_urls=p.repo_urls or [],
|
repo_urls=p.repo_urls or [],
|
||||||
status=p.status,
|
status=p.status,
|
||||||
task_counter=p.task_counter,
|
task_counter=p.task_counter,
|
||||||
chat_id=str(chat.id) if chat else None,
|
|
||||||
auto_assign=p.auto_assign,
|
auto_assign=p.auto_assign,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -103,11 +97,6 @@ async def create_project(req: ProjectCreate, db: AsyncSession = Depends(get_db))
|
|||||||
repo_urls=req.repo_urls,
|
repo_urls=req.repo_urls,
|
||||||
)
|
)
|
||||||
db.add(project)
|
db.add(project)
|
||||||
await db.flush()
|
|
||||||
|
|
||||||
chat = Chat(project_id=project.id, kind=ChatKind.PROJECT)
|
|
||||||
db.add(chat)
|
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
out = await _project_out(project, db)
|
out = await _project_out(project, db)
|
||||||
|
|||||||
@ -62,22 +62,6 @@ class SubtaskBrief(BaseModel):
|
|||||||
assignee: MemberBrief | None = None
|
assignee: MemberBrief | None = None
|
||||||
|
|
||||||
|
|
||||||
class MessageOut(BaseModel):
|
|
||||||
id: str
|
|
||||||
chat_id: str | None = None
|
|
||||||
task_id: str | None = None
|
|
||||||
parent_id: str | None = None
|
|
||||||
author_type: str
|
|
||||||
author_id: str | None = None
|
|
||||||
author: MemberBrief | None = None
|
|
||||||
content: str
|
|
||||||
thinking: str | None = None
|
|
||||||
tool_log: list[dict] | None = None
|
|
||||||
mentions: list[MemberBrief] = []
|
|
||||||
actor: MemberBrief | None = None
|
|
||||||
voice_url: str | None = None
|
|
||||||
attachments: list[AttachmentOut] = []
|
|
||||||
created_at: str
|
|
||||||
|
|
||||||
|
|
||||||
class EventOut(BaseModel):
|
class EventOut(BaseModel):
|
||||||
@ -142,7 +126,6 @@ class ProjectOut(BaseModel):
|
|||||||
repo_urls: list[str] = []
|
repo_urls: list[str] = []
|
||||||
status: str
|
status: str
|
||||||
task_counter: int
|
task_counter: int
|
||||||
chat_id: str | None = None
|
|
||||||
auto_assign: bool = False
|
auto_assign: bool = False
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@ -12,13 +12,12 @@ from sqlalchemy.orm import selectinload, joinedload
|
|||||||
|
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..enums import (
|
from ..enums import (
|
||||||
AuthorType, ChatKind, MemberType, TaskLinkType, TaskPriority, TaskStatus, TaskType, WSEventType,
|
AuthorType, MemberType, TaskLinkType, TaskPriority, TaskStatus, TaskType, WSEventType,
|
||||||
)
|
)
|
||||||
from ..models import Task, Step, Project, Member, TaskLink, Event
|
from ..models import Task, Step, Project, Member, TaskLink, Event
|
||||||
from ..models.chat import Message, Chat # Legacy imports
|
|
||||||
from .auth import get_current_member
|
from .auth import get_current_member
|
||||||
from .schemas import TaskOut, MessageOut, MemberBrief
|
from .schemas import TaskOut, MemberBrief
|
||||||
from .converters import task_out, message_out, member_brief
|
from .converters import task_out, member_brief
|
||||||
|
|
||||||
router = APIRouter(tags=["tasks"])
|
router = APIRouter(tags=["tasks"])
|
||||||
|
|
||||||
|
|||||||
@ -169,15 +169,14 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Routers
|
# Routers
|
||||||
from .api import auth, members, projects, tasks, messages, events, steps, attachments, project_files, labels # noqa: E402
|
from .api import auth, members, projects, tasks, events, steps, attachments, project_files, labels # noqa: E402
|
||||||
from .ws.handler import router as ws_router # noqa: E402
|
from .ws.handler import router as ws_router # noqa: E402
|
||||||
|
|
||||||
app.include_router(auth.router, prefix="/api/v1")
|
app.include_router(auth.router, prefix="/api/v1")
|
||||||
app.include_router(members.router, prefix="/api/v1")
|
app.include_router(members.router, prefix="/api/v1")
|
||||||
app.include_router(projects.router, prefix="/api/v1")
|
app.include_router(projects.router, prefix="/api/v1")
|
||||||
app.include_router(tasks.router, prefix="/api/v1")
|
app.include_router(tasks.router, prefix="/api/v1")
|
||||||
app.include_router(messages.router, prefix="/api/v1") # Backward compatibility
|
app.include_router(events.router, prefix="/api/v1") # Events API (replaces messages)
|
||||||
app.include_router(events.router, prefix="/api/v1") # New events API
|
|
||||||
app.include_router(steps.router, prefix="/api/v1")
|
app.include_router(steps.router, prefix="/api/v1")
|
||||||
app.include_router(attachments.router, prefix="/api/v1")
|
app.include_router(attachments.router, prefix="/api/v1")
|
||||||
app.include_router(project_files.router, prefix="/api/v1")
|
app.include_router(project_files.router, prefix="/api/v1")
|
||||||
|
|||||||
@ -32,10 +32,6 @@ class AuthMethod(StrEnum):
|
|||||||
TOKEN = "token"
|
TOKEN = "token"
|
||||||
|
|
||||||
|
|
||||||
class ChatKind(StrEnum):
|
|
||||||
LOBBY = "lobby"
|
|
||||||
PROJECT = "project"
|
|
||||||
|
|
||||||
|
|
||||||
class ListenMode(StrEnum):
|
class ListenMode(StrEnum):
|
||||||
ALL = "all"
|
ALL = "all"
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
"""Chat and Message models — unified message for chats and task comments."""
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, Integer, JSON, String, Text
|
|
||||||
from sqlalchemy.dialects.postgresql import ARRAY, UUID
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from ..enums import ChatKind
|
|
||||||
from .base import Base
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .member import Member
|
|
||||||
from .project import Project
|
|
||||||
|
|
||||||
|
|
||||||
class Chat(Base):
|
|
||||||
"""Chat room — lobby, project, or custom."""
|
|
||||||
__tablename__ = "chats"
|
|
||||||
|
|
||||||
project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"))
|
|
||||||
kind: Mapped[str] = mapped_column(String(20), default=ChatKind.PROJECT) # lobby | project
|
|
||||||
|
|
||||||
project: Mapped["Project | None"] = relationship(back_populates="chats")
|
|
||||||
messages: Mapped[list["Message"]] = relationship(
|
|
||||||
back_populates="chat", cascade="all, delete-orphan",
|
|
||||||
foreign_keys="Message.chat_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Message(Base):
|
|
||||||
"""Unified message — works for chat messages AND task comments."""
|
|
||||||
__tablename__ = "messages"
|
|
||||||
|
|
||||||
# Context: one of chat_id or task_id must be set
|
|
||||||
chat_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("chats.id"))
|
|
||||||
task_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="CASCADE"))
|
|
||||||
|
|
||||||
# Thread support
|
|
||||||
parent_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("messages.id"))
|
|
||||||
|
|
||||||
# Author
|
|
||||||
author_type: Mapped[str] = mapped_column(String(20), nullable=False) # human | agent | system
|
|
||||||
author_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("members.id"), nullable=True)
|
|
||||||
|
|
||||||
# Actor — who initiated the action (for system messages: who created/moved/assigned the task)
|
|
||||||
actor_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("members.id"), nullable=True)
|
|
||||||
|
|
||||||
# Tool log — JSON array of tool calls [{name, args, result, error?}]
|
|
||||||
tool_log: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
||||||
|
|
||||||
# Content
|
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
thinking: Mapped[str | None] = mapped_column(Text, nullable=True) # LLM reasoning/thinking block
|
|
||||||
mentions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
|
|
||||||
voice_url: Mapped[str | None] = mapped_column(String(500))
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
chat: Mapped["Chat | None"] = relationship(back_populates="messages", foreign_keys=[chat_id])
|
|
||||||
author: Mapped["Member"] = relationship(foreign_keys=[author_id])
|
|
||||||
actor: Mapped["Member | None"] = relationship(foreign_keys=[actor_id])
|
|
||||||
attachments: Mapped[list["Attachment"]] = relationship(back_populates="message", cascade="all, delete-orphan")
|
|
||||||
replies: Mapped[list["Message"]] = relationship(back_populates="parent")
|
|
||||||
parent: Mapped["Message | None"] = relationship(back_populates="replies", remote_side="Message.id")
|
|
||||||
|
|
||||||
|
|
||||||
class Attachment(Base):
|
|
||||||
"""File attachment on a message."""
|
|
||||||
__tablename__ = "attachments"
|
|
||||||
|
|
||||||
message_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("messages.id"), nullable=False)
|
|
||||||
filename: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
||||||
mime_type: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
size: Mapped[int] = mapped_column(Integer, default=0) # bytes
|
|
||||||
storage_path: Mapped[str] = mapped_column(String(1000), nullable=False)
|
|
||||||
|
|
||||||
message: Mapped["Message"] = relationship(back_populates="attachments")
|
|
||||||
@ -12,7 +12,6 @@ from .base import Base
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .task import Task
|
from .task import Task
|
||||||
from .chat import Chat
|
|
||||||
from .member import Member
|
from .member import Member
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +27,6 @@ class Project(Base):
|
|||||||
auto_assign: Mapped[bool] = mapped_column(Boolean, default=False) # auto-assign tasks by labels ↔ capabilities
|
auto_assign: Mapped[bool] = mapped_column(Boolean, default=False) # auto-assign tasks by labels ↔ capabilities
|
||||||
|
|
||||||
tasks: Mapped[list["Task"]] = relationship(back_populates="project", cascade="all, delete-orphan")
|
tasks: Mapped[list["Task"]] = relationship(back_populates="project", cascade="all, delete-orphan")
|
||||||
chats: Mapped[list["Chat"]] = relationship(back_populates="project", cascade="all, delete-orphan")
|
|
||||||
members: Mapped[list["ProjectMember"]] = relationship(back_populates="project", cascade="all, delete-orphan")
|
members: Mapped[list["ProjectMember"]] = relationship(back_populates="project", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,11 @@ from sqlalchemy.orm import selectinload
|
|||||||
|
|
||||||
from ..database import async_session
|
from ..database import async_session
|
||||||
from ..enums import (
|
from ..enums import (
|
||||||
AuthMethod, ChatKind, ListenMode, MemberRole, MemberStatus, MemberType,
|
AuthMethod, ListenMode, MemberRole, MemberStatus, MemberType,
|
||||||
ProjectStatus, WSEventType,
|
ProjectStatus, WSEventType,
|
||||||
)
|
)
|
||||||
from ..models import Member, AgentConfig, Project, ProjectMember, Task, Event
|
from ..models import Member, AgentConfig, Project, ProjectMember, Task, Event
|
||||||
from ..models.chat import Chat, Message # Legacy imports
|
from ..api.schemas import MemberBrief
|
||||||
from ..api.schemas import MessageOut, MemberBrief
|
|
||||||
from .manager import ConnectedClient, manager
|
from .manager import ConnectedClient, manager
|
||||||
|
|
||||||
logger = logging.getLogger("tracker.ws")
|
logger = logging.getLogger("tracker.ws")
|
||||||
@ -31,26 +30,7 @@ def _to_member_brief(member: Member) -> MemberBrief:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _to_message_out(msg: Message, author: Member | None = None) -> MessageOut:
|
|
||||||
# mentions stored as member IDs in DB
|
|
||||||
mention_briefs = [MemberBrief(id=mid, slug="", name="") for mid in (msg.mentions or [])]
|
|
||||||
return MessageOut(
|
|
||||||
id=str(msg.id),
|
|
||||||
chat_id=str(msg.chat_id) if msg.chat_id else None,
|
|
||||||
task_id=str(msg.task_id) if msg.task_id else None,
|
|
||||||
parent_id=str(msg.parent_id) if msg.parent_id else None,
|
|
||||||
author_type=msg.author_type,
|
|
||||||
author_id=str(msg.author_id) if msg.author_id else None,
|
|
||||||
author=_to_member_brief(author) if author else None,
|
|
||||||
content=msg.content,
|
|
||||||
thinking=msg.thinking,
|
|
||||||
tool_log=msg.tool_log,
|
|
||||||
mentions=mention_briefs,
|
|
||||||
actor=_to_member_brief(msg.actor) if hasattr(msg, 'actor') and msg.actor else None,
|
|
||||||
voice_url=msg.voice_url,
|
|
||||||
attachments=[],
|
|
||||||
created_at=msg.created_at.isoformat() if msg.created_at else "",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/ws")
|
@router.websocket("/ws")
|
||||||
@ -334,14 +314,8 @@ async def _handle_chat_send(session_id: str, data: dict):
|
|||||||
if project_id:
|
if project_id:
|
||||||
resolved_project_id = uuid.UUID(project_id)
|
resolved_project_id = uuid.UUID(project_id)
|
||||||
elif chat_id:
|
elif chat_id:
|
||||||
# Legacy: resolve project_id from chat_id
|
# Legacy chat_id support removed - use project_id instead
|
||||||
chat_result = await db.execute(select(Chat).where(Chat.id == uuid.UUID(chat_id)))
|
await client.ws.send_json({"type": WSEventType.ERROR, "message": "chat_id deprecated, use project_id"})
|
||||||
chat = chat_result.scalar_one_or_none()
|
|
||||||
if chat and chat.project_id:
|
|
||||||
resolved_project_id = chat.project_id
|
|
||||||
elif chat and chat.kind == ChatKind.LOBBY:
|
|
||||||
# Skip lobby for now - Events need project_id
|
|
||||||
await client.ws.send_json({"type": WSEventType.ERROR, "message": "Lobby chat not supported yet"})
|
|
||||||
return
|
return
|
||||||
elif task_id:
|
elif task_id:
|
||||||
# Task comment
|
# Task comment
|
||||||
@ -420,14 +394,8 @@ async def _handle_agent_stream(session_id: str, event_type: str, data: dict):
|
|||||||
chat_id = data.get("chat_id")
|
chat_id = data.get("chat_id")
|
||||||
task_id = data.get("task_id")
|
task_id = data.get("task_id")
|
||||||
|
|
||||||
if not project_id and (chat_id or task_id):
|
if not project_id and task_id:
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
if chat_id:
|
|
||||||
chat_result = await db.execute(select(Chat).where(Chat.id == uuid.UUID(chat_id)))
|
|
||||||
chat = chat_result.scalar_one_or_none()
|
|
||||||
if chat and chat.project_id:
|
|
||||||
project_id = str(chat.project_id)
|
|
||||||
elif task_id:
|
|
||||||
from ..models import Task
|
from ..models import Task
|
||||||
task_result = await db.execute(select(Task).where(Task.id == uuid.UUID(task_id)))
|
task_result = await db.execute(select(Task).where(Task.id == uuid.UUID(task_id)))
|
||||||
task = task_result.scalar_one_or_none()
|
task = task_result.scalar_one_or_none()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user