"""Project agent — tool definitions for project lifecycle CRUD.""" from __future__ import annotations from typing import Any from langchain_core.tools import tool from app.core.ws_context import execute_on_client @tool async def list_projects( client_id: str = "", include_archived: int = 0, ) -> str: """List projects, optionally filtered by client_id. include_archived: 1 to include archived projects, 0 for active only (default). """ result = await execute_on_client( action="select", table="projects", filters={ "clientId": client_id or None, "includeArchived": bool(include_archived), }, ) rows = result.get("rows", []) if not rows: return "No projects found." lines = [f"- {r['name']} (status: {r['status']}, id: {r['id']})" for r in rows] return f"Found {len(rows)} project(s):\n" + "\n".join(lines) @tool async def list_all_projects() -> str: """List every project regardless of client or status. Use only when the user wants a complete cross-client overview. """ result = await execute_on_client(action="select", table="projects") rows = result.get("rows", []) if not rows: return "No projects found." lines = [f"- {r['name']} (status: {r['status']}, id: {r['id']})" for r in rows] return f"All projects ({len(rows)}):\n" + "\n".join(lines) @tool async def get_project(project_id: str) -> str: """Fetch a single project by its UUID.""" result = await execute_on_client(action="get", table="projects", data={"id": project_id}) row = result.get("row") if not row: return f"Project {project_id} not found." return ( f"Project: '{row['name']}' (id: {row['id']}, status: {row['status']}, " f"clientId: {row.get('clientId', 'none')})" ) @tool async def create_project( name: str, client_id: str = "", ) -> str: """Create a new project. name: human-readable project name (required) client_id: optional UUID of the owning client """ result = await execute_on_client( action="insert", table="projects", data={"name": name, "clientId": client_id or None}, ) row = result["row"] return f"Project created: '{row['name']}' (id: {row['id']})" @tool async def update_project( project_id: str, name: str = "", client_id: str = "", status: str = "", ai_summary: str = "", ) -> str: """Update a project. Only pass fields that should change. project_id: UUID of the project (required) status: active | archived ai_summary: AI-generated summary text (populate only when explicitly requested) """ updates: dict[str, Any] = {} if name: updates["name"] = name if client_id: updates["clientId"] = client_id if status: updates["status"] = status if ai_summary: updates["aiSummary"] = ai_summary result = await execute_on_client( action="update", table="projects", data={"id": project_id, "updates": updates}, ) row = result["row"] return f"Project updated: '{row['name']}' (id: {row['id']}, status: {row['status']})" @tool async def delete_project(project_id: str) -> str: """Permanently delete a project and orphan its tasks. IMPORTANT: prefer update_project(status='archived') unless the user has explicitly confirmed they want permanent deletion. """ await execute_on_client(action="delete", table="projects", data={"id": project_id}) return f"Project {project_id} permanently deleted."