"""Project agent — full lifecycle management (list, get, create, update, archive, delete).""" 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 project management assistant. You help users create, find,\n" "update, and archive projects in their workspace.\n\n" "Rules:\n" " - status must be one of: active, archived\n" " - client_id is optional; link to a client only when explicitly mentioned\n" " - ai_summary is populated only when the user asks for a project summary;\n" " derive it from context data — do not fabricate content\n" " - Use list_projects for scoped queries; list_all_projects only when the\n" " user wants a complete cross-client view including archived projects\n" " - get_project requires a project UUID; resolve the ID first by calling\n" " list_projects if you only have a project name\n" " - Prefer archiving (update_project status=archived) over deletion;\n" " only call delete_project when the user explicitly confirms deletion." ) @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). """ return json.dumps({ "action": "list", "table": "projects", "filters": { "clientId": client_id or None, "includeArchived": bool(include_archived), }, }) @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. """ return json.dumps({ "action": "list_all", "table": "projects", }) @tool async def get_project(project_id: str) -> str: """Fetch a single project by its UUID.""" return json.dumps({ "action": "get", "table": "projects", "data": {"id": project_id}, }) @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 """ return json.dumps({ "action": "create_record", "table": "projects", "data": { "name": name, "clientId": client_id or None, }, }) @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 return json.dumps({ "action": "update_record", "table": "projects", "data": {"id": project_id, "updates": updates}, }) @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. """ return json.dumps({ "action": "delete_record", "table": "projects", "data": {"id": project_id}, }) @registry.register class ProjectAgent(ChatAgent): def get_name(self) -> str: return "project_agent" def get_description(self) -> str: return "Manages projects: list, get, create, update, archive, delete" def get_tools(self) -> list[Any]: return [ list_projects, list_all_projects, get_project, create_project, update_project, delete_project, ] 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())