feat(agents): Add task assignment and comments functionality
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"""add task assigned agent
|
||||
|
||||
Revision ID: 8045fbfb157f
|
||||
Revises: 6df47d330227
|
||||
Create Date: 2026-02-04 17:28:57.465934
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8045fbfb157f'
|
||||
down_revision = '6df47d330227'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS assigned_agent_id UUID"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE tasks ADD CONSTRAINT IF NOT EXISTS tasks_assigned_agent_id_fkey "
|
||||
"FOREIGN KEY (assigned_agent_id) REFERENCES agents(id)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_assigned_agent_id_fkey"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE tasks DROP COLUMN IF EXISTS assigned_agent_id"
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""add task comments index
|
||||
|
||||
Revision ID: b9d22e2a4d50
|
||||
Revises: 8045fbfb157f
|
||||
Create Date: 2026-02-04 17:32:06.204331
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b9d22e2a4d50'
|
||||
down_revision = '8045fbfb157f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_activity_events_task_comment "
|
||||
"ON activity_events (task_id, created_at) "
|
||||
"WHERE event_type = 'task.comment'"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_activity_events_task_comment")
|
||||
@@ -3,7 +3,8 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from sqlalchemy import asc
|
||||
from sqlmodel import Session, col, select
|
||||
|
||||
from app.api.deps import (
|
||||
ActorContext,
|
||||
@@ -14,9 +15,17 @@ from app.api.deps import (
|
||||
)
|
||||
from app.core.auth import AuthContext
|
||||
from app.db.session import get_session
|
||||
from app.models.agents import Agent
|
||||
from app.models.activity_events import ActivityEvent
|
||||
from app.models.boards import Board
|
||||
from app.models.tasks import Task
|
||||
from app.schemas.tasks import TaskCreate, TaskRead, TaskUpdate
|
||||
from app.schemas.tasks import (
|
||||
TaskCommentCreate,
|
||||
TaskCommentRead,
|
||||
TaskCreate,
|
||||
TaskRead,
|
||||
TaskUpdate,
|
||||
)
|
||||
from app.services.activity_log import record_activity
|
||||
|
||||
router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"])
|
||||
@@ -66,9 +75,25 @@ def update_task(
|
||||
previous_status = task.status
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
if actor.actor_type == "agent":
|
||||
if actor.agent and actor.agent.board_id and task.board_id:
|
||||
if actor.agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
allowed_fields = {"status"}
|
||||
if not set(updates).issubset(allowed_fields):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
if "status" in updates:
|
||||
if updates["status"] == "inbox":
|
||||
task.assigned_agent_id = None
|
||||
else:
|
||||
task.assigned_agent_id = actor.agent.id if actor.agent else None
|
||||
elif "status" in updates and updates["status"] == "inbox":
|
||||
task.assigned_agent_id = None
|
||||
if "assigned_agent_id" in updates and updates["assigned_agent_id"]:
|
||||
agent = session.get(Agent, updates["assigned_agent_id"])
|
||||
if agent is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
if agent.board_id and task.board_id and agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||
for key, value in updates.items():
|
||||
setattr(task, key, value)
|
||||
task.updated_at = datetime.utcnow()
|
||||
@@ -103,3 +128,45 @@ def delete_task(
|
||||
session.delete(task)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/{task_id}/comments", response_model=list[TaskCommentRead])
|
||||
def list_task_comments(
|
||||
task: Task = Depends(get_task_or_404),
|
||||
session: Session = Depends(get_session),
|
||||
actor: ActorContext = Depends(require_admin_or_agent),
|
||||
) -> list[ActivityEvent]:
|
||||
if actor.actor_type == "agent" and actor.agent:
|
||||
if actor.agent.board_id and task.board_id and actor.agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
statement = (
|
||||
select(ActivityEvent)
|
||||
.where(col(ActivityEvent.task_id) == task.id)
|
||||
.where(col(ActivityEvent.event_type) == "task.comment")
|
||||
.order_by(asc(col(ActivityEvent.created_at)))
|
||||
)
|
||||
return list(session.exec(statement))
|
||||
|
||||
|
||||
@router.post("/{task_id}/comments", response_model=TaskCommentRead)
|
||||
def create_task_comment(
|
||||
payload: TaskCommentCreate,
|
||||
task: Task = Depends(get_task_or_404),
|
||||
session: Session = Depends(get_session),
|
||||
actor: ActorContext = Depends(require_admin_or_agent),
|
||||
) -> ActivityEvent:
|
||||
if actor.actor_type == "agent" and actor.agent:
|
||||
if actor.agent.board_id and task.board_id and actor.agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
if not payload.message.strip():
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
event = ActivityEvent(
|
||||
event_type="task.comment",
|
||||
message=payload.message.strip(),
|
||||
task_id=task.id,
|
||||
agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None,
|
||||
)
|
||||
session.add(event)
|
||||
session.commit()
|
||||
session.refresh(event)
|
||||
return event
|
||||
|
||||
@@ -21,6 +21,7 @@ class Task(TenantScoped, table=True):
|
||||
due_at: datetime | None = None
|
||||
|
||||
created_by_user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
assigned_agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True)
|
||||
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
@@ -12,6 +12,7 @@ class TaskBase(SQLModel):
|
||||
status: str = "inbox"
|
||||
priority: str = "medium"
|
||||
due_at: datetime | None = None
|
||||
assigned_agent_id: UUID | None = None
|
||||
|
||||
|
||||
class TaskCreate(TaskBase):
|
||||
@@ -24,6 +25,7 @@ class TaskUpdate(SQLModel):
|
||||
status: str | None = None
|
||||
priority: str | None = None
|
||||
due_at: datetime | None = None
|
||||
assigned_agent_id: UUID | None = None
|
||||
|
||||
|
||||
class TaskRead(TaskBase):
|
||||
@@ -32,3 +34,15 @@ class TaskRead(TaskBase):
|
||||
created_by_user_id: UUID | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class TaskCommentCreate(SQLModel):
|
||||
message: str
|
||||
|
||||
|
||||
class TaskCommentRead(SQLModel):
|
||||
id: UUID
|
||||
message: str | None
|
||||
agent_id: UUID | None
|
||||
task_id: UUID | None
|
||||
created_at: datetime
|
||||
|
||||
Reference in New Issue
Block a user