refactor: replace interrupt calls with request_approval utility in Linear and Notion tools

Updated the create, delete, and update functions in Linear and Notion tools to utilize the new request_approval utility for handling user approvals. This change improves 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:17:36 +05:30
parent 4875fd9211
commit 8d8ba6cbe8
6 changed files with 114 additions and 296 deletions

View file

@ -2,7 +2,7 @@ import logging
from typing import Any from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt from app.agents.new_chat.tools.hitl import request_approval
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.connectors.linear_connector import LinearAPIError, LinearConnector from app.connectors.linear_connector import LinearAPIError, LinearConnector
@ -94,65 +94,37 @@ def create_create_linear_issue_tool(
} }
logger.info(f"Requesting approval for creating Linear issue: '{title}'") logger.info(f"Requesting approval for creating Linear issue: '{title}'")
approval = interrupt( result = request_approval(
{ action_type="linear_issue_creation",
"type": "linear_issue_creation", tool_name="create_linear_issue",
"action": { params={
"tool": "create_linear_issue", "title": title,
"params": { "description": description,
"title": title, "team_id": None,
"description": description, "state_id": None,
"team_id": None, "assignee_id": None,
"state_id": None, "priority": None,
"assignee_id": None, "label_ids": [],
"priority": None, "connector_id": connector_id,
"label_ids": [], },
"connector_id": connector_id, 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":
logger.info("Linear issue creation rejected by user") logger.info("Linear issue creation rejected by user")
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The issue was not created. Do not ask again or suggest alternatives.", "message": "User declined. Do not retry or suggest alternatives.",
} }
final_params: dict[str, Any] = {} final_title = result.params.get("title", title)
edited_action = decision.get("edited_action") final_description = result.params.get("description", description)
if isinstance(edited_action, dict): final_team_id = result.params.get("team_id")
edited_args = edited_action.get("args") final_state_id = result.params.get("state_id")
if isinstance(edited_args, dict): final_assignee_id = result.params.get("assignee_id")
final_params = edited_args final_priority = result.params.get("priority")
elif isinstance(decision.get("args"), dict): final_label_ids = result.params.get("label_ids") or []
final_params = decision["args"] final_connector_id = result.params.get("connector_id", connector_id)
final_title = final_params.get("title", title)
final_description = final_params.get("description", description)
final_team_id = final_params.get("team_id")
final_state_id = final_params.get("state_id")
final_assignee_id = final_params.get("assignee_id")
final_priority = final_params.get("priority")
final_label_ids = final_params.get("label_ids") or []
final_connector_id = final_params.get("connector_id", connector_id)
if not final_title or not final_title.strip(): if not final_title or not final_title.strip():
logger.error("Title is empty or contains only whitespace") logger.error("Title is empty or contains only whitespace")

View file

@ -2,7 +2,7 @@ import logging
from typing import Any from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt from app.agents.new_chat.tools.hitl import request_approval
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.connectors.linear_connector import LinearAPIError, LinearConnector from app.connectors.linear_connector import LinearAPIError, LinearConnector
@ -114,57 +114,29 @@ def create_delete_linear_issue_tool(
f"Requesting approval for deleting Linear issue: '{issue_ref}' " f"Requesting approval for deleting Linear issue: '{issue_ref}' "
f"(id={issue_id}, delete_from_kb={delete_from_kb})" f"(id={issue_id}, delete_from_kb={delete_from_kb})"
) )
approval = interrupt( result = request_approval(
{ action_type="linear_issue_deletion",
"type": "linear_issue_deletion", tool_name="delete_linear_issue",
"action": { params={
"tool": "delete_linear_issue", "issue_id": issue_id,
"params": { "connector_id": connector_id_from_context,
"issue_id": issue_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":
logger.info("Linear issue deletion rejected by user") logger.info("Linear issue deletion rejected by user")
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The issue was not deleted. Do not ask again or suggest alternatives.", "message": "User declined. Do not retry or suggest alternatives.",
} }
edited_action = decision.get("edited_action") final_issue_id = result.params.get("issue_id", issue_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_issue_id = final_params.get("issue_id", issue_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)
logger.info( logger.info(
f"Deleting Linear issue with final params: issue_id={final_issue_id}, " f"Deleting Linear issue with final params: issue_id={final_issue_id}, "

View file

@ -2,7 +2,7 @@ import logging
from typing import Any from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt from app.agents.new_chat.tools.hitl import request_approval
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.connectors.linear_connector import LinearAPIError, LinearConnector from app.connectors.linear_connector import LinearAPIError, LinearConnector
@ -130,69 +130,41 @@ def create_update_linear_issue_tool(
logger.info( logger.info(
f"Requesting approval for updating Linear issue: '{issue_ref}' (id={issue_id})" f"Requesting approval for updating Linear issue: '{issue_ref}' (id={issue_id})"
) )
approval = interrupt( result = request_approval(
{ action_type="linear_issue_update",
"type": "linear_issue_update", tool_name="update_linear_issue",
"action": { params={
"tool": "update_linear_issue", "issue_id": issue_id,
"params": { "document_id": document_id,
"issue_id": issue_id, "new_title": new_title,
"document_id": document_id, "new_description": new_description,
"new_title": new_title, "new_state_id": new_state_id,
"new_description": new_description, "new_assignee_id": new_assignee_id,
"new_state_id": new_state_id, "new_priority": new_priority,
"new_assignee_id": new_assignee_id, "new_label_ids": new_label_ids,
"new_priority": new_priority, "connector_id": connector_id_from_context,
"new_label_ids": new_label_ids, },
"connector_id": connector_id_from_context, 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":
logger.info("Linear issue update rejected by user") logger.info("Linear issue update rejected by user")
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The issue was not updated. Do not ask again or suggest alternatives.", "message": "User declined. Do not retry or suggest alternatives.",
} }
edited_action = decision.get("edited_action") final_issue_id = result.params.get("issue_id", issue_id)
final_params: dict[str, Any] = {} final_document_id = result.params.get("document_id", document_id)
if isinstance(edited_action, dict): final_new_title = result.params.get("new_title", new_title)
edited_args = edited_action.get("args") final_new_description = result.params.get("new_description", new_description)
if isinstance(edited_args, dict): final_new_state_id = result.params.get("new_state_id", new_state_id)
final_params = edited_args final_new_assignee_id = result.params.get("new_assignee_id", new_assignee_id)
elif isinstance(decision.get("args"), dict): final_new_priority = result.params.get("new_priority", new_priority)
final_params = decision["args"] final_new_label_ids: list[str] | None = result.params.get(
final_issue_id = final_params.get("issue_id", issue_id)
final_document_id = final_params.get("document_id", document_id)
final_new_title = final_params.get("new_title", new_title)
final_new_description = final_params.get("new_description", new_description)
final_new_state_id = final_params.get("new_state_id", new_state_id)
final_new_assignee_id = final_params.get("new_assignee_id", new_assignee_id)
final_new_priority = final_params.get("new_priority", new_priority)
final_new_label_ids: list[str] | None = final_params.get(
"new_label_ids", new_label_ids "new_label_ids", new_label_ids
) )
final_connector_id = final_params.get( final_connector_id = result.params.get(
"connector_id", connector_id_from_context "connector_id", connector_id_from_context
) )

View file

@ -2,7 +2,7 @@ import logging
from typing import Any from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt from app.agents.new_chat.tools.hitl import request_approval
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
@ -99,61 +99,29 @@ def create_create_notion_page_tool(
} }
logger.info(f"Requesting approval for creating Notion page: '{title}'") logger.info(f"Requesting approval for creating Notion page: '{title}'")
approval = interrupt( result = request_approval(
{ action_type="notion_page_creation",
"type": "notion_page_creation", tool_name="create_notion_page",
"action": { params={
"tool": "create_notion_page", "title": title,
"params": { "content": content,
"title": title, "parent_page_id": None,
"content": content, "connector_id": connector_id,
"parent_page_id": None, },
"connector_id": connector_id, 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":
logger.info("Notion page creation rejected by user") logger.info("Notion page creation rejected by user")
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The page was not created. Do not ask again or suggest alternatives.", "message": "User declined. Do not retry or suggest alternatives.",
} }
edited_action = decision.get("edited_action") final_title = result.params.get("title", title)
final_params: dict[str, Any] = {} final_content = result.params.get("content", content)
if isinstance(edited_action, dict): final_parent_page_id = result.params.get("parent_page_id")
edited_args = edited_action.get("args") final_connector_id = result.params.get("connector_id", connector_id)
if isinstance(edited_args, dict):
final_params = edited_args
elif isinstance(decision.get("args"), dict):
# Some interrupt payloads place args directly on the decision.
final_params = decision["args"]
final_title = final_params.get("title", title)
final_content = final_params.get("content", content)
final_parent_page_id = final_params.get("parent_page_id")
final_connector_id = final_params.get("connector_id", connector_id)
if not final_title or not final_title.strip(): if not final_title or not final_title.strip():
logger.error("Title is empty or contains only whitespace") logger.error("Title is empty or contains only whitespace")

View file

@ -2,7 +2,7 @@ import logging
from typing import Any from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt from app.agents.new_chat.tools.hitl import request_approval
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
@ -114,63 +114,29 @@ def create_delete_notion_page_tool(
f"Requesting approval for deleting Notion page: '{page_title}' (page_id={page_id}, delete_from_kb={delete_from_kb})" f"Requesting approval for deleting Notion page: '{page_title}' (page_id={page_id}, delete_from_kb={delete_from_kb})"
) )
# Request approval before deleting result = request_approval(
approval = interrupt( action_type="notion_page_deletion",
{ tool_name="delete_notion_page",
"type": "notion_page_deletion", params={
"action": { "page_id": page_id,
"tool": "delete_notion_page", "connector_id": connector_id_from_context,
"params": { "delete_from_kb": delete_from_kb,
"page_id": page_id, },
"connector_id": connector_id_from_context, context=context,
"delete_from_kb": delete_from_kb,
},
},
"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":
logger.info("Notion page deletion rejected by user") logger.info("Notion page deletion rejected by user")
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The page was not deleted. Do not ask again or suggest alternatives.", "message": "User declined. Do not retry or suggest alternatives.",
} }
# Extract edited action arguments (if user modified the checkbox) final_page_id = result.params.get("page_id", page_id)
edited_action = decision.get("edited_action") final_connector_id = result.params.get(
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):
# Some interrupt payloads place args directly on the decision.
final_params = decision["args"]
final_page_id = final_params.get("page_id", page_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)
logger.info( logger.info(
f"Deleting Notion page with final params: page_id={final_page_id}, connector_id={final_connector_id}, delete_from_kb={final_delete_from_kb}" f"Deleting Notion page with final params: page_id={final_page_id}, connector_id={final_connector_id}, delete_from_kb={final_delete_from_kb}"

View file

@ -2,7 +2,7 @@ import logging
from typing import Any from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.types import interrupt from app.agents.new_chat.tools.hitl import request_approval
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
@ -127,59 +127,27 @@ def create_update_notion_page_tool(
logger.info( logger.info(
f"Requesting approval for updating Notion page: '{page_title}' (page_id={page_id})" f"Requesting approval for updating Notion page: '{page_title}' (page_id={page_id})"
) )
approval = interrupt( result = request_approval(
{ action_type="notion_page_update",
"type": "notion_page_update", tool_name="update_notion_page",
"action": { params={
"tool": "update_notion_page", "page_id": page_id,
"params": { "content": content,
"page_id": page_id, "connector_id": connector_id_from_context,
"content": content, },
"connector_id": connector_id_from_context, 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":
logger.info("Notion page update rejected by user") logger.info("Notion page update rejected by user")
return { return {
"status": "rejected", "status": "rejected",
"message": "User declined. The page was not updated. Do not ask again or suggest alternatives.", "message": "User declined. Do not retry or suggest alternatives.",
} }
edited_action = decision.get("edited_action") final_page_id = result.params.get("page_id", page_id)
final_params: dict[str, Any] = {} final_content = result.params.get("content", content)
if isinstance(edited_action, dict): final_connector_id = result.params.get(
edited_args = edited_action.get("args")
if isinstance(edited_args, dict):
final_params = edited_args
elif isinstance(decision.get("args"), dict):
# Some interrupt payloads place args directly on the decision.
final_params = decision["args"]
final_page_id = final_params.get("page_id", page_id)
final_content = final_params.get("content", content)
final_connector_id = final_params.get(
"connector_id", connector_id_from_context "connector_id", connector_id_from_context
) )