diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py b/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py index 592ced5ec..37bcf083e 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_calendar/create_event.py @@ -6,9 +6,9 @@ from typing import Any from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from langchain_core.tools import tool -from langgraph.types import interrupt from sqlalchemy.ext.asyncio import AsyncSession +from app.agents.new_chat.tools.hitl import request_approval from app.services.google_calendar import GoogleCalendarToolMetadataService logger = logging.getLogger(__name__) @@ -90,63 +90,35 @@ def create_create_calendar_event_tool( logger.info( f"Requesting approval for creating calendar event: summary='{summary}'" ) - approval = interrupt( - { - "type": "google_calendar_event_creation", - "action": { - "tool": "create_calendar_event", - "params": { - "summary": summary, - "start_datetime": start_datetime, - "end_datetime": end_datetime, - "description": description, - "location": location, - "attendees": attendees, - "timezone": context.get("timezone"), - "connector_id": None, - }, - }, - "context": context, - } + result = request_approval( + action_type="google_calendar_event_creation", + tool_name="create_calendar_event", + params={ + "summary": summary, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "description": description, + "location": location, + "attendees": attendees, + "timezone": context.get("timezone"), + "connector_id": None, + }, + context=context, ) - decisions_raw = ( - approval.get("decisions", []) if isinstance(approval, dict) else [] - ) - decisions = ( - decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] - ) - decisions = [d for d in decisions if isinstance(d, dict)] - if not decisions: - logger.warning("No approval decision received") - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - logger.info(f"User decision: {decision_type}") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", "message": "User declined. The event was not created. Do not ask again or suggest alternatives.", } - final_params: dict[str, Any] = {} - edited_action = decision.get("edited_action") - if isinstance(edited_action, dict): - edited_args = edited_action.get("args") - if isinstance(edited_args, dict): - final_params = edited_args - elif isinstance(decision.get("args"), dict): - final_params = decision["args"] - - final_summary = final_params.get("summary", summary) - final_start_datetime = final_params.get("start_datetime", start_datetime) - final_end_datetime = final_params.get("end_datetime", end_datetime) - final_description = final_params.get("description", description) - final_location = final_params.get("location", location) - final_attendees = final_params.get("attendees", attendees) - final_connector_id = final_params.get("connector_id") + final_summary = result.params.get("summary", summary) + final_start_datetime = result.params.get("start_datetime", start_datetime) + final_end_datetime = result.params.get("end_datetime", end_datetime) + final_description = result.params.get("description", description) + final_location = result.params.get("location", location) + final_attendees = result.params.get("attendees", attendees) + final_connector_id = result.params.get("connector_id") if not final_summary or not final_summary.strip(): return {"status": "error", "message": "Event summary cannot be empty."} diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py b/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py index 8b088487c..4d9d69b4b 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_calendar/delete_event.py @@ -6,9 +6,9 @@ from typing import Any from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from langchain_core.tools import tool -from langgraph.types import interrupt from sqlalchemy.ext.asyncio import AsyncSession +from app.agents.new_chat.tools.hitl import request_approval from app.services.google_calendar import GoogleCalendarToolMetadataService logger = logging.getLogger(__name__) @@ -100,56 +100,28 @@ def create_delete_calendar_event_tool( logger.info( f"Requesting approval for deleting calendar event: '{event_title_or_id}' (event_id={event_id}, delete_from_kb={delete_from_kb})" ) - approval = interrupt( - { - "type": "google_calendar_event_deletion", - "action": { - "tool": "delete_calendar_event", - "params": { - "event_id": event_id, - "connector_id": connector_id_from_context, - "delete_from_kb": delete_from_kb, - }, - }, - "context": context, - } + result = request_approval( + action_type="google_calendar_event_deletion", + tool_name="delete_calendar_event", + params={ + "event_id": event_id, + "connector_id": connector_id_from_context, + "delete_from_kb": delete_from_kb, + }, + context=context, ) - decisions_raw = ( - approval.get("decisions", []) if isinstance(approval, dict) else [] - ) - decisions = ( - decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] - ) - decisions = [d for d in decisions if isinstance(d, dict)] - if not decisions: - logger.warning("No approval decision received") - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - logger.info(f"User decision: {decision_type}") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", "message": "User declined. The event was not deleted. Do not ask again or suggest alternatives.", } - edited_action = decision.get("edited_action") - final_params: dict[str, Any] = {} - if isinstance(edited_action, dict): - edited_args = edited_action.get("args") - if isinstance(edited_args, dict): - final_params = edited_args - elif isinstance(decision.get("args"), dict): - final_params = decision["args"] - - final_event_id = final_params.get("event_id", event_id) - final_connector_id = final_params.get( + final_event_id = result.params.get("event_id", event_id) + final_connector_id = result.params.get( "connector_id", connector_id_from_context ) - final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb) + final_delete_from_kb = result.params.get("delete_from_kb", delete_from_kb) if not final_connector_id: return { diff --git a/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py b/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py index ed826f1b8..45ff6dfb9 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_calendar/update_event.py @@ -6,9 +6,9 @@ from typing import Any from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from langchain_core.tools import tool -from langgraph.types import interrupt from sqlalchemy.ext.asyncio import AsyncSession +from app.agents.new_chat.tools.hitl import request_approval from app.services.google_calendar import GoogleCalendarToolMetadataService logger = logging.getLogger(__name__) @@ -116,71 +116,43 @@ def create_update_calendar_event_tool( logger.info( f"Requesting approval for updating calendar event: '{event_title_or_id}' (event_id={event_id})" ) - approval = interrupt( - { - "type": "google_calendar_event_update", - "action": { - "tool": "update_calendar_event", - "params": { - "event_id": event_id, - "document_id": document_id, - "connector_id": connector_id_from_context, - "new_summary": new_summary, - "new_start_datetime": new_start_datetime, - "new_end_datetime": new_end_datetime, - "new_description": new_description, - "new_location": new_location, - "new_attendees": new_attendees, - }, - }, - "context": context, - } + result = request_approval( + action_type="google_calendar_event_update", + tool_name="update_calendar_event", + params={ + "event_id": event_id, + "document_id": document_id, + "connector_id": connector_id_from_context, + "new_summary": new_summary, + "new_start_datetime": new_start_datetime, + "new_end_datetime": new_end_datetime, + "new_description": new_description, + "new_location": new_location, + "new_attendees": new_attendees, + }, + context=context, ) - decisions_raw = ( - approval.get("decisions", []) if isinstance(approval, dict) else [] - ) - decisions = ( - decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] - ) - decisions = [d for d in decisions if isinstance(d, dict)] - if not decisions: - logger.warning("No approval decision received") - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - logger.info(f"User decision: {decision_type}") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", "message": "User declined. The event was not updated. Do not ask again or suggest alternatives.", } - edited_action = decision.get("edited_action") - final_params: dict[str, Any] = {} - if isinstance(edited_action, dict): - edited_args = edited_action.get("args") - if isinstance(edited_args, dict): - final_params = edited_args - elif isinstance(decision.get("args"), dict): - final_params = decision["args"] - - final_event_id = final_params.get("event_id", event_id) - final_connector_id = final_params.get( + final_event_id = result.params.get("event_id", event_id) + final_connector_id = result.params.get( "connector_id", connector_id_from_context ) - final_new_summary = final_params.get("new_summary", new_summary) - final_new_start_datetime = final_params.get( + final_new_summary = result.params.get("new_summary", new_summary) + final_new_start_datetime = result.params.get( "new_start_datetime", new_start_datetime ) - final_new_end_datetime = final_params.get( + final_new_end_datetime = result.params.get( "new_end_datetime", new_end_datetime ) - final_new_description = final_params.get("new_description", new_description) - final_new_location = final_params.get("new_location", new_location) - final_new_attendees = final_params.get("new_attendees", new_attendees) + final_new_description = result.params.get("new_description", new_description) + final_new_location = result.params.get("new_location", new_location) + final_new_attendees = result.params.get("new_attendees", new_attendees) if not final_connector_id: return { diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py index a4fee0965..f36db8f3f 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_drive/create_file.py @@ -3,9 +3,9 @@ from typing import Any, Literal from googleapiclient.errors import HttpError from langchain_core.tools import tool -from langgraph.types import interrupt from sqlalchemy.ext.asyncio import AsyncSession +from app.agents.new_chat.tools.hitl import request_approval from app.connectors.google_drive.client import GoogleDriveClient from app.connectors.google_drive.file_types import GOOGLE_DOC, GOOGLE_SHEET from app.services.google_drive import GoogleDriveToolMetadataService @@ -99,58 +99,30 @@ def create_create_google_drive_file_tool( logger.info( f"Requesting approval for creating Google Drive file: name='{name}', type='{file_type}'" ) - approval = interrupt( - { - "type": "google_drive_file_creation", - "action": { - "tool": "create_google_drive_file", - "params": { - "name": name, - "file_type": file_type, - "content": content, - "connector_id": None, - "parent_folder_id": None, - }, - }, - "context": context, - } + result = request_approval( + action_type="google_drive_file_creation", + tool_name="create_google_drive_file", + params={ + "name": name, + "file_type": file_type, + "content": content, + "connector_id": None, + "parent_folder_id": None, + }, + context=context, ) - decisions_raw = ( - approval.get("decisions", []) if isinstance(approval, dict) else [] - ) - decisions = ( - decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] - ) - decisions = [d for d in decisions if isinstance(d, dict)] - if not decisions: - logger.warning("No approval decision received") - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - logger.info(f"User decision: {decision_type}") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", "message": "User declined. The file was not created. Do not ask again or suggest alternatives.", } - final_params: dict[str, Any] = {} - edited_action = decision.get("edited_action") - if isinstance(edited_action, dict): - edited_args = edited_action.get("args") - if isinstance(edited_args, dict): - final_params = edited_args - elif isinstance(decision.get("args"), dict): - final_params = decision["args"] - - final_name = final_params.get("name", name) - final_file_type = final_params.get("file_type", file_type) - final_content = final_params.get("content", content) - final_connector_id = final_params.get("connector_id") - final_parent_folder_id = final_params.get("parent_folder_id") + final_name = result.params.get("name", name) + final_file_type = result.params.get("file_type", file_type) + final_content = result.params.get("content", content) + final_connector_id = result.params.get("connector_id") + final_parent_folder_id = result.params.get("parent_folder_id") if not final_name or not final_name.strip(): return {"status": "error", "message": "File name cannot be empty."} diff --git a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py b/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py index fdf7f9cd3..832afff0d 100644 --- a/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/google_drive/trash_file.py @@ -3,9 +3,9 @@ from typing import Any from googleapiclient.errors import HttpError from langchain_core.tools import tool -from langgraph.types import interrupt from sqlalchemy.ext.asyncio import AsyncSession +from app.agents.new_chat.tools.hitl import request_approval from app.connectors.google_drive.client import GoogleDriveClient from app.services.google_drive import GoogleDriveToolMetadataService @@ -101,56 +101,28 @@ def create_delete_google_drive_file_tool( logger.info( f"Requesting approval for deleting Google Drive file: '{file_name}' (file_id={file_id}, delete_from_kb={delete_from_kb})" ) - approval = interrupt( - { - "type": "google_drive_file_trash", - "action": { - "tool": "delete_google_drive_file", - "params": { - "file_id": file_id, - "connector_id": connector_id_from_context, - "delete_from_kb": delete_from_kb, - }, - }, - "context": context, - } + result = request_approval( + action_type="google_drive_file_trash", + tool_name="delete_google_drive_file", + params={ + "file_id": file_id, + "connector_id": connector_id_from_context, + "delete_from_kb": delete_from_kb, + }, + context=context, ) - decisions_raw = ( - approval.get("decisions", []) if isinstance(approval, dict) else [] - ) - decisions = ( - decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] - ) - decisions = [d for d in decisions if isinstance(d, dict)] - if not decisions: - logger.warning("No approval decision received") - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - logger.info(f"User decision: {decision_type}") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", "message": "User declined. The file was not trashed. Do not ask again or suggest alternatives.", } - edited_action = decision.get("edited_action") - final_params: dict[str, Any] = {} - if isinstance(edited_action, dict): - edited_args = edited_action.get("args") - if isinstance(edited_args, dict): - final_params = edited_args - elif isinstance(decision.get("args"), dict): - final_params = decision["args"] - - final_file_id = final_params.get("file_id", file_id) - final_connector_id = final_params.get( + final_file_id = result.params.get("file_id", file_id) + final_connector_id = result.params.get( "connector_id", connector_id_from_context ) - final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb) + final_delete_from_kb = result.params.get("delete_from_kb", delete_from_kb) if not final_connector_id: return {