"""Note agent — Markdown note management (list, get, create, update, 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 note-taking assistant. You help users create, retrieve, update,\n" "and delete Markdown notes in their workspace.\n\n" "Rules:\n" " - content is always Markdown; preserve formatting when updating\n" " - project_id is optional; link a note to a project when mentioned\n" " - When updating, call get_note first if you need to read existing content\n" " before appending or replacing sections\n" " - list_notes without project_id returns all notes; scope with project_id\n" " when the user is working within a specific project\n" " - Do not fabricate note content — reflect what the user provides or what\n" " is already in the note (retrieved via get_note)." ) @tool async def list_notes(project_id: str = "") -> str: """List notes, optionally scoped to a project by project_id.""" return json.dumps({ "action": "list", "table": "notes", "filters": {"projectId": project_id or None}, }) @tool async def get_note(note_id: str) -> str: """Fetch a single note by its UUID to read its full Markdown content.""" return json.dumps({ "action": "get", "table": "notes", "data": {"id": note_id}, }) @tool async def create_note( title: str, content: str, project_id: str = "", ) -> str: """Create a new note. title: note heading (required) content: Markdown body text (required) project_id: optional UUID linking this note to a project """ return json.dumps({ "action": "create_record", "table": "notes", "data": { "title": title, "content": content, "projectId": project_id or None, }, }) @tool async def update_note( note_id: str, title: str = "", content: str = "", ) -> str: """Update an existing note. Only pass fields that should change. note_id: UUID of the note (required) If you need to preserve existing content, call get_note first. """ updates: dict[str, Any] = {} if title: updates["title"] = title if content: updates["content"] = content return json.dumps({ "action": "update_record", "table": "notes", "data": {"id": note_id, "updates": updates}, }) @tool async def delete_note(note_id: str) -> str: """Delete a note permanently by its UUID.""" return json.dumps({ "action": "delete_record", "table": "notes", "data": {"id": note_id}, }) @registry.register class NoteAgent(ChatAgent): def get_name(self) -> str: return "note_agent" def get_description(self) -> str: return "Manages notes: list, get, create, update, delete" def get_tools(self) -> list[Any]: return [list_notes, get_note, create_note, update_note, delete_note] 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())