mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 09:46:25 +02:00
chore: ran linting
This commit is contained in:
parent
772150eb66
commit
de8841fb86
110 changed files with 2673 additions and 1918 deletions
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"] = (
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue