feat(agents): Update task comment requirements and add in_progress_at tracking

This commit is contained in:
Abhimanyu Saharan
2026-02-04 19:08:14 +05:30
parent 554ecc4c85
commit 3a2bc5135e
7 changed files with 142 additions and 15 deletions

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import asc
from sqlalchemy import asc, desc
from sqlmodel import Session, col, select
from app.api.deps import (
@@ -30,6 +31,42 @@ from app.services.activity_log import record_activity
router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"])
REQUIRED_COMMENT_FIELDS = ("summary:", "details:", "next:")
def is_valid_markdown_comment(message: str) -> bool:
content = message.strip()
if not content:
return False
lowered = content.lower()
if not all(field in lowered for field in REQUIRED_COMMENT_FIELDS):
return False
if "- " not in content and "* " not in content:
return False
return True
def has_valid_recent_comment(
session: Session,
task: Task,
agent_id: UUID | None,
since: datetime | None,
) -> bool:
if agent_id is None or since is None:
return False
statement = (
select(ActivityEvent)
.where(col(ActivityEvent.task_id) == task.id)
.where(col(ActivityEvent.event_type) == "task.comment")
.where(col(ActivityEvent.agent_id) == agent_id)
.where(col(ActivityEvent.created_at) >= since)
.order_by(desc(col(ActivityEvent.created_at)))
)
event = session.exec(statement).first()
if event is None or event.message is None:
return False
return is_valid_markdown_comment(event.message)
@router.get("", response_model=list[TaskRead])
def list_tasks(
@@ -74,20 +111,28 @@ def update_task(
) -> Task:
previous_status = task.status
updates = payload.model_dump(exclude_unset=True)
comment = updates.pop("comment", None)
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"}
allowed_fields = {"status", "comment"}
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
task.in_progress_at = 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 updates["status"] == "in_progress":
task.in_progress_at = datetime.utcnow()
elif "status" in updates:
if updates["status"] == "inbox":
task.assigned_agent_id = None
task.in_progress_at = None
elif updates["status"] == "in_progress":
task.in_progress_at = datetime.utcnow()
if "assigned_agent_id" in updates and updates["assigned_agent_id"]:
agent = session.get(Agent, updates["assigned_agent_id"])
if agent is None:
@@ -98,10 +143,35 @@ def update_task(
setattr(task, key, value)
task.updated_at = datetime.utcnow()
if "status" in updates and updates["status"] == "review":
if comment is not None and comment.strip():
if not is_valid_markdown_comment(comment):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
else:
if not has_valid_recent_comment(
session,
task,
task.assigned_agent_id,
task.in_progress_at,
):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
session.add(task)
session.commit()
session.refresh(task)
if comment is not None and comment.strip():
if actor.actor_type == "agent" and not is_valid_markdown_comment(comment):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
event = ActivityEvent(
event_type="task.comment",
message=comment.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()
if "status" in updates and task.status != previous_status:
event_type = "task.status_changed"
message = f"Task moved to {task.status}: {task.title}."
@@ -160,6 +230,8 @@ def create_task_comment(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
if not payload.message.strip():
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
if actor.actor_type == "agent" and not is_valid_markdown_comment(payload.message):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
event = ActivityEvent(
event_type="task.comment",
message=payload.message.strip(),

View File

@@ -19,6 +19,7 @@ class Task(TenantScoped, table=True):
status: str = Field(default="inbox", index=True)
priority: str = Field(default="medium", index=True)
due_at: datetime | None = None
in_progress_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)

View File

@@ -26,12 +26,14 @@ class TaskUpdate(SQLModel):
priority: str | None = None
due_at: datetime | None = None
assigned_agent_id: UUID | None = None
comment: str | None = None
class TaskRead(TaskBase):
id: UUID
board_id: UUID | None
created_by_user_id: UUID | None
in_progress_at: datetime | None
created_at: datetime
updated_at: datetime