"""Task agent — full CRUD for tasks and task comments.""" from __future__ import annotations import json from typing import Any from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.tools import tool from langchain_openai import ChatOpenAI from app.config.settings import settings from app.core.agent_registry import ChatAgent, registry _SYSTEM_PROMPT = ( "You are a task management assistant for a project workspace.\n" "You create, update, list, and track tasks and their comments.\n\n" "Rules:\n" " - status must be one of: todo, in_progress, done\n" " - priority must be one of: high, medium, low\n" " - due_date is a Unix timestamp in milliseconds; convert human dates\n" " - assignees is a JSON-encoded array of strings (e.g. '[\"Alice\",\"Bob\"]')\n" " - project_id is optional; link to a project when the user mentions one\n" " - is_ai_suggested: 1 only when proactively proposing a task the user\n" " did not explicitly request; 0 otherwise\n" " - is_approved defaults to 0; set to 1 only when the user confirms\n" " - Use list_tasks_due_today for 'what's due today' queries\n" " - For update_task, use -1 for integer fields you do not want to change\n" " - Always confirm the action in plain, user-friendly language." ) # ── 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).""" return json.dumps({ "action": "list", "table": "tasks", "filters": { "projectId": project_id or None, "status": status or None, "search": search or None, "orderBy": order_by or None, }, }) @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, is_approved: 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 is_approved: 0 until the user confirms; 1 when confirmed """ return json.dumps({ "action": "create_record", "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, "isApproved": is_approved, }, }) @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 = "", is_approved: int = -1, ) -> 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 is_approved: -1 means unchanged; 0 or 1 sets the value """ 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 if is_approved != -1: updates["isApproved"] = is_approved return json.dumps({ "action": "update_record", "table": "tasks", "data": {"id": task_id, "updates": updates}, }) @tool async def delete_task(task_id: str) -> str: """Delete a task permanently by its UUID.""" return json.dumps({ "action": "delete_record", "table": "tasks", "data": {"id": task_id}, }) @tool async def list_tasks_due_today() -> str: """List all tasks whose due date falls on today's date.""" return json.dumps({ "action": "list_due_today", "table": "tasks", }) # ── Task comment tools ──────────────────────────────────────────────── @tool async def list_task_comments(task_id: str) -> str: """List all comments on a task by its UUID.""" return json.dumps({ "action": "list", "table": "taskComments", "filters": {"taskId": task_id}, }) @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 """ return json.dumps({ "action": "create_record", "table": "taskComments", "data": { "taskId": task_id, "author": author, "content": content, }, }) @tool async def delete_task_comment(comment_id: str) -> str: """Delete a task comment by its UUID.""" return json.dumps({ "action": "delete_record", "table": "taskComments", "data": {"id": comment_id}, }) # ── Agent ───────────────────────────────────────────────────────────── @registry.register class TaskAgent(ChatAgent): def get_name(self) -> str: return "task_agent" def get_description(self) -> str: return "Manages tasks and comments: list, create, update, delete, due-today, comments" def get_tools(self) -> list[Any]: return [ list_tasks, create_task, update_task, delete_task, list_tasks_due_today, list_task_comments, add_task_comment, delete_task_comment, ] async def handle(self, query: str, context: dict[str, Any]) -> str: llm = ChatOpenAI(model="gpt-4o", temperature=0, api_key=settings.OPENAI_API_KEY) messages = [ SystemMessage(content=_SYSTEM_PROMPT), HumanMessage( content=f"User query: {query}\nContext: {json.dumps(context)[:1000]}" ), ] return await self._tool_loop(llm, messages, self.get_tools())