diff --git a/surfsense_backend/app/agents/new_chat/tools/jira/create_issue.py b/surfsense_backend/app/agents/new_chat/tools/jira/create_issue.py index d441c49f3..0b3332694 100644 --- a/surfsense_backend/app/agents/new_chat/tools/jira/create_issue.py +++ b/surfsense_backend/app/agents/new_chat/tools/jira/create_issue.py @@ -3,7 +3,7 @@ import logging from typing import Any 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.orm.attributes import flag_modified @@ -69,58 +69,32 @@ def create_create_jira_issue_tool( "connector_type": "jira", } - approval = interrupt( - { - "type": "jira_issue_creation", - "action": { - "tool": "create_jira_issue", - "params": { - "project_key": project_key, - "summary": summary, - "issue_type": issue_type, - "description": description, - "priority": priority, - "connector_id": connector_id, - }, - }, - "context": context, - } + result = request_approval( + action_type="jira_issue_creation", + tool_name="create_jira_issue", + params={ + "project_key": project_key, + "summary": summary, + "issue_type": issue_type, + "description": description, + "priority": priority, + "connector_id": connector_id, + }, + 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: - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", - "message": "User declined. The issue was not created.", + "message": "User declined. Do not retry 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_project_key = final_params.get("project_key", project_key) - final_summary = final_params.get("summary", summary) - final_issue_type = final_params.get("issue_type", issue_type) - final_description = final_params.get("description", description) - final_priority = final_params.get("priority", priority) - final_connector_id = final_params.get("connector_id", connector_id) + final_project_key = result.params.get("project_key", project_key) + final_summary = result.params.get("summary", summary) + final_issue_type = result.params.get("issue_type", issue_type) + final_description = result.params.get("description", description) + final_priority = result.params.get("priority", priority) + final_connector_id = result.params.get("connector_id", connector_id) if not final_summary or not final_summary.strip(): return {"status": "error", "message": "Issue summary cannot be empty."} diff --git a/surfsense_backend/app/agents/new_chat/tools/jira/delete_issue.py b/surfsense_backend/app/agents/new_chat/tools/jira/delete_issue.py index 2f8c370ad..52d4556a5 100644 --- a/surfsense_backend/app/agents/new_chat/tools/jira/delete_issue.py +++ b/surfsense_backend/app/agents/new_chat/tools/jira/delete_issue.py @@ -3,7 +3,7 @@ import logging from typing import Any 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.orm.attributes import flag_modified @@ -71,54 +71,28 @@ def create_delete_jira_issue_tool( document_id = issue_data["document_id"] connector_id_from_context = context.get("account", {}).get("id") - approval = interrupt( - { - "type": "jira_issue_deletion", - "action": { - "tool": "delete_jira_issue", - "params": { - "issue_key": issue_key, - "connector_id": connector_id_from_context, - "delete_from_kb": delete_from_kb, - }, - }, - "context": context, - } + result = request_approval( + action_type="jira_issue_deletion", + tool_name="delete_jira_issue", + params={ + "issue_key": issue_key, + "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: - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", - "message": "User declined. The issue was not deleted.", + "message": "User declined. Do not retry 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_issue_key = final_params.get("issue_key", issue_key) - final_connector_id = final_params.get( + final_issue_key = result.params.get("issue_key", issue_key) + 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) from sqlalchemy.future import select diff --git a/surfsense_backend/app/agents/new_chat/tools/jira/update_issue.py b/surfsense_backend/app/agents/new_chat/tools/jira/update_issue.py index c2b948ae3..9c676fea3 100644 --- a/surfsense_backend/app/agents/new_chat/tools/jira/update_issue.py +++ b/surfsense_backend/app/agents/new_chat/tools/jira/update_issue.py @@ -3,7 +3,7 @@ import logging from typing import Any 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.orm.attributes import flag_modified @@ -75,60 +75,34 @@ def create_update_jira_issue_tool( document_id = issue_data.get("document_id") connector_id_from_context = context.get("account", {}).get("id") - approval = interrupt( - { - "type": "jira_issue_update", - "action": { - "tool": "update_jira_issue", - "params": { - "issue_key": issue_key, - "document_id": document_id, - "new_summary": new_summary, - "new_description": new_description, - "new_priority": new_priority, - "connector_id": connector_id_from_context, - }, - }, - "context": context, - } + result = request_approval( + action_type="jira_issue_update", + tool_name="update_jira_issue", + params={ + "issue_key": issue_key, + "document_id": document_id, + "new_summary": new_summary, + "new_description": new_description, + "new_priority": new_priority, + "connector_id": connector_id_from_context, + }, + 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: - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", - "message": "User declined. The issue was not updated.", + "message": "User declined. Do not retry 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_issue_key = final_params.get("issue_key", issue_key) - final_summary = final_params.get("new_summary", new_summary) - final_description = final_params.get("new_description", new_description) - final_priority = final_params.get("new_priority", new_priority) - final_connector_id = final_params.get( + final_issue_key = result.params.get("issue_key", issue_key) + final_summary = result.params.get("new_summary", new_summary) + final_description = result.params.get("new_description", new_description) + final_priority = result.params.get("new_priority", new_priority) + final_connector_id = result.params.get( "connector_id", connector_id_from_context ) - final_document_id = final_params.get("document_id", document_id) + final_document_id = result.params.get("document_id", document_id) from sqlalchemy.future import select diff --git a/surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py b/surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py index 8dffb18dd..5050c7885 100644 --- a/surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/onedrive/create_file.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any 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.future import select @@ -145,54 +145,28 @@ def create_create_onedrive_file_tool( "parent_folders": parent_folders, } - approval = interrupt( - { - "type": "onedrive_file_creation", - "action": { - "tool": "create_onedrive_file", - "params": { - "name": name, - "content": content, - "connector_id": None, - "parent_folder_id": None, - }, - }, - "context": context, - } + result = request_approval( + action_type="onedrive_file_creation", + tool_name="create_onedrive_file", + params={ + "name": name, + "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: - return {"status": "error", "message": "No approval decision received"} - - decision = decisions[0] - decision_type = decision.get("type") or decision.get("decision_type") - - if decision_type == "reject": + if result.rejected: return { "status": "rejected", - "message": "User declined. The file was not created.", + "message": "User declined. Do not retry 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_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_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/onedrive/trash_file.py b/surfsense_backend/app/agents/new_chat/tools/onedrive/trash_file.py index 79d8222fd..6997e1d52 100644 --- a/surfsense_backend/app/agents/new_chat/tools/onedrive/trash_file.py +++ b/surfsense_backend/app/agents/new_chat/tools/onedrive/trash_file.py @@ -2,7 +2,7 @@ import logging from typing import Any from langchain_core.tools import tool -from langgraph.types import interrupt +from app.agents.new_chat.tools.hitl import request_approval from sqlalchemy import String, and_, cast, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select @@ -174,53 +174,26 @@ def create_delete_onedrive_file_tool( }, } - approval = interrupt( - { - "type": "onedrive_file_trash", - "action": { - "tool": "delete_onedrive_file", - "params": { - "file_id": file_id, - "connector_id": connector.id, - "delete_from_kb": delete_from_kb, - }, - }, - "context": context, - } + result = request_approval( + action_type="onedrive_file_trash", + tool_name="delete_onedrive_file", + params={ + "file_id": file_id, + "connector_id": connector.id, + "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: - 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.", + "message": "User declined. Do not retry 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_file_id = final_params.get("file_id", file_id) - final_connector_id = final_params.get("connector_id", connector.id) - final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb) + final_file_id = result.params.get("file_id", file_id) + final_connector_id = result.params.get("connector_id", connector.id) + final_delete_from_kb = result.params.get("delete_from_kb", delete_from_kb) if final_connector_id != connector.id: result = await db_session.execute(