From de8841fb86f615b5e4e25d604e2adc3635afe63d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:20:13 +0530 Subject: [PATCH] chore: ran linting --- .../app/agents/new_chat/chat_deepagent.py | 6 +- .../new_chat/tools/confluence/create_page.py | 80 ++++-- .../new_chat/tools/confluence/delete_page.py | 83 ++++-- .../new_chat/tools/confluence/update_page.py | 101 +++++-- .../new_chat/tools/gmail/create_draft.py | 23 +- .../agents/new_chat/tools/gmail/send_email.py | 19 +- .../new_chat/tools/gmail/trash_email.py | 24 +- .../new_chat/tools/gmail/update_draft.py | 27 +- .../tools/google_calendar/create_event.py | 21 +- .../tools/google_calendar/delete_event.py | 21 +- .../tools/google_calendar/update_event.py | 67 +++-- .../tools/google_drive/create_file.py | 5 +- .../new_chat/tools/google_drive/trash_file.py | 10 +- .../new_chat/tools/jira/create_issue.py | 73 +++-- .../new_chat/tools/jira/delete_issue.py | 73 +++-- .../new_chat/tools/jira/update_issue.py | 102 +++++-- .../agents/new_chat/tools/knowledge_base.py | 5 +- .../new_chat/tools/notion/create_page.py | 4 +- .../new_chat/tools/notion/delete_page.py | 4 +- .../new_chat/tools/notion/update_page.py | 4 +- surfsense_backend/app/app.py | 2 +- .../app/connectors/confluence_history.py | 12 +- .../connectors/google_calendar_connector.py | 4 +- .../app/connectors/google_gmail_connector.py | 14 +- .../app/retriever/chunks_hybrid_search.py | 4 +- .../app/retriever/documents_hybrid_search.py | 4 +- .../app/routes/composio_routes.py | 49 +++- .../google_drive_add_connector_route.py | 16 +- .../app/routes/linear_add_connector_route.py | 4 +- .../routes/search_source_connectors_routes.py | 10 +- .../app/services/composio_service.py | 4 +- .../services/confluence/kb_sync_service.py | 27 +- .../confluence/tool_metadata_service.py | 33 ++- .../services/gmail/tool_metadata_service.py | 38 +-- .../google_calendar/kb_sync_service.py | 49 +++- .../google_calendar/tool_metadata_service.py | 68 +++-- .../services/google_drive/kb_sync_service.py | 8 +- .../google_drive/tool_metadata_service.py | 20 +- .../app/services/jira/kb_sync_service.py | 47 ++- .../services/jira/tool_metadata_service.py | 23 +- .../app/services/linear/kb_sync_service.py | 4 +- .../services/linear/tool_metadata_service.py | 6 +- .../services/notion/tool_metadata_service.py | 5 +- .../google_calendar_indexer.py | 21 +- .../google_drive_indexer.py | 58 ++-- .../google_gmail_indexer.py | 21 +- .../document_processors/file_processors.py | 19 +- .../google_unification/conftest.py | 8 +- .../test_calendar_indexer_credentials.py | 68 +++-- .../test_drive_indexer_credentials.py | 11 +- .../test_gmail_indexer_credentials.py | 60 +++- .../test_hybrid_search_type_filtering.py | 4 +- .../test_connector_credential_acceptance.py | 32 ++- .../test_schedule_checker_routing.py | 45 ++- .../app/(home)/login/LocalLoginForm.tsx | 6 +- .../connectors/callback/route.ts | 5 +- .../components/DocumentsTableShell.tsx | 20 +- .../new-chat/[[...chat_id]]/page.tsx | 20 +- .../[search_space_id]/team/team-content.tsx | 2 +- surfsense_web/app/global-error.tsx | 2 +- .../atoms/chat/hitl-edit-panel.atom.ts | 4 +- .../assistant-ui/connector-popup.tsx | 71 +++-- .../components/elasticsearch-connect-form.tsx | 2 +- .../components/composio-drive-config.tsx | 84 +++--- .../components/elasticsearch-config.tsx | 2 +- .../components/google-drive-config.tsx | 35 ++- .../views/connector-edit-view.tsx | 56 ++-- .../hooks/use-connector-dialog.ts | 227 +++++++-------- .../views/connector-accounts-list-view.tsx | 7 +- .../assistant-ui/thinking-steps.tsx | 6 +- .../components/assistant-ui/thread.tsx | 269 +++++++++--------- .../connectors/composio-drive-folder-tree.tsx | 37 ++- .../hitl-edit-panel/hitl-edit-panel.tsx | 29 +- .../layout/providers/LayoutDataProvider.tsx | 17 +- .../layout/ui/right-panel/RightPanel.tsx | 24 +- .../ui/sidebar/AllPrivateChatsSidebar.tsx | 2 +- .../ui/sidebar/AllSharedChatsSidebar.tsx | 2 +- .../settings/image-model-manager.tsx | 4 +- .../settings/model-config-manager.tsx | 2 +- .../components/settings/team-dialog.tsx | 2 +- .../settings/user-settings-dialog.tsx | 2 +- .../confluence/create-confluence-page.tsx | 55 ++-- .../confluence/delete-confluence-page.tsx | 45 ++- .../confluence/update-confluence-page.tsx | 69 +++-- .../components/tool-ui/gmail/create-draft.tsx | 80 ++++-- .../components/tool-ui/gmail/send-email.tsx | 133 +++++---- .../components/tool-ui/gmail/trash-email.tsx | 25 +- .../components/tool-ui/gmail/update-draft.tsx | 105 ++----- .../tool-ui/google-calendar/create-event.tsx | 124 +++++--- .../tool-ui/google-calendar/delete-event.tsx | 25 +- .../tool-ui/google-calendar/index.ts | 2 +- .../tool-ui/google-calendar/update-event.tsx | 143 +++++++--- .../tool-ui/google-drive/create-file.tsx | 161 ++++++----- .../tool-ui/google-drive/trash-file.tsx | 82 +++--- .../tool-ui/jira/create-jira-issue.tsx | 83 +++--- .../tool-ui/jira/delete-jira-issue.tsx | 20 +- .../tool-ui/jira/update-jira-issue.tsx | 63 ++-- .../tool-ui/linear/create-linear-issue.tsx | 160 ++++++----- .../tool-ui/linear/delete-linear-issue.tsx | 62 ++-- .../tool-ui/linear/update-linear-issue.tsx | 79 +++-- .../tool-ui/notion/create-notion-page.tsx | 116 ++++---- .../tool-ui/notion/delete-notion-page.tsx | 22 +- .../tool-ui/notion/update-notion-page.tsx | 42 ++- surfsense_web/components/ui/button.tsx | 3 +- surfsense_web/components/ui/hero-carousel.tsx | 4 +- surfsense_web/components/ui/radio-group.tsx | 68 ++--- surfsense_web/components/ui/toggle-group.tsx | 138 +++++---- surfsense_web/components/ui/toggle.tsx | 77 +++-- surfsense_web/hooks/use-google-picker.ts | 114 ++++---- surfsense_web/next.config.ts | 2 +- 110 files changed, 2673 insertions(+), 1918 deletions(-) diff --git a/surfsense_backend/app/agents/new_chat/chat_deepagent.py b/surfsense_backend/app/agents/new_chat/chat_deepagent.py index c1ad36252..a00cb4b7b 100644 --- a/surfsense_backend/app/agents/new_chat/chat_deepagent.py +++ b/surfsense_backend/app/agents/new_chat/chat_deepagent.py @@ -298,8 +298,7 @@ async def create_surfsense_deep_agent( # Disable Google Drive action tools if no Google Drive connector is configured has_google_drive_connector = ( - available_connectors is not None - and "GOOGLE_DRIVE_FILE" in available_connectors + available_connectors is not None and "GOOGLE_DRIVE_FILE" in available_connectors ) if not has_google_drive_connector: google_drive_tools = [ @@ -337,8 +336,7 @@ async def create_surfsense_deep_agent( # Disable Jira action tools if no Jira connector is configured has_jira_connector = ( - available_connectors is not None - and "JIRA_CONNECTOR" in available_connectors + available_connectors is not None and "JIRA_CONNECTOR" in available_connectors ) if not has_jira_connector: jira_tools = [ diff --git a/surfsense_backend/app/agents/new_chat/tools/confluence/create_page.py b/surfsense_backend/app/agents/new_chat/tools/confluence/create_page.py index 6e69ca591..b7decbe0c 100644 --- a/surfsense_backend/app/agents/new_chat/tools/confluence/create_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/confluence/create_page.py @@ -43,11 +43,16 @@ def create_create_confluence_page_tool( logger.info(f"create_confluence_page called: title='{title}'") if db_session is None or search_space_id is None or user_id is None: - return {"status": "error", "message": "Confluence tool not properly configured."} + return { + "status": "error", + "message": "Confluence tool not properly configured.", + } try: metadata_service = ConfluenceToolMetadataService(db_session) - context = await metadata_service.get_creation_context(search_space_id, user_id) + context = await metadata_service.get_creation_context( + search_space_id, user_id + ) if "error" in context: return {"status": "error", "message": context["error"]} @@ -60,22 +65,28 @@ def create_create_confluence_page_tool( "connector_type": "confluence", } - approval = interrupt({ - "type": "confluence_page_creation", - "action": { - "tool": "create_confluence_page", - "params": { - "title": title, - "content": content, - "space_id": space_id, - "connector_id": connector_id, + approval = interrupt( + { + "type": "confluence_page_creation", + "action": { + "tool": "create_confluence_page", + "params": { + "title": title, + "content": content, + "space_id": space_id, + "connector_id": connector_id, + }, }, - }, - "context": 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_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"} @@ -84,7 +95,10 @@ def create_create_confluence_page_tool( decision_type = decision.get("type") or decision.get("decision_type") if decision_type == "reject": - return {"status": "rejected", "message": "User declined. The page was not created."} + return { + "status": "rejected", + "message": "User declined. The page was not created.", + } final_params: dict[str, Any] = {} edited_action = decision.get("edited_action") @@ -114,12 +128,16 @@ def create_create_confluence_page_tool( select(SearchSourceConnector).filter( SearchSourceConnector.search_space_id == search_space_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, ) ) connector = result.scalars().first() if not connector: - return {"status": "error", "message": "No Confluence connector found."} + return { + "status": "error", + "message": "No Confluence connector found.", + } actual_connector_id = connector.id else: result = await db_session.execute( @@ -127,15 +145,21 @@ def create_create_confluence_page_tool( SearchSourceConnector.id == actual_connector_id, SearchSourceConnector.search_space_id == search_space_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, ) ) connector = result.scalars().first() if not connector: - return {"status": "error", "message": "Selected Confluence connector is invalid."} + return { + "status": "error", + "message": "Selected Confluence connector is invalid.", + } try: - client = ConfluenceHistoryConnector(session=db_session, connector_id=actual_connector_id) + client = ConfluenceHistoryConnector( + session=db_session, connector_id=actual_connector_id + ) api_result = await client.create_page( space_id=final_space_id, title=final_title, @@ -143,7 +167,10 @@ def create_create_confluence_page_tool( ) await client.close() except Exception as api_err: - if "http 403" in str(api_err).lower() or "status code 403" in str(api_err).lower(): + if ( + "http 403" in str(api_err).lower() + or "status code 403" in str(api_err).lower() + ): try: _conn = connector _conn.config = {**_conn.config, "auth_expired": True} @@ -163,6 +190,7 @@ def create_create_confluence_page_tool( kb_message_suffix = "" try: from app.services.confluence import ConfluenceKBSyncService + kb_service = ConfluenceKBSyncService(db_session) kb_result = await kb_service.sync_after_create( page_id=page_id, @@ -189,9 +217,13 @@ def create_create_confluence_page_tool( except Exception as e: from langgraph.errors import GraphInterrupt + if isinstance(e, GraphInterrupt): raise logger.error(f"Error creating Confluence page: {e}", exc_info=True) - return {"status": "error", "message": "Something went wrong while creating the page."} + return { + "status": "error", + "message": "Something went wrong while creating the page.", + } return create_confluence_page diff --git a/surfsense_backend/app/agents/new_chat/tools/confluence/delete_page.py b/surfsense_backend/app/agents/new_chat/tools/confluence/delete_page.py index e398aed32..285b1058d 100644 --- a/surfsense_backend/app/agents/new_chat/tools/confluence/delete_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/confluence/delete_page.py @@ -39,14 +39,21 @@ def create_delete_confluence_page_tool( - If status is "not_found", relay the message to the user. - If status is "insufficient_permissions", inform user to re-authenticate. """ - logger.info(f"delete_confluence_page called: page_title_or_id='{page_title_or_id}'") + logger.info( + f"delete_confluence_page called: page_title_or_id='{page_title_or_id}'" + ) if db_session is None or search_space_id is None or user_id is None: - return {"status": "error", "message": "Confluence tool not properly configured."} + return { + "status": "error", + "message": "Confluence tool not properly configured.", + } try: metadata_service = ConfluenceToolMetadataService(db_session) - context = await metadata_service.get_deletion_context(search_space_id, user_id, page_title_or_id) + context = await metadata_service.get_deletion_context( + search_space_id, user_id, page_title_or_id + ) if "error" in context: error_msg = context["error"] @@ -67,21 +74,27 @@ def create_delete_confluence_page_tool( document_id = page_data["document_id"] connector_id_from_context = context.get("account", {}).get("id") - approval = interrupt({ - "type": "confluence_page_deletion", - "action": { - "tool": "delete_confluence_page", - "params": { - "page_id": page_id, - "connector_id": connector_id_from_context, - "delete_from_kb": delete_from_kb, + approval = interrupt( + { + "type": "confluence_page_deletion", + "action": { + "tool": "delete_confluence_page", + "params": { + "page_id": page_id, + "connector_id": connector_id_from_context, + "delete_from_kb": delete_from_kb, + }, }, - }, - "context": 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_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"} @@ -90,7 +103,10 @@ def create_delete_confluence_page_tool( decision_type = decision.get("type") or decision.get("decision_type") if decision_type == "reject": - return {"status": "rejected", "message": "User declined. The page was not deleted."} + return { + "status": "rejected", + "message": "User declined. The page was not deleted.", + } final_params: dict[str, Any] = {} edited_action = decision.get("edited_action") @@ -102,33 +118,47 @@ def create_delete_confluence_page_tool( 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) + final_connector_id = final_params.get( + "connector_id", connector_id_from_context + ) final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb) from sqlalchemy.future import select from app.db import SearchSourceConnector, SearchSourceConnectorType if not final_connector_id: - return {"status": "error", "message": "No connector found for this page."} + return { + "status": "error", + "message": "No connector found for this page.", + } 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.CONFLUENCE_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, ) ) connector = result.scalars().first() if not connector: - return {"status": "error", "message": "Selected Confluence connector is invalid."} + return { + "status": "error", + "message": "Selected Confluence connector is invalid.", + } try: - client = ConfluenceHistoryConnector(session=db_session, connector_id=final_connector_id) + client = ConfluenceHistoryConnector( + session=db_session, connector_id=final_connector_id + ) await client.delete_page(final_page_id) await client.close() except Exception as api_err: - if "http 403" in str(api_err).lower() or "status code 403" in str(api_err).lower(): + if ( + "http 403" in str(api_err).lower() + or "status code 403" in str(api_err).lower() + ): try: connector.config = {**connector.config, "auth_expired": True} flag_modified(connector, "config") @@ -146,6 +176,7 @@ def create_delete_confluence_page_tool( if final_delete_from_kb and document_id: try: from app.db import Document + doc_result = await db_session.execute( select(Document).filter(Document.id == document_id) ) @@ -171,9 +202,13 @@ def create_delete_confluence_page_tool( except Exception as e: from langgraph.errors import GraphInterrupt + if isinstance(e, GraphInterrupt): raise logger.error(f"Error deleting Confluence page: {e}", exc_info=True) - return {"status": "error", "message": "Something went wrong while deleting the page."} + return { + "status": "error", + "message": "Something went wrong while deleting the page.", + } return delete_confluence_page diff --git a/surfsense_backend/app/agents/new_chat/tools/confluence/update_page.py b/surfsense_backend/app/agents/new_chat/tools/confluence/update_page.py index 7c7be1c66..e43aef59d 100644 --- a/surfsense_backend/app/agents/new_chat/tools/confluence/update_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/confluence/update_page.py @@ -41,14 +41,21 @@ def create_update_confluence_page_tool( - If status is "not_found", relay the message to the user. - If status is "insufficient_permissions", inform user to re-authenticate. """ - logger.info(f"update_confluence_page called: page_title_or_id='{page_title_or_id}'") + logger.info( + f"update_confluence_page called: page_title_or_id='{page_title_or_id}'" + ) if db_session is None or search_space_id is None or user_id is None: - return {"status": "error", "message": "Confluence tool not properly configured."} + return { + "status": "error", + "message": "Confluence tool not properly configured.", + } try: metadata_service = ConfluenceToolMetadataService(db_session) - context = await metadata_service.get_update_context(search_space_id, user_id, page_title_or_id) + context = await metadata_service.get_update_context( + search_space_id, user_id, page_title_or_id + ) if "error" in context: error_msg = context["error"] @@ -71,24 +78,30 @@ def create_update_confluence_page_tool( document_id = page_data.get("document_id") connector_id_from_context = context.get("account", {}).get("id") - approval = interrupt({ - "type": "confluence_page_update", - "action": { - "tool": "update_confluence_page", - "params": { - "page_id": page_id, - "document_id": document_id, - "new_title": new_title, - "new_content": new_content, - "version": current_version, - "connector_id": connector_id_from_context, + approval = interrupt( + { + "type": "confluence_page_update", + "action": { + "tool": "update_confluence_page", + "params": { + "page_id": page_id, + "document_id": document_id, + "new_title": new_title, + "new_content": new_content, + "version": current_version, + "connector_id": connector_id_from_context, + }, }, - }, - "context": 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_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"} @@ -97,7 +110,10 @@ def create_update_confluence_page_tool( decision_type = decision.get("type") or decision.get("decision_type") if decision_type == "reject": - return {"status": "rejected", "message": "User declined. The page was not updated."} + return { + "status": "rejected", + "message": "User declined. The page was not updated.", + } final_params: dict[str, Any] = {} edited_action = decision.get("edited_action") @@ -114,29 +130,40 @@ def create_update_confluence_page_tool( if final_content is None: final_content = current_body final_version = final_params.get("version", current_version) - final_connector_id = final_params.get("connector_id", connector_id_from_context) + final_connector_id = final_params.get( + "connector_id", connector_id_from_context + ) final_document_id = final_params.get("document_id", document_id) from sqlalchemy.future import select from app.db import SearchSourceConnector, SearchSourceConnectorType if not final_connector_id: - return {"status": "error", "message": "No connector found for this page."} + return { + "status": "error", + "message": "No connector found for this page.", + } 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.CONFLUENCE_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.CONFLUENCE_CONNECTOR, ) ) connector = result.scalars().first() if not connector: - return {"status": "error", "message": "Selected Confluence connector is invalid."} + return { + "status": "error", + "message": "Selected Confluence connector is invalid.", + } try: - client = ConfluenceHistoryConnector(session=db_session, connector_id=final_connector_id) + client = ConfluenceHistoryConnector( + session=db_session, connector_id=final_connector_id + ) await client.update_page( page_id=final_page_id, title=final_title, @@ -145,7 +172,10 @@ def create_update_confluence_page_tool( ) await client.close() except Exception as api_err: - if "http 403" in str(api_err).lower() or "status code 403" in str(api_err).lower(): + if ( + "http 403" in str(api_err).lower() + or "status code 403" in str(api_err).lower() + ): try: connector.config = {**connector.config, "auth_expired": True} flag_modified(connector, "config") @@ -163,6 +193,7 @@ def create_update_confluence_page_tool( if final_document_id: try: from app.services.confluence import ConfluenceKBSyncService + kb_service = ConfluenceKBSyncService(db_session) kb_result = await kb_service.sync_after_update( document_id=final_document_id, @@ -171,12 +202,18 @@ def create_update_confluence_page_tool( search_space_id=search_space_id, ) if kb_result["status"] == "success": - kb_message_suffix = " Your knowledge base has also been updated." + kb_message_suffix = ( + " Your knowledge base has also been updated." + ) else: - kb_message_suffix = " The knowledge base will be updated in the next sync." + kb_message_suffix = ( + " The knowledge base will be updated in the next sync." + ) except Exception as kb_err: logger.warning(f"KB sync after update failed: {kb_err}") - kb_message_suffix = " The knowledge base will be updated in the next sync." + kb_message_suffix = ( + " The knowledge base will be updated in the next sync." + ) return { "status": "success", @@ -186,9 +223,13 @@ def create_update_confluence_page_tool( except Exception as e: from langgraph.errors import GraphInterrupt + if isinstance(e, GraphInterrupt): raise logger.error(f"Error updating Confluence page: {e}", exc_info=True) - return {"status": "error", "message": "Something went wrong while updating the page."} + return { + "status": "error", + "message": "Something went wrong while updating the page.", + } return update_confluence_page diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py b/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py index cd8f5eb37..26de5d394 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py +++ b/surfsense_backend/app/agents/new_chat/tools/gmail/create_draft.py @@ -55,9 +55,7 @@ def create_create_gmail_draft_tool( - "Draft an email to alice@example.com about the meeting" - "Compose a reply to Bob about the project update" """ - logger.info( - f"create_gmail_draft called: to='{to}', subject='{subject}'" - ) + logger.info(f"create_gmail_draft called: to='{to}', subject='{subject}'") if db_session is None or search_space_id is None or user_id is None: return { @@ -187,7 +185,10 @@ def create_create_gmail_draft_tool( f"Creating Gmail draft: to='{final_to}', subject='{final_subject}', connector={actual_connector_id}" ) - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -251,10 +252,12 @@ def create_create_gmail_draft_tool( try: created = await asyncio.get_event_loop().run_in_executor( None, - lambda: gmail_service.users() - .drafts() - .create(userId="me", body={"message": {"raw": raw}}) - .execute(), + lambda: ( + gmail_service.users() + .drafts() + .create(userId="me", body={"message": {"raw": raw}}) + .execute() + ), ) except Exception as api_err: from googleapiclient.errors import HttpError @@ -289,9 +292,7 @@ def create_create_gmail_draft_tool( } raise - logger.info( - f"Gmail draft created: id={created.get('id')}" - ) + logger.info(f"Gmail draft created: id={created.get('id')}") kb_message_suffix = "" try: diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py b/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py index dd4d66a57..1b46d405a 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py +++ b/surfsense_backend/app/agents/new_chat/tools/gmail/send_email.py @@ -56,9 +56,7 @@ def create_send_gmail_email_tool( - "Send an email to alice@example.com about the meeting" - "Email Bob the project update" """ - logger.info( - f"send_gmail_email called: to='{to}', subject='{subject}'" - ) + logger.info(f"send_gmail_email called: to='{to}', subject='{subject}'") if db_session is None or search_space_id is None or user_id is None: return { @@ -188,7 +186,10 @@ def create_send_gmail_email_tool( f"Sending Gmail email: to='{final_to}', subject='{final_subject}', connector={actual_connector_id}" ) - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -252,10 +253,12 @@ def create_send_gmail_email_tool( try: sent = await asyncio.get_event_loop().run_in_executor( None, - lambda: gmail_service.users() - .messages() - .send(userId="me", body={"raw": raw}) - .execute(), + lambda: ( + gmail_service.users() + .messages() + .send(userId="me", body={"raw": raw}) + .execute() + ), ) except Exception as api_err: from googleapiclient.errors import HttpError diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py b/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py index b129888f9..25b669c7d 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py +++ b/surfsense_backend/app/agents/new_chat/tools/gmail/trash_email.py @@ -186,7 +186,10 @@ def create_trash_gmail_email_tool( f"Trashing Gmail email: message_id='{final_message_id}', connector={final_connector_id}" ) - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -241,10 +244,12 @@ def create_trash_gmail_email_tool( try: await asyncio.get_event_loop().run_in_executor( None, - lambda: gmail_service.users() - .messages() - .trash(userId="me", id=final_message_id) - .execute(), + lambda: ( + gmail_service.users() + .messages() + .trash(userId="me", id=final_message_id) + .execute() + ), ) except Exception as api_err: from googleapiclient.errors import HttpError @@ -257,7 +262,10 @@ def create_trash_gmail_email_tool( from sqlalchemy.orm.attributes import flag_modified if not connector.config.get("auth_expired"): - connector.config = {**connector.config, "auth_expired": True} + connector.config = { + **connector.config, + "auth_expired": True, + } flag_modified(connector, "config") await db_session.commit() except Exception: @@ -273,9 +281,7 @@ def create_trash_gmail_email_tool( } raise - logger.info( - f"Gmail email trashed: message_id={final_message_id}" - ) + logger.info(f"Gmail email trashed: message_id={final_message_id}") trash_result: dict[str, Any] = { "status": "success", diff --git a/surfsense_backend/app/agents/new_chat/tools/gmail/update_draft.py b/surfsense_backend/app/agents/new_chat/tools/gmail/update_draft.py index f5864bd58..bcc884e8d 100644 --- a/surfsense_backend/app/agents/new_chat/tools/gmail/update_draft.py +++ b/surfsense_backend/app/agents/new_chat/tools/gmail/update_draft.py @@ -216,7 +216,10 @@ def create_update_gmail_draft_tool( f"Updating Gmail draft: subject='{final_subject}', connector={final_connector_id}" ) - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -299,14 +302,16 @@ def create_update_gmail_draft_tool( try: updated = await asyncio.get_event_loop().run_in_executor( None, - lambda: gmail_service.users() - .drafts() - .update( - userId="me", - id=final_draft_id, - body={"message": {"raw": raw}}, - ) - .execute(), + lambda: ( + gmail_service.users() + .drafts() + .update( + userId="me", + id=final_draft_id, + body={"message": {"raw": raw}}, + ) + .execute() + ), ) except Exception as api_err: from googleapiclient.errors import HttpError @@ -369,7 +374,9 @@ def create_update_gmail_draft_tool( document.document_metadata = meta flag_modified(document, "document_metadata") await db_session.commit() - kb_message_suffix = " Your knowledge base has also been updated." + kb_message_suffix = ( + " Your knowledge base has also been updated." + ) logger.info( f"KB document {document_id} updated for draft {final_draft_id}" ) 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 14a43975c..1ac51df0e 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 @@ -78,7 +78,9 @@ def create_create_calendar_event_tool( accounts = context.get("accounts", []) if accounts and all(a.get("auth_expired") for a in accounts): - logger.warning("All Google Calendar accounts have expired authentication") + logger.warning( + "All Google Calendar accounts have expired authentication" + ) return { "status": "auth_error", "message": "All connected Google Calendar accounts need re-authentication. Please re-authenticate in your connector settings.", @@ -194,7 +196,10 @@ def create_create_calendar_event_tool( f"Creating calendar event: summary='{final_summary}', connector={actual_connector_id}" ) - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -216,7 +221,9 @@ def create_create_calendar_event_tool( token_encryption = TokenEncryption(app_config.SECRET_KEY) for key in ("token", "refresh_token", "client_secret"): if config_data.get(key): - config_data[key] = token_encryption.decrypt_token(config_data[key]) + config_data[key] = token_encryption.decrypt_token( + config_data[key] + ) exp = config_data.get("expiry", "") if exp: @@ -254,9 +261,11 @@ def create_create_calendar_event_tool( try: created = await asyncio.get_event_loop().run_in_executor( None, - lambda: service.events() - .insert(calendarId="primary", body=event_body) - .execute(), + lambda: ( + service.events() + .insert(calendarId="primary", body=event_body) + .execute() + ), ) except Exception as api_err: from googleapiclient.errors import HttpError 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 8baf866b5..7dcfec213 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 @@ -187,7 +187,10 @@ def create_delete_calendar_event_tool( f"Deleting calendar event: event_id='{final_event_id}', connector={actual_connector_id}" ) - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -209,7 +212,9 @@ def create_delete_calendar_event_tool( token_encryption = TokenEncryption(app_config.SECRET_KEY) for key in ("token", "refresh_token", "client_secret"): if config_data.get(key): - config_data[key] = token_encryption.decrypt_token(config_data[key]) + config_data[key] = token_encryption.decrypt_token( + config_data[key] + ) exp = config_data.get("expiry", "") if exp: @@ -232,9 +237,11 @@ def create_delete_calendar_event_tool( try: await asyncio.get_event_loop().run_in_executor( None, - lambda: service.events() - .delete(calendarId="primary", eventId=final_event_id) - .execute(), + lambda: ( + service.events() + .delete(calendarId="primary", eventId=final_event_id) + .execute() + ), ) except Exception as api_err: from googleapiclient.errors import HttpError @@ -269,9 +276,7 @@ def create_delete_calendar_event_tool( } raise - logger.info( - f"Calendar event deleted: event_id={final_event_id}" - ) + logger.info(f"Calendar event deleted: event_id={final_event_id}") delete_result: dict[str, Any] = { "status": "success", 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 ba60b6036..bc9707fb5 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 @@ -58,9 +58,7 @@ def create_update_calendar_event_tool( - "Reschedule the team standup to 3pm" - "Change the location of my dentist appointment" """ - logger.info( - f"update_calendar_event called: event_ref='{event_title_or_id}'" - ) + logger.info(f"update_calendar_event called: event_ref='{event_title_or_id}'") if db_session is None or search_space_id is None or user_id is None: return { @@ -83,9 +81,7 @@ def create_update_calendar_event_tool( return {"status": "error", "message": error_msg} if context.get("auth_expired"): - logger.warning( - "Google Calendar account has expired authentication" - ) + logger.warning("Google Calendar account has expired authentication") return { "status": "auth_error", "message": "The Google Calendar account for this event needs re-authentication. Please re-authenticate in your connector settings.", @@ -162,8 +158,12 @@ def create_update_calendar_event_tool( "connector_id", connector_id_from_context ) final_new_summary = final_params.get("new_summary", new_summary) - final_new_start_datetime = final_params.get("new_start_datetime", new_start_datetime) - final_new_end_datetime = final_params.get("new_end_datetime", new_end_datetime) + final_new_start_datetime = final_params.get( + "new_start_datetime", new_start_datetime + ) + final_new_end_datetime = final_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) @@ -204,7 +204,10 @@ def create_update_calendar_event_tool( f"Updating calendar event: event_id='{final_event_id}', connector={actual_connector_id}" ) - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -226,7 +229,9 @@ def create_update_calendar_event_tool( token_encryption = TokenEncryption(app_config.SECRET_KEY) for key in ("token", "refresh_token", "client_secret"): if config_data.get(key): - config_data[key] = token_encryption.decrypt_token(config_data[key]) + config_data[key] = token_encryption.decrypt_token( + config_data[key] + ) exp = config_data.get("expiry", "") if exp: @@ -250,11 +255,25 @@ def create_update_calendar_event_tool( if final_new_summary is not None: update_body["summary"] = final_new_summary if final_new_start_datetime is not None: - tz = context.get("timezone", "UTC") if isinstance(context, dict) else "UTC" - update_body["start"] = {"dateTime": final_new_start_datetime, "timeZone": tz} + tz = ( + context.get("timezone", "UTC") + if isinstance(context, dict) + else "UTC" + ) + update_body["start"] = { + "dateTime": final_new_start_datetime, + "timeZone": tz, + } if final_new_end_datetime is not None: - tz = context.get("timezone", "UTC") if isinstance(context, dict) else "UTC" - update_body["end"] = {"dateTime": final_new_end_datetime, "timeZone": tz} + tz = ( + context.get("timezone", "UTC") + if isinstance(context, dict) + else "UTC" + ) + update_body["end"] = { + "dateTime": final_new_end_datetime, + "timeZone": tz, + } if final_new_description is not None: update_body["description"] = final_new_description if final_new_location is not None: @@ -273,9 +292,15 @@ def create_update_calendar_event_tool( try: updated = await asyncio.get_event_loop().run_in_executor( None, - lambda: service.events() - .patch(calendarId="primary", eventId=final_event_id, body=update_body) - .execute(), + lambda: ( + service.events() + .patch( + calendarId="primary", + eventId=final_event_id, + body=update_body, + ) + .execute() + ), ) except Exception as api_err: from googleapiclient.errors import HttpError @@ -310,9 +335,7 @@ def create_update_calendar_event_tool( } raise - logger.info( - f"Calendar event updated: event_id={final_event_id}" - ) + logger.info(f"Calendar event updated: event_id={final_event_id}") kb_message_suffix = "" if document_id is not None: @@ -328,7 +351,9 @@ def create_update_calendar_event_tool( user_id=user_id, ) if kb_result["status"] == "success": - kb_message_suffix = " Your knowledge base has also been updated." + kb_message_suffix = ( + " Your knowledge base has also been updated." + ) else: kb_message_suffix = " The knowledge base will be updated in the next scheduled sync." except Exception as kb_err: 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 d39e9c640..20c34aafd 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 @@ -208,7 +208,10 @@ def create_create_google_drive_file_tool( ) pre_built_creds = None - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") 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 742e84ad0..206f2e2fd 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 @@ -187,7 +187,10 @@ def create_delete_google_drive_file_tool( ) pre_built_creds = None - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR + ): from app.utils.google_credentials import build_composio_credentials cca_id = connector.config.get("composio_connected_account_id") @@ -210,7 +213,10 @@ def create_delete_google_drive_file_tool( from sqlalchemy.orm.attributes import flag_modified if not connector.config.get("auth_expired"): - connector.config = {**connector.config, "auth_expired": True} + connector.config = { + **connector.config, + "auth_expired": True, + } flag_modified(connector, "config") await db_session.commit() except Exception: 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 d072254d4..273cb163b 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 @@ -45,14 +45,18 @@ def create_create_jira_issue_tool( - If status is "rejected", the user declined. Do NOT retry. - If status is "insufficient_permissions", inform user to re-authenticate. """ - logger.info(f"create_jira_issue called: project_key='{project_key}', summary='{summary}'") + logger.info( + f"create_jira_issue called: project_key='{project_key}', summary='{summary}'" + ) if db_session is None or search_space_id is None or user_id is None: return {"status": "error", "message": "Jira tool not properly configured."} try: metadata_service = JiraToolMetadataService(db_session) - context = await metadata_service.get_creation_context(search_space_id, user_id) + context = await metadata_service.get_creation_context( + search_space_id, user_id + ) if "error" in context: return {"status": "error", "message": context["error"]} @@ -65,24 +69,30 @@ 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, + 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, - }) + "context": context, + } + ) - decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else [] - decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] + 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"} @@ -91,7 +101,10 @@ def create_create_jira_issue_tool( decision_type = decision.get("type") or decision.get("decision_type") if decision_type == "reject": - return {"status": "rejected", "message": "User declined. The issue was not created."} + return { + "status": "rejected", + "message": "User declined. The issue was not created.", + } final_params: dict[str, Any] = {} edited_action = decision.get("edited_action") @@ -123,7 +136,8 @@ def create_create_jira_issue_tool( select(SearchSourceConnector).filter( SearchSourceConnector.search_space_id == search_space_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.JIRA_CONNECTOR, ) ) connector = result.scalars().first() @@ -136,15 +150,21 @@ def create_create_jira_issue_tool( SearchSourceConnector.id == actual_connector_id, SearchSourceConnector.search_space_id == search_space_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.JIRA_CONNECTOR, ) ) connector = result.scalars().first() if not connector: - return {"status": "error", "message": "Selected Jira connector is invalid."} + return { + "status": "error", + "message": "Selected Jira connector is invalid.", + } try: - jira_history = JiraHistoryConnector(session=db_session, connector_id=actual_connector_id) + jira_history = JiraHistoryConnector( + session=db_session, connector_id=actual_connector_id + ) jira_client = await jira_history._get_jira_client() api_result = await asyncio.to_thread( jira_client.create_issue, @@ -175,6 +195,7 @@ def create_create_jira_issue_tool( kb_message_suffix = "" try: from app.services.jira import JiraKBSyncService + kb_service = JiraKBSyncService(db_session) kb_result = await kb_service.sync_after_create( issue_id=issue_key, @@ -202,9 +223,13 @@ def create_create_jira_issue_tool( except Exception as e: from langgraph.errors import GraphInterrupt + if isinstance(e, GraphInterrupt): raise logger.error(f"Error creating Jira issue: {e}", exc_info=True) - return {"status": "error", "message": "Something went wrong while creating the issue."} + return { + "status": "error", + "message": "Something went wrong while creating the issue.", + } return create_jira_issue 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 46e97d3d5..0f656fbb0 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 @@ -40,14 +40,18 @@ def create_delete_jira_issue_tool( - If status is "not_found", relay the message to the user. - If status is "insufficient_permissions", inform user to re-authenticate. """ - logger.info(f"delete_jira_issue called: issue_title_or_key='{issue_title_or_key}'") + logger.info( + f"delete_jira_issue called: issue_title_or_key='{issue_title_or_key}'" + ) if db_session is None or search_space_id is None or user_id is None: return {"status": "error", "message": "Jira tool not properly configured."} try: metadata_service = JiraToolMetadataService(db_session) - context = await metadata_service.get_deletion_context(search_space_id, user_id, issue_title_or_key) + context = await metadata_service.get_deletion_context( + search_space_id, user_id, issue_title_or_key + ) if "error" in context: error_msg = context["error"] @@ -67,21 +71,27 @@ 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, + 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, - }) + "context": context, + } + ) - decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else [] - decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] + 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"} @@ -90,7 +100,10 @@ def create_delete_jira_issue_tool( decision_type = decision.get("type") or decision.get("decision_type") if decision_type == "reject": - return {"status": "rejected", "message": "User declined. The issue was not deleted."} + return { + "status": "rejected", + "message": "User declined. The issue was not deleted.", + } final_params: dict[str, Any] = {} edited_action = decision.get("edited_action") @@ -102,29 +115,40 @@ def create_delete_jira_issue_tool( final_params = decision["args"] final_issue_key = final_params.get("issue_key", issue_key) - final_connector_id = final_params.get("connector_id", connector_id_from_context) + final_connector_id = final_params.get( + "connector_id", connector_id_from_context + ) final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb) from sqlalchemy.future import select from app.db import SearchSourceConnector, SearchSourceConnectorType if not final_connector_id: - return {"status": "error", "message": "No connector found for this issue."} + return { + "status": "error", + "message": "No connector found for this issue.", + } 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.JIRA_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.JIRA_CONNECTOR, ) ) connector = result.scalars().first() if not connector: - return {"status": "error", "message": "Selected Jira connector is invalid."} + return { + "status": "error", + "message": "Selected Jira connector is invalid.", + } try: - jira_history = JiraHistoryConnector(session=db_session, connector_id=final_connector_id) + jira_history = JiraHistoryConnector( + session=db_session, connector_id=final_connector_id + ) jira_client = await jira_history._get_jira_client() await asyncio.to_thread(jira_client.delete_issue, final_issue_key) except Exception as api_err: @@ -146,6 +170,7 @@ def create_delete_jira_issue_tool( if final_delete_from_kb and document_id: try: from app.db import Document + doc_result = await db_session.execute( select(Document).filter(Document.id == document_id) ) @@ -171,9 +196,13 @@ def create_delete_jira_issue_tool( except Exception as e: from langgraph.errors import GraphInterrupt + if isinstance(e, GraphInterrupt): raise logger.error(f"Error deleting Jira issue: {e}", exc_info=True) - return {"status": "error", "message": "Something went wrong while deleting the issue."} + return { + "status": "error", + "message": "Something went wrong while deleting the issue.", + } return delete_jira_issue 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 a3ffa3020..f25eb826f 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 @@ -44,14 +44,18 @@ def create_update_jira_issue_tool( - If status is "not_found", relay the message and ask user to verify. - If status is "insufficient_permissions", inform user to re-authenticate. """ - logger.info(f"update_jira_issue called: issue_title_or_key='{issue_title_or_key}'") + logger.info( + f"update_jira_issue called: issue_title_or_key='{issue_title_or_key}'" + ) if db_session is None or search_space_id is None or user_id is None: return {"status": "error", "message": "Jira tool not properly configured."} try: metadata_service = JiraToolMetadataService(db_session) - context = await metadata_service.get_update_context(search_space_id, user_id, issue_title_or_key) + context = await metadata_service.get_update_context( + search_space_id, user_id, issue_title_or_key + ) if "error" in context: error_msg = context["error"] @@ -71,24 +75,30 @@ 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, + 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, - }) + "context": context, + } + ) - decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else [] - decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw] + 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"} @@ -97,7 +107,10 @@ def create_update_jira_issue_tool( decision_type = decision.get("type") or decision.get("decision_type") if decision_type == "reject": - return {"status": "rejected", "message": "User declined. The issue was not updated."} + return { + "status": "rejected", + "message": "User declined. The issue was not updated.", + } final_params: dict[str, Any] = {} edited_action = decision.get("edited_action") @@ -112,26 +125,35 @@ def create_update_jira_issue_tool( 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("connector_id", connector_id_from_context) + final_connector_id = final_params.get( + "connector_id", connector_id_from_context + ) final_document_id = final_params.get("document_id", document_id) from sqlalchemy.future import select from app.db import SearchSourceConnector, SearchSourceConnectorType if not final_connector_id: - return {"status": "error", "message": "No connector found for this issue."} + return { + "status": "error", + "message": "No connector found for this issue.", + } 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.JIRA_CONNECTOR, + SearchSourceConnector.connector_type + == SearchSourceConnectorType.JIRA_CONNECTOR, ) ) connector = result.scalars().first() if not connector: - return {"status": "error", "message": "Selected Jira connector is invalid."} + return { + "status": "error", + "message": "Selected Jira connector is invalid.", + } fields: dict[str, Any] = {} if final_summary: @@ -140,7 +162,12 @@ def create_update_jira_issue_tool( fields["description"] = { "type": "doc", "version": 1, - "content": [{"type": "paragraph", "content": [{"type": "text", "text": final_description}]}], + "content": [ + { + "type": "paragraph", + "content": [{"type": "text", "text": final_description}], + } + ], } if final_priority: fields["priority"] = {"name": final_priority} @@ -149,9 +176,13 @@ def create_update_jira_issue_tool( return {"status": "error", "message": "No changes specified."} try: - jira_history = JiraHistoryConnector(session=db_session, connector_id=final_connector_id) + jira_history = JiraHistoryConnector( + session=db_session, connector_id=final_connector_id + ) jira_client = await jira_history._get_jira_client() - await asyncio.to_thread(jira_client.update_issue, final_issue_key, fields) + await asyncio.to_thread( + jira_client.update_issue, final_issue_key, fields + ) except Exception as api_err: if "status code 403" in str(api_err).lower(): try: @@ -171,6 +202,7 @@ def create_update_jira_issue_tool( if final_document_id: try: from app.services.jira import JiraKBSyncService + kb_service = JiraKBSyncService(db_session) kb_result = await kb_service.sync_after_update( document_id=final_document_id, @@ -179,12 +211,18 @@ def create_update_jira_issue_tool( search_space_id=search_space_id, ) if kb_result["status"] == "success": - kb_message_suffix = " Your knowledge base has also been updated." + kb_message_suffix = ( + " Your knowledge base has also been updated." + ) else: - kb_message_suffix = " The knowledge base will be updated in the next sync." + kb_message_suffix = ( + " The knowledge base will be updated in the next sync." + ) except Exception as kb_err: logger.warning(f"KB sync after update failed: {kb_err}") - kb_message_suffix = " The knowledge base will be updated in the next sync." + kb_message_suffix = ( + " The knowledge base will be updated in the next sync." + ) return { "status": "success", @@ -194,9 +232,13 @@ def create_update_jira_issue_tool( except Exception as e: from langgraph.errors import GraphInterrupt + if isinstance(e, GraphInterrupt): raise logger.error(f"Error updating Jira issue: {e}", exc_info=True) - return {"status": "error", "message": "Something went wrong while updating the issue."} + return { + "status": "error", + "message": "Something went wrong while updating the issue.", + } return update_jira_issue diff --git a/surfsense_backend/app/agents/new_chat/tools/knowledge_base.py b/surfsense_backend/app/agents/new_chat/tools/knowledge_base.py index 57f9bb124..12b8d5749 100644 --- a/surfsense_backend/app/agents/new_chat/tools/knowledge_base.py +++ b/surfsense_backend/app/agents/new_chat/tools/knowledge_base.py @@ -43,6 +43,7 @@ _DEGENERATE_QUERY_RE = re.compile( # a real search. We want breadth (many docs) over depth (many chunks). _BROWSE_MAX_CHUNKS_PER_DOC = 5 + def _is_degenerate_query(query: str) -> bool: """Return True when the query carries no meaningful search signal. @@ -82,7 +83,9 @@ async def _browse_recent_documents( base_conditions = [Document.search_space_id == search_space_id] if document_type is not None: - type_list = document_type if isinstance(document_type, list) else [document_type] + type_list = ( + document_type if isinstance(document_type, list) else [document_type] + ) doc_type_enums = [] for dt in type_list: if isinstance(dt, str): diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py b/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py index 13a930b03..5bb0c52d1 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/notion/create_page.py @@ -245,7 +245,9 @@ def create_create_notion_page_tool( user_id=user_id, ) if kb_result["status"] == "success": - kb_message_suffix = " Your knowledge base has also been updated." + kb_message_suffix = ( + " Your knowledge base has also been updated." + ) else: kb_message_suffix = " This page will be added to your knowledge base in the next scheduled sync." except Exception as kb_err: diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py b/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py index 91c31519a..fbb7c5004 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/notion/delete_page.py @@ -280,7 +280,9 @@ def create_delete_notion_page_tool( return { "status": "auth_error", "message": str(e), - "connector_id": connector_id_from_context if "connector_id_from_context" in dir() else None, + "connector_id": connector_id_from_context + if "connector_id_from_context" in dir() + else None, "connector_type": "notion", } if isinstance(e, ValueError | NotionAPIError): diff --git a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py b/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py index 2d8d234fa..25f2b9918 100644 --- a/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py +++ b/surfsense_backend/app/agents/new_chat/tools/notion/update_page.py @@ -281,7 +281,9 @@ def create_update_notion_page_tool( return { "status": "auth_error", "message": str(e), - "connector_id": connector_id_from_context if "connector_id_from_context" in dir() else None, + "connector_id": connector_id_from_context + if "connector_id_from_context" in dir() + else None, "connector_type": "notion", } if isinstance(e, ValueError | NotionAPIError): diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index 6c6b12e3a..bba2f1f3a 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -341,7 +341,7 @@ if config.NEXT_FRONTEND_URL: allowed_origins.append(www_url) allowed_origins.extend( - [ # For local development and desktop app + [ # For local development and desktop app "http://localhost:3000", "http://127.0.0.1:3000", ] diff --git a/surfsense_backend/app/connectors/confluence_history.py b/surfsense_backend/app/connectors/confluence_history.py index e9e547453..0dbf868c6 100644 --- a/surfsense_backend/app/connectors/confluence_history.py +++ b/surfsense_backend/app/connectors/confluence_history.py @@ -190,7 +190,9 @@ class ConfluenceHistoryConnector: ) # Lazy import to avoid circular dependency - from app.routes.confluence_add_connector_route import refresh_confluence_token + from app.routes.confluence_add_connector_route import ( + refresh_confluence_token, + ) connector = await refresh_confluence_token(self._session, connector) @@ -375,13 +377,9 @@ class ConfluenceHistoryConnector: url, headers=headers, json=json_payload, params=params ) elif method_upper == "DELETE": - response = await http_client.delete( - url, headers=headers, params=params - ) + response = await http_client.delete(url, headers=headers, params=params) else: - response = await http_client.get( - url, headers=headers, params=params - ) + response = await http_client.get(url, headers=headers, params=params) response.raise_for_status() if response.status_code == 204 or not response.text: diff --git a/surfsense_backend/app/connectors/google_calendar_connector.py b/surfsense_backend/app/connectors/google_calendar_connector.py index 0350413e2..6150562c5 100644 --- a/surfsense_backend/app/connectors/google_calendar_connector.py +++ b/surfsense_backend/app/connectors/google_calendar_connector.py @@ -60,9 +60,7 @@ class GoogleCalendarConnector: has_standard_refresh = bool(self._credentials.refresh_token) if has_standard_refresh: - if not all( - [self._credentials.client_id, self._credentials.client_secret] - ): + if not all([self._credentials.client_id, self._credentials.client_secret]): raise ValueError( "Google OAuth credentials (client_id, client_secret) must be set" ) diff --git a/surfsense_backend/app/connectors/google_gmail_connector.py b/surfsense_backend/app/connectors/google_gmail_connector.py index 0491aba62..d116fb5d6 100644 --- a/surfsense_backend/app/connectors/google_gmail_connector.py +++ b/surfsense_backend/app/connectors/google_gmail_connector.py @@ -89,9 +89,7 @@ class GoogleGmailConnector: has_standard_refresh = bool(self._credentials.refresh_token) if has_standard_refresh: - if not all( - [self._credentials.client_id, self._credentials.client_secret] - ): + if not all([self._credentials.client_id, self._credentials.client_secret]): raise ValueError( "Google OAuth credentials (client_id, client_secret) must be set" ) @@ -139,17 +137,13 @@ class GoogleGmailConnector: from app.utils.oauth_security import TokenEncryption creds_dict = json.loads(self._credentials.to_json()) - token_encrypted = connector.config.get( - "_token_encrypted", False - ) + token_encrypted = connector.config.get("_token_encrypted", False) if token_encrypted and config.SECRET_KEY: token_encryption = TokenEncryption(config.SECRET_KEY) if creds_dict.get("token"): - creds_dict["token"] = ( - token_encryption.encrypt_token( - creds_dict["token"] - ) + creds_dict["token"] = token_encryption.encrypt_token( + creds_dict["token"] ) if creds_dict.get("refresh_token"): creds_dict["refresh_token"] = ( diff --git a/surfsense_backend/app/retriever/chunks_hybrid_search.py b/surfsense_backend/app/retriever/chunks_hybrid_search.py index e37d140d8..daac2069b 100644 --- a/surfsense_backend/app/retriever/chunks_hybrid_search.py +++ b/surfsense_backend/app/retriever/chunks_hybrid_search.py @@ -219,7 +219,9 @@ class ChucksHybridSearchRetriever: # Add document type filter if provided (single string or list of strings) if document_type is not None: - type_list = document_type if isinstance(document_type, list) else [document_type] + type_list = ( + document_type if isinstance(document_type, list) else [document_type] + ) doc_type_enums = [] for dt in type_list: if isinstance(dt, str): diff --git a/surfsense_backend/app/retriever/documents_hybrid_search.py b/surfsense_backend/app/retriever/documents_hybrid_search.py index ff2a50db7..12fc55659 100644 --- a/surfsense_backend/app/retriever/documents_hybrid_search.py +++ b/surfsense_backend/app/retriever/documents_hybrid_search.py @@ -199,7 +199,9 @@ class DocumentHybridSearchRetriever: # Add document type filter if provided (single string or list of strings) if document_type is not None: - type_list = document_type if isinstance(document_type, list) else [document_type] + type_list = ( + document_type if isinstance(document_type, list) else [document_type] + ) doc_type_enums = [] for dt in type_list: if isinstance(dt, str): diff --git a/surfsense_backend/app/routes/composio_routes.py b/surfsense_backend/app/routes/composio_routes.py index cbcb96853..4bf360365 100644 --- a/surfsense_backend/app/routes/composio_routes.py +++ b/surfsense_backend/app/routes/composio_routes.py @@ -461,10 +461,14 @@ async def reauth_composio_connector( return_url: Optional frontend path to redirect to after completion """ if not ComposioService.is_enabled(): - raise HTTPException(status_code=503, detail="Composio integration is not enabled.") + raise HTTPException( + status_code=503, detail="Composio integration is not enabled." + ) if not config.SECRET_KEY: - raise HTTPException(status_code=500, detail="SECRET_KEY not configured for OAuth security.") + raise HTTPException( + status_code=500, detail="SECRET_KEY not configured for OAuth security." + ) try: result = await session.execute( @@ -502,7 +506,9 @@ async def reauth_composio_connector( callback_base = config.COMPOSIO_REDIRECT_URI if not callback_base: backend_url = config.BACKEND_URL or "http://localhost:8000" - callback_base = f"{backend_url}/api/v1/auth/composio/connector/reauth/callback" + callback_base = ( + f"{backend_url}/api/v1/auth/composio/connector/reauth/callback" + ) else: # Replace the normal callback path with the reauth one callback_base = callback_base.replace( @@ -524,8 +530,13 @@ async def reauth_composio_connector( connector.config = {**connector.config, "auth_expired": False} flag_modified(connector, "config") await session.commit() - logger.info(f"Composio account {connected_account_id} refreshed server-side (no redirect needed)") - return {"success": True, "message": "Authentication refreshed successfully."} + logger.info( + f"Composio account {connected_account_id} refreshed server-side (no redirect needed)" + ) + return { + "success": True, + "message": "Authentication refreshed successfully.", + } logger.info(f"Initiating Composio re-auth for connector {connector_id}") return {"auth_url": refresh_result["redirect_url"]} @@ -679,9 +690,7 @@ async def list_composio_drive_folders( ) credentials = build_composio_credentials(composio_connected_account_id) - drive_client = GoogleDriveClient( - session, connector_id, credentials=credentials - ) + drive_client = GoogleDriveClient(session, connector_id, credentials=credentials) items, error = await list_folder_contents(drive_client, parent_id=parent_id) @@ -699,11 +708,17 @@ async def list_composio_drive_folders( connector.config = {**connector.config, "auth_expired": True} flag_modified(connector, "config") await session.commit() - logger.info(f"Marked Composio connector {connector_id} as auth_expired") + logger.info( + f"Marked Composio connector {connector_id} as auth_expired" + ) except Exception: - logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True) + logger.warning( + f"Failed to persist auth_expired for connector {connector_id}", + exc_info=True, + ) raise HTTPException( - status_code=400, detail="Google Drive authentication expired. Please re-authenticate." + status_code=400, + detail="Google Drive authentication expired. Please re-authenticate.", ) raise HTTPException( status_code=500, detail=f"Failed to list folder contents: {error}" @@ -736,11 +751,17 @@ async def list_composio_drive_folders( connector.config = {**connector.config, "auth_expired": True} flag_modified(connector, "config") await session.commit() - logger.info(f"Marked Composio connector {connector_id} as auth_expired") + logger.info( + f"Marked Composio connector {connector_id} as auth_expired" + ) except Exception: - logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True) + logger.warning( + f"Failed to persist auth_expired for connector {connector_id}", + exc_info=True, + ) raise HTTPException( - status_code=400, detail="Google Drive authentication expired. Please re-authenticate." + status_code=400, + detail="Google Drive authentication expired. Please re-authenticate.", ) from e raise HTTPException( status_code=500, detail=f"Failed to list Drive contents: {e!s}" diff --git a/surfsense_backend/app/routes/google_drive_add_connector_route.py b/surfsense_backend/app/routes/google_drive_add_connector_route.py index 2fef58fec..1c9391610 100644 --- a/surfsense_backend/app/routes/google_drive_add_connector_route.py +++ b/surfsense_backend/app/routes/google_drive_add_connector_route.py @@ -520,9 +520,13 @@ async def list_google_drive_folders( await session.commit() logger.info(f"Marked connector {connector_id} as auth_expired") except Exception: - logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True) + logger.warning( + f"Failed to persist auth_expired for connector {connector_id}", + exc_info=True, + ) raise HTTPException( - status_code=400, detail="Google Drive authentication expired. Please re-authenticate." + status_code=400, + detail="Google Drive authentication expired. Please re-authenticate.", ) raise HTTPException( status_code=500, detail=f"Failed to list folder contents: {error}" @@ -562,9 +566,13 @@ async def list_google_drive_folders( await session.commit() logger.info(f"Marked connector {connector_id} as auth_expired") except Exception: - logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True) + logger.warning( + f"Failed to persist auth_expired for connector {connector_id}", + exc_info=True, + ) raise HTTPException( - status_code=400, detail="Google Drive authentication expired. Please re-authenticate." + status_code=400, + detail="Google Drive authentication expired. Please re-authenticate.", ) from e raise HTTPException( status_code=500, detail=f"Failed to list Drive contents: {e!s}" diff --git a/surfsense_backend/app/routes/linear_add_connector_route.py b/surfsense_backend/app/routes/linear_add_connector_route.py index b5974c83b..310e3736a 100644 --- a/surfsense_backend/app/routes/linear_add_connector_route.py +++ b/surfsense_backend/app/routes/linear_add_connector_route.py @@ -580,7 +580,9 @@ async def refresh_linear_token( credentials_dict = credentials.to_dict() credentials_dict["_token_encrypted"] = True if connector.config.get("organization_name"): - credentials_dict["organization_name"] = connector.config["organization_name"] + credentials_dict["organization_name"] = connector.config[ + "organization_name" + ] credentials_dict.pop("auth_expired", None) connector.config = credentials_dict flag_modified(connector, "config") diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index e6ed007d7..769186867 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -2374,7 +2374,11 @@ async def run_google_drive_indexing( # Index each folder with indexing options for folder in items.folders: try: - indexed_count, skipped_count, error_message = await index_google_drive_files( + ( + indexed_count, + skipped_count, + error_message, + ) = await index_google_drive_files( session, connector_id, search_space_id, @@ -2429,7 +2433,9 @@ async def run_google_drive_indexing( ) if _is_auth_error(error_message): await _persist_auth_expired(session, connector_id) - error_message = "Google Drive authentication expired. Please re-authenticate." + error_message = ( + "Google Drive authentication expired. Please re-authenticate." + ) else: # Update notification to storing stage if notification: diff --git a/surfsense_backend/app/services/composio_service.py b/surfsense_backend/app/services/composio_service.py index 5f4f39b97..13fe37832 100644 --- a/surfsense_backend/app/services/composio_service.py +++ b/surfsense_backend/app/services/composio_service.py @@ -283,9 +283,7 @@ class ComposioService: timeout=timeout, ) status = getattr(account, "status", "UNKNOWN") - logger.info( - f"Composio account {connected_account_id} is now {status}" - ) + logger.info(f"Composio account {connected_account_id} is now {status}") return status except Exception as e: logger.error( diff --git a/surfsense_backend/app/services/confluence/kb_sync_service.py b/surfsense_backend/app/services/confluence/kb_sync_service.py index a63a22f3f..f786a9920 100644 --- a/surfsense_backend/app/services/confluence/kb_sync_service.py +++ b/surfsense_backend/app/services/confluence/kb_sync_service.py @@ -67,7 +67,10 @@ class ConfluenceKBSyncService: content_hash = unique_hash user_llm = await get_user_long_context_llm( - self.db_session, user_id, search_space_id, disable_streaming=True, + self.db_session, + user_id, + search_space_id, + disable_streaming=True, ) doc_metadata_for_summary = { @@ -116,17 +119,26 @@ class ConfluenceKBSyncService: logger.info( "KB sync after create succeeded: doc_id=%s, page=%s", - document.id, page_title, + document.id, + page_title, ) return {"status": "success"} except Exception as e: error_str = str(e).lower() - if "duplicate key value violates unique constraint" in error_str or "uniqueviolationerror" in error_str: + if ( + "duplicate key value violates unique constraint" in error_str + or "uniqueviolationerror" in error_str + ): await self.db_session.rollback() return {"status": "error", "message": "Duplicate document detected"} - logger.error("KB sync after create failed for page %s: %s", page_title, e, exc_info=True) + logger.error( + "KB sync after create failed for page %s: %s", + page_title, + e, + exc_info=True, + ) await self.db_session.rollback() return {"status": "error", "message": str(e)} @@ -215,11 +227,14 @@ class ConfluenceKBSyncService: logger.info( "KB sync successful for document %s (%s)", - document_id, page_title, + document_id, + page_title, ) return {"status": "success"} except Exception as e: - logger.error("KB sync failed for document %s: %s", document_id, e, exc_info=True) + logger.error( + "KB sync failed for document %s: %s", document_id, e, exc_info=True + ) await self.db_session.rollback() return {"status": "error", "message": str(e)} diff --git a/surfsense_backend/app/services/confluence/tool_metadata_service.py b/surfsense_backend/app/services/confluence/tool_metadata_service.py index 31b6d68f1..a66725bc6 100644 --- a/surfsense_backend/app/services/confluence/tool_metadata_service.py +++ b/surfsense_backend/app/services/confluence/tool_metadata_service.py @@ -83,7 +83,7 @@ class ConfluenceToolMetadataService: async def _check_account_health(self, connector: SearchSourceConnector) -> bool: """Check if the Confluence connector auth is still valid. - + Returns True if auth is expired/invalid, False if healthy. """ try: @@ -112,7 +112,7 @@ class ConfluenceToolMetadataService: async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: """Return context needed to create a new Confluence page. - + Fetches all connected accounts, and for the first healthy one fetches spaces. """ connectors = await self._get_all_confluence_connectors(search_space_id, user_id) @@ -126,10 +126,12 @@ class ConfluenceToolMetadataService: for connector in connectors: auth_expired = await self._check_account_health(connector) workspace = ConfluenceWorkspace.from_connector(connector) - accounts.append({ - **workspace.to_dict(), - "auth_expired": auth_expired, - }) + accounts.append( + { + **workspace.to_dict(), + "auth_expired": auth_expired, + } + ) if not auth_expired and not fetched_context: try: @@ -146,7 +148,8 @@ class ConfluenceToolMetadataService: except Exception as e: logger.warning( "Failed to fetch Confluence spaces for connector %s: %s", - connector.id, e, + connector.id, + e, ) return { @@ -158,7 +161,7 @@ class ConfluenceToolMetadataService: self, search_space_id: int, user_id: str, page_ref: str ) -> dict: """Return context needed to update an indexed Confluence page. - + Resolves the page from KB, then fetches current content and version from API. """ document = await self._resolve_page(search_space_id, user_id, page_ref) @@ -191,7 +194,11 @@ class ConfluenceToolMetadataService: await client.close() except Exception as e: error_str = str(e).lower() - if "401" in error_str or "403" in error_str or "authentication" in error_str: + if ( + "401" in error_str + or "403" in error_str + or "authentication" in error_str + ): return { "error": f"Failed to fetch Confluence page: {e!s}", "auth_expired": True, @@ -207,7 +214,9 @@ class ConfluenceToolMetadataService: body_storage = storage.get("value", "") version_obj = page_data.get("version", {}) - version_number = version_obj.get("number", 1) if isinstance(version_obj, dict) else 1 + version_number = ( + version_obj.get("number", 1) if isinstance(version_obj, dict) else 1 + ) return { "account": {**workspace.to_dict(), "auth_expired": False}, @@ -263,9 +272,7 @@ class ConfluenceToolMetadataService: Document.document_type == DocumentType.CONFLUENCE_CONNECTOR, SearchSourceConnector.user_id == user_id, or_( - func.lower( - Document.document_metadata.op("->>")("page_title") - ) + func.lower(Document.document_metadata.op("->>")("page_title")) == ref_lower, func.lower(Document.title) == ref_lower, ), diff --git a/surfsense_backend/app/services/gmail/tool_metadata_service.py b/surfsense_backend/app/services/gmail/tool_metadata_service.py index 3292ae2f1..524e682cf 100644 --- a/surfsense_backend/app/services/gmail/tool_metadata_service.py +++ b/surfsense_backend/app/services/gmail/tool_metadata_service.py @@ -183,10 +183,12 @@ class GmailToolMetadataService: and_( SearchSourceConnector.search_space_id == search_space_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type.in_([ - SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, - SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR, - ]), + SearchSourceConnector.connector_type.in_( + [ + SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, + SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR, + ] + ), ) ) .order_by(SearchSourceConnector.last_indexed_at.desc()) @@ -223,9 +225,7 @@ class GmailToolMetadataService: service = build("gmail", "v1", credentials=creds) profile = await asyncio.get_event_loop().run_in_executor( None, - lambda: service.users() - .getProfile(userId="me") - .execute(), + lambda: service.users().getProfile(userId="me").execute(), ) acc_dict["email"] = profile.get("emailAddress", "") except Exception: @@ -306,10 +306,12 @@ class GmailToolMetadataService: draft = await asyncio.get_event_loop().run_in_executor( None, - lambda: service.users() - .drafts() - .get(userId="me", id=draft_id, format="full") - .execute(), + lambda: ( + service.users() + .drafts() + .get(userId="me", id=draft_id, format="full") + .execute() + ), ) payload = draft.get("message", {}).get("payload", {}) @@ -422,15 +424,15 @@ class GmailToolMetadataService: .filter( and_( Document.search_space_id == search_space_id, - Document.document_type.in_([ - DocumentType.GOOGLE_GMAIL_CONNECTOR, - DocumentType.COMPOSIO_GMAIL_CONNECTOR, - ]), + Document.document_type.in_( + [ + DocumentType.GOOGLE_GMAIL_CONNECTOR, + DocumentType.COMPOSIO_GMAIL_CONNECTOR, + ] + ), SearchSourceConnector.user_id == user_id, or_( - func.lower( - cast(Document.document_metadata["subject"], String) - ) + func.lower(cast(Document.document_metadata["subject"], String)) == func.lower(email_ref), func.lower(Document.title) == func.lower(email_ref), ), diff --git a/surfsense_backend/app/services/google_calendar/kb_sync_service.py b/surfsense_backend/app/services/google_calendar/kb_sync_service.py index 1fe6bef7f..59afa116e 100644 --- a/surfsense_backend/app/services/google_calendar/kb_sync_service.py +++ b/surfsense_backend/app/services/google_calendar/kb_sync_service.py @@ -8,7 +8,12 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy.orm.attributes import flag_modified -from app.db import Document, DocumentType, SearchSourceConnector, SearchSourceConnectorType +from app.db import ( + Document, + DocumentType, + SearchSourceConnector, + SearchSourceConnectorType, +) from app.services.llm_service import get_user_long_context_llm from app.utils.document_converters import ( create_document_chunks, @@ -107,7 +112,9 @@ class GoogleCalendarKBSyncService: ) else: logger.warning("No LLM configured -- using fallback summary") - summary_content = f"Google Calendar Event: {event_summary}\n\n{indexable_content}" + summary_content = ( + f"Google Calendar Event: {event_summary}\n\n{indexable_content}" + ) summary_embedding = embed_text(summary_content) chunks = await create_document_chunks(indexable_content) @@ -201,12 +208,16 @@ class GoogleCalendarKBSyncService: None, lambda: build("calendar", "v3", credentials=creds) ) - calendar_id = (document.document_metadata or {}).get("calendar_id", "primary") + calendar_id = (document.document_metadata or {}).get( + "calendar_id", "primary" + ) live_event = await loop.run_in_executor( None, - lambda: service.events() - .get(calendarId=calendar_id, eventId=event_id) - .execute(), + lambda: ( + service.events() + .get(calendarId=calendar_id, eventId=event_id) + .execute() + ), ) event_summary = live_event.get("summary", "") @@ -220,7 +231,10 @@ class GoogleCalendarKBSyncService: end_time = end_data.get("dateTime", end_data.get("date", "")) attendees = [ - {"email": a.get("email", ""), "responseStatus": a.get("responseStatus", "")} + { + "email": a.get("email", ""), + "responseStatus": a.get("responseStatus", ""), + } for a in live_event.get("attendees", []) ] @@ -252,7 +266,9 @@ class GoogleCalendarKBSyncService: indexable_content, user_llm, doc_metadata_for_summary ) else: - summary_content = f"Google Calendar Event: {event_summary}\n\n{indexable_content}" + summary_content = ( + f"Google Calendar Event: {event_summary}\n\n{indexable_content}" + ) summary_embedding = embed_text(summary_content) chunks = await create_document_chunks(indexable_content) @@ -313,7 +329,10 @@ class GoogleCalendarKBSyncService: if not connector: raise ValueError(f"Connector {connector_id} not found") - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + ): cca_id = connector.config.get("composio_connected_account_id") if cca_id: return build_composio_credentials(cca_id) @@ -328,11 +347,17 @@ class GoogleCalendarKBSyncService: if token_encrypted and app_config.SECRET_KEY: token_encryption = TokenEncryption(app_config.SECRET_KEY) if config_data.get("token"): - config_data["token"] = token_encryption.decrypt_token(config_data["token"]) + config_data["token"] = token_encryption.decrypt_token( + config_data["token"] + ) if config_data.get("refresh_token"): - config_data["refresh_token"] = token_encryption.decrypt_token(config_data["refresh_token"]) + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) if config_data.get("client_secret"): - config_data["client_secret"] = token_encryption.decrypt_token(config_data["client_secret"]) + config_data["client_secret"] = token_encryption.decrypt_token( + config_data["client_secret"] + ) exp = config_data.get("expiry", "") if exp: diff --git a/surfsense_backend/app/services/google_calendar/tool_metadata_service.py b/surfsense_backend/app/services/google_calendar/tool_metadata_service.py index d25736d1d..c7bfe1d50 100644 --- a/surfsense_backend/app/services/google_calendar/tool_metadata_service.py +++ b/surfsense_backend/app/services/google_calendar/tool_metadata_service.py @@ -37,7 +37,9 @@ class GoogleCalendarAccount: name: str @classmethod - def from_connector(cls, connector: SearchSourceConnector) -> "GoogleCalendarAccount": + def from_connector( + cls, connector: SearchSourceConnector + ) -> "GoogleCalendarAccount": return cls(id=connector.id, name=connector.name) def to_dict(self) -> dict: @@ -93,7 +95,10 @@ class GoogleCalendarToolMetadataService: self._db_session = db_session async def _build_credentials(self, connector: SearchSourceConnector) -> Credentials: - if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR: + if ( + connector.connector_type + == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + ): cca_id = connector.config.get("composio_connected_account_id") if cca_id: return build_composio_credentials(cca_id) @@ -108,11 +113,17 @@ class GoogleCalendarToolMetadataService: if token_encrypted and app_config.SECRET_KEY: token_encryption = TokenEncryption(app_config.SECRET_KEY) if config_data.get("token"): - config_data["token"] = token_encryption.decrypt_token(config_data["token"]) + config_data["token"] = token_encryption.decrypt_token( + config_data["token"] + ) if config_data.get("refresh_token"): - config_data["refresh_token"] = token_encryption.decrypt_token(config_data["refresh_token"]) + config_data["refresh_token"] = token_encryption.decrypt_token( + config_data["refresh_token"] + ) if config_data.get("client_secret"): - config_data["client_secret"] = token_encryption.decrypt_token(config_data["client_secret"]) + config_data["client_secret"] = token_encryption.decrypt_token( + config_data["client_secret"] + ) exp = config_data.get("expiry", "") if exp: @@ -149,10 +160,12 @@ class GoogleCalendarToolMetadataService: loop = asyncio.get_event_loop() await loop.run_in_executor( None, - lambda: build("calendar", "v3", credentials=creds) - .calendarList() - .list(maxResults=1) - .execute(), + lambda: ( + build("calendar", "v3", credentials=creds) + .calendarList() + .list(maxResults=1) + .execute() + ), ) return False except Exception as e: @@ -252,11 +265,13 @@ class GoogleCalendarToolMetadataService: None, lambda: service.calendarList().list().execute() ) for cal in cal_list.get("items", []): - calendars.append({ - "id": cal.get("id", ""), - "summary": cal.get("summary", ""), - "primary": cal.get("primary", False), - }) + calendars.append( + { + "id": cal.get("id", ""), + "summary": cal.get("summary", ""), + "primary": cal.get("primary", False), + } + ) tz_setting = await loop.run_in_executor( None, @@ -314,23 +329,34 @@ class GoogleCalendarToolMetadataService: calendar_id = event.calendar_id or "primary" live_event = await loop.run_in_executor( None, - lambda: service.events() - .get(calendarId=calendar_id, eventId=event.event_id) - .execute(), + lambda: ( + service.events() + .get(calendarId=calendar_id, eventId=event.event_id) + .execute() + ), ) event_dict["summary"] = live_event.get("summary", event_dict["summary"]) - event_dict["description"] = live_event.get("description", event_dict["description"]) + event_dict["description"] = live_event.get( + "description", event_dict["description"] + ) event_dict["location"] = live_event.get("location", event_dict["location"]) start_data = live_event.get("start", {}) - event_dict["start"] = start_data.get("dateTime", start_data.get("date", event_dict["start"])) + event_dict["start"] = start_data.get( + "dateTime", start_data.get("date", event_dict["start"]) + ) end_data = live_event.get("end", {}) - event_dict["end"] = end_data.get("dateTime", end_data.get("date", event_dict["end"])) + event_dict["end"] = end_data.get( + "dateTime", end_data.get("date", event_dict["end"]) + ) event_dict["attendees"] = [ - {"email": a.get("email", ""), "responseStatus": a.get("responseStatus", "")} + { + "email": a.get("email", ""), + "responseStatus": a.get("responseStatus", ""), + } for a in live_event.get("attendees", []) ] except Exception: diff --git a/surfsense_backend/app/services/google_drive/kb_sync_service.py b/surfsense_backend/app/services/google_drive/kb_sync_service.py index 2bcd69e0c..92a39f7b9 100644 --- a/surfsense_backend/app/services/google_drive/kb_sync_service.py +++ b/surfsense_backend/app/services/google_drive/kb_sync_service.py @@ -56,7 +56,9 @@ class GoogleDriveKBSyncService: indexable_content = (content or "").strip() if not indexable_content: - indexable_content = f"Google Drive file: {file_name} (type: {mime_type})" + indexable_content = ( + f"Google Drive file: {file_name} (type: {mime_type})" + ) content_hash = generate_content_hash(indexable_content, search_space_id) @@ -93,7 +95,9 @@ class GoogleDriveKBSyncService: ) else: logger.warning("No LLM configured — using fallback summary") - summary_content = f"Google Drive File: {file_name}\n\n{indexable_content}" + summary_content = ( + f"Google Drive File: {file_name}\n\n{indexable_content}" + ) summary_embedding = embed_text(summary_content) chunks = await create_document_chunks(indexable_content) diff --git a/surfsense_backend/app/services/google_drive/tool_metadata_service.py b/surfsense_backend/app/services/google_drive/tool_metadata_service.py index f118c2c7a..221bee14a 100644 --- a/surfsense_backend/app/services/google_drive/tool_metadata_service.py +++ b/surfsense_backend/app/services/google_drive/tool_metadata_service.py @@ -133,10 +133,12 @@ class GoogleDriveToolMetadataService: and_( SearchSourceConnector.id == document.connector_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type.in_([ - SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, - SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, - ]), + SearchSourceConnector.connector_type.in_( + [ + SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, + SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, + ] + ), ) ) ) @@ -168,10 +170,12 @@ class GoogleDriveToolMetadataService: and_( SearchSourceConnector.search_space_id == search_space_id, SearchSourceConnector.user_id == user_id, - SearchSourceConnector.connector_type.in_([ - SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, - SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, - ]), + SearchSourceConnector.connector_type.in_( + [ + SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR, + SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, + ] + ), ) ) .order_by(SearchSourceConnector.last_indexed_at.desc()) diff --git a/surfsense_backend/app/services/jira/kb_sync_service.py b/surfsense_backend/app/services/jira/kb_sync_service.py index 8d7fd6bb4..4d2a66e52 100644 --- a/surfsense_backend/app/services/jira/kb_sync_service.py +++ b/surfsense_backend/app/services/jira/kb_sync_service.py @@ -53,7 +53,8 @@ class JiraKBSyncService: if existing: logger.info( "Document for Jira issue %s already exists (doc_id=%s), skipping", - issue_identifier, existing.id, + issue_identifier, + existing.id, ) return {"status": "success"} @@ -61,7 +62,9 @@ class JiraKBSyncService: if not indexable_content: indexable_content = f"Jira Issue {issue_identifier}: {issue_title}" - issue_content = f"# {issue_identifier}: {issue_title}\n\n{indexable_content}" + issue_content = ( + f"# {issue_identifier}: {issue_title}\n\n{indexable_content}" + ) content_hash = generate_content_hash(issue_content, search_space_id) @@ -73,7 +76,10 @@ class JiraKBSyncService: content_hash = unique_hash user_llm = await get_user_long_context_llm( - self.db_session, user_id, search_space_id, disable_streaming=True, + self.db_session, + user_id, + search_space_id, + disable_streaming=True, ) doc_metadata_for_summary = { @@ -88,7 +94,9 @@ class JiraKBSyncService: issue_content, user_llm, doc_metadata_for_summary ) else: - summary_content = f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}" + summary_content = ( + f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}" + ) summary_embedding = embed_text(summary_content) chunks = await create_document_chunks(issue_content) @@ -122,17 +130,26 @@ class JiraKBSyncService: logger.info( "KB sync after create succeeded: doc_id=%s, issue=%s", - document.id, issue_identifier, + document.id, + issue_identifier, ) return {"status": "success"} except Exception as e: error_str = str(e).lower() - if "duplicate key value violates unique constraint" in error_str or "uniqueviolationerror" in error_str: + if ( + "duplicate key value violates unique constraint" in error_str + or "uniqueviolationerror" in error_str + ): await self.db_session.rollback() return {"status": "error", "message": "Duplicate document detected"} - logger.error("KB sync after create failed for issue %s: %s", issue_identifier, e, exc_info=True) + logger.error( + "KB sync after create failed for issue %s: %s", + issue_identifier, + e, + exc_info=True, + ) await self.db_session.rollback() return {"status": "error", "message": str(e)} @@ -189,14 +206,18 @@ class JiraKBSyncService: issue_content, user_llm, doc_meta ) else: - summary_content = f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}" + summary_content = ( + f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}" + ) summary_embedding = embed_text(summary_content) chunks = await create_document_chunks(issue_content) document.title = f"{issue_identifier}: {issue_title}" document.content = summary_content - document.content_hash = generate_content_hash(issue_content, search_space_id) + document.content_hash = generate_content_hash( + issue_content, search_space_id + ) document.embedding = summary_embedding from sqlalchemy.orm.attributes import flag_modified @@ -219,11 +240,15 @@ class JiraKBSyncService: logger.info( "KB sync successful for document %s (%s: %s)", - document_id, issue_identifier, issue_title, + document_id, + issue_identifier, + issue_title, ) return {"status": "success"} except Exception as e: - logger.error("KB sync failed for document %s: %s", document_id, e, exc_info=True) + logger.error( + "KB sync failed for document %s: %s", document_id, e, exc_info=True + ) await self.db_session.rollback() return {"status": "error", "message": str(e)} diff --git a/surfsense_backend/app/services/jira/tool_metadata_service.py b/surfsense_backend/app/services/jira/tool_metadata_service.py index d8e7f2d3d..aaec89cd5 100644 --- a/surfsense_backend/app/services/jira/tool_metadata_service.py +++ b/surfsense_backend/app/services/jira/tool_metadata_service.py @@ -87,7 +87,7 @@ class JiraToolMetadataService: async def _check_account_health(self, connector: SearchSourceConnector) -> bool: """Check if the Jira connector auth is still valid. - + Returns True if auth is expired/invalid, False if healthy. """ try: @@ -98,9 +98,7 @@ class JiraToolMetadataService: await asyncio.to_thread(jira_client.get_myself) return False except Exception as e: - logger.warning( - "Jira connector %s health check failed: %s", connector.id, e - ) + logger.warning("Jira connector %s health check failed: %s", connector.id, e) try: connector.config = {**connector.config, "auth_expired": True} flag_modified(connector, "config") @@ -116,7 +114,7 @@ class JiraToolMetadataService: async def get_creation_context(self, search_space_id: int, user_id: str) -> dict: """Return context needed to create a new Jira issue. - + Fetches all connected Jira accounts, and for the first healthy one fetches projects, issue types, and priorities. """ @@ -165,7 +163,8 @@ class JiraToolMetadataService: except Exception as e: logger.warning( "Failed to fetch Jira context for connector %s: %s", - connector.id, e, + connector.id, + e, ) return { @@ -179,7 +178,7 @@ class JiraToolMetadataService: self, search_space_id: int, user_id: str, issue_ref: str ) -> dict: """Return context needed to update an indexed Jira issue. - + Resolves the issue from the KB, then fetches current details from the Jira API. """ document = await self._resolve_issue(search_space_id, user_id, issue_ref) @@ -209,13 +208,15 @@ class JiraToolMetadataService: session=self._db_session, connector_id=connector.id ) jira_client = await jira_history._get_jira_client() - issue_data = await asyncio.to_thread( - jira_client.get_issue, issue.issue_id - ) + issue_data = await asyncio.to_thread(jira_client.get_issue, issue.issue_id) formatted = jira_client.format_issue(issue_data) except Exception as e: error_str = str(e).lower() - if "401" in error_str or "403" in error_str or "authentication" in error_str: + if ( + "401" in error_str + or "403" in error_str + or "authentication" in error_str + ): return { "error": f"Failed to fetch Jira issue: {e!s}", "auth_expired": True, diff --git a/surfsense_backend/app/services/linear/kb_sync_service.py b/surfsense_backend/app/services/linear/kb_sync_service.py index 08d04d0d5..dab42af55 100644 --- a/surfsense_backend/app/services/linear/kb_sync_service.py +++ b/surfsense_backend/app/services/linear/kb_sync_service.py @@ -66,7 +66,9 @@ class LinearKBSyncService: if not indexable_content: indexable_content = f"Linear Issue {issue_identifier}: {issue_title}" - issue_content = f"# {issue_identifier}: {issue_title}\n\n{indexable_content}" + issue_content = ( + f"# {issue_identifier}: {issue_title}\n\n{indexable_content}" + ) content_hash = generate_content_hash(issue_content, search_space_id) diff --git a/surfsense_backend/app/services/linear/tool_metadata_service.py b/surfsense_backend/app/services/linear/tool_metadata_service.py index ebd4ead92..9848534b0 100644 --- a/surfsense_backend/app/services/linear/tool_metadata_service.py +++ b/surfsense_backend/app/services/linear/tool_metadata_service.py @@ -190,7 +190,11 @@ class LinearToolMetadataService: issue_api = await self._fetch_issue_context(linear_client, issue.id) except Exception as e: error_str = str(e).lower() - if "401" in error_str or "authentication" in error_str or "re-authenticate" in error_str: + if ( + "401" in error_str + or "authentication" in error_str + or "re-authenticate" in error_str + ): return { "error": f"Failed to fetch Linear issue context: {e!s}", "auth_expired": True, diff --git a/surfsense_backend/app/services/notion/tool_metadata_service.py b/surfsense_backend/app/services/notion/tool_metadata_service.py index f6e55bdab..097ef3461 100644 --- a/surfsense_backend/app/services/notion/tool_metadata_service.py +++ b/surfsense_backend/app/services/notion/tool_metadata_service.py @@ -102,7 +102,10 @@ class NotionToolMetadataService: ) db_connector = result.scalar_one_or_none() if db_connector and not db_connector.config.get("auth_expired"): - db_connector.config = {**db_connector.config, "auth_expired": True} + db_connector.config = { + **db_connector.config, + "auth_expired": True, + } flag_modified(db_connector, "config") await self._db_session.commit() await self._db_session.refresh(db_connector) diff --git a/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py index 2dc6fd18b..c558fb38a 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_calendar_indexer.py @@ -114,9 +114,7 @@ async def index_google_calendar_events( # Build credentials based on connector type if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES: - connected_account_id = connector.config.get( - "composio_connected_account_id" - ) + connected_account_id = connector.config.get("composio_connected_account_id") if not connected_account_id: await task_logger.log_task_failure( log_entry, @@ -396,10 +394,19 @@ async def index_google_calendar_events( session, legacy_hash ) if existing_document: - existing_document.unique_identifier_hash = unique_identifier_hash - if existing_document.document_type == DocumentType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR: - existing_document.document_type = DocumentType.GOOGLE_CALENDAR_CONNECTOR - logger.info(f"Migrated legacy Composio Calendar document: {event_id}") + existing_document.unique_identifier_hash = ( + unique_identifier_hash + ) + if ( + existing_document.document_type + == DocumentType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + ): + existing_document.document_type = ( + DocumentType.GOOGLE_CALENDAR_CONNECTOR + ) + logger.info( + f"Migrated legacy Composio Calendar document: {event_id}" + ) if existing_document: # Document exists - check if content has changed diff --git a/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py index a865bee28..260db0ce6 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_drive_indexer.py @@ -121,13 +121,13 @@ async def index_google_drive_files( # Build credentials based on connector type pre_built_credentials = None if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES: - connected_account_id = connector.config.get( - "composio_connected_account_id" - ) + connected_account_id = connector.config.get("composio_connected_account_id") if not connected_account_id: error_msg = f"Composio connected_account_id not found for connector {connector_id}" await task_logger.log_task_failure( - log_entry, error_msg, "Missing Composio account", + log_entry, + error_msg, + "Missing Composio account", {"error_type": "MissingComposioAccount"}, ) return 0, 0, error_msg @@ -355,13 +355,13 @@ async def index_google_drive_single_file( pre_built_credentials = None if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES: - connected_account_id = connector.config.get( - "composio_connected_account_id" - ) + connected_account_id = connector.config.get("composio_connected_account_id") if not connected_account_id: error_msg = f"Composio connected_account_id not found for connector {connector_id}" await task_logger.log_task_failure( - log_entry, error_msg, "Missing Composio account", + log_entry, + error_msg, + "Missing Composio account", {"error_type": "MissingComposioAccount"}, ) return 0, error_msg @@ -611,7 +611,11 @@ async def _index_full_scan( if not files_to_process and first_listing_error: error_lower = first_listing_error.lower() - if "401" in first_listing_error or "invalid credentials" in error_lower or "authError" in first_listing_error: + if ( + "401" in first_listing_error + or "invalid credentials" in error_lower + or "authError" in first_listing_error + ): raise Exception( f"Google Drive authentication failed. Please re-authenticate. " f"(Error: {first_listing_error})" @@ -704,7 +708,11 @@ async def _index_with_delta_sync( if error: logger.error(f"Error fetching changes: {error}") error_lower = error.lower() - if "401" in error or "invalid credentials" in error_lower or "authError" in error: + if ( + "401" in error + or "invalid credentials" in error_lower + or "authError" in error + ): raise Exception( f"Google Drive authentication failed. Please re-authenticate. " f"(Error: {error})" @@ -872,7 +880,10 @@ async def _create_pending_document_for_file( ) if existing_document: existing_document.unique_identifier_hash = unique_identifier_hash - if existing_document.document_type == DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR: + if ( + existing_document.document_type + == DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR + ): existing_document.document_type = DocumentType.GOOGLE_DRIVE_FILE logger.info(f"Migrated legacy Composio document to native type: {file_id}") @@ -984,10 +995,12 @@ async def _check_rename_only_update( result = await session.execute( select(Document).where( Document.search_space_id == search_space_id, - Document.document_type.in_([ - DocumentType.GOOGLE_DRIVE_FILE, - DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, - ]), + Document.document_type.in_( + [ + DocumentType.GOOGLE_DRIVE_FILE, + DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, + ] + ), cast(Document.document_metadata["google_drive_file_id"], String) == file_id, ) @@ -1000,7 +1013,10 @@ async def _check_rename_only_update( if existing_document: if existing_document.unique_identifier_hash != primary_hash: existing_document.unique_identifier_hash = primary_hash - if existing_document.document_type == DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR: + if ( + existing_document.document_type + == DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR + ): existing_document.document_type = DocumentType.GOOGLE_DRIVE_FILE logger.info(f"Migrated legacy Composio Drive document: {file_id}") @@ -1232,10 +1248,12 @@ async def _remove_document(session: AsyncSession, file_id: str, search_space_id: result = await session.execute( select(Document).where( Document.search_space_id == search_space_id, - Document.document_type.in_([ - DocumentType.GOOGLE_DRIVE_FILE, - DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, - ]), + Document.document_type.in_( + [ + DocumentType.GOOGLE_DRIVE_FILE, + DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, + ] + ), cast(Document.document_metadata["google_drive_file_id"], String) == file_id, ) diff --git a/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py b/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py index edd03ae02..e7e4a8615 100644 --- a/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py +++ b/surfsense_backend/app/tasks/connector_indexers/google_gmail_indexer.py @@ -119,9 +119,7 @@ async def index_google_gmail_messages( # Build credentials based on connector type if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES: - connected_account_id = connector.config.get( - "composio_connected_account_id" - ) + connected_account_id = connector.config.get("composio_connected_account_id") if not connected_account_id: await task_logger.log_task_failure( log_entry, @@ -323,10 +321,19 @@ async def index_google_gmail_messages( session, legacy_hash ) if existing_document: - existing_document.unique_identifier_hash = unique_identifier_hash - if existing_document.document_type == DocumentType.COMPOSIO_GMAIL_CONNECTOR: - existing_document.document_type = DocumentType.GOOGLE_GMAIL_CONNECTOR - logger.info(f"Migrated legacy Composio Gmail document: {message_id}") + existing_document.unique_identifier_hash = ( + unique_identifier_hash + ) + if ( + existing_document.document_type + == DocumentType.COMPOSIO_GMAIL_CONNECTOR + ): + existing_document.document_type = ( + DocumentType.GOOGLE_GMAIL_CONNECTOR + ) + logger.info( + f"Migrated legacy Composio Gmail document: {message_id}" + ) if existing_document: # Document exists - check if content has changed diff --git a/surfsense_backend/app/tasks/document_processors/file_processors.py b/surfsense_backend/app/tasks/document_processors/file_processors.py index 78d9315e9..6c0ae1870 100644 --- a/surfsense_backend/app/tasks/document_processors/file_processors.py +++ b/surfsense_backend/app/tasks/document_processors/file_processors.py @@ -1270,9 +1270,16 @@ async def process_file_in_background( print("Error deleting temp file", e) pass - enable_summary = connector.get("enable_summary", True) if connector else True + enable_summary = ( + connector.get("enable_summary", True) if connector else True + ) result = await add_received_file_document_using_unstructured( - session, filename, docs, search_space_id, user_id, connector, + session, + filename, + docs, + search_space_id, + user_id, + connector, enable_summary=enable_summary, ) @@ -1414,7 +1421,9 @@ async def process_file_in_background( # Extract text content from the markdown documents markdown_content = doc.text - enable_summary = connector.get("enable_summary", True) if connector else True + enable_summary = ( + connector.get("enable_summary", True) if connector else True + ) doc_result = await add_received_file_document_using_llamacloud( session, filename, @@ -1569,7 +1578,9 @@ async def process_file_in_background( session, notification, stage="chunking" ) - enable_summary = connector.get("enable_summary", True) if connector else True + enable_summary = ( + connector.get("enable_summary", True) if connector else True + ) doc_result = await add_received_file_document_using_docling( session, filename, diff --git a/surfsense_backend/tests/integration/google_unification/conftest.py b/surfsense_backend/tests/integration/google_unification/conftest.py index 768bab84b..4483cf7dd 100644 --- a/surfsense_backend/tests/integration/google_unification/conftest.py +++ b/surfsense_backend/tests/integration/google_unification/conftest.py @@ -156,9 +156,7 @@ async def committed_google_data(async_engine): session.add(user) await session.flush() - space = SearchSpace( - name=f"Google Test {uuid.uuid4().hex[:6]}", user_id=user.id - ) + space = SearchSpace(name=f"Google Test {uuid.uuid4().hex[:6]}", user_id=user.id) session.add(space) await session.flush() space_id = space.id @@ -215,7 +213,9 @@ async def committed_google_data(async_engine): def patched_session_factory(async_engine, monkeypatch): """Replace ``async_session_maker`` in connector_service with one bound to the test engine.""" test_maker = async_sessionmaker(async_engine, expire_on_commit=False) - monkeypatch.setattr("app.services.connector_service.async_session_maker", test_maker) + monkeypatch.setattr( + "app.services.connector_service.async_session_maker", test_maker + ) return test_maker diff --git a/surfsense_backend/tests/integration/google_unification/test_calendar_indexer_credentials.py b/surfsense_backend/tests/integration/google_unification/test_calendar_indexer_credentials.py index 7e4723dce..795f0d564 100644 --- a/surfsense_backend/tests/integration/google_unification/test_calendar_indexer_credentials.py +++ b/surfsense_backend/tests/integration/google_unification/test_calendar_indexer_credentials.py @@ -14,7 +14,12 @@ import pytest_asyncio from app.db import SearchSourceConnectorType -from .conftest import cleanup_space, make_session_factory, mock_task_logger, seed_connector +from .conftest import ( + cleanup_space, + make_session_factory, + mock_task_logger, + seed_connector, +) pytestmark = pytest.mark.integration @@ -52,8 +57,10 @@ async def native_calendar(async_engine): async_engine, connector_type=SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR, config={ - "token": "fake", "refresh_token": "fake", - "client_id": "fake", "client_secret": "fake", + "token": "fake", + "refresh_token": "fake", + "client_id": "fake", + "client_secret": "fake", "token_uri": "https://oauth2.googleapis.com/token", }, name_prefix="cal-native", @@ -66,10 +73,16 @@ async def native_calendar(async_engine): @patch(f"{_INDEXER_MODULE}.GoogleCalendarConnector") @patch(f"{_INDEXER_MODULE}.build_composio_credentials") async def test_composio_calendar_uses_composio_credentials( - mock_build_creds, mock_cal_cls, mock_tl_cls, async_engine, composio_calendar, + mock_build_creds, + mock_cal_cls, + mock_tl_cls, + async_engine, + composio_calendar, ): """Calendar indexer calls build_composio_credentials for a Composio connector.""" - from app.tasks.connector_indexers.google_calendar_indexer import index_google_calendar_events + from app.tasks.connector_indexers.google_calendar_indexer import ( + index_google_calendar_events, + ) data = composio_calendar mock_creds = MagicMock(name="composio-creds") @@ -77,14 +90,18 @@ async def test_composio_calendar_uses_composio_credentials( mock_tl_cls.return_value = mock_task_logger() mock_cal_instance = MagicMock() - mock_cal_instance.get_all_primary_calendar_events = AsyncMock(return_value=([], None)) + mock_cal_instance.get_all_primary_calendar_events = AsyncMock( + return_value=([], None) + ) mock_cal_cls.return_value = mock_cal_instance maker = make_session_factory(async_engine) async with maker() as session: await index_google_calendar_events( - session=session, connector_id=data["connector_id"], - search_space_id=data["search_space_id"], user_id=data["user_id"], + session=session, + connector_id=data["connector_id"], + search_space_id=data["search_space_id"], + user_id=data["user_id"], ) mock_build_creds.assert_called_once_with(_COMPOSIO_ACCOUNT_ID) @@ -96,10 +113,15 @@ async def test_composio_calendar_uses_composio_credentials( @patch(f"{_INDEXER_MODULE}.TaskLoggingService") @patch(f"{_INDEXER_MODULE}.build_composio_credentials") async def test_composio_calendar_without_account_id_returns_error( - mock_build_creds, mock_tl_cls, async_engine, composio_calendar_no_id, + mock_build_creds, + mock_tl_cls, + async_engine, + composio_calendar_no_id, ): """Calendar indexer returns error when Composio connector lacks connected_account_id.""" - from app.tasks.connector_indexers.google_calendar_indexer import index_google_calendar_events + from app.tasks.connector_indexers.google_calendar_indexer import ( + index_google_calendar_events, + ) data = composio_calendar_no_id mock_tl_cls.return_value = mock_task_logger() @@ -107,8 +129,10 @@ async def test_composio_calendar_without_account_id_returns_error( maker = make_session_factory(async_engine) async with maker() as session: count, _skipped, error = await index_google_calendar_events( - session=session, connector_id=data["connector_id"], - search_space_id=data["search_space_id"], user_id=data["user_id"], + session=session, + connector_id=data["connector_id"], + search_space_id=data["search_space_id"], + user_id=data["user_id"], ) assert count == 0 @@ -121,23 +145,33 @@ async def test_composio_calendar_without_account_id_returns_error( @patch(f"{_INDEXER_MODULE}.GoogleCalendarConnector") @patch(f"{_INDEXER_MODULE}.build_composio_credentials") async def test_native_calendar_does_not_use_composio_credentials( - mock_build_creds, mock_cal_cls, mock_tl_cls, async_engine, native_calendar, + mock_build_creds, + mock_cal_cls, + mock_tl_cls, + async_engine, + native_calendar, ): """Calendar indexer does NOT call build_composio_credentials for a native connector.""" - from app.tasks.connector_indexers.google_calendar_indexer import index_google_calendar_events + from app.tasks.connector_indexers.google_calendar_indexer import ( + index_google_calendar_events, + ) data = native_calendar mock_tl_cls.return_value = mock_task_logger() mock_cal_instance = MagicMock() - mock_cal_instance.get_all_primary_calendar_events = AsyncMock(return_value=([], None)) + mock_cal_instance.get_all_primary_calendar_events = AsyncMock( + return_value=([], None) + ) mock_cal_cls.return_value = mock_cal_instance maker = make_session_factory(async_engine) async with maker() as session: await index_google_calendar_events( - session=session, connector_id=data["connector_id"], - search_space_id=data["search_space_id"], user_id=data["user_id"], + session=session, + connector_id=data["connector_id"], + search_space_id=data["search_space_id"], + user_id=data["user_id"], ) mock_build_creds.assert_not_called() diff --git a/surfsense_backend/tests/integration/google_unification/test_drive_indexer_credentials.py b/surfsense_backend/tests/integration/google_unification/test_drive_indexer_credentials.py index b6ffa7936..f17fc69f5 100644 --- a/surfsense_backend/tests/integration/google_unification/test_drive_indexer_credentials.py +++ b/surfsense_backend/tests/integration/google_unification/test_drive_indexer_credentials.py @@ -14,7 +14,12 @@ import pytest_asyncio from app.db import SearchSourceConnectorType -from .conftest import cleanup_space, make_session_factory, mock_task_logger, seed_connector +from .conftest import ( + cleanup_space, + make_session_factory, + mock_task_logger, + seed_connector, +) pytestmark = pytest.mark.integration @@ -129,7 +134,9 @@ async def test_composio_connector_without_account_id_returns_error( assert count == 0 assert error is not None - assert "composio_connected_account_id" in error.lower() or "composio" in error.lower() + assert ( + "composio_connected_account_id" in error.lower() or "composio" in error.lower() + ) mock_build_creds.assert_not_called() diff --git a/surfsense_backend/tests/integration/google_unification/test_gmail_indexer_credentials.py b/surfsense_backend/tests/integration/google_unification/test_gmail_indexer_credentials.py index 4097b2e95..afb3e64c3 100644 --- a/surfsense_backend/tests/integration/google_unification/test_gmail_indexer_credentials.py +++ b/surfsense_backend/tests/integration/google_unification/test_gmail_indexer_credentials.py @@ -14,7 +14,12 @@ import pytest_asyncio from app.db import SearchSourceConnectorType -from .conftest import cleanup_space, make_session_factory, mock_task_logger, seed_connector +from .conftest import ( + cleanup_space, + make_session_factory, + mock_task_logger, + seed_connector, +) pytestmark = pytest.mark.integration @@ -52,8 +57,10 @@ async def native_gmail(async_engine): async_engine, connector_type=SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR, config={ - "token": "fake", "refresh_token": "fake", - "client_id": "fake", "client_secret": "fake", + "token": "fake", + "refresh_token": "fake", + "client_id": "fake", + "client_secret": "fake", "token_uri": "https://oauth2.googleapis.com/token", }, name_prefix="gmail-native", @@ -66,10 +73,16 @@ async def native_gmail(async_engine): @patch(f"{_INDEXER_MODULE}.GoogleGmailConnector") @patch(f"{_INDEXER_MODULE}.build_composio_credentials") async def test_composio_gmail_uses_composio_credentials( - mock_build_creds, mock_gmail_cls, mock_tl_cls, async_engine, composio_gmail, + mock_build_creds, + mock_gmail_cls, + mock_tl_cls, + async_engine, + composio_gmail, ): """Gmail indexer calls build_composio_credentials for a Composio connector.""" - from app.tasks.connector_indexers.google_gmail_indexer import index_google_gmail_messages + from app.tasks.connector_indexers.google_gmail_indexer import ( + index_google_gmail_messages, + ) data = composio_gmail mock_creds = MagicMock(name="composio-creds") @@ -83,8 +96,10 @@ async def test_composio_gmail_uses_composio_credentials( maker = make_session_factory(async_engine) async with maker() as session: await index_google_gmail_messages( - session=session, connector_id=data["connector_id"], - search_space_id=data["search_space_id"], user_id=data["user_id"], + session=session, + connector_id=data["connector_id"], + search_space_id=data["search_space_id"], + user_id=data["user_id"], ) mock_build_creds.assert_called_once_with(_COMPOSIO_ACCOUNT_ID) @@ -96,10 +111,15 @@ async def test_composio_gmail_uses_composio_credentials( @patch(f"{_INDEXER_MODULE}.TaskLoggingService") @patch(f"{_INDEXER_MODULE}.build_composio_credentials") async def test_composio_gmail_without_account_id_returns_error( - mock_build_creds, mock_tl_cls, async_engine, composio_gmail_no_id, + mock_build_creds, + mock_tl_cls, + async_engine, + composio_gmail_no_id, ): """Gmail indexer returns error when Composio connector lacks connected_account_id.""" - from app.tasks.connector_indexers.google_gmail_indexer import index_google_gmail_messages + from app.tasks.connector_indexers.google_gmail_indexer import ( + index_google_gmail_messages, + ) data = composio_gmail_no_id mock_tl_cls.return_value = mock_task_logger() @@ -107,8 +127,10 @@ async def test_composio_gmail_without_account_id_returns_error( maker = make_session_factory(async_engine) async with maker() as session: count, _skipped, error = await index_google_gmail_messages( - session=session, connector_id=data["connector_id"], - search_space_id=data["search_space_id"], user_id=data["user_id"], + session=session, + connector_id=data["connector_id"], + search_space_id=data["search_space_id"], + user_id=data["user_id"], ) assert count == 0 @@ -121,10 +143,16 @@ async def test_composio_gmail_without_account_id_returns_error( @patch(f"{_INDEXER_MODULE}.GoogleGmailConnector") @patch(f"{_INDEXER_MODULE}.build_composio_credentials") async def test_native_gmail_does_not_use_composio_credentials( - mock_build_creds, mock_gmail_cls, mock_tl_cls, async_engine, native_gmail, + mock_build_creds, + mock_gmail_cls, + mock_tl_cls, + async_engine, + native_gmail, ): """Gmail indexer does NOT call build_composio_credentials for a native connector.""" - from app.tasks.connector_indexers.google_gmail_indexer import index_google_gmail_messages + from app.tasks.connector_indexers.google_gmail_indexer import ( + index_google_gmail_messages, + ) data = native_gmail mock_tl_cls.return_value = mock_task_logger() @@ -136,8 +164,10 @@ async def test_native_gmail_does_not_use_composio_credentials( maker = make_session_factory(async_engine) async with maker() as session: await index_google_gmail_messages( - session=session, connector_id=data["connector_id"], - search_space_id=data["search_space_id"], user_id=data["user_id"], + session=session, + connector_id=data["connector_id"], + search_space_id=data["search_space_id"], + user_id=data["user_id"], ) mock_build_creds.assert_not_called() diff --git a/surfsense_backend/tests/integration/google_unification/test_hybrid_search_type_filtering.py b/surfsense_backend/tests/integration/google_unification/test_hybrid_search_type_filtering.py index bd84d5bb3..5a29b8333 100644 --- a/surfsense_backend/tests/integration/google_unification/test_hybrid_search_type_filtering.py +++ b/surfsense_backend/tests/integration/google_unification/test_hybrid_search_type_filtering.py @@ -39,9 +39,7 @@ async def test_list_of_types_returns_both_matching_doc_types( assert "FILE" not in returned_types -async def test_single_string_type_returns_only_that_type( - db_session, seed_google_docs -): +async def test_single_string_type_returns_only_that_type(db_session, seed_google_docs): """Searching with a single string type returns only documents of that exact type.""" space_id = seed_google_docs["search_space"].id diff --git a/surfsense_backend/tests/unit/google_unification/test_connector_credential_acceptance.py b/surfsense_backend/tests/unit/google_unification/test_connector_credential_acceptance.py index 689aea40f..bb7bce8ed 100644 --- a/surfsense_backend/tests/unit/google_unification/test_connector_credential_acceptance.py +++ b/surfsense_backend/tests/unit/google_unification/test_connector_credential_acceptance.py @@ -64,7 +64,9 @@ async def test_gmail_accepts_valid_composio_credentials(mock_build): mock_build.return_value = mock_service connector = GoogleGmailConnector( - creds, session=MagicMock(), user_id="test-user", + creds, + session=MagicMock(), + user_id="test-user", ) profile, error = await connector.get_user_profile() @@ -76,7 +78,9 @@ async def test_gmail_accepts_valid_composio_credentials(mock_build): @patch("app.connectors.google_gmail_connector.Request") @patch("app.connectors.google_gmail_connector.build") -async def test_gmail_refreshes_expired_composio_credentials(mock_build, mock_request_cls): +async def test_gmail_refreshes_expired_composio_credentials( + mock_build, mock_request_cls +): """GoogleGmailConnector handles expired Composio credentials via refresh_handler without attempting DB persistence.""" from app.connectors.google_gmail_connector import GoogleGmailConnector @@ -95,7 +99,9 @@ async def test_gmail_refreshes_expired_composio_credentials(mock_build, mock_req mock_session = AsyncMock() connector = GoogleGmailConnector( - creds, session=mock_session, user_id="test-user", + creds, + session=mock_session, + user_id="test-user", ) profile, error = await connector.get_user_profile() @@ -128,7 +134,9 @@ async def test_calendar_accepts_valid_composio_credentials(mock_build): mock_build.return_value = mock_service connector = GoogleCalendarConnector( - creds, session=MagicMock(), user_id="test-user", + creds, + session=MagicMock(), + user_id="test-user", ) calendars, error = await connector.get_calendars() @@ -141,7 +149,9 @@ async def test_calendar_accepts_valid_composio_credentials(mock_build): @patch("app.connectors.google_calendar_connector.Request") @patch("app.connectors.google_calendar_connector.build") -async def test_calendar_refreshes_expired_composio_credentials(mock_build, mock_request_cls): +async def test_calendar_refreshes_expired_composio_credentials( + mock_build, mock_request_cls +): """GoogleCalendarConnector handles expired Composio credentials via refresh_handler without attempting DB persistence.""" from app.connectors.google_calendar_connector import GoogleCalendarConnector @@ -157,7 +167,9 @@ async def test_calendar_refreshes_expired_composio_credentials(mock_build, mock_ mock_session = AsyncMock() connector = GoogleCalendarConnector( - creds, session=mock_session, user_id="test-user", + creds, + session=mock_session, + user_id="test-user", ) calendars, error = await connector.get_calendars() @@ -191,7 +203,9 @@ async def test_drive_client_uses_prebuilt_composio_credentials(mock_build): mock_build.return_value = mock_service client = GoogleDriveClient( - session=MagicMock(), connector_id=999, credentials=creds, + session=MagicMock(), + connector_id=999, + credentials=creds, ) files, next_token, error = await client.list_files() @@ -218,7 +232,9 @@ async def test_drive_client_prebuilt_creds_skip_db_loading(mock_build, mock_get_ mock_build.return_value = mock_service client = GoogleDriveClient( - session=MagicMock(), connector_id=999, credentials=creds, + session=MagicMock(), + connector_id=999, + credentials=creds, ) await client.list_files() diff --git a/surfsense_backend/tests/unit/google_unification/test_schedule_checker_routing.py b/surfsense_backend/tests/unit/google_unification/test_schedule_checker_routing.py index 38ef4d553..d2ced30b6 100644 --- a/surfsense_backend/tests/unit/google_unification/test_schedule_checker_routing.py +++ b/surfsense_backend/tests/unit/google_unification/test_schedule_checker_routing.py @@ -20,8 +20,14 @@ def test_drive_indexer_accepts_both_native_and_composio(): ACCEPTED_DRIVE_CONNECTOR_TYPES, ) - assert SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES - assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES + assert ( + SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR + in ACCEPTED_DRIVE_CONNECTOR_TYPES + ) + assert ( + SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR + in ACCEPTED_DRIVE_CONNECTOR_TYPES + ) def test_gmail_indexer_accepts_both_native_and_composio(): @@ -30,8 +36,14 @@ def test_gmail_indexer_accepts_both_native_and_composio(): ACCEPTED_GMAIL_CONNECTOR_TYPES, ) - assert SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES - assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES + assert ( + SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR + in ACCEPTED_GMAIL_CONNECTOR_TYPES + ) + assert ( + SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR + in ACCEPTED_GMAIL_CONNECTOR_TYPES + ) def test_calendar_indexer_accepts_both_native_and_composio(): @@ -40,14 +52,29 @@ def test_calendar_indexer_accepts_both_native_and_composio(): ACCEPTED_CALENDAR_CONNECTOR_TYPES, ) - assert SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES - assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES + assert ( + SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR + in ACCEPTED_CALENDAR_CONNECTOR_TYPES + ) + assert ( + SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + in ACCEPTED_CALENDAR_CONNECTOR_TYPES + ) def test_composio_connector_types_set_covers_all_google_services(): """COMPOSIO_GOOGLE_CONNECTOR_TYPES should contain all three Composio Google types.""" from app.utils.google_credentials import COMPOSIO_GOOGLE_CONNECTOR_TYPES - assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES - assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES - assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES + assert ( + SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR + in COMPOSIO_GOOGLE_CONNECTOR_TYPES + ) + assert ( + SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR + in COMPOSIO_GOOGLE_CONNECTOR_TYPES + ) + assert ( + SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR + in COMPOSIO_GOOGLE_CONNECTOR_TYPES + ) diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx index b7a056d7f..5b5147926 100644 --- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx +++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx @@ -213,11 +213,7 @@ export function LocalLoginForm() { disabled={isLoggingIn} className="w-full rounded-md bg-blue-600 px-4 py-1.5 md:py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base flex items-center justify-center gap-2" > - {isLoggingIn ? ( - - ) : ( - t("sign_in") - )} + {isLoggingIn ? : t("sign_in")} diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts b/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts index ca55913f0..14573066d 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts @@ -16,10 +16,7 @@ export async function GET( connectorId: searchParams.get("connectorId"), }); - const redirectUrl = new URL( - `/dashboard/${search_space_id}/new-chat`, - request.url - ); + const redirectUrl = new URL(`/dashboard/${search_space_id}/new-chat`, request.url); const response = NextResponse.redirect(redirectUrl, { status: 302 }); response.cookies.set(OAUTH_RESULT_COOKIE, result, { diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx index 85d5db28a..314ce1439 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx @@ -1,5 +1,6 @@ "use client"; +import { useAtomValue, useSetAtom } from "jotai"; import { AlertCircle, CheckCircle2, @@ -16,12 +17,11 @@ import { Trash2, User, } from "lucide-react"; -import { useAtomValue, useSetAtom } from "jotai"; import { useTranslations } from "next-intl"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { membersAtom } from "@/atoms/members/members-query.atoms"; -import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { toast } from "sonner"; +import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; +import { membersAtom } from "@/atoms/members/members-query.atoms"; import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; import { MarkdownViewer } from "@/components/markdown-viewer"; @@ -35,14 +35,9 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Drawer, @@ -51,7 +46,12 @@ import { DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index a5cadad4e..8dd888ef0 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -37,21 +37,26 @@ import { Thread } from "@/components/assistant-ui/thread"; import { MobileEditorPanel } from "@/components/editor-panel/editor-panel"; import { MobileHitlEditPanel } from "@/components/hitl-edit-panel/hitl-edit-panel"; import { MobileReportPanel } from "@/components/report-panel/report-panel"; +import { + CreateConfluencePageToolUI, + DeleteConfluencePageToolUI, + UpdateConfluencePageToolUI, +} from "@/components/tool-ui/confluence"; import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import { DisplayImageToolUI } from "@/components/tool-ui/display-image"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { GenerateReportToolUI } from "@/components/tool-ui/generate-report"; -import { - CreateCalendarEventToolUI, - UpdateCalendarEventToolUI, - DeleteCalendarEventToolUI, -} from "@/components/tool-ui/google-calendar"; import { CreateGmailDraftToolUI, SendGmailEmailToolUI, TrashGmailEmailToolUI, UpdateGmailDraftToolUI, } from "@/components/tool-ui/gmail"; +import { + CreateCalendarEventToolUI, + DeleteCalendarEventToolUI, + UpdateCalendarEventToolUI, +} from "@/components/tool-ui/google-calendar"; import { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI, @@ -61,11 +66,6 @@ import { DeleteJiraIssueToolUI, UpdateJiraIssueToolUI, } from "@/components/tool-ui/jira"; -import { - CreateConfluencePageToolUI, - DeleteConfluencePageToolUI, - UpdateConfluencePageToolUI, -} from "@/components/tool-ui/confluence"; import { CreateLinearIssueToolUI, DeleteLinearIssueToolUI, diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx index 3e1ab0af1..43b4d0b7a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/team-content.tsx @@ -20,7 +20,6 @@ import { UserPlus, Users, } from "lucide-react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { @@ -44,6 +43,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Calendar as CalendarComponent } from "@/components/ui/calendar"; import { diff --git a/surfsense_web/app/global-error.tsx b/surfsense_web/app/global-error.tsx index ec558052d..8e6f1e445 100644 --- a/surfsense_web/app/global-error.tsx +++ b/surfsense_web/app/global-error.tsx @@ -1,7 +1,7 @@ "use client"; -import posthog from "posthog-js"; import NextError from "next/error"; +import posthog from "posthog-js"; import { useEffect } from "react"; export default function GlobalError({ diff --git a/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts b/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts index 455402503..1d106796a 100644 --- a/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts +++ b/surfsense_web/atoms/chat/hitl-edit-panel.atom.ts @@ -14,7 +14,9 @@ interface HitlEditPanelState { content: string; toolName: string; extraFields?: ExtraField[]; - onSave: ((title: string, content: string, extraFieldValues?: Record) => void) | null; + onSave: + | ((title: string, content: string, extraFieldValues?: Record) => void) + | null; onClose: (() => void) | null; } diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 6aef4b52a..569b2ece4 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -214,11 +214,7 @@ export const ConnectorIndicator = forwardRef + {showTrigger && ( { const cfg = connectorConfig || editingConnector.config; - const isDrive = editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || + const isDrive = + editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR"; const hasDriveItems = isDrive ? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 || - ((cfg?.selected_files as unknown[]) ?? []).length > 0 + ((cfg?.selected_files as unknown[]) ?? []).length > 0 : true; if (!hasDriveItems) return undefined; return () => { @@ -376,37 +373,37 @@ export const ConnectorIndicator = forwardRef ) : indexingConfig ? ( - { - if (indexingConfig.connectorId) { - startIndexing(indexingConfig.connectorId); + refreshConnectors()); - }} - onSkip={handleSkipIndexing} - /> + startDate={startDate} + endDate={endDate} + periodicEnabled={periodicEnabled} + frequencyMinutes={frequencyMinutes} + enableSummary={enableSummary} + isStartingIndexing={isStartingIndexing} + isFromOAuth={isFromOAuth} + onStartDateChange={setStartDate} + onEndDateChange={setEndDate} + onPeriodicEnabledChange={setPeriodicEnabled} + onFrequencyChange={setFrequencyMinutes} + onEnableSummaryChange={setEnableSummary} + onConfigChange={setIndexingConnectorConfig} + onStartIndexing={() => { + if (indexingConfig.connectorId) { + startIndexing(indexingConfig.connectorId); + } + handleStartIndexing(() => refreshConnectors()); + }} + onSkip={handleSkipIndexing} + /> ) : ( - Your Google Drive authentication has expired. Please re-authenticate using the button below. -

- )} + {isAuthExpired && ( +

+ Your Google Drive authentication has expired. Please re-authenticate using the button + below. +

+ )} - {isEditMode ? ( -
- + {isFolderTreeOpen && ( + )} - - {isFolderTreeOpen && ( - - )} -
- ) : ( - - )} + + ) : ( + + )} {/* Indexing Options */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/elasticsearch-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/elasticsearch-config.tsx index 3e514fafc..c5123e922 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/elasticsearch-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/elasticsearch-config.tsx @@ -1,12 +1,12 @@ "use client"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { KeyRound, Server } from "lucide-react"; import type { FC } from "react"; import { useEffect, useId, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import type { ConnectorConfigProps } from "../index"; export interface ElasticsearchConfigProps extends ConnectorConfigProps { diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/google-drive-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/google-drive-config.tsx index 36bc9d5ce..6a5d34c7f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/google-drive-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/google-drive-config.tsx @@ -231,26 +231,25 @@ export const GoogleDriveConfig: FC = ({ connector, onConfi )} - + - {pickerError && !isAuthExpired && ( -

{pickerError}

- )} + {pickerError && !isAuthExpired &&

{pickerError}

} - {isAuthExpired && ( -

- Your Google Drive authentication has expired. Please re-authenticate using the button below. -

- )} + {isAuthExpired && ( +

+ Your Google Drive authentication has expired. Please re-authenticate using the button + below. +

+ )} {/* Indexing Options */} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index e950cb648..93d280a15 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -220,10 +220,8 @@ export const ConnectorEditView: FC = ({

- {/* Quick Index Button - hidden when auth is expired */} - {connector.is_indexable && - onQuickIndex && - !isAuthExpired && ( + {/* Quick Index Button - hidden when auth is expired */} + {connector.is_indexable && onQuickIndex && !isAuthExpired && ( )} - {isAuthExpired && reauthEndpoint ? ( - - ) : ( - - )} + {isAuthExpired && reauthEndpoint ? ( + + ) : ( + + )} ); diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index a79e15c7f..c710347e3 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -261,35 +261,28 @@ export const useConnectorDialog = () => { | (typeof COMPOSIO_CONNECTORS)[number] | undefined; - if (result.connectorId) { - const connectorId = parseInt(result.connectorId, 10); - newConnector = fetchResult.data.find( - (c: SearchSourceConnector) => c.id === connectorId - ); - if (newConnector) { - const connectorType = newConnector.connector_type; - oauthConnector = - OAUTH_CONNECTORS.find( - (c) => c.connectorType === connectorType - ) || - COMPOSIO_CONNECTORS.find( - (c) => c.connectorType === connectorType - ); + if (result.connectorId) { + const connectorId = parseInt(result.connectorId, 10); + newConnector = fetchResult.data.find((c: SearchSourceConnector) => c.id === connectorId); + if (newConnector) { + const connectorType = newConnector.connector_type; + oauthConnector = + OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) || + COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType); + } } - } - if (!newConnector && result.connector) { - oauthConnector = - OAUTH_CONNECTORS.find((c) => c.id === result.connector) || - COMPOSIO_CONNECTORS.find((c) => c.id === result.connector); - if (oauthConnector) { - const oauthType = oauthConnector.connectorType; - newConnector = fetchResult.data.find( - (c: SearchSourceConnector) => - c.connector_type === oauthType - ); + if (!newConnector && result.connector) { + oauthConnector = + OAUTH_CONNECTORS.find((c) => c.id === result.connector) || + COMPOSIO_CONNECTORS.find((c) => c.id === result.connector); + if (oauthConnector) { + const oauthType = oauthConnector.connectorType; + newConnector = fetchResult.data.find( + (c: SearchSourceConnector) => c.connector_type === oauthType + ); + } } - } if (newConnector && oauthConnector) { const connectorValidation = searchSourceConnector.safeParse(newConnector); @@ -599,17 +592,17 @@ export const useConnectorDialog = () => { : `${connectorTitle} connected and syncing started!`; toast.success(successMessage); - setIsOpen(false); + setIsOpen(false); - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); - await refetchAllConnectors(); + await refetchAllConnectors(); } else { // Non-indexable connector // For Circleback, transition to edit view to show webhook URL @@ -631,11 +624,11 @@ export const useConnectorDialog = () => { setStartDate(undefined); setEndDate(undefined); - toast.success(`${connectorTitle} connected successfully!`, { - description: "Configure the webhook URL in your Circleback settings.", - }); + toast.success(`${connectorTitle} connected successfully!`, { + description: "Configure the webhook URL in your Circleback settings.", + }); - await refetchAllConnectors(); + await refetchAllConnectors(); } else { // Other non-indexable connectors - just show success message and close const successMessage = @@ -644,13 +637,13 @@ export const useConnectorDialog = () => { : `${connectorTitle} connected successfully!`; toast.success(successMessage); - await refetchAllConnectors(); + await refetchAllConnectors(); - setIsOpen(false); + setIsOpen(false); - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); } } } @@ -870,12 +863,12 @@ export const useConnectorDialog = () => { ); } - toast.success(`${indexingConfig.connectorTitle} indexing started`); + toast.success(`${indexingConfig.connectorTitle} indexing started`); - setIsOpen(false); - setIsFromOAuth(false); + setIsOpen(false); + setIsFromOAuth(false); - refreshConnectors(); + refreshConnectors(); queryClient.invalidateQueries({ queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); @@ -927,21 +920,21 @@ export const useConnectorDialog = () => { return; } - // Track if we came from accounts list view so handleBackFromEdit can restore it - if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) { - setCameFromAccountsList(viewingAccountsType); - } else { - setCameFromAccountsList(null); - } - setViewingAccountsType(null); + // Track if we came from accounts list view so handleBackFromEdit can restore it + if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) { + setCameFromAccountsList(viewingAccountsType); + } else { + setCameFromAccountsList(null); + } + setViewingAccountsType(null); - // Track if we came from MCP list view so handleBackFromEdit can restore it - if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") { - setCameFromMCPList(true); - } else { - setCameFromMCPList(false); - } - setViewingMCPList(false); + // Track if we came from MCP list view so handleBackFromEdit can restore it + if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") { + setCameFromMCPList(true); + } else { + setCameFromMCPList(false); + } + setViewingMCPList(false); // Track index with date range opened event if (connector.is_indexable) { @@ -952,15 +945,15 @@ export const useConnectorDialog = () => { ); } - setEditingConnector(connector); - setConnectorName(connector.name); - setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled); - setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); - setEnableSummary(connector.enable_summary ?? false); - setStartDate(undefined); - setEndDate(undefined); - }, - [searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab] + setEditingConnector(connector); + setConnectorName(connector.name); + setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled); + setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); + setEnableSummary(connector.enable_summary ?? false); + setStartDate(undefined); + setEndDate(undefined); + }, + [searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab] ); // Handle saving connector changes @@ -1139,35 +1132,35 @@ export const useConnectorDialog = () => { : indexingDescription, }); - setIsOpen(false); + setIsOpen(false); - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error saving connector:", error); - toast.error("Failed to save connector changes"); - } finally { - setIsSaving(false); - } - }, - [ - editingConnector, - searchSpaceId, - isSaving, - startDate, - endDate, - indexConnector, - updateConnector, - periodicEnabled, - frequencyMinutes, - enableSummary, - getFrequencyLabel, - connectorConfig, - connectorName, - setIsOpen, - ] + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error saving connector:", error); + toast.error("Failed to save connector changes"); + } finally { + setIsSaving(false); + } + }, + [ + editingConnector, + searchSpaceId, + isSaving, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + enableSummary, + getFrequencyLabel, + connectorConfig, + connectorName, + setIsOpen, + ] ); // Handle disconnecting connector @@ -1194,19 +1187,19 @@ export const useConnectorDialog = () => { : `${editingConnector.name} disconnected successfully` ); - if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) { - setViewingMCPList(true); - setEditingConnector(null); - setConnectorName(null); - setConnectorConfig(null); - } else { - setEditingConnector(null); - setConnectorName(null); - setConnectorConfig(null); - setIsOpen(false); - } + if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) { + setViewingMCPList(true); + setEditingConnector(null); + setConnectorName(null); + setConnectorConfig(null); + } else { + setEditingConnector(null); + setConnectorName(null); + setConnectorConfig(null); + setIsOpen(false); + } - refreshConnectors(); + refreshConnectors(); queryClient.invalidateQueries({ queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); @@ -1312,13 +1305,13 @@ export const useConnectorDialog = () => { setEditingConnector(null); setConnectorName(null); setConnectorConfig(null); - setConnectingConnectorType(null); - setViewingAccountsType(null); - setViewingMCPList(false); - setCameFromAccountsList(null); - setCameFromMCPList(false); - setConnectCameFromMCPList(false); - setStartDate(undefined); + setConnectingConnectorType(null); + setViewingAccountsType(null); + setViewingMCPList(false); + setCameFromAccountsList(null); + setCameFromMCPList(false); + setConnectCameFromMCPList(false); + setStartDate(undefined); setEndDate(undefined); setPeriodicEnabled(false); setFrequencyMinutes("1440"); diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx index 30b63f54a..da6ad8540 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -203,8 +203,7 @@ export const ConnectorAccountsListView: FC = ({
{typeConnectors.map((connector) => { const isIndexing = indexingConnectorIds.has(connector.id); - const isAuthExpired = - !!reauthEndpoint && connector.config?.auth_expired === true; + const isAuthExpired = !!reauthEndpoint && connector.config?.auth_expired === true; return (
= ({ onClick={() => handleReauth(connector.id)} disabled={reauthingId === connector.id} > - + Re-authenticate ) : ( diff --git a/surfsense_web/components/assistant-ui/thinking-steps.tsx b/surfsense_web/components/assistant-ui/thinking-steps.tsx index 437e395f2..c773824f8 100644 --- a/surfsense_web/components/assistant-ui/thinking-steps.tsx +++ b/surfsense_web/components/assistant-ui/thinking-steps.tsx @@ -32,7 +32,9 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: const inProgressStep = steps.find((s) => getEffectiveStatus(s) === "in_progress"); const allCompleted = - steps.length > 0 && !isThreadRunning && steps.every((s) => getEffectiveStatus(s) === "completed"); + steps.length > 0 && + !isThreadRunning && + steps.every((s) => getEffectiveStatus(s) === "completed"); const isProcessing = isThreadRunning && !allCompleted; // Auto-collapse when all tasks are completed @@ -127,7 +129,7 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: effectiveStatus === "pending" && "text-muted-foreground/60" )} > - {step.title} + {step.title}
{/* Step items (sub-content) */} diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index a223ca4a2..65e6db08c 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -90,7 +90,11 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import { CONNECTOR_ICON_TO_TYPES, CONNECTOR_TOOL_ICON_PATHS, getToolIcon } from "@/contracts/enums/toolIcons"; +import { + CONNECTOR_ICON_TO_TYPES, + CONNECTOR_TOOL_ICON_PATHS, + getToolIcon, +} from "@/contracts/enums/toolIcons"; import type { Document } from "@/contracts/types/document.types"; import { useBatchCommentsPreload } from "@/hooks/use-comments"; import { useCommentsElectric } from "@/hooks/use-comments-electric"; @@ -735,71 +739,75 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false
- {groupedTools.filter((g) => !g.connectorIcon).map((group) => ( -
-
- {group.label} + {groupedTools + .filter((g) => !g.connectorIcon) + .map((group) => ( +
+
+ {group.label} +
+ {group.tools.map((tool) => { + const isDisabled = disabledTools.includes(tool.name); + const ToolIcon = getToolIcon(tool.name); + return ( +
+ + + {formatToolName(tool.name)} + + toggleTool(tool.name)} + className="shrink-0" + /> +
+ ); + })}
- {group.tools.map((tool) => { - const isDisabled = disabledTools.includes(tool.name); - const ToolIcon = getToolIcon(tool.name); - return ( -
- - - {formatToolName(tool.name)} - - toggleTool(tool.name)} - className="shrink-0" - /> -
- ); - })} -
- ))} + ))} {groupedTools.some((g) => g.connectorIcon) && (
Connector Actions
- {groupedTools.filter((g) => g.connectorIcon).map((group) => { - const iconKey = group.connectorIcon ?? ""; - const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey]; - const toolNames = group.tools.map((t) => t.name); - const allDisabled = toolNames.every((n) => disabledTools.includes(n)); - return ( -
- {iconInfo ? ( - {iconInfo.alt} g.connectorIcon) + .map((group) => { + const iconKey = group.connectorIcon ?? ""; + const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey]; + const toolNames = group.tools.map((t) => t.name); + const allDisabled = toolNames.every((n) => disabledTools.includes(n)); + return ( +
+ {iconInfo ? ( + {iconInfo.alt} + ) : ( + + )} + + {group.label} + + toggleToolGroup(toolNames)} + className="shrink-0" /> - ) : ( - - )} - - {group.label} - - toggleToolGroup(toolNames)} - className="shrink-0" - /> -
- ); - })} +
+ ); + })}
)} {!filteredTools?.length && ( @@ -857,82 +865,87 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`, }} > - {groupedTools.filter((g) => !g.connectorIcon).map((group) => ( -
-
- {group.label} + {groupedTools + .filter((g) => !g.connectorIcon) + .map((group) => ( +
+
+ {group.label} +
+ {group.tools.map((tool) => { + const isDisabled = disabledTools.includes(tool.name); + const ToolIcon = getToolIcon(tool.name); + const row = ( +
+ + + {formatToolName(tool.name)} + + toggleTool(tool.name)} + className="shrink-0 scale-[0.6] sm:scale-75" + /> +
+ ); + return ( + + {row} + + {tool.description} + + + ); + })}
- {group.tools.map((tool) => { - const isDisabled = disabledTools.includes(tool.name); - const ToolIcon = getToolIcon(tool.name); - const row = ( -
- - - {formatToolName(tool.name)} - - toggleTool(tool.name)} - className="shrink-0 scale-[0.6] sm:scale-75" - /> -
- ); - return ( - - {row} - - {tool.description} - - - ); - })} -
- ))} + ))} {groupedTools.some((g) => g.connectorIcon) && (
Connector Actions
- {groupedTools.filter((g) => g.connectorIcon).map((group) => { - const iconKey = group.connectorIcon ?? ""; - const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey]; - const toolNames = group.tools.map((t) => t.name); - const allDisabled = toolNames.every((n) => disabledTools.includes(n)); - const groupDef = TOOL_GROUPS.find((g) => g.label === group.label); - const row = ( -
- {iconInfo ? ( - {iconInfo.alt} g.connectorIcon) + .map((group) => { + const iconKey = group.connectorIcon ?? ""; + const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey]; + const toolNames = group.tools.map((t) => t.name); + const allDisabled = toolNames.every((n) => disabledTools.includes(n)); + const groupDef = TOOL_GROUPS.find((g) => g.label === group.label); + const row = ( +
+ {iconInfo ? ( + {iconInfo.alt} + ) : ( + + )} + + {group.label} + + toggleToolGroup(toolNames)} + className="shrink-0 scale-[0.6] sm:scale-75" /> - ) : ( - - )} - - {group.label} - - toggleToolGroup(toolNames)} - className="shrink-0 scale-[0.6] sm:scale-75" - /> -
- ); - return ( - - {row} - - {groupDef?.tooltip ?? group.tools.map((t) => t.description).join(" · ")} - - - ); - })} +
+ ); + return ( + + {row} + + {groupDef?.tooltip ?? + group.tools.map((t) => t.description).join(" · ")} + + + ); + })}
)} {!filteredTools?.length && ( diff --git a/surfsense_web/components/connectors/composio-drive-folder-tree.tsx b/surfsense_web/components/connectors/composio-drive-folder-tree.tsx index db4f2940a..48f52ffb1 100644 --- a/surfsense_web/components/connectors/composio-drive-folder-tree.tsx +++ b/surfsense_web/components/connectors/composio-drive-folder-tree.tsx @@ -78,14 +78,21 @@ export function ComposioDriveFolderTree({ }: ComposioDriveFolderTreeProps) { const [itemStates, setItemStates] = useState>(new Map()); - const { data: rootData, isLoading: isLoadingRoot, error: rootError } = useComposioDriveFolders({ + const { + data: rootData, + isLoading: isLoadingRoot, + error: rootError, + } = useComposioDriveFolders({ connectorId, }); useEffect(() => { if (rootError && onAuthError) { const msg = rootError instanceof Error ? rootError.message : String(rootError); - if (msg.toLowerCase().includes("authentication expired") || msg.toLowerCase().includes("re-authenticate")) { + if ( + msg.toLowerCase().includes("authentication expired") || + msg.toLowerCase().includes("re-authenticate") + ) { onAuthError(msg); } } @@ -363,19 +370,21 @@ export function ComposioDriveFolderTree({ {!isLoadingRoot && rootItems.map((item) => renderItem(item, 0))}
- {!isLoadingRoot && rootError && ( -
- {(rootError instanceof Error ? rootError.message : String(rootError)).includes("authentication expired") - ? "Google Drive authentication has expired. Please re-authenticate above." - : "Failed to load Google Drive contents."} -
- )} + {!isLoadingRoot && rootError && ( +
+ {(rootError instanceof Error ? rootError.message : String(rootError)).includes( + "authentication expired" + ) + ? "Google Drive authentication has expired. Please re-authenticate above." + : "Failed to load Google Drive contents."} +
+ )} - {!isLoadingRoot && !rootError && rootItems.length === 0 && ( -
- No files or folders found in your Google Drive -
- )} + {!isLoadingRoot && !rootError && rootItems.length === 0 && ( +
+ No files or folders found in your Google Drive +
+ )}
diff --git a/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx index b68f335b4..b11e9aa84 100644 --- a/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx +++ b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx @@ -1,18 +1,15 @@ "use client"; -import { TagInput, type Tag as TagType } from "emblor"; import { format } from "date-fns"; +import { TagInput, type Tag as TagType } from "emblor"; import { useAtomValue, useSetAtom } from "jotai"; import { CalendarIcon, XIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - closeHitlEditPanelAtom, - hitlEditPanelAtom, -} from "@/atoms/chat/hitl-edit-panel.atom"; import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom"; -import { Calendar } from "@/components/ui/calendar"; +import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { PlateEditor } from "@/components/editor/plate-editor"; import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -58,14 +55,9 @@ function EmailsTagField({ onChangeRef.current(tagsToEmailString(tags)); }, [tags]); - const handleSetTags = useCallback( - (newTags: TagType[] | ((prev: TagType[]) => TagType[])) => { - setTags((prev) => - typeof newTags === "function" ? newTags(prev) : newTags - ); - }, - [] - ); + const handleSetTags = useCallback((newTags: TagType[] | ((prev: TagType[]) => TagType[])) => { + setTags((prev) => (typeof newTags === "function" ? newTags(prev) : newTags)); + }, []); const handleAddTag = useCallback( (text: string) => { @@ -265,7 +257,10 @@ export function HitlEditPanelContent({
{extraFields.map((field) => (
-