diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py b/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py index 6a122bd50..09507f465 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py @@ -1,3 +1,4 @@ +import logging from typing import Any from langchain_core.tools import tool @@ -7,6 +8,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.connectors.notion_history import NotionHistoryConnector from app.services.notion import NotionToolMetadataService +logger = logging.getLogger(__name__) + def create_update_notion_page_tool( db_session: AsyncSession | None = None, @@ -29,20 +32,20 @@ def create_update_notion_page_tool( @tool async def update_notion_page( - page_id: str, - title: str | None = None, - content: str | None = None, + page_title: str, + new_title: str | None = None, + new_content: str | None = None, ) -> dict[str, Any]: """Update an existing Notion page's title and/or content. Use this tool when the user asks you to modify, edit, or update - a Notion page. At least one of title or content must be provided. + a Notion page. At least one of new_title or new_content must be provided. Args: - page_id: The ID of the Notion page to update (required). - title: New title for the page (optional). - content: New markdown content for the page body (optional). - If provided, replaces all existing content. + page_title: The current title of the Notion page to update (required). + new_title: New title for the page (optional). + new_content: New markdown content for the page body (optional). + If provided, replaces all existing content. Returns: Dictionary with: @@ -57,49 +60,61 @@ def create_update_notion_page_tool( and move on. Do NOT ask for alternatives or troubleshoot. Examples: - - "Update the Notion page abc123 with title 'Updated Meeting Notes'" - - "Change the content of page xyz789 to 'New content here'" - - "Update page abc123 with new title 'Final Report' and content '# Summary...'" + - "Update the 'Meeting Notes' page with new title 'Updated Meeting Notes'" + - "Change the content of 'Project Plan' page to 'New content here'" + - "Update 'Weekly Report' with new title 'Final Report' and content '# Summary...'" """ + logger.info(f"update_notion_page called: page_title='{page_title}', new_title={new_title}, has_content={new_content is not None}") + if db_session is None or search_space_id is None or user_id is None: + logger.error("Notion tool not properly configured - missing required parameters") return { "status": "error", "message": "Notion tool not properly configured. Please contact support.", } - if not title and not content: + if not new_title and not new_content: return { "status": "error", - "message": "At least one of 'title' or 'content' must be provided to update the page.", + "message": "At least one of 'new_title' or 'new_content' must be provided to update the page.", } try: metadata_service = NotionToolMetadataService(db_session) context = await metadata_service.get_update_context( - search_space_id, user_id, page_id + search_space_id, user_id, page_title ) if "error" in context: + logger.error(f"Failed to fetch update context: {context['error']}") return { "status": "error", "message": context["error"], } - approval = interrupt({ - "type": "notion_page_update", - "action": { - "tool": "update_notion_page", - "params": { - "page_id": page_id, - "title": title, - "content": content, + page_id = context.get("page_id") + connector_id_from_context = context.get("account", {}).get("id") + + logger.info(f"Requesting approval for updating Notion page: '{page_title}' (page_id={page_id})") + approval = interrupt( + { + "type": "notion_page_update", + "action": { + "tool": "update_notion_page", + "params": { + "page_id": page_id, + "title": new_title, + "content": new_content, + "connector_id": connector_id_from_context, + }, }, - }, - "context": context, - }) + "context": context, + } + ) decisions = approval.get("decisions", []) if not decisions: + logger.warning("No approval decision received") return { "status": "error", "message": "No approval decision received", @@ -107,8 +122,10 @@ def create_update_notion_page_tool( decision = decisions[0] decision_type = decision.get("type") or decision.get("decision_type") + logger.info(f"User decision: {decision_type}") if decision_type == "reject": + logger.info("Notion page update rejected by user") return { "status": "rejected", "message": "User declined. The page was not updated. Do not ask again or suggest alternatives.", @@ -118,25 +135,28 @@ def create_update_notion_page_tool( final_params = edited_action.get("args", {}) if edited_action else {} final_page_id = final_params.get("page_id", page_id) - final_title = final_params.get("title", title) - final_content = final_params.get("content", content) + final_title = final_params.get("title", new_title) + final_content = final_params.get("content", new_content) + final_connector_id = final_params.get("connector_id", connector_id_from_context) - if final_title and (not final_title or not final_title.strip()): + # Validate title if it's being updated + if final_title is not None and not final_title.strip(): + logger.error("Title is empty or contains only whitespace") return { "status": "error", "message": "Page title cannot be empty. Please provide a valid title.", } + logger.info(f"Updating Notion page with final params: page_id={final_page_id}, title={final_title}, has_content={final_content is not None}") + from sqlalchemy.future import select from app.db import SearchSourceConnector, SearchSourceConnectorType - connector_id_from_context = context.get("account", {}).get("id") - - if connector_id_from_context: + if final_connector_id: result = await db_session.execute( select(SearchSourceConnector).filter( - SearchSourceConnector.id == connector_id_from_context, + SearchSourceConnector.id == final_connector_id, SearchSourceConnector.search_space_id == search_space_id, SearchSourceConnector.user_id == user_id, SearchSourceConnector.connector_type @@ -146,12 +166,17 @@ def create_update_notion_page_tool( connector = result.scalars().first() if not connector: + logger.error( + f"Invalid connector_id={final_connector_id} for search_space_id={search_space_id}" + ) return { "status": "error", "message": "Selected Notion account is invalid or has been disconnected. Please select a valid account.", } actual_connector_id = connector.id + logger.info(f"Validated Notion connector: id={actual_connector_id}") else: + logger.error("No connector found for this page") return { "status": "error", "message": "No connector found for this page.", @@ -167,6 +192,7 @@ def create_update_notion_page_tool( title=final_title, content=final_content, ) + logger.info(f"update_page result: {result.get('status')} - {result.get('message', '')}") return result except Exception as e: @@ -175,9 +201,12 @@ def create_update_notion_page_tool( if isinstance(e, GraphInterrupt): raise + logger.error(f"Error updating Notion page: {e}", exc_info=True) return { "status": "error", - "message": str(e) if isinstance(e, ValueError) else f"Unexpected error: {e!s}", + "message": str(e) + if isinstance(e, ValueError) + else f"Unexpected error: {e!s}", } return update_notion_page diff --git a/surfsense_backend/app/services/notion/tool_metadata_service.py b/surfsense_backend/app/services/notion/tool_metadata_service.py index 9098d6deb..521bfece0 100644 --- a/surfsense_backend/app/services/notion/tool_metadata_service.py +++ b/surfsense_backend/app/services/notion/tool_metadata_service.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from sqlalchemy import String, and_, cast +from sqlalchemy import and_, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select @@ -79,7 +79,9 @@ class NotionToolMetadataService: "error": "No Notion accounts connected", } - parent_pages = await self._get_parent_pages_by_account(search_space_id, accounts) + parent_pages = await self._get_parent_pages_by_account( + search_space_id, accounts + ) return { "accounts": [acc.to_dict() for acc in accounts], @@ -87,16 +89,18 @@ class NotionToolMetadataService: } async def get_update_context( - self, search_space_id: int, user_id: str, page_id: str + self, search_space_id: int, user_id: str, page_title: str ) -> dict: result = await self._db_session.execute( select(Document) - .join(SearchSourceConnector, Document.connector_id == SearchSourceConnector.id) + .join( + SearchSourceConnector, Document.connector_id == SearchSourceConnector.id + ) .filter( and_( Document.search_space_id == search_space_id, Document.document_type == DocumentType.NOTION_CONNECTOR, - cast(Document.document_metadata["page_id"], String) == page_id, + func.lower(Document.title) == func.lower(page_title), SearchSourceConnector.user_id == user_id, ) ) @@ -104,7 +108,11 @@ class NotionToolMetadataService: document = result.scalars().first() if not document: - return {"error": f"Page {page_id} not found in your indexed documents"} + return { + "error": f"Page '{page_title}' not found in your indexed Notion pages. " + "This could mean: (1) the page doesn't exist, (2) it hasn't been indexed yet, " + "or (3) the page title is different. Please check the exact page title in Notion." + } if not document.connector_id: return {"error": "Document has no associated connector"} @@ -124,6 +132,10 @@ class NotionToolMetadataService: account = NotionAccount.from_connector(connector) + page_id = document.document_metadata.get("page_id") + if not page_id: + return {"error": "Page ID not found in document metadata"} + return { "account": account.to_dict(), "page_id": page_id, @@ -134,9 +146,9 @@ class NotionToolMetadataService: } async def get_delete_context( - self, search_space_id: int, user_id: str, page_id: str + self, search_space_id: int, user_id: str, page_title: str ) -> dict: - return await self.get_update_context(search_space_id, user_id, page_id) + return await self.get_update_context(search_space_id, user_id, page_title) async def _get_notion_accounts( self, search_space_id: int, user_id: str diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 83ac3ef2b..d529e0598 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -136,6 +136,7 @@ const TOOLS_WITH_UI = new Set([ "delete_notion_page", "scrape_webpage", "create_notion_page", + "update_notion_page", // "write_todos", // Disabled for now ]); diff --git a/surfsense_web/app/globals.css b/surfsense_web/app/globals.css index babbabb0b..422f1b997 100644 --- a/surfsense_web/app/globals.css +++ b/surfsense_web/app/globals.css @@ -189,7 +189,8 @@ button { /* Human-in-the-loop approval card animations */ @keyframes pulse-subtle { - 0%, 100% { + 0%, + 100% { opacity: 1; box-shadow: 0 0 0 0 rgb(0 0 0 / 0.15); } diff --git a/surfsense_web/components/tool-ui/update-notion-page.tsx b/surfsense_web/components/tool-ui/update-notion-page.tsx index 671509077..7fde7b75e 100644 --- a/surfsense_web/components/tool-ui/update-notion-page.tsx +++ b/surfsense_web/components/tool-ui/update-notion-page.tsx @@ -5,14 +5,13 @@ import { AlertTriangleIcon, CheckIcon, Loader2Icon, - Maximize2Icon, + MaximizeIcon, + MinimizeIcon, PencilIcon, XIcon, } from "lucide-react"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; interface InterruptResult { @@ -51,6 +50,8 @@ interface SuccessResult { page_id: string; title: string; url: string; + content_preview?: string; + content_length?: number; message?: string; } @@ -79,109 +80,6 @@ function isErrorResult(result: unknown): result is ErrorResult { ); } -function PageContextDisplay({ - account, - currentTitle, - currentContent, -}: { - account?: { - id: number; - name: string; - workspace_id: string | null; - workspace_name: string; - workspace_icon: string; - }; - currentTitle?: string; - currentContent?: string; -}) { - return ( - <> - {account && ( -
-
Notion Account
-
- {account.workspace_name} -
-
- )} - - {currentTitle && ( -
-
Current Page Title
-
- {currentTitle} -
-
- )} - - {currentContent && ( -
-
- Current Content (first 200 chars) -
-
- {currentContent.slice(0, 200)} - {currentContent.length > 200 && "..."} -
-
- )} - - ); -} - -function EditFormFields({ - editedArgs, - setEditedArgs, - isTitleValid, - idPrefix = "", - rows = 8, -}: { - editedArgs: Record; - setEditedArgs: (args: Record) => void; - isTitleValid: boolean; - idPrefix?: string; - rows?: number; -}) { - return ( - <> -
- - setEditedArgs({ ...editedArgs, title: e.target.value })} - placeholder="Enter page title" - className={!isTitleValid ? "border-destructive" : ""} - /> - {!isTitleValid && ( -

Title is required and cannot be empty

- )} -
-
- -