refactor: replace interrupt calls with request_approval utility in Google Calendar and Drive tools

Updated the create, delete, and update functions in Google Calendar and Google Drive tools to utilize the new request_approval utility for handling user approvals. This change enhances code consistency and simplifies decision handling by directly merging parameters from the approval response.
This commit is contained in:
Anish Sarkar 2026-04-13 20:16:09 +05:30
parent 85baaacd0a
commit 2f59fc9c72
5 changed files with 94 additions and 234 deletions

View file

@ -6,9 +6,9 @@ from typing import Any
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build from googleapiclient.discovery import build
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.new_chat.tools.hitl import request_approval
from app.services.google_calendar import GoogleCalendarToolMetadataService from app.services.google_calendar import GoogleCalendarToolMetadataService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -90,63 +90,35 @@ def create_create_calendar_event_tool(
logger.info( logger.info(
f"Requesting approval for creating calendar event: summary='{summary}'" f"Requesting approval for creating calendar event: summary='{summary}'"
) )
approval = interrupt( result = request_approval(
{ action_type="google_calendar_event_creation",
"type": "google_calendar_event_creation", tool_name="create_calendar_event",
"action": { params={
"tool": "create_calendar_event", "summary": summary,
"params": { "start_datetime": start_datetime,
"summary": summary, "end_datetime": end_datetime,
"start_datetime": start_datetime, "description": description,
"end_datetime": end_datetime, "location": location,
"description": description, "attendees": attendees,
"location": location, "timezone": context.get("timezone"),
"attendees": attendees, "connector_id": None,
"timezone": context.get("timezone"), },
"connector_id": None, context=context,
},
},
"context": context,
}
) )
decisions_raw = ( if result.rejected:
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":
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The event was not created. Do not ask again or suggest alternatives.", "message": "User declined. The event was not created. Do not ask again or suggest alternatives.",
} }
final_params: dict[str, Any] = {} final_summary = result.params.get("summary", summary)
edited_action = decision.get("edited_action") final_start_datetime = result.params.get("start_datetime", start_datetime)
if isinstance(edited_action, dict): final_end_datetime = result.params.get("end_datetime", end_datetime)
edited_args = edited_action.get("args") final_description = result.params.get("description", description)
if isinstance(edited_args, dict): final_location = result.params.get("location", location)
final_params = edited_args final_attendees = result.params.get("attendees", attendees)
elif isinstance(decision.get("args"), dict): final_connector_id = result.params.get("connector_id")
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")
if not final_summary or not final_summary.strip(): if not final_summary or not final_summary.strip():
return {"status": "error", "message": "Event summary cannot be empty."} return {"status": "error", "message": "Event summary cannot be empty."}

View file

@ -6,9 +6,9 @@ from typing import Any
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build from googleapiclient.discovery import build
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.new_chat.tools.hitl import request_approval
from app.services.google_calendar import GoogleCalendarToolMetadataService from app.services.google_calendar import GoogleCalendarToolMetadataService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -100,56 +100,28 @@ def create_delete_calendar_event_tool(
logger.info( logger.info(
f"Requesting approval for deleting calendar event: '{event_title_or_id}' (event_id={event_id}, delete_from_kb={delete_from_kb})" f"Requesting approval for deleting calendar event: '{event_title_or_id}' (event_id={event_id}, delete_from_kb={delete_from_kb})"
) )
approval = interrupt( result = request_approval(
{ action_type="google_calendar_event_deletion",
"type": "google_calendar_event_deletion", tool_name="delete_calendar_event",
"action": { params={
"tool": "delete_calendar_event", "event_id": event_id,
"params": { "connector_id": connector_id_from_context,
"event_id": event_id, "delete_from_kb": delete_from_kb,
"connector_id": connector_id_from_context, },
"delete_from_kb": delete_from_kb, context=context,
},
},
"context": context,
}
) )
decisions_raw = ( if result.rejected:
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":
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The event was not deleted. Do not ask again or suggest alternatives.", "message": "User declined. The event was not deleted. Do not ask again or suggest alternatives.",
} }
edited_action = decision.get("edited_action") final_event_id = result.params.get("event_id", event_id)
final_params: dict[str, Any] = {} final_connector_id = result.params.get(
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(
"connector_id", connector_id_from_context "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: if not final_connector_id:
return { return {

View file

@ -6,9 +6,9 @@ from typing import Any
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build from googleapiclient.discovery import build
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.new_chat.tools.hitl import request_approval
from app.services.google_calendar import GoogleCalendarToolMetadataService from app.services.google_calendar import GoogleCalendarToolMetadataService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -116,71 +116,43 @@ def create_update_calendar_event_tool(
logger.info( logger.info(
f"Requesting approval for updating calendar event: '{event_title_or_id}' (event_id={event_id})" f"Requesting approval for updating calendar event: '{event_title_or_id}' (event_id={event_id})"
) )
approval = interrupt( result = request_approval(
{ action_type="google_calendar_event_update",
"type": "google_calendar_event_update", tool_name="update_calendar_event",
"action": { params={
"tool": "update_calendar_event", "event_id": event_id,
"params": { "document_id": document_id,
"event_id": event_id, "connector_id": connector_id_from_context,
"document_id": document_id, "new_summary": new_summary,
"connector_id": connector_id_from_context, "new_start_datetime": new_start_datetime,
"new_summary": new_summary, "new_end_datetime": new_end_datetime,
"new_start_datetime": new_start_datetime, "new_description": new_description,
"new_end_datetime": new_end_datetime, "new_location": new_location,
"new_description": new_description, "new_attendees": new_attendees,
"new_location": new_location, },
"new_attendees": new_attendees, context=context,
},
},
"context": context,
}
) )
decisions_raw = ( if result.rejected:
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":
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The event was not updated. Do not ask again or suggest alternatives.", "message": "User declined. The event was not updated. Do not ask again or suggest alternatives.",
} }
edited_action = decision.get("edited_action") final_event_id = result.params.get("event_id", event_id)
final_params: dict[str, Any] = {} final_connector_id = result.params.get(
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(
"connector_id", connector_id_from_context "connector_id", connector_id_from_context
) )
final_new_summary = final_params.get("new_summary", new_summary) final_new_summary = result.params.get("new_summary", new_summary)
final_new_start_datetime = final_params.get( final_new_start_datetime = result.params.get(
"new_start_datetime", new_start_datetime "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 "new_end_datetime", new_end_datetime
) )
final_new_description = final_params.get("new_description", new_description) final_new_description = result.params.get("new_description", new_description)
final_new_location = final_params.get("new_location", new_location) final_new_location = result.params.get("new_location", new_location)
final_new_attendees = final_params.get("new_attendees", new_attendees) final_new_attendees = result.params.get("new_attendees", new_attendees)
if not final_connector_id: if not final_connector_id:
return { return {

View file

@ -3,9 +3,9 @@ from typing import Any, Literal
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt
from sqlalchemy.ext.asyncio import AsyncSession 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.client import GoogleDriveClient
from app.connectors.google_drive.file_types import GOOGLE_DOC, GOOGLE_SHEET from app.connectors.google_drive.file_types import GOOGLE_DOC, GOOGLE_SHEET
from app.services.google_drive import GoogleDriveToolMetadataService from app.services.google_drive import GoogleDriveToolMetadataService
@ -99,58 +99,30 @@ def create_create_google_drive_file_tool(
logger.info( logger.info(
f"Requesting approval for creating Google Drive file: name='{name}', type='{file_type}'" f"Requesting approval for creating Google Drive file: name='{name}', type='{file_type}'"
) )
approval = interrupt( result = request_approval(
{ action_type="google_drive_file_creation",
"type": "google_drive_file_creation", tool_name="create_google_drive_file",
"action": { params={
"tool": "create_google_drive_file", "name": name,
"params": { "file_type": file_type,
"name": name, "content": content,
"file_type": file_type, "connector_id": None,
"content": content, "parent_folder_id": None,
"connector_id": None, },
"parent_folder_id": None, context=context,
},
},
"context": context,
}
) )
decisions_raw = ( if result.rejected:
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":
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The file was not created. Do not ask again or suggest alternatives.", "message": "User declined. The file was not created. Do not ask again or suggest alternatives.",
} }
final_params: dict[str, Any] = {} final_name = result.params.get("name", name)
edited_action = decision.get("edited_action") final_file_type = result.params.get("file_type", file_type)
if isinstance(edited_action, dict): final_content = result.params.get("content", content)
edited_args = edited_action.get("args") final_connector_id = result.params.get("connector_id")
if isinstance(edited_args, dict): final_parent_folder_id = result.params.get("parent_folder_id")
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")
if not final_name or not final_name.strip(): if not final_name or not final_name.strip():
return {"status": "error", "message": "File name cannot be empty."} return {"status": "error", "message": "File name cannot be empty."}

View file

@ -3,9 +3,9 @@ from typing import Any
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt
from sqlalchemy.ext.asyncio import AsyncSession 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.client import GoogleDriveClient
from app.services.google_drive import GoogleDriveToolMetadataService from app.services.google_drive import GoogleDriveToolMetadataService
@ -101,56 +101,28 @@ def create_delete_google_drive_file_tool(
logger.info( logger.info(
f"Requesting approval for deleting Google Drive file: '{file_name}' (file_id={file_id}, delete_from_kb={delete_from_kb})" f"Requesting approval for deleting Google Drive file: '{file_name}' (file_id={file_id}, delete_from_kb={delete_from_kb})"
) )
approval = interrupt( result = request_approval(
{ action_type="google_drive_file_trash",
"type": "google_drive_file_trash", tool_name="delete_google_drive_file",
"action": { params={
"tool": "delete_google_drive_file", "file_id": file_id,
"params": { "connector_id": connector_id_from_context,
"file_id": file_id, "delete_from_kb": delete_from_kb,
"connector_id": connector_id_from_context, },
"delete_from_kb": delete_from_kb, context=context,
},
},
"context": context,
}
) )
decisions_raw = ( if result.rejected:
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":
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The file was not trashed. Do not ask again or suggest alternatives.", "message": "User declined. The file was not trashed. Do not ask again or suggest alternatives.",
} }
edited_action = decision.get("edited_action") final_file_id = result.params.get("file_id", file_id)
final_params: dict[str, Any] = {} final_connector_id = result.params.get(
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(
"connector_id", connector_id_from_context "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: if not final_connector_id:
return { return {