feat: Update Gmail component (#7530)

* chore: action params naming

* chore: remove comments

* chore: replaced MessageTextInput field with StrInput

* feat: add google calendar component

* feat: replaced loops with hardcoded display-name action-enum mapping to improve performance

* chore: format

* fix: add type ignore for action_key in getattr call

* feat: add google sheets component

* fix: format google calendar utils

* feat: add google meet Component

* chore: minor improvement

* chore: format & lint

* fix: google meet component

* feat: add GitHub component

* fix: format

* fix: lint

* fix: typo

* feat: add Slack Component

* fix: format

* fix: rest bool value to None

* chore: disabled slack tools temporarily

* fix: add condition to set list variables to None in when action is changed

* chore: capitalise display names

* fix: update list issues field to MessateTextInput

* fix: format/lint in slack component

* fix: google calendar logo

* fix: revert setting bool field to None

* feat: composio-core & composio-core version bump to 0.7.10

* fix: minor bugs

* feat: add accepted values to AccessType field in google meet component

* feat: add accepted values for entry point access field in Google meet component

* fix: Google Calendar display names

* feat: replace list with nested list for batch update field in Google sheets

* fix: display name in Google sheets

* fix: format

* fix: titlecase display name in google meet component

* feat: set advaced to true for advanced fields

* feat: add condition to skip empty list fields in execute_action

* chore: improve display names GitHub Component

* fix: slack component display names & minor enhancements

* feat: update condition to skip empty fields while executing action

* feat: fix google calendar field description

* feat: update googlemeet component to use new inputs & composio base class

* chore: update googlemeet component filename

* feat: update github component to use new inputs & composio base class

* feat: update google calendar to use new inputs & composio base class

* feat: update google sheets component to use new inputs & Composio base class

* feat: update slack component to use new inputs & Composio base class

* fix: format

* chore: cleanup un-used code

* chore: format

* feat: add missing fields & actions

* chore: fix typo

* feat: rm other components

* feat: improve error message format & revert composio libs bump

* chore: revert uv.lock file

* update tests

* fix: remove duplicate action field in GMAIL_FETCH_EMAILS

* fix: remove unused code

* fix: add ignore statement

---------

Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
Abhishek Patil 2025-04-15 23:46:36 +05:30 committed by GitHub
commit dc35b4ec9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 61 deletions

View file

@ -1,4 +1,7 @@
from .composio_api import ComposioAPIComponent
from .gmail_composio import ComposioGmailAPIComponent
__all__ = ["ComposioAPIComponent", "ComposioGmailAPIComponent"]
__all__ = [
"ComposioAPIComponent",
"ComposioGmailAPIComponent",
]

View file

@ -1,3 +1,4 @@
import json
from typing import Any
from composio import Action
@ -25,57 +26,86 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
_actions_data: dict = {
"GMAIL_SEND_EMAIL": {
"display_name": "Send Email",
"action_fields": ["recipient_email", "subject", "body", "cc", "bcc", "is_html"],
"action_fields": [
"recipient_email",
"subject",
"body",
"cc",
"bcc",
"is_html",
"gmail_user_id",
"attachment",
],
},
"GMAIL_FETCH_EMAILS": {
"display_name": "Fetch Emails",
"action_fields": ["max_results", "query"],
"action_fields": [
"gmail_user_id",
"max_results",
"query",
"page_token",
"label_ids",
"include_spam_trash",
],
"get_result_field": True,
"result_field": "messages",
},
"GMAIL_GET_PROFILE": {
"display_name": "Get User Profile",
"action_fields": [],
"action_fields": ["gmail_user_id"],
},
"GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID": {
"display_name": "Get Email By ID",
"action_fields": ["message_id"],
"action_fields": ["message_id", "gmail_user_id", "format"],
"get_result_field": False,
},
"GMAIL_CREATE_EMAIL_DRAFT": {
"display_name": "Create Draft Email",
"action_fields": ["recipient_email", "subject", "body", "cc", "bcc", "is_html"],
"action_fields": [
"recipient_email",
"subject",
"body",
"cc",
"bcc",
"is_html",
"attachment",
"gmail_user_id",
],
},
"GMAIL_FETCH_MESSAGE_BY_THREAD_ID": {
"display_name": "Get Message By Thread ID",
"action_fields": ["thread_id"],
"action_fields": ["thread_id", "page_token", "gmail_user_id"],
"get_result_field": False,
},
"GMAIL_LIST_THREADS": {
"display_name": "List Email Threads",
"action_fields": ["max_results", "query"],
"action_fields": ["max_results", "query", "gmail_user_id", "page_token"],
},
"GMAIL_REPLY_TO_THREAD": {
"display_name": "Reply To Thread",
"action_fields": ["thread_id", "message_body", "recipient_email"],
"action_fields": ["thread_id", "message_body", "recipient_email", "gmail_user_id", "cc", "bcc", "is_html"],
},
"GMAIL_LIST_LABELS": {
"display_name": "List Email Labels",
"action_fields": [],
"action_fields": ["gmail_user_id"],
},
"GMAIL_CREATE_LABEL": {
"display_name": "Create Email Label",
"action_fields": ["label_name"],
"action_fields": ["label_name", "label_list_visibility", "message_list_visibility", "gmail_user_id"],
},
"GMAIL_GET_PEOPLE": {
"display_name": "Get Contacts",
"action_fields": [],
"action_fields": ["resource_name", "person_fields"],
},
"GMAIL_REMOVE_LABEL": {
"display_name": "Delete Email Label",
"action_fields": ["label_id"],
"action_fields": ["label_id", "gmail_user_id"],
"get_result_field": False,
},
"GMAIL_GET_ATTACHMENT": {
"display_name": "Get Attachment",
"action_fields": ["message_id", "attachment_id", "file_name", "gmail_user_id"],
},
}
_all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
_bool_variables = {"is_html", "include_spam_trash"}
@ -144,6 +174,13 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
advanced=True,
),
# Email retrieval and management fields
MessageTextInput(
name="gmail_user_id",
display_name="User ID",
info="The user's email address or 'me' for the authenticated user",
show=False,
advanced=True,
),
IntInput(
name="max_results",
display_name="Max Results",
@ -330,23 +367,35 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
params[field] = value
if params.get("gmail_user_id"):
params["user_id"] = params.pop("gmail_user_id")
result = toolset.execute_action(
action=enum_name,
params=params,
).get("data", [])
)
if not result.get("successful"):
message_str = result.get("data", {}).get("message", "{}")
try:
error_data = json.loads(message_str).get("error", {})
except json.JSONDecodeError:
error_data = {"error": "Failed to get exact error details"}
return {
"code": error_data.get("code"),
"message": error_data.get("message"),
"errors": error_data.get("errors", []),
"status": error_data.get("status"),
}
result_data = result.get("data", [])
if (
len(result) != 1
len(result_data) != 1
and not self._actions_data.get(action_key, {}).get("result_field")
and self._actions_data.get(action_key, {}).get("get_result_field")
):
msg = f"Expected a dict with a single key, got {len(result)} keys: {result.keys()}"
msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}"
raise ValueError(msg)
if result:
get_result_field = self._actions_data.get(action_key, {}).get("get_result_field", True)
if get_result_field:
key = self._actions_data.get(action_key, {}).get("result_field", next(iter(result)))
return result.get(key)
return result
return result_data # noqa: TRY300
except Exception as e:
logger.error(f"Error executing action: {e}")
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else str(self.action)

View file

@ -15,7 +15,7 @@ class MockComposioToolSet:
return []
def execute_action(self, *_, **__):
return {"data": {"response": "mocked response"}}
return {"successful": True, "data": {"result": "mocked response"}}
class TestComposioBase(ComponentTestBaseWithoutClient):

View file

@ -72,9 +72,18 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
component.body = "Test Body"
component.is_html = False
# For this specific test, customize the _actions_data to not use get_result_field
component._actions_data = {
"GMAIL_SEND_EMAIL": {
"display_name": "Send Email",
"action_fields": ["recipient_email", "subject", "body", "is_html"],
"get_result_field": False,
}
}
# Execute action
result = component.execute_action()
assert result == "mocked response"
assert result == {"result": "mocked response"}
def test_execute_action_fetch_emails(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
@ -87,27 +96,24 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
component.max_results = 10
component.query = "from:test@example.com"
# Create a mock for the toolset
mock_toolset = MagicMock()
# The execute_action method needs to return a structure that works with the component's logic
# Based on the error, we need to make sure the 'data' key contains a dictionary with at least one key
mock_toolset.execute_action.return_value = {"data": {"response": "mocked response"}}
# Patch the _build_wrapper method to return our mock
with patch.object(component, "_build_wrapper", return_value=mock_toolset):
# Also patch the _actions_data to ensure it has the correct structure for GMAIL_FETCH_EMAILS
# This ensures the result_field is set correctly
component._actions_data = {
"GMAIL_FETCH_EMAILS": {
"action_fields": ["max_results", "query"],
"result_field": "response",
"get_result_field": True,
}
# For this specific test, we need to customize the action_data to handle results field
component._actions_data = {
"GMAIL_FETCH_EMAILS": {
"action_fields": ["max_results", "query"],
"result_field": "messages",
"get_result_field": True,
}
}
# Execute action
# Create a mock for the toolset with specific structure for this test
mock_toolset = MagicMock()
mock_toolset.execute_action.return_value = {"successful": True, "data": {"messages": "mocked response"}}
# Patch the _build_wrapper method
with patch.object(component, "_build_wrapper", return_value=mock_toolset):
result = component.execute_action()
assert result == "mocked response"
# Based on the component's actual behavior, it returns the entire data dict
assert result == {"messages": "mocked response"}
def test_execute_action_get_profile(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
@ -118,9 +124,18 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
component.api_key = "test_key"
component.action = [{"name": "Get User Profile"}]
# For this specific test, customize the _actions_data to not use get_result_field
component._actions_data = {
"GMAIL_GET_PROFILE": {
"display_name": "Get User Profile",
"action_fields": ["gmail_user_id"],
"get_result_field": False,
}
}
# Execute action
result = component.execute_action()
assert result == "mocked response"
assert result == {"result": "mocked response"}
def test_execute_action_invalid_action(self, component_class, default_kwargs):
# Setup component
@ -173,24 +188,9 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
# Verify the DataFrame is not empty
assert not result.empty
# Verify the DataFrame contains our mock data
# This will depend on how the component processes the data
# We can check for specific column names or values
if hasattr(result, "columns"):
# If the DataFrame has columns, check for expected ones
expected_columns = ["id", "threadId", "subject", "from", "date", "snippet"]
for col in expected_columns:
if col in result.columns:
assert True
break
else:
# If none of the expected columns are found, check if data is in the DataFrame
assert any(
"Test Email" in str(cell) for cell in result.values.flat if hasattr(cell, "__contains__")
)
else:
# If the DataFrame structure is different, just check for some expected content
assert "Test Email" in str(result)
# Check for expected content in the DataFrame string representation
data_str = str(result)
assert "test email" in data_str
def test_update_build_config(self, component_class, default_kwargs):
# Test that the Gmail component properly inherits and uses the base component's