mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-01 11:56:25 +02:00
feat: add OneDrive file creation and deletion tools with connector checks
This commit is contained in:
parent
5bddde60cb
commit
5f0a4d1a0f
6 changed files with 410 additions and 0 deletions
|
|
@ -305,6 +305,12 @@ async def create_surfsense_deep_agent(
|
||||||
]
|
]
|
||||||
modified_disabled_tools.extend(google_drive_tools)
|
modified_disabled_tools.extend(google_drive_tools)
|
||||||
|
|
||||||
|
has_onedrive_connector = (
|
||||||
|
available_connectors is not None and "ONEDRIVE_FILE" in available_connectors
|
||||||
|
)
|
||||||
|
if not has_onedrive_connector:
|
||||||
|
modified_disabled_tools.extend(["create_onedrive_file", "delete_onedrive_file"])
|
||||||
|
|
||||||
# Disable Google Calendar action tools if no Google Calendar connector is configured
|
# Disable Google Calendar action tools if no Google Calendar connector is configured
|
||||||
has_google_calendar_connector = (
|
has_google_calendar_connector = (
|
||||||
available_connectors is not None
|
available_connectors is not None
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ _HITL_TOOL_DEDUP_KEYS: dict[str, str] = {
|
||||||
"trash_gmail_email": "email_subject_or_id",
|
"trash_gmail_email": "email_subject_or_id",
|
||||||
"update_gmail_draft": "draft_subject_or_id",
|
"update_gmail_draft": "draft_subject_or_id",
|
||||||
"delete_google_drive_file": "file_name",
|
"delete_google_drive_file": "file_name",
|
||||||
|
"delete_onedrive_file": "file_name",
|
||||||
"delete_notion_page": "page_title",
|
"delete_notion_page": "page_title",
|
||||||
"update_notion_page": "page_title",
|
"update_notion_page": "page_title",
|
||||||
"delete_linear_issue": "issue_ref",
|
"delete_linear_issue": "issue_ref",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from app.agents.new_chat.tools.onedrive.create_file import (
|
||||||
|
create_create_onedrive_file_tool,
|
||||||
|
)
|
||||||
|
from app.agents.new_chat.tools.onedrive.trash_file import (
|
||||||
|
create_delete_onedrive_file_tool,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"create_create_onedrive_file_tool",
|
||||||
|
"create_delete_onedrive_file_tool",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.types import interrupt
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
|
from app.connectors.onedrive.client import OneDriveClient
|
||||||
|
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_create_onedrive_file_tool(
|
||||||
|
db_session: AsyncSession | None = None,
|
||||||
|
search_space_id: int | None = None,
|
||||||
|
user_id: str | None = None,
|
||||||
|
):
|
||||||
|
@tool
|
||||||
|
async def create_onedrive_file(
|
||||||
|
name: str,
|
||||||
|
content: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Create a new file in Microsoft OneDrive.
|
||||||
|
|
||||||
|
Use this tool when the user explicitly asks to create a new document
|
||||||
|
in OneDrive. The user MUST specify a topic before you call this tool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The file name (with extension, e.g. "notes.txt" or "report.docx").
|
||||||
|
content: Optional initial content as plain text or markdown.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with status, file_id, name, web_url, and message.
|
||||||
|
"""
|
||||||
|
logger.info(f"create_onedrive_file called: name='{name}'")
|
||||||
|
|
||||||
|
if db_session is None or search_space_id is None or user_id is None:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "OneDrive tool not properly configured.",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(SearchSourceConnector).filter(
|
||||||
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
|
SearchSourceConnector.user_id == user_id,
|
||||||
|
SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connectors = result.scalars().all()
|
||||||
|
|
||||||
|
if not connectors:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "No OneDrive connector found. Please connect OneDrive in your workspace settings.",
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts = []
|
||||||
|
for c in connectors:
|
||||||
|
cfg = c.config or {}
|
||||||
|
accounts.append({
|
||||||
|
"id": c.id,
|
||||||
|
"name": c.name,
|
||||||
|
"user_email": cfg.get("user_email"),
|
||||||
|
"auth_expired": cfg.get("auth_expired", False),
|
||||||
|
})
|
||||||
|
|
||||||
|
if all(a.get("auth_expired") for a in accounts):
|
||||||
|
return {
|
||||||
|
"status": "auth_error",
|
||||||
|
"message": "All connected OneDrive accounts need re-authentication.",
|
||||||
|
"connector_type": "onedrive",
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {"accounts": accounts}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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":
|
||||||
|
return {
|
||||||
|
"status": "rejected",
|
||||||
|
"message": "User declined. The file was not created.",
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
if not final_name or not final_name.strip():
|
||||||
|
return {"status": "error", "message": "File name cannot be empty."}
|
||||||
|
|
||||||
|
if final_connector_id is not None:
|
||||||
|
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.ONEDRIVE_CONNECTOR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connector = result.scalars().first()
|
||||||
|
else:
|
||||||
|
connector = connectors[0]
|
||||||
|
|
||||||
|
if not connector:
|
||||||
|
return {"status": "error", "message": "Selected OneDrive connector is invalid."}
|
||||||
|
|
||||||
|
client = OneDriveClient(session=db_session, connector_id=connector.id)
|
||||||
|
created = await client.create_file(
|
||||||
|
name=final_name,
|
||||||
|
parent_id=final_parent_folder_id,
|
||||||
|
content=final_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"OneDrive file created: id={created.get('id')}, name={created.get('name')}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"file_id": created.get("id"),
|
||||||
|
"name": created.get("name"),
|
||||||
|
"web_url": created.get("webUrl"),
|
||||||
|
"message": f"Successfully created '{created.get('name')}' in OneDrive.",
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
|
if isinstance(e, GraphInterrupt):
|
||||||
|
raise
|
||||||
|
logger.error(f"Error creating OneDrive file: {e}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Something went wrong while creating the file. Please try again.",
|
||||||
|
}
|
||||||
|
|
||||||
|
return create_onedrive_file
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.types import interrupt
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
|
from app.connectors.onedrive.client import OneDriveClient
|
||||||
|
from app.db import (
|
||||||
|
Document,
|
||||||
|
DocumentType,
|
||||||
|
SearchSourceConnector,
|
||||||
|
SearchSourceConnectorType,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_delete_onedrive_file_tool(
|
||||||
|
db_session: AsyncSession | None = None,
|
||||||
|
search_space_id: int | None = None,
|
||||||
|
user_id: str | None = None,
|
||||||
|
):
|
||||||
|
@tool
|
||||||
|
async def delete_onedrive_file(
|
||||||
|
file_name: str,
|
||||||
|
delete_from_kb: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Move a OneDrive file to the recycle bin.
|
||||||
|
|
||||||
|
Use this tool when the user explicitly asks to delete, remove, or trash
|
||||||
|
a file in OneDrive.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_name: The exact name of the file to trash.
|
||||||
|
delete_from_kb: Whether to also remove the file from the knowledge base.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with status, file_id, deleted_from_kb, and message.
|
||||||
|
"""
|
||||||
|
logger.info(f"delete_onedrive_file called: file_name='{file_name}', delete_from_kb={delete_from_kb}")
|
||||||
|
|
||||||
|
if db_session is None or search_space_id is None or user_id is None:
|
||||||
|
return {"status": "error", "message": "OneDrive tool not properly configured."}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sqlalchemy import String, cast
|
||||||
|
|
||||||
|
doc_result = await db_session.execute(
|
||||||
|
select(Document).where(
|
||||||
|
Document.search_space_id == search_space_id,
|
||||||
|
Document.document_type == DocumentType.ONEDRIVE_FILE,
|
||||||
|
Document.title == file_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
document = doc_result.scalars().first()
|
||||||
|
|
||||||
|
if not document:
|
||||||
|
doc_result = await db_session.execute(
|
||||||
|
select(Document).where(
|
||||||
|
Document.search_space_id == search_space_id,
|
||||||
|
Document.document_type == DocumentType.ONEDRIVE_FILE,
|
||||||
|
cast(Document.document_metadata["onedrive_file_name"], String) == file_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
document = doc_result.scalars().first()
|
||||||
|
|
||||||
|
if not document:
|
||||||
|
return {"status": "not_found", "message": f"File '{file_name}' not found in your OneDrive knowledge base."}
|
||||||
|
|
||||||
|
meta = document.document_metadata or {}
|
||||||
|
file_id = meta.get("onedrive_file_id")
|
||||||
|
connector_id = meta.get("connector_id")
|
||||||
|
document_id = document.id
|
||||||
|
|
||||||
|
if not file_id:
|
||||||
|
return {"status": "error", "message": "File ID is missing. Please re-index the file."}
|
||||||
|
|
||||||
|
conn_result = await db_session.execute(
|
||||||
|
select(SearchSourceConnector).filter(
|
||||||
|
SearchSourceConnector.id == connector_id,
|
||||||
|
SearchSourceConnector.connector_type == SearchSourceConnectorType.ONEDRIVE_CONNECTOR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connector = conn_result.scalars().first()
|
||||||
|
if not connector:
|
||||||
|
return {"status": "error", "message": "OneDrive connector not found for this file."}
|
||||||
|
|
||||||
|
cfg = connector.config or {}
|
||||||
|
if cfg.get("auth_expired"):
|
||||||
|
return {
|
||||||
|
"status": "auth_error",
|
||||||
|
"message": "OneDrive account needs re-authentication.",
|
||||||
|
"connector_type": "onedrive",
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"file": {
|
||||||
|
"file_id": file_id,
|
||||||
|
"name": file_name,
|
||||||
|
"document_id": document_id,
|
||||||
|
"web_url": meta.get("web_url"),
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": connector.id,
|
||||||
|
"name": connector.name,
|
||||||
|
"user_email": cfg.get("user_email"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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":
|
||||||
|
return {"status": "rejected", "message": "User declined. The file was not trashed."}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
client = OneDriveClient(session=db_session, connector_id=final_connector_id)
|
||||||
|
await client.trash_file(final_file_id)
|
||||||
|
|
||||||
|
logger.info(f"OneDrive file deleted (moved to recycle bin): file_id={final_file_id}")
|
||||||
|
|
||||||
|
trash_result: dict[str, Any] = {
|
||||||
|
"status": "success",
|
||||||
|
"file_id": final_file_id,
|
||||||
|
"message": f"Successfully moved '{file_name}' to the recycle bin.",
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted_from_kb = False
|
||||||
|
if final_delete_from_kb and document_id:
|
||||||
|
try:
|
||||||
|
doc_result = await db_session.execute(
|
||||||
|
select(Document).filter(Document.id == document_id)
|
||||||
|
)
|
||||||
|
doc = doc_result.scalars().first()
|
||||||
|
if doc:
|
||||||
|
await db_session.delete(doc)
|
||||||
|
await db_session.commit()
|
||||||
|
deleted_from_kb = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to delete document from KB: {e}")
|
||||||
|
await db_session.rollback()
|
||||||
|
|
||||||
|
trash_result["deleted_from_kb"] = deleted_from_kb
|
||||||
|
if deleted_from_kb:
|
||||||
|
trash_result["message"] += " (also removed from knowledge base)"
|
||||||
|
|
||||||
|
return trash_result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
|
if isinstance(e, GraphInterrupt):
|
||||||
|
raise
|
||||||
|
logger.error(f"Error deleting OneDrive file: {e}", exc_info=True)
|
||||||
|
return {"status": "error", "message": "Something went wrong while trashing the file."}
|
||||||
|
|
||||||
|
return delete_onedrive_file
|
||||||
|
|
@ -83,6 +83,10 @@ from .notion import (
|
||||||
create_delete_notion_page_tool,
|
create_delete_notion_page_tool,
|
||||||
create_update_notion_page_tool,
|
create_update_notion_page_tool,
|
||||||
)
|
)
|
||||||
|
from .onedrive import (
|
||||||
|
create_create_onedrive_file_tool,
|
||||||
|
create_delete_onedrive_file_tool,
|
||||||
|
)
|
||||||
from .podcast import create_generate_podcast_tool
|
from .podcast import create_generate_podcast_tool
|
||||||
from .report import create_generate_report_tool
|
from .report import create_generate_report_tool
|
||||||
from .scrape_webpage import create_scrape_webpage_tool
|
from .scrape_webpage import create_scrape_webpage_tool
|
||||||
|
|
@ -354,6 +358,30 @@ BUILTIN_TOOLS: list[ToolDefinition] = [
|
||||||
requires=["db_session", "search_space_id", "user_id"],
|
requires=["db_session", "search_space_id", "user_id"],
|
||||||
),
|
),
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
# ONEDRIVE TOOLS - create and trash files
|
||||||
|
# Auto-disabled when no OneDrive connector is configured (see chat_deepagent.py)
|
||||||
|
# =========================================================================
|
||||||
|
ToolDefinition(
|
||||||
|
name="create_onedrive_file",
|
||||||
|
description="Create a new file in Microsoft OneDrive",
|
||||||
|
factory=lambda deps: create_create_onedrive_file_tool(
|
||||||
|
db_session=deps["db_session"],
|
||||||
|
search_space_id=deps["search_space_id"],
|
||||||
|
user_id=deps["user_id"],
|
||||||
|
),
|
||||||
|
requires=["db_session", "search_space_id", "user_id"],
|
||||||
|
),
|
||||||
|
ToolDefinition(
|
||||||
|
name="delete_onedrive_file",
|
||||||
|
description="Move a OneDrive file to the recycle bin",
|
||||||
|
factory=lambda deps: create_delete_onedrive_file_tool(
|
||||||
|
db_session=deps["db_session"],
|
||||||
|
search_space_id=deps["search_space_id"],
|
||||||
|
user_id=deps["user_id"],
|
||||||
|
),
|
||||||
|
requires=["db_session", "search_space_id", "user_id"],
|
||||||
|
),
|
||||||
|
# =========================================================================
|
||||||
# GOOGLE CALENDAR TOOLS - create, update, delete events
|
# GOOGLE CALENDAR TOOLS - create, update, delete events
|
||||||
# Auto-disabled when no Google Calendar connector is configured
|
# Auto-disabled when no Google Calendar connector is configured
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue