mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-10 22:32:16 +02:00
Fix delete notion page tool implementation
This commit is contained in:
parent
11e1c01847
commit
97d5b046d4
5 changed files with 216 additions and 44 deletions
|
|
@ -273,9 +273,6 @@ async def create_surfsense_deep_agent(
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
context_schema=SurfSenseContextSchema,
|
context_schema=SurfSenseContextSchema,
|
||||||
checkpointer=checkpointer,
|
checkpointer=checkpointer,
|
||||||
interrupt_on={
|
|
||||||
"delete_notion_page": {"allowed_decisions": ["approve", "reject"]},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return agent
|
return agent
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ def create_delete_notion_page_tool(
|
||||||
document_id = context.get("document_id")
|
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})")
|
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
|
# Request approval before deleting
|
||||||
approval = interrupt(
|
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.",
|
"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
|
# Create connector instance
|
||||||
notion_connector = NotionHistoryConnector(
|
notion_connector = NotionHistoryConnector(
|
||||||
session=db_session,
|
session=db_session,
|
||||||
connector_id=connector_id_from_context,
|
connector_id=actual_connector_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete the page from Notion
|
# 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', '')}")
|
logger.info(f"delete_page result: {result.get('status')} - {result.get('message', '')}")
|
||||||
|
|
||||||
# If deletion was successful and user wants to delete from DB
|
# If deletion was successful and user wants to delete from DB
|
||||||
deleted_from_db = False
|
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:
|
try:
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
|
|
@ -178,17 +221,18 @@ def create_delete_notion_page_tool(
|
||||||
|
|
||||||
return result
|
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:
|
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 {
|
return {
|
||||||
"status": "error",
|
"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
|
return delete_notion_page
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ BUILTIN_TOOLS: list[ToolDefinition] = [
|
||||||
),
|
),
|
||||||
ToolDefinition(
|
ToolDefinition(
|
||||||
name="delete_notion_page",
|
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(
|
factory=lambda deps: create_delete_notion_page_tool(
|
||||||
db_session=deps["db_session"],
|
db_session=deps["db_session"],
|
||||||
search_space_id=deps["search_space_id"],
|
search_space_id=deps["search_space_id"],
|
||||||
|
|
|
||||||
|
|
@ -1113,7 +1113,14 @@ class NotionHistoryConnector:
|
||||||
|
|
||||||
except APIResponseError as e:
|
except APIResponseError as e:
|
||||||
logger.error(f"Notion API error deleting page: {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 {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"message": f"Failed to delete Notion page: {error_msg}",
|
"message": f"Failed to delete Notion page: {error_msg}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
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 { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
|
@ -17,6 +24,8 @@ interface InterruptResult {
|
||||||
action_name: string;
|
action_name: string;
|
||||||
allowed_decisions: Array<"approve" | "reject">;
|
allowed_decisions: Array<"approve" | "reject">;
|
||||||
}>;
|
}>;
|
||||||
|
interrupt_type?: string;
|
||||||
|
message?: string;
|
||||||
context?: {
|
context?: {
|
||||||
account?: {
|
account?: {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -36,7 +45,9 @@ interface InterruptResult {
|
||||||
interface SuccessResult {
|
interface SuccessResult {
|
||||||
status: "success";
|
status: "success";
|
||||||
page_id: string;
|
page_id: string;
|
||||||
|
title?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
deleted_from_db?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorResult {
|
interface ErrorResult {
|
||||||
|
|
@ -44,7 +55,20 @@ interface ErrorResult {
|
||||||
message: string;
|
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 {
|
function isInterruptResult(result: unknown): result is InterruptResult {
|
||||||
return (
|
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({
|
function ApprovalCard({
|
||||||
args,
|
args,
|
||||||
interruptData,
|
interruptData,
|
||||||
|
|
@ -82,6 +126,9 @@ function ApprovalCard({
|
||||||
);
|
);
|
||||||
const [deleteFromDb, setDeleteFromDb] = useState(false);
|
const [deleteFromDb, setDeleteFromDb] = useState(false);
|
||||||
|
|
||||||
|
const account = interruptData.context?.account;
|
||||||
|
const currentTitle = interruptData.context?.current_title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`my-4 max-w-full overflow-hidden rounded-xl transition-all duration-300 ${
|
className={`my-4 max-w-full overflow-hidden rounded-xl transition-all duration-300 ${
|
||||||
|
|
@ -116,23 +163,34 @@ function ApprovalCard({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 px-4 py-3 bg-card">
|
{/* Context section - READ ONLY account and page info */}
|
||||||
{interruptData.context?.account && (
|
{!decided && interruptData.context && (
|
||||||
<div>
|
<div className="border-b border-border px-4 py-3 bg-muted/30 space-y-3">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Notion Account</p>
|
{interruptData.context.error ? (
|
||||||
<p className="text-sm text-foreground">
|
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||||
{interruptData.context.account.workspace_icon}{" "}
|
) : (
|
||||||
{interruptData.context.account.workspace_name}
|
<>
|
||||||
</p>
|
{account && (
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
)}
|
<div className="text-xs font-medium text-muted-foreground">Notion Account</div>
|
||||||
{interruptData.context?.current_title && (
|
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
||||||
<div>
|
{account.workspace_icon} {account.workspace_name}
|
||||||
<p className="text-xs font-medium text-muted-foreground">Page</p>
|
</div>
|
||||||
<p className="text-sm text-foreground">📄 {interruptData.context.current_title}</p>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
{currentTitle && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">Page to Delete</div>
|
||||||
|
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
||||||
|
📄 {currentTitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Checkbox for deleting from knowledge base */}
|
{/* Checkbox for deleting from knowledge base */}
|
||||||
{!decided && (
|
{!decided && (
|
||||||
|
|
@ -184,7 +242,8 @@ function ApprovalCard({
|
||||||
edited_action: {
|
edited_action: {
|
||||||
name: interruptData.action_requests[0].name,
|
name: interruptData.action_requests[0].name,
|
||||||
args: {
|
args: {
|
||||||
...interruptData.action_requests[0].args,
|
page_id: interruptData.context?.page_id,
|
||||||
|
connector_id: account?.id,
|
||||||
delete_from_db: deleteFromDb,
|
delete_from_db: deleteFromDb,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -230,6 +289,45 @@ function ErrorCard({ result }: { result: ErrorResult }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function InfoCard({ result }: { result: InfoResult }) {
|
||||||
|
return (
|
||||||
|
<div className="my-4 max-w-md overflow-hidden rounded-xl border border-amber-500/50 bg-card">
|
||||||
|
<div className="flex items-start gap-3 px-4 py-3">
|
||||||
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-amber-500/10">
|
||||||
|
<InfoIcon className="size-4 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 pt-2">
|
||||||
|
<p className="text-sm text-muted-foreground">{result.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WarningCard({ result }: { result: WarningResult }) {
|
||||||
|
return (
|
||||||
|
<div className="my-4 max-w-md overflow-hidden rounded-xl border border-amber-500/50 bg-card">
|
||||||
|
<div className="flex items-center gap-3 border-b border-amber-500/50 px-4 py-3">
|
||||||
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-amber-500/10">
|
||||||
|
<TriangleAlertIcon className="size-4 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium text-amber-600 dark:text-amber-500">Partial success</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 px-4 py-3 text-xs">
|
||||||
|
<p className="text-sm text-muted-foreground">{result.warning}</p>
|
||||||
|
{result.title && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<span className="font-medium text-muted-foreground">Deleted page: </span>
|
||||||
|
<span>{result.title}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SuccessCard({ result }: { result: SuccessResult }) {
|
function SuccessCard({ result }: { result: SuccessResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-md overflow-hidden rounded-xl border border-border bg-card">
|
<div className="my-4 max-w-md overflow-hidden rounded-xl border border-border bg-card">
|
||||||
|
|
@ -238,18 +336,27 @@ function SuccessCard({ result }: { result: SuccessResult }) {
|
||||||
<CheckIcon className="size-4 text-green-500" />
|
<CheckIcon className="size-4 text-green-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className=" text-[.8rem] text-muted-foreground">
|
<p className="text-[.8rem] text-muted-foreground">
|
||||||
{result.message || "Notion page deleted successfully"}
|
{result.message || "Notion page deleted successfully"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{result.deleted_from_db || result.title && (
|
||||||
<div className="space-y-2 px-4 py-3 text-xs">
|
<div className="space-y-2 px-4 py-3 text-xs">
|
||||||
<div>
|
{result.title && (
|
||||||
<span className="font-medium text-muted-foreground">Page ID: </span>
|
<div>
|
||||||
<span className="font-mono">{result.page_id}</span>
|
<span className="font-medium text-muted-foreground">Deleted page: </span>
|
||||||
</div>
|
<span>{result.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{result.deleted_from_db && (
|
||||||
|
<div className="pt-1">
|
||||||
|
<span className="text-green-600 dark:text-green-500">
|
||||||
|
✓ Also removed from knowledge base
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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 <InfoCard result={result} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWarningResult(result)) {
|
||||||
|
return <WarningCard result={result} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isErrorResult(result)) {
|
if (isErrorResult(result)) {
|
||||||
return <ErrorCard result={result} />;
|
return <ErrorCard result={result} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue