diff --git a/surfsense_backend/app/agents/new_chat/chat_deepagent.py b/surfsense_backend/app/agents/new_chat/chat_deepagent.py index 656137f30..d58a0fadb 100644 --- a/surfsense_backend/app/agents/new_chat/chat_deepagent.py +++ b/surfsense_backend/app/agents/new_chat/chat_deepagent.py @@ -273,9 +273,6 @@ async def create_surfsense_deep_agent( system_prompt=system_prompt, context_schema=SurfSenseContextSchema, checkpointer=checkpointer, - interrupt_on={ - "delete_notion_page": {"allowed_decisions": ["approve", "reject"]}, - }, ) return agent diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py b/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py index 05b0fbfe8..4e992d87c 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py @@ -96,6 +96,7 @@ def create_delete_notion_page_tool( document_id = context.get("document_id") logger.info(f"Requesting approval for deleting Notion page: '{page_title}' (page_id={page_id}, delete_from_db={delete_from_db})") + # Request approval before deleting approval = interrupt( { @@ -131,21 +132,63 @@ def create_delete_notion_page_tool( "message": "User declined. The page was not deleted. Do not ask again or suggest alternatives.", } - logger.info(f"Deleting Notion page: page_id={page_id}") + # Extract edited action arguments (if user modified the checkbox) + edited_action = decision.get("edited_action", {}) + final_params = edited_action.get("args", {}) if edited_action else {} + + final_page_id = final_params.get("page_id", page_id) + final_connector_id = final_params.get("connector_id", connector_id_from_context) + final_delete_from_db = final_params.get("delete_from_db", delete_from_db) + + logger.info(f"Deleting Notion page with final params: page_id={final_page_id}, connector_id={final_connector_id}, delete_from_db={final_delete_from_db}") + + from sqlalchemy.future import select + + from app.db import SearchSourceConnector, SearchSourceConnectorType + + # Validate the connector + if final_connector_id: + result = await db_session.execute( + select(SearchSourceConnector).filter( + SearchSourceConnector.id == final_connector_id, + SearchSourceConnector.search_space_id == search_space_id, + SearchSourceConnector.user_id == user_id, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.NOTION_CONNECTOR, + ) + ) + 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.", + } # Create connector instance notion_connector = NotionHistoryConnector( session=db_session, - connector_id=connector_id_from_context, + connector_id=actual_connector_id, ) # Delete the page from Notion - result = await notion_connector.delete_page(page_id=page_id) + result = await notion_connector.delete_page(page_id=final_page_id) logger.info(f"delete_page result: {result.get('status')} - {result.get('message', '')}") # If deletion was successful and user wants to delete from DB deleted_from_db = False - if result.get("status") == "success" and delete_from_db and document_id: + if result.get("status") == "success" and final_delete_from_db and document_id: try: from sqlalchemy.future import select @@ -178,17 +221,18 @@ def create_delete_notion_page_tool( return result - except ValueError as e: - logger.error(f"ValueError in delete_notion_page: {e}") - return { - "status": "error", - "message": str(e), - } except Exception as e: - logger.error(f"Unexpected error in delete_notion_page: {e}") + from langgraph.errors import GraphInterrupt + + if isinstance(e, GraphInterrupt): + raise + + logger.error(f"Error deleting Notion page: {e}", exc_info=True) return { "status": "error", - "message": f"Unexpected error deleting Notion page: {e!s}", + "message": str(e) + if isinstance(e, ValueError) + else f"Unexpected error: {e!s}", } return delete_notion_page diff --git a/surfsense_backend/app/agents/new_chat/tools/registry.py b/surfsense_backend/app/agents/new_chat/tools/registry.py index 8da9ee116..3e71299a0 100644 --- a/surfsense_backend/app/agents/new_chat/tools/registry.py +++ b/surfsense_backend/app/agents/new_chat/tools/registry.py @@ -229,7 +229,7 @@ BUILTIN_TOOLS: list[ToolDefinition] = [ ), ToolDefinition( name="delete_notion_page", - description="Delete a Notion page by title", + description="Delete an existing Notion page", factory=lambda deps: create_delete_notion_page_tool( db_session=deps["db_session"], search_space_id=deps["search_space_id"], diff --git a/surfsense_backend/app/connectors/notion_history.py b/surfsense_backend/app/connectors/notion_history.py index b6fda8b8b..6b2dff586 100644 --- a/surfsense_backend/app/connectors/notion_history.py +++ b/surfsense_backend/app/connectors/notion_history.py @@ -1113,7 +1113,14 @@ class NotionHistoryConnector: except APIResponseError as e: logger.error(f"Notion API error deleting page: {e}") - error_msg = e.body.get("message", str(e)) if hasattr(e, "body") else str(e) + # Handle both dict and string body formats + if hasattr(e, "body"): + if isinstance(e.body, dict): + error_msg = e.body.get("message", str(e)) + else: + error_msg = str(e.body) if e.body else str(e) + else: + error_msg = str(e) return { "status": "error", "message": f"Failed to delete Notion page: {error_msg}", diff --git a/surfsense_web/components/tool-ui/delete-notion-page.tsx b/surfsense_web/components/tool-ui/delete-notion-page.tsx index 59d04f2b7..70c07c00b 100644 --- a/surfsense_web/components/tool-ui/delete-notion-page.tsx +++ b/surfsense_web/components/tool-ui/delete-notion-page.tsx @@ -1,7 +1,14 @@ "use client"; import { makeAssistantToolUI } from "@assistant-ui/react"; -import { AlertTriangleIcon, CheckIcon, Loader2Icon, XIcon } from "lucide-react"; +import { + AlertTriangleIcon, + CheckIcon, + InfoIcon, + Loader2Icon, + TriangleAlertIcon, + XIcon, +} from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; @@ -17,6 +24,8 @@ interface InterruptResult { action_name: string; allowed_decisions: Array<"approve" | "reject">; }>; + interrupt_type?: string; + message?: string; context?: { account?: { id: number; @@ -36,7 +45,9 @@ interface InterruptResult { interface SuccessResult { status: "success"; page_id: string; + title?: string; message?: string; + deleted_from_db?: boolean; } interface ErrorResult { @@ -44,7 +55,20 @@ interface ErrorResult { message: string; } -type DeleteNotionPageResult = InterruptResult | SuccessResult | ErrorResult; +interface InfoResult { + status: "not_found"; + message: string; +} + +interface WarningResult { + status: "success"; + warning: string; + page_id?: string; + title?: string; + message?: string; +} + +type DeleteNotionPageResult = InterruptResult | SuccessResult | ErrorResult | InfoResult | WarningResult; function isInterruptResult(result: unknown): result is InterruptResult { return ( @@ -64,6 +88,26 @@ function isErrorResult(result: unknown): result is ErrorResult { ); } +function isInfoResult(result: unknown): result is InfoResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as InfoResult).status === "not_found" + ); +} + +function isWarningResult(result: unknown): result is WarningResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as WarningResult).status === "success" && + "warning" in result && + typeof (result as WarningResult).warning === "string" + ); +} + function ApprovalCard({ args, interruptData, @@ -82,6 +126,9 @@ function ApprovalCard({ ); const [deleteFromDb, setDeleteFromDb] = useState(false); + const account = interruptData.context?.account; + const currentTitle = interruptData.context?.current_title; + return (
-
- {interruptData.context?.account && ( -
-

Notion Account

-

- {interruptData.context.account.workspace_icon}{" "} - {interruptData.context.account.workspace_name} -

-
- )} - {interruptData.context?.current_title && ( -
-

Page

-

📄 {interruptData.context.current_title}

-
- )} -
+ {/* Context section - READ ONLY account and page info */} + {!decided && interruptData.context && ( +
+ {interruptData.context.error ? ( +

{interruptData.context.error}

+ ) : ( + <> + {account && ( +
+
Notion Account
+
+ {account.workspace_icon} {account.workspace_name} +
+
+ )} + + {currentTitle && ( +
+
Page to Delete
+
+ 📄 {currentTitle} +
+
+ )} + + )} +
+ )} {/* Checkbox for deleting from knowledge base */} {!decided && ( @@ -184,7 +242,8 @@ function ApprovalCard({ edited_action: { name: interruptData.action_requests[0].name, args: { - ...interruptData.action_requests[0].args, + page_id: interruptData.context?.page_id, + connector_id: account?.id, delete_from_db: deleteFromDb, }, }, @@ -230,6 +289,45 @@ function ErrorCard({ result }: { result: ErrorResult }) { ); } +function InfoCard({ result }: { result: InfoResult }) { + return ( +
+
+
+ +
+
+

{result.message}

+
+
+
+ ); +} + +function WarningCard({ result }: { result: WarningResult }) { + return ( +
+
+
+ +
+
+

Partial success

+
+
+
+

{result.warning}

+ {result.title && ( +
+ Deleted page: + {result.title} +
+ )} +
+
+ ); +} + function SuccessCard({ result }: { result: SuccessResult }) { return (
@@ -238,18 +336,27 @@ function SuccessCard({ result }: { result: SuccessResult }) {
-

+

{result.message || "Notion page deleted successfully"}

- +{result.deleted_from_db || result.title && (
-
- Page ID: - {result.page_id} -
-
+ {result.title && ( +
+ Deleted page: + {result.title} +
+ )} + {result.deleted_from_db && ( +
+ + ✓ Also removed from knowledge base + +
+ )} + )} ); } @@ -288,6 +395,23 @@ export const DeleteNotionPageToolUI = makeAssistantToolUI< ); } + if ( + typeof result === "object" && + result !== null && + "status" in result && + (result as { status: string }).status === "rejected" + ) { + return null; + } + + if (isInfoResult(result)) { + return ; + } + + if (isWarningResult(result)) { + return ; + } + if (isErrorResult(result)) { return ; }