- Replaced direct instantiation of ChatOpenAI with a centralized get_llm function in CheckpointAgent, NoteAgent, ProjectAgent, and TaskAgent. - Introduced a new llm.py module to handle LLM model instantiation and API key management. - Updated settings.py to include LLM_MODEL and LLM_ROUTER_MODEL configurations. - Modified orchestrator.py to use get_router_llm for intent classification. - Updated requirements.txt to include litellm for LLM management. - Adjusted tests to mock get_llm instead of ChatOpenAI directly.
229 lines
7.0 KiB
Python
229 lines
7.0 KiB
Python
"""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 app.core.agent_registry import ChatAgent, registry
|
|
from app.core.llm import get_llm
|
|
|
|
_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 = get_llm()
|
|
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())
|