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 .composio_api import ComposioAPIComponent
|
||||||
from .gmail_composio import ComposioGmailAPIComponent
|
from .gmail_composio import ComposioGmailAPIComponent
|
||||||
|
|
||||||
__all__ = ["ComposioAPIComponent", "ComposioGmailAPIComponent"]
|
__all__ = [
|
||||||
|
"ComposioAPIComponent",
|
||||||
|
"ComposioGmailAPIComponent",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from composio import Action
|
from composio import Action
|
||||||
|
|
@ -25,57 +26,86 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
|
||||||
_actions_data: dict = {
|
_actions_data: dict = {
|
||||||
"GMAIL_SEND_EMAIL": {
|
"GMAIL_SEND_EMAIL": {
|
||||||
"display_name": "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": {
|
"GMAIL_FETCH_EMAILS": {
|
||||||
"display_name": "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,
|
"get_result_field": True,
|
||||||
"result_field": "messages",
|
"result_field": "messages",
|
||||||
},
|
},
|
||||||
"GMAIL_GET_PROFILE": {
|
"GMAIL_GET_PROFILE": {
|
||||||
"display_name": "Get User Profile",
|
"display_name": "Get User Profile",
|
||||||
"action_fields": [],
|
"action_fields": ["gmail_user_id"],
|
||||||
},
|
},
|
||||||
"GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID": {
|
"GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID": {
|
||||||
"display_name": "Get Email By ID",
|
"display_name": "Get Email By ID",
|
||||||
"action_fields": ["message_id"],
|
"action_fields": ["message_id", "gmail_user_id", "format"],
|
||||||
"get_result_field": False,
|
"get_result_field": False,
|
||||||
},
|
},
|
||||||
"GMAIL_CREATE_EMAIL_DRAFT": {
|
"GMAIL_CREATE_EMAIL_DRAFT": {
|
||||||
"display_name": "Create Draft Email",
|
"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": {
|
"GMAIL_FETCH_MESSAGE_BY_THREAD_ID": {
|
||||||
"display_name": "Get 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,
|
"get_result_field": False,
|
||||||
},
|
},
|
||||||
"GMAIL_LIST_THREADS": {
|
"GMAIL_LIST_THREADS": {
|
||||||
"display_name": "List Email 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": {
|
"GMAIL_REPLY_TO_THREAD": {
|
||||||
"display_name": "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": {
|
"GMAIL_LIST_LABELS": {
|
||||||
"display_name": "List Email Labels",
|
"display_name": "List Email Labels",
|
||||||
"action_fields": [],
|
"action_fields": ["gmail_user_id"],
|
||||||
},
|
},
|
||||||
"GMAIL_CREATE_LABEL": {
|
"GMAIL_CREATE_LABEL": {
|
||||||
"display_name": "Create Email 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": {
|
"GMAIL_GET_PEOPLE": {
|
||||||
"display_name": "Get Contacts",
|
"display_name": "Get Contacts",
|
||||||
"action_fields": [],
|
"action_fields": ["resource_name", "person_fields"],
|
||||||
},
|
},
|
||||||
"GMAIL_REMOVE_LABEL": {
|
"GMAIL_REMOVE_LABEL": {
|
||||||
"display_name": "Delete Email Label",
|
"display_name": "Delete Email Label",
|
||||||
"action_fields": ["label_id"],
|
"action_fields": ["label_id", "gmail_user_id"],
|
||||||
"get_result_field": False,
|
"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"]}
|
_all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
|
||||||
_bool_variables = {"is_html", "include_spam_trash"}
|
_bool_variables = {"is_html", "include_spam_trash"}
|
||||||
|
|
@ -144,6 +174,13 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
|
||||||
advanced=True,
|
advanced=True,
|
||||||
),
|
),
|
||||||
# Email retrieval and management fields
|
# 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(
|
IntInput(
|
||||||
name="max_results",
|
name="max_results",
|
||||||
display_name="Max Results",
|
display_name="Max Results",
|
||||||
|
|
@ -330,23 +367,35 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
|
||||||
|
|
||||||
params[field] = value
|
params[field] = value
|
||||||
|
|
||||||
|
if params.get("gmail_user_id"):
|
||||||
|
params["user_id"] = params.pop("gmail_user_id")
|
||||||
|
|
||||||
result = toolset.execute_action(
|
result = toolset.execute_action(
|
||||||
action=enum_name,
|
action=enum_name,
|
||||||
params=params,
|
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 (
|
if (
|
||||||
len(result) != 1
|
len(result_data) != 1
|
||||||
and not self._actions_data.get(action_key, {}).get("result_field")
|
and not self._actions_data.get(action_key, {}).get("result_field")
|
||||||
and self._actions_data.get(action_key, {}).get("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)
|
raise ValueError(msg)
|
||||||
if result:
|
return result_data # noqa: TRY300
|
||||||
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
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error executing action: {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)
|
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 []
|
return []
|
||||||
|
|
||||||
def execute_action(self, *_, **__):
|
def execute_action(self, *_, **__):
|
||||||
return {"data": {"response": "mocked response"}}
|
return {"successful": True, "data": {"result": "mocked response"}}
|
||||||
|
|
||||||
|
|
||||||
class TestComposioBase(ComponentTestBaseWithoutClient):
|
class TestComposioBase(ComponentTestBaseWithoutClient):
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,18 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
|
||||||
component.body = "Test Body"
|
component.body = "Test Body"
|
||||||
component.is_html = False
|
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
|
# Execute action
|
||||||
result = component.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):
|
def test_execute_action_fetch_emails(self, component_class, default_kwargs, monkeypatch):
|
||||||
# Mock Action enum
|
# Mock Action enum
|
||||||
|
|
@ -87,27 +96,24 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
|
||||||
component.max_results = 10
|
component.max_results = 10
|
||||||
component.query = "from:test@example.com"
|
component.query = "from:test@example.com"
|
||||||
|
|
||||||
# Create a mock for the toolset
|
# For this specific test, we need to customize the action_data to handle results field
|
||||||
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 = {
|
component._actions_data = {
|
||||||
"GMAIL_FETCH_EMAILS": {
|
"GMAIL_FETCH_EMAILS": {
|
||||||
"action_fields": ["max_results", "query"],
|
"action_fields": ["max_results", "query"],
|
||||||
"result_field": "response",
|
"result_field": "messages",
|
||||||
"get_result_field": True,
|
"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()
|
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):
|
def test_execute_action_get_profile(self, component_class, default_kwargs, monkeypatch):
|
||||||
# Mock Action enum
|
# Mock Action enum
|
||||||
|
|
@ -118,9 +124,18 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
|
||||||
component.api_key = "test_key"
|
component.api_key = "test_key"
|
||||||
component.action = [{"name": "Get User Profile"}]
|
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
|
# Execute action
|
||||||
result = component.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):
|
def test_execute_action_invalid_action(self, component_class, default_kwargs):
|
||||||
# Setup component
|
# Setup component
|
||||||
|
|
@ -173,24 +188,9 @@ class TestGmailComponent(ComponentTestBaseWithoutClient):
|
||||||
# Verify the DataFrame is not empty
|
# Verify the DataFrame is not empty
|
||||||
assert not result.empty
|
assert not result.empty
|
||||||
|
|
||||||
# Verify the DataFrame contains our mock data
|
# Check for expected content in the DataFrame string representation
|
||||||
# This will depend on how the component processes the data
|
data_str = str(result)
|
||||||
# We can check for specific column names or values
|
assert "test email" in data_str
|
||||||
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)
|
|
||||||
|
|
||||||
def test_update_build_config(self, component_class, default_kwargs):
|
def test_update_build_config(self, component_class, default_kwargs):
|
||||||
# Test that the Gmail component properly inherits and uses the base component's
|
# Test that the Gmail component properly inherits and uses the base component's
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue