128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""Checkpoint agent — project milestone management (list, create, update, delete)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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
|
|
from app.core.ws_context import execute_on_client
|
|
|
|
_SYSTEM_PROMPT = (
|
|
"You are a project checkpoint assistant. Checkpoints are milestone dates that\n"
|
|
"track progress on a project — they are not calendar events.\n\n"
|
|
"Rules:\n"
|
|
" - project_id is REQUIRED for every create; confirm with the user if unknown\n"
|
|
" - date is a Unix timestamp in milliseconds; convert human-readable dates\n"
|
|
" - is_ai_suggested: 1 when proactively proposing a checkpoint, 0 otherwise\n"
|
|
" - is_approved: 0 until the user explicitly confirms; then 1\n"
|
|
" - For update_checkpoint, use -1 for integer fields you do not want to change\n"
|
|
" - Listing without a project_id returns all checkpoints across projects\n"
|
|
" - Always echo the title and formatted date in your confirmation."
|
|
)
|
|
|
|
|
|
@tool
|
|
async def list_checkpoints(project_id: str = "") -> str:
|
|
"""List checkpoints. Provide project_id to scope to a specific project."""
|
|
result = await execute_on_client(
|
|
action="select",
|
|
table="checkpoints",
|
|
filters={"projectId": project_id or None},
|
|
)
|
|
rows = result.get("rows", [])
|
|
if not rows:
|
|
return "No checkpoints found."
|
|
lines = [f"- {r['title']} (date: {r['date']}, id: {r['id']})" for r in rows]
|
|
return f"Found {len(rows)} checkpoint(s):\n" + "\n".join(lines)
|
|
|
|
|
|
@tool
|
|
async def create_checkpoint(
|
|
project_id: str,
|
|
title: str,
|
|
date: int,
|
|
is_ai_suggested: int = 0,
|
|
is_approved: int = 0,
|
|
) -> str:
|
|
"""Create a project checkpoint (milestone).
|
|
project_id: REQUIRED UUID of the parent project
|
|
title: descriptive name for the milestone
|
|
date: Unix timestamp in milliseconds
|
|
is_ai_suggested: 1 if proactively suggested, 0 if user-requested
|
|
is_approved: 0 until the user confirms
|
|
"""
|
|
result = await execute_on_client(
|
|
action="insert",
|
|
table="checkpoints",
|
|
data={
|
|
"projectId": project_id,
|
|
"title": title,
|
|
"date": date,
|
|
"isAiSuggested": is_ai_suggested,
|
|
"isApproved": is_approved,
|
|
},
|
|
)
|
|
row = result["row"]
|
|
return f"Checkpoint created: '{row['title']}' (id: {row['id']}, date: {row['date']})"
|
|
|
|
|
|
@tool
|
|
async def update_checkpoint(
|
|
checkpoint_id: str,
|
|
title: str = "",
|
|
date: int = -1,
|
|
is_approved: int = -1,
|
|
) -> str:
|
|
"""Update a checkpoint. Only pass fields that should change.
|
|
checkpoint_id: UUID of the checkpoint (required)
|
|
date: -1 means unchanged; any other value sets the new date (ms timestamp)
|
|
is_approved: -1 means unchanged; 0 or 1 sets the approval state
|
|
"""
|
|
updates: dict[str, Any] = {}
|
|
if title:
|
|
updates["title"] = title
|
|
if date != -1:
|
|
updates["date"] = date
|
|
if is_approved != -1:
|
|
updates["isApproved"] = is_approved
|
|
result = await execute_on_client(
|
|
action="update",
|
|
table="checkpoints",
|
|
data={"id": checkpoint_id, "updates": updates},
|
|
)
|
|
row = result["row"]
|
|
return f"Checkpoint updated: '{row['title']}' (id: {row['id']})"
|
|
|
|
|
|
@tool
|
|
async def delete_checkpoint(checkpoint_id: str) -> str:
|
|
"""Delete a checkpoint permanently by its UUID."""
|
|
await execute_on_client(action="delete", table="checkpoints", data={"id": checkpoint_id})
|
|
return f"Checkpoint {checkpoint_id} deleted."
|
|
|
|
|
|
@registry.register
|
|
class CheckpointAgent(ChatAgent):
|
|
def get_name(self) -> str:
|
|
return "checkpoint_agent"
|
|
|
|
def get_description(self) -> str:
|
|
return "Manages project checkpoints (milestones): list, create, update, delete"
|
|
|
|
def get_tools(self) -> list[Any]:
|
|
return [list_checkpoints, create_checkpoint, update_checkpoint, delete_checkpoint]
|
|
|
|
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())
|