222 lines
7.0 KiB
Python
222 lines
7.0 KiB
Python
"""Task agent — full CRUD for tasks and task comments."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
import re
|
|
from typing import Any
|
|
|
|
from langchain_core.tools import tool
|
|
|
|
from app.core.ws_context import execute_on_client
|
|
|
|
_UUID_RE = re.compile(
|
|
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
|
|
)
|
|
|
|
|
|
def _is_uuid(value: str) -> bool:
|
|
return bool(_UUID_RE.match(value))
|
|
|
|
|
|
# ── Task tools ────────────────────────────────────────────────────────
|
|
|
|
|
|
@tool
|
|
async def list_tasks(
|
|
project_id: str = "",
|
|
status: str = "",
|
|
search: str = "",
|
|
order_by: str = "",
|
|
) -> str:
|
|
"""List tasks, optionally filtered by project_id, status (todo|in_progress|done),
|
|
a search string, or an order_by field name (dueDate|priority|createdAt)."""
|
|
normalized_project_id = project_id if (project_id and _is_uuid(project_id)) else ""
|
|
result = await execute_on_client(
|
|
action="select",
|
|
table="tasks",
|
|
filters={
|
|
"projectId": normalized_project_id or None,
|
|
"status": status or None,
|
|
"search": search or None,
|
|
"orderBy": order_by or None,
|
|
},
|
|
)
|
|
rows = result.get("rows", [])
|
|
if not rows:
|
|
return "No tasks found matching the given filters."
|
|
lines = [
|
|
f"- {r['title']} (status: {r['status']}, priority: {r['priority']}, id: {r['id']})"
|
|
for r in rows
|
|
]
|
|
return f"Found {len(rows)} task(s):\n" + "\n".join(lines)
|
|
|
|
|
|
@tool
|
|
async def create_task(
|
|
title: str,
|
|
description: str = "",
|
|
status: str = "todo",
|
|
priority: str = "medium",
|
|
assignees: str = "[]",
|
|
due_date: int = 0,
|
|
project_id: str = "",
|
|
is_ai_suggested: int = 0,
|
|
) -> str:
|
|
"""Create a new task.
|
|
title: task title (required)
|
|
description: optional details
|
|
status: todo | in_progress | done (default: todo)
|
|
priority: high | medium | low (default: medium)
|
|
assignees: JSON-encoded array of assignee names, e.g. '["Alice"]'
|
|
due_date: Unix timestamp in milliseconds; 0 means no due date
|
|
project_id: optional UUID of the parent project
|
|
is_ai_suggested: 1 if proactively suggested, 0 if user-requested
|
|
"""
|
|
result = await execute_on_client(
|
|
action="insert",
|
|
table="tasks",
|
|
data={
|
|
"title": title,
|
|
"description": description or None,
|
|
"status": status,
|
|
"priority": priority,
|
|
"assignee": assignees,
|
|
"dueDate": due_date or None,
|
|
"projectId": project_id or None,
|
|
"isAiSuggested": is_ai_suggested,
|
|
},
|
|
)
|
|
row = result["row"]
|
|
return (
|
|
f"Task created: '{row['title']}' "
|
|
f"(id: {row['id']}, status: {row['status']}, priority: {row['priority']})"
|
|
)
|
|
|
|
|
|
@tool
|
|
async def update_task(
|
|
task_id: str,
|
|
title: str = "",
|
|
description: str = "",
|
|
status: str = "",
|
|
priority: str = "",
|
|
assignees: str = "",
|
|
due_date: int = -1,
|
|
project_id: str = "",
|
|
) -> str:
|
|
"""Update fields on an existing task. Only pass fields you want to change.
|
|
task_id: the task's UUID (required)
|
|
due_date: -1 means unchanged; 0 clears the due date; any positive value sets it
|
|
"""
|
|
updates: dict[str, Any] = {}
|
|
if title:
|
|
updates["title"] = title
|
|
if description:
|
|
updates["description"] = description
|
|
if status:
|
|
updates["status"] = status
|
|
if priority:
|
|
updates["priority"] = priority
|
|
if assignees:
|
|
updates["assignee"] = assignees
|
|
if due_date != -1:
|
|
updates["dueDate"] = due_date or None
|
|
if project_id:
|
|
updates["projectId"] = project_id
|
|
result = await execute_on_client(
|
|
action="update",
|
|
table="tasks",
|
|
data={"id": task_id, "updates": updates},
|
|
)
|
|
row = result["row"]
|
|
return f"Task updated: '{row['title']}' (id: {row['id']}, status: {row['status']})"
|
|
|
|
|
|
@tool
|
|
async def delete_task(task_id: str) -> str:
|
|
"""Delete a task permanently by its UUID."""
|
|
await execute_on_client(action="delete", table="tasks", data={"id": task_id})
|
|
return f"Task {task_id} deleted."
|
|
|
|
|
|
@tool
|
|
async def list_tasks_due_today() -> str:
|
|
"""List all tasks whose due date falls on today's date."""
|
|
now = datetime.now(tz=timezone.utc)
|
|
start_ms = int(datetime(now.year, now.month, now.day, tzinfo=timezone.utc).timestamp() * 1000)
|
|
end_ms = start_ms + 86_400_000 - 1 # last ms of today
|
|
result = await execute_on_client(
|
|
action="select",
|
|
table="tasks",
|
|
filters={"dueDateFrom": start_ms, "dueDateTo": end_ms},
|
|
)
|
|
rows = result.get("rows", [])
|
|
if not rows:
|
|
return "No tasks are due today."
|
|
lines = [
|
|
f"- {r['title']} (priority: {r['priority']}, status: {r['status']}, id: {r['id']})"
|
|
for r in rows
|
|
]
|
|
return f"Tasks due today ({len(rows)}):\n" + "\n".join(lines)
|
|
|
|
|
|
# ── Task comment tools ────────────────────────────────────────────────
|
|
|
|
|
|
@tool
|
|
async def list_task_comments(task_id: str) -> str:
|
|
"""List all comments on a task by its UUID."""
|
|
result = await execute_on_client(
|
|
action="select",
|
|
table="taskComments",
|
|
filters={"taskId": task_id},
|
|
)
|
|
rows = result.get("rows", [])
|
|
if not rows:
|
|
return f"No comments found for task {task_id}."
|
|
lines = [f"- [{r['author']}]: {r['content']} (id: {r['id']})" for r in rows]
|
|
return f"Found {len(rows)} comment(s):\n" + "\n".join(lines)
|
|
|
|
|
|
@tool
|
|
async def add_task_comment(task_id: str, author: str, content: str) -> str:
|
|
"""Add a comment to a task.
|
|
task_id: UUID of the task to comment on
|
|
author: name or ID of the comment author
|
|
content: comment text
|
|
"""
|
|
result = await execute_on_client(
|
|
action="insert",
|
|
table="taskComments",
|
|
data={"taskId": task_id, "author": author, "content": content},
|
|
)
|
|
row = result.get("row", {})
|
|
row_author = row.get("author", author)
|
|
# Electron payloads can vary (taskId vs task_id). Fall back to input task_id.
|
|
row_task_id = row.get("taskId") or row.get("task_id") or task_id
|
|
row_comment_id = row.get("id", "unknown")
|
|
return f"Comment added by {row_author} on task {row_task_id} (comment id: {row_comment_id})."
|
|
|
|
|
|
@tool
|
|
async def delete_task_comment(comment_id: str) -> str:
|
|
"""Delete a task comment by its UUID."""
|
|
await execute_on_client(action="delete", table="taskComments", data={"id": comment_id})
|
|
return f"Comment {comment_id} deleted."
|
|
|
|
|
|
# ── Agent ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
TASK_TOOLS: list[Any] = [
|
|
list_tasks,
|
|
create_task,
|
|
update_task,
|
|
delete_task,
|
|
list_tasks_due_today,
|
|
list_task_comments,
|
|
add_task_comment,
|
|
delete_task_comment,
|
|
]
|