chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-21 13:20:13 +05:30
parent 772150eb66
commit de8841fb86
110 changed files with 2673 additions and 1918 deletions

View file

@ -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 = [

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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",

View file

@ -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}"
)

View file

@ -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

View file

@ -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",

View file

@ -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:

View file

@ -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")

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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:

View file

@ -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):

View file

@ -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):

View file

@ -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",
]

View file

@ -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:

View file

@ -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"
)

View file

@ -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"] = (

View file

@ -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):

View file

@ -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):

View file

@ -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}"

View file

@ -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}"

View file

@ -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")

View file

@ -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:

View file

@ -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(

View file

@ -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)}

View file

@ -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,
),

View file

@ -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),
),

View file

@ -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:

View file

@ -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:

View file

@ -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)

View file

@ -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())

View file

@ -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)}

View file

@ -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,

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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

View file

@ -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,
)

View file

@ -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

View file

@ -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,