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:
parent
0bf81f0bdc
commit
dc35b4ec9e
4 changed files with 113 additions and 61 deletions
|
|
@ -1,4 +1,7 @@
|
|||
from .composio_api import ComposioAPIComponent
|
||||
from .gmail_composio import ComposioGmailAPIComponent
|
||||
|
||||
__all__ = ["ComposioAPIComponent", "ComposioGmailAPIComponent"]
|
||||
__all__ = [
|
||||
"ComposioAPIComponent",
|
||||
"ComposioGmailAPIComponent",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue