diff --git a/griptape/tools/__init__.py b/griptape/tools/__init__.py index 848f49d..ff75cd3 100644 --- a/griptape/tools/__init__.py +++ b/griptape/tools/__init__.py @@ -13,11 +13,17 @@ from .aws_s3_client.tool import AwsS3Client from .computer.tool import Computer from .proxycurl_client.tool import ProxycurlClient +from .base_google_client import BaseGoogleClient +from .google_gmail.tool import GoogleGmailClient +from .google_cal.tool import GoogleCalendarClient __all__ = [ "BaseAwsClient", "AwsIamClient", "AwsS3Client", + "BaseGoogleClient", + "GoogleGmailClient", + "GoogleCalendarClient", "Calculator", "WebSearch", "WebScraper", diff --git a/griptape/tools/base_aws_client.py b/griptape/tools/base_aws_client.py index 4d2ffce..ff235f2 100644 --- a/griptape/tools/base_aws_client.py +++ b/griptape/tools/base_aws_client.py @@ -5,7 +5,6 @@ from griptape.core import BaseTool from griptape.core.decorators import activity - @define class BaseAwsClient(BaseTool, ABC): session: boto3.session = field(kw_only=True) diff --git a/griptape/tools/base_google_client.py b/griptape/tools/base_google_client.py new file mode 100644 index 0000000..0ecb205 --- /dev/null +++ b/griptape/tools/base_google_client.py @@ -0,0 +1,7 @@ +from abc import ABC +from attr import define, field +from griptape.core import BaseTool + +@define +class BaseGoogleClient(BaseTool, ABC): + service_account_credentials: dict = field(kw_only=True) diff --git a/griptape/tools/google_cal/__init__.py b/griptape/tools/google_cal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/griptape/tools/google_cal/manifest.yml b/griptape/tools/google_cal/manifest.yml new file mode 100644 index 0000000..ffc4765 --- /dev/null +++ b/griptape/tools/google_cal/manifest.yml @@ -0,0 +1,5 @@ +version: "v1" +name: Google Calendar Tool +description: Tool for working with Google Calendar. +contact_email: hello@griptape.ai +legal_info_url: https://www.griptape.ai/legal \ No newline at end of file diff --git a/griptape/tools/google_cal/requirements.txt b/griptape/tools/google_cal/requirements.txt new file mode 100644 index 0000000..704bcd5 --- /dev/null +++ b/griptape/tools/google_cal/requirements.txt @@ -0,0 +1 @@ +google-api-python-client \ No newline at end of file diff --git a/griptape/tools/google_cal/tool.py b/griptape/tools/google_cal/tool.py new file mode 100644 index 0000000..54a9d3f --- /dev/null +++ b/griptape/tools/google_cal/tool.py @@ -0,0 +1,129 @@ +from __future__ import annotations +import logging +import datetime +from schema import Schema, Literal, Optional +from attr import define +from griptape.artifacts import TextArtifact, ErrorArtifact, InfoArtifact +from griptape.core.decorators import activity +from griptape.tools import BaseGoogleClient + + +@define +class GoogleCalendarClient(BaseGoogleClient): + CREATE_EVENT_SCOPES = ['https://www.googleapis.com/auth/calendar'] + GET_UPCOMING_EVENTS_SCOPES = ['https://www.googleapis.com/auth/calendar'] + + @activity(config={ + "description": "Can be used to get upcoming events from a google calendar", + "schema": Schema({ + Literal( + "calendar_id", + description="id of the google calendar such as 'primary'" + ): str, + Literal( + "calendar_owner_email", + description="email of the calendar's owner" + ): str, + Literal( + "max_events", + description="maximum number of events to return" + ): int + }) + }) + def get_upcoming_events(self, params: dict) -> list[TextArtifact] | ErrorArtifact: + from google.oauth2 import service_account + from googleapiclient.discovery import build + + values = params["values"] + + try: + credentials = service_account.Credentials.from_service_account_info( + self.service_account_credentials, scopes=self.GET_UPCOMING_EVENTS_SCOPES + ) + delegated_credentials = credentials.with_subject(values["calendar_owner_email"]) + service = build('calendar', 'v3', credentials=delegated_credentials) + now = datetime.datetime.utcnow().isoformat() + 'Z' + + events_result = service.events().list( + calendarId=values["calendar_id"], timeMin=now, + maxResults=values['max_events'], singleEvents=True, + orderBy='startTime').execute() + events = events_result.get('items', []) + return [TextArtifact(str(e)) for e in events] + except Exception as e: + logging.error(e) + return ErrorArtifact(f"error retrieving calendar events {e}") + + @activity(config={ + "description": "Can be used to create an event on a google calendar", + "schema": Schema({ + Literal( + "calendar_owner_email", + description="email of the calendar's owner" + ): str, + Literal( + "start_datetime", + description="combined date-time value in string format according to RFC3399 excluding the timezone for when the meeting starts" + ): str, + Literal( + "start_time_zone", + description="time zone in which the start time is specified in string format according to IANA time zone data base name, such as 'Europe/Zurich'" + ): str, + Literal( + "end_datetime", + description="combined date-time value in string format according to RFC3399 excluding the timezone for when the meeting ends" + ): str, + Literal( + "end_time_zone", + description="time zone in which the end time is specified in string format according to IANA time zone data base name, such as 'Europe/Zurich'" + ): str, + Literal( + "title", + description="title of the event" + ): str, + Literal( + "description", + description="description of the event" + ): str, + Literal( + "attendees", + description="list of the email addresses of attendees using 'email' as key" + ): list, + Optional(Literal( + "location", + description="location of the event" + )): str + }) + }) + def create_event(self, params: dict) -> InfoArtifact | ErrorArtifact: + from google.oauth2 import service_account + from googleapiclient.discovery import build + + values = params['values'] + + try: + credentials = service_account.Credentials.from_service_account_info( + self.service_account_credentials, scopes=self.CREATE_EVENT_SCOPES + ) + delegated_credentials = credentials.with_subject(values["calendar_owner_email"]) + service = build('calendar', 'v3', credentials=delegated_credentials) + + event = { + 'summary': values['title'], + 'location': values.get('location'), + 'description': values['description'], + 'start': { + 'dateTime': values['start_datetime'], + 'timeZone': values['start_time_zone'] + }, + 'end': { + 'dateTime': values['end_datetime'], + 'timeZone': values['end_time_zone'] + }, + 'attendees': values['attendees'] + } + event = service.events().insert(calendarId='primary', body=event).execute() + return InfoArtifact(f'A calendar event was successfully created. (Link:{event.get("htmlLink")})') + except Exception as e: + logging.error(e) + return ErrorArtifact(f"error creating calendar event: {e}") \ No newline at end of file diff --git a/griptape/tools/google_gmail/__init__.py b/griptape/tools/google_gmail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/griptape/tools/google_gmail/manifest.yml b/griptape/tools/google_gmail/manifest.yml new file mode 100644 index 0000000..262e3a6 --- /dev/null +++ b/griptape/tools/google_gmail/manifest.yml @@ -0,0 +1,5 @@ +version: "v1" +name: Google Gmail Client +description: Tool for working with Google Gmail. +contact_email: hello@griptape.ai +legal_info_url: https://www.griptape.ai/legal \ No newline at end of file diff --git a/griptape/tools/google_gmail/requirements.txt b/griptape/tools/google_gmail/requirements.txt new file mode 100644 index 0000000..704bcd5 --- /dev/null +++ b/griptape/tools/google_gmail/requirements.txt @@ -0,0 +1 @@ +google-api-python-client \ No newline at end of file diff --git a/griptape/tools/google_gmail/tool.py b/griptape/tools/google_gmail/tool.py new file mode 100644 index 0000000..c21573e --- /dev/null +++ b/griptape/tools/google_gmail/tool.py @@ -0,0 +1,71 @@ +from __future__ import annotations +import logging +import base64 +from email.message import EmailMessage +from schema import Schema, Literal +from attr import define +from griptape.artifacts import InfoArtifact, ErrorArtifact +from griptape.core.decorators import activity +from griptape.tools import BaseGoogleClient + + +@define +class GoogleGmailClient(BaseGoogleClient): + CREATE_DRAFT_EMAIL_SCOPES = ['https://www.googleapis.com/auth/gmail.compose'] + @activity(config={ + "description": "Can be used to create a draft email in GMail", + "schema": Schema({ + Literal( + "to", + description="email address which to send to" + ): str, + Literal( + "subject", + description="subject of the email" + ): str, + Literal( + "from", + description="email address which to send from" + ): str, + Literal( + "body", + description="body of the email" + ): str, + Literal( + "inbox_owner", + description="email address of the inbox owner where the draft will be created. if not provided, use the from address" + ): str + }) + }) + def create_draft_email(self, params: dict) -> InfoArtifact | ErrorArtifact: + from google.oauth2 import service_account + from googleapiclient.discovery import build + + values = params["values"] + + try: + credentials = service_account.Credentials.from_service_account_info( + self.service_account_credentials, scopes=self.CREATE_DRAFT_EMAIL_SCOPES + ) + + delegated_creds = credentials.with_subject(values["inbox_owner"]) + service = build('gmail', 'v1', credentials=delegated_creds) + + message = EmailMessage() + message.set_content(values["body"]) + message['To'] = values["to"] + message['From'] = values["from"] + message['Subject'] = values["subject"] + + encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode() + create_message = { + 'message': { + 'raw': encoded_message + } + } + draft = service.users().drafts().create(userId='me', body=create_message).execute() + return InfoArtifact(f'An email draft was successfully created (ID: {draft["id"]})') + + except Exception as error: + logging.error(error) + return ErrorArtifact(f'error creating draft email: {error}') diff --git a/tests/unit/test_google_cal.py b/tests/unit/test_google_cal.py new file mode 100644 index 0000000..ca660e0 --- /dev/null +++ b/tests/unit/test_google_cal.py @@ -0,0 +1,28 @@ +from griptape.tools import GoogleCalendarClient + + +class TestGoogleCalClient: + def test_get_upcoming_events(self): + value = { + "calendar_id": "primary", + "calendar_owner_email": "tony@griptape.ai", + "max_events": 10 + } + assert "error retrieving calendar events" in GoogleCalendarClient( + service_account_credentials={} + ).get_upcoming_events({"values": value}).value + + def test_create_event(self): + value = { + "calendar_owner_email": "tony@griptape.ai", + "start_datetime": "2023-07-28T13:00:00", + "start_time_zone": "America/Los_Angeles", + "end_datetime": "2023-07-28T14:00:00", + "end_time_zone": "America/Los_Angeles", + "title": "could have been an email", + "description": "why wasn't this an email?", + "location": "not rto" + } + assert "error creating calendar event" in GoogleCalendarClient( + service_account_credentials={} + ).create_event({"values": value}).value \ No newline at end of file diff --git a/tests/unit/test_google_gmail.py b/tests/unit/test_google_gmail.py new file mode 100644 index 0000000..a97a5c3 --- /dev/null +++ b/tests/unit/test_google_gmail.py @@ -0,0 +1,15 @@ +from griptape.tools import GoogleGmailClient + + +class TestGoogleGmailClient: + def test_create_draft_email(self): + value = { + "to": "tony@griptape.ai", + "subject": "stacey's mom", + "from": "test@test.com", + "body": "got it going on", + "inbox_owner": "tony@griptape.ai" + } + assert "error creating draft email" in GoogleGmailClient( + service_account_credentials={} + ).create_draft_email({"values": value}).value