diff --git a/.gitignore b/.gitignore index 2dc53ca..3dd9cda 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ +.aider* diff --git a/README.md b/README.md index 2ac08b2..17bb8ca 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,47 @@ -# Griptape Extension Template +# Griptape Google Extension -A Github template repository for creating Griptape extensions. +## Overview +This extension provides several [Tools](https://docs.griptape.ai/stable/griptape-tools/) for various Google services. -## Getting Started +```python +import os -Via github web page: +from griptape.structures import Agent +from griptape.tools import GoogleGmailTool -Click on `Use this template` +gmail_tool = GoogleGmailTool( + service_account_credentials={ + "type": os.environ["GOOGLE_ACCOUNT_TYPE"], + "project_id": os.environ["GOOGLE_PROJECT_ID"], + "private_key_id": os.environ["GOOGLE_PRIVATE_KEY_ID"], + "private_key": os.environ["GOOGLE_PRIVATE_KEY"], + "client_email": os.environ["GOOGLE_CLIENT_EMAIL"], + "client_id": os.environ["GOOGLE_CLIENT_ID"], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": os.environ["GOOGLE_CERT_URL"], + }, + owner_email=os.environ["GOOGLE_OWNER_EMAIL"], +) -![](https://docs.github.com/assets/cb-36544/images/help/repository/use-this-template-button.png) +agent = Agent(tools=[gmail_tool]) - -Via `gh`: - -``` -$ gh repo create griptape-extension-name -p griptape/griptape-extension-template -``` - -## What is a Griptape Extension? - -Griptape Extensions can add new functionality to the [Griptape framework](https://github.com/griptape-ai/griptape), such as new Tools, Drivers, Tasks, or Structures. -With extensions, you can integrate custom APIs, tools, and services into the Griptape ecosystem. - -This repository provides a recommended structure for organizing your extension code, as well as helpful tools for testing and development. - -## Extension Structure - -The template repository is structured as follows: - -```bash -tree -I __init__.py -I __pycache__ - -├── griptape -│ └── extension_name # Name whatever you want -│ └── tools -│ └── reverse_string -│ └── tool.py - ...more directories for other interfaces (drivers, tasks, structures, etc)... -└── tests - └── unit - └── tools - └── test_reverse_string_tool.py -├── examples - └── tools - └── example_agent.py # Example usage of the extension -├── LICENSE # Choose the appropriate license -├── Makefile # Contains useful commands for development -├── pyproject.toml # Contains the project's metadata -├── README.md # Describes the extension and how to use it +agent.run( + "Create a draft email in Gmail to example@email.com with the subject 'Test Draft', the body " + "'This is a test draft email.'", +) ``` -## Development - -### Poetry - -This project uses [Poetry](https://python-poetry.org/) for dependency management. -It is recommended to configure Poetry to use [in-project](https://python-poetry.org/docs/configuration/#virtualenvsin-project) virtual environments: - -```bash -poetry config virtualenvs.in-project true -``` - -This will create a `.venv` directory in the project root, where the virtual environment will be stored. -This ensures that the virtual environment is always in the same location, regardless of where the project is cloned. - -### Useful Commands - -#### Installing Dependencies +## Installation +Poetry: ```bash -make install +poetry add https://github.com/griptape-ai/griptape-google.git ``` -#### Running Tests - +Pip: ```bash -make test +pip install git+https://github.com/griptape-ai/griptape-google.git ``` -#### Running Checks (linting, formatting, etc) - -```bash -make check -``` - -#### Running Formatter - -```bash -make format -``` - -#### Running Example - -This template includes an [example](https://github.com/griptape-ai/tool-template/blob/main/examples/tools/example_agent.py) demonstrating how to use the extension. It shows how to import the `ReverseStringTool`, provide it to an Agent, and run it. - -1. Set the required environment variables. The example needs the `OPENAI_API_KEY` environment variable to be set. -2. Run the example: - -```bash -poetry run python examples/tools/example_agent.py -``` - -If successful, you should see: -``` -[11/18/24 14:55:14] INFO ToolkitTask 6bb7fa5581d147b2a39e801631c98005 - Input: Use the ReverseStringTool to reverse 'Griptape' -[11/18/24 14:55:15] INFO Subtask c3036471831144529b8d5300c6849203 - Actions: [ - { - "tag": "call_VE4tGBFL7iB7VDbkKaIFIkwY", - "name": "ReverseStringTool", - "path": "reverse_string", - "input": { - "values": { - "input": "Griptape" - } - } - } - ] - INFO Subtask c3036471831144529b8d5300c6849203 - Response: epatpirG -[11/18/24 14:55:16] INFO ToolkitTask 6bb7fa5581d147b2a39e801631c98005 - Output: The reversed string of "Griptape" is "epatpirG". -``` - -## Installing in Other Projects - -Extensions are designed to be shared. Extensions are made to easily install into existing Python projects. - -The easiest way to include your extension into an existing project is to install directly from the repository, like so: -```bash -poetry add git+https://github.com/{your-org}/{your-extension-name}.git -``` - -To install a local copy of the extension for development, run: -```bash -poetry add -e /path/to/your/extension -``` - -Any changes made to the extension will be automatically reflected in the project without needing to reinstall it. - -Advanced customers may seek to publish their extensions to PyPi. Those instructions are beyond the scope of this README. diff --git a/examples/tools/example_agent.py b/examples/tools/example_agent.py deleted file mode 100644 index 3fac506..0000000 --- a/examples/tools/example_agent.py +++ /dev/null @@ -1,7 +0,0 @@ -from griptape.structures import Agent -from griptape.plugin_name.tools.reverse_string import ReverseStringTool - - -agent = Agent(tools=[ReverseStringTool()]) - -agent.run("Use the ReverseStringTool to reverse 'Griptape'") diff --git a/examples/tools/google_calendar_tool.py b/examples/tools/google_calendar_tool.py new file mode 100644 index 0000000..afbb20c --- /dev/null +++ b/examples/tools/google_calendar_tool.py @@ -0,0 +1,29 @@ +import os + +from griptape.structures import Agent +from griptape.tools import GoogleCalendarTool + +# Create the GoogleCalendarTool tool +google_calendarendar_tool = GoogleCalendarTool( + service_account_credentials={ + "type": os.environ["GOOGLE_ACCOUNT_TYPE"], + "project_id": os.environ["GOOGLE_PROJECT_ID"], + "private_key_id": os.environ["GOOGLE_PRIVATE_KEY_ID"], + "private_key": os.environ["GOOGLE_PRIVATE_KEY"], + "client_email": os.environ["GOOGLE_CLIENT_EMAIL"], + "client_id": os.environ["GOOGLE_CLIENT_ID"], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": os.environ["GOOGLE_CERT_URL"], + }, + owner_email=os.environ["GOOGLE_OWNER_EMAIL"], +) + +# Set up an agent using the GoogleCalendarTool tool +agent = Agent(tools=[google_calendarendar_tool]) + +# Task: Get upcoming events from a Google calendar +agent.run( + "Get me the details of the next upcoming event from my primary calendar.", +) diff --git a/examples/tools/google_docs_tool.py b/examples/tools/google_docs_tool.py new file mode 100644 index 0000000..0d8e8a3 --- /dev/null +++ b/examples/tools/google_docs_tool.py @@ -0,0 +1,29 @@ +import os + +from griptape.structures import Agent +from griptape.tools import GoogleDocsTool + +# Create the GoogleDocsTool tool +google_docs_tool = GoogleDocsTool( + service_account_credentials={ + "type": os.environ["GOOGLE_ACCOUNT_TYPE"], + "project_id": os.environ["GOOGLE_PROJECT_ID"], + "private_key_id": os.environ["GOOGLE_PRIVATE_KEY_ID"], + "private_key": os.environ["GOOGLE_PRIVATE_KEY"], + "client_email": os.environ["GOOGLE_CLIENT_EMAIL"], + "client_id": os.environ["GOOGLE_CLIENT_ID"], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": os.environ["GOOGLE_CERT_URL"], + }, + owner_email=os.environ["GOOGLE_OWNER_EMAIL"], +) + +# Set up an agent using the GoogleDocsTool tool +agent = Agent(tools=[google_docs_tool]) + +# Task: Create a new Google Doc and save content to it +agent.run( + "Create doc with name 'test_creation' in test folder with content 'Hey, Tony.", +) diff --git a/examples/tools/google_drive_tool.py b/examples/tools/google_drive_tool.py new file mode 100644 index 0000000..d8e43a6 --- /dev/null +++ b/examples/tools/google_drive_tool.py @@ -0,0 +1,29 @@ +import os + +from griptape.structures import Agent +from griptape.tools import GoogleDriveTool + +# Create the GoogleDriveTool tool +google_drive_tool = GoogleDriveTool( + service_account_credentials={ + "type": os.environ["GOOGLE_ACCOUNT_TYPE"], + "project_id": os.environ["GOOGLE_PROJECT_ID"], + "private_key_id": os.environ["GOOGLE_PRIVATE_KEY_ID"], + "private_key": os.environ["GOOGLE_PRIVATE_KEY"], + "client_email": os.environ["GOOGLE_CLIENT_EMAIL"], + "client_id": os.environ["GOOGLE_CLIENT_ID"], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": os.environ["GOOGLE_CERT_URL"], + }, + owner_email=os.environ["GOOGLE_OWNER_EMAIL"], +) + +# Set up an agent using the GoogleDriveTool tool +agent = Agent(tools=[google_drive_tool]) + +# Task: Save content to my Google Drive (default directory is root) +agent.run( + "Save the content 'Hi this is Tony' in a filed named 'hello.txt' to my Drive.", +) diff --git a/examples/tools/google_gmail_tool.py b/examples/tools/google_gmail_tool.py new file mode 100644 index 0000000..b3e38fb --- /dev/null +++ b/examples/tools/google_gmail_tool.py @@ -0,0 +1,30 @@ +import os + +from griptape.structures import Agent +from griptape.tools import GoogleGmailTool + +# Create the GoogleGmailTool tool +gmail_tool = GoogleGmailTool( + service_account_credentials={ + "type": os.environ["GOOGLE_ACCOUNT_TYPE"], + "project_id": os.environ["GOOGLE_PROJECT_ID"], + "private_key_id": os.environ["GOOGLE_PRIVATE_KEY_ID"], + "private_key": os.environ["GOOGLE_PRIVATE_KEY"], + "client_email": os.environ["GOOGLE_CLIENT_EMAIL"], + "client_id": os.environ["GOOGLE_CLIENT_ID"], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": os.environ["GOOGLE_CERT_URL"], + }, + owner_email=os.environ["GOOGLE_OWNER_EMAIL"], +) + +# Set up an agent using the GoogleGmailTool tool +agent = Agent(tools=[gmail_tool]) + +# Task: Create a draft email in Gmail +agent.run( + "Create a draft email in Gmail to example@email.com with the subject 'Test Draft', the body " + "'This is a test draft email.'", +) diff --git a/griptape/plugin_name/__init__.py b/griptape/google/__init__.py similarity index 100% rename from griptape/plugin_name/__init__.py rename to griptape/google/__init__.py diff --git a/griptape/google/tools/__init__.py b/griptape/google/tools/__init__.py new file mode 100644 index 0000000..b9a1f78 --- /dev/null +++ b/griptape/google/tools/__init__.py @@ -0,0 +1,13 @@ +from .base_google_tool import BaseGoogleTool +from .google_calendar.tool import GoogleCalendarTool +from .google_docs.tool import GoogleDocsTool +from .google_drive.tool import GoogleDriveTool +from .google_gmail.tool import GoogleGmailTool + +__all__ = [ + "BaseGoogleTool", + "GoogleCalendarTool", + "GoogleDocsTool", + "GoogleDriveTool", + "GoogleGmailTool", +] diff --git a/griptape/google/tools/base_google_tool.py b/griptape/google/tools/base_google_tool.py new file mode 100644 index 0000000..902ae12 --- /dev/null +++ b/griptape/google/tools/base_google_tool.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from abc import ABC +from typing import Any + +from attrs import define, field + +from griptape.tools import BaseTool + + +@define +class BaseGoogleTool(BaseTool, ABC): + DRIVE_FILE_SCOPES = ["https://www.googleapis.com/auth/drive.file"] + + DRIVE_AUTH_SCOPES = ["https://www.googleapis.com/auth/drive"] + + owner_email: str = field(kw_only=True) + service_account_credentials: dict = field(kw_only=True) + + def _build_client(self, scopes: list[str], service_name: str, version: str) -> Any: + from google.oauth2 import service_account + from googleapiclient.discovery import build + + credentials = service_account.Credentials.from_service_account_info( + self.service_account_credentials, + scopes=scopes, + ) + + return build( + serviceName=service_name, + version=version, + credentials=credentials.with_subject(self.owner_email), + ) + + def _convert_path_to_file_id(self, service: Any, path: str) -> str | None: + parts = path.split("/") + current_id = "root" + + for idx, part in enumerate(parts): + if idx == len(parts) - 1: + query = f"name='{part}' and '{current_id}' in parents" + else: + query = f"name='{part}' and '{current_id}' in parents and mimeType='application/vnd.google-apps.folder'" + + response = service.files().list(q=query).execute() + files = response.get("files", []) + + if not files: + if idx != len(parts) - 1: + folder_metadata = { + "name": part, + "mimeType": "application/vnd.google-apps.folder", + "parents": [current_id], + } + folder = ( + service.files() + .create(body=folder_metadata, fields="id") + .execute() + ) + current_id = folder.get("id") + else: + current_id = None + else: + current_id = files[0]["id"] + + return current_id diff --git a/griptape/plugin_name/tools/__init__.py b/griptape/google/tools/google_calendar/__init__.py similarity index 100% rename from griptape/plugin_name/tools/__init__.py rename to griptape/google/tools/google_calendar/__init__.py diff --git a/griptape/google/tools/google_calendar/requirements.txt b/griptape/google/tools/google_calendar/requirements.txt new file mode 100644 index 0000000..704bcd5 --- /dev/null +++ b/griptape/google/tools/google_calendar/requirements.txt @@ -0,0 +1 @@ +google-api-python-client \ No newline at end of file diff --git a/griptape/google/tools/google_calendar/tool.py b/griptape/google/tools/google_calendar/tool.py new file mode 100644 index 0000000..e27fab5 --- /dev/null +++ b/griptape/google/tools/google_calendar/tool.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import datetime +import logging + +from attrs import define +from schema import Literal, Optional, Schema + +from griptape.artifacts import ErrorArtifact, InfoArtifact, ListArtifact, TextArtifact +from griptape.google.tools import BaseGoogleTool +from griptape.utils.decorators import activity + + +@define +class GoogleCalendarTool(BaseGoogleTool): + 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( + "max_events", description="maximum number of events to return" + ): int, + }, + ), + }, + ) + def get_upcoming_events(self, params: dict) -> ListArtifact | ErrorArtifact: + values = params["values"] + + try: + service = self._build_client( + scopes=self.GET_UPCOMING_EVENTS_SCOPES, + service_name="calendar", + version="v3", + ) + 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 ListArtifact([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( + "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[str], + Optional( + Literal("location", description="location of the event") + ): str, + }, + ), + }, + ) + def create_event(self, params: dict) -> InfoArtifact | ErrorArtifact: + values = params["values"] + + try: + service = self._build_client( + scopes=self.CREATE_EVENT_SCOPES, + service_name="calendar", + version="v3", + ) + + 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}") diff --git a/griptape/google/tools/google_docs/__init__.py b/griptape/google/tools/google_docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/griptape/google/tools/google_docs/requirements.txt b/griptape/google/tools/google_docs/requirements.txt new file mode 100644 index 0000000..704bcd5 --- /dev/null +++ b/griptape/google/tools/google_docs/requirements.txt @@ -0,0 +1 @@ +google-api-python-client \ No newline at end of file diff --git a/griptape/google/tools/google_docs/tool.py b/griptape/google/tools/google_docs/tool.py new file mode 100644 index 0000000..33d7255 --- /dev/null +++ b/griptape/google/tools/google_docs/tool.py @@ -0,0 +1,286 @@ +from __future__ import annotations + +import logging + +from attrs import define +from schema import Literal, Optional, Schema + +from griptape.artifacts import ErrorArtifact, InfoArtifact +from griptape.google.tools import BaseGoogleTool +from griptape.utils.decorators import activity + + +@define +class GoogleDocsTool(BaseGoogleTool): + DOCS_SCOPES = ["https://www.googleapis.com/auth/documents"] + + DEFAULT_FOLDER_PATH = "root" + + @activity( + config={ + "description": "Can be used to append text to a Google Doc.", + "schema": Schema( + { + Literal( + "file_path", + description="Destination file path of Google Doc in the POSIX format. " + "For example, 'foo/bar/baz.txt'", + ): str, + Literal( + "text", description="Text to be appended to the Google Doc." + ): str, + }, + ), + }, + ) + def append_text_to_google_doc(self, params: dict) -> InfoArtifact | ErrorArtifact: + values = params["values"] + file_path = values.get("file_path") + text = values.get("text") + + try: + docs_service = self._build_client( + scopes=self.DOCS_SCOPES, + service_name="docs", + version="v1", + ) + drive_service = self._build_client( + scopes=self.DRIVE_FILE_SCOPES, + service_name="drive", + version="v3", + ) + + document_id = self._convert_path_to_file_id(drive_service, file_path) + if document_id: + doc = docs_service.documents().get(documentId=document_id).execute() + content = doc["body"]["content"] + last_text = content[-1]["paragraph"]["elements"][-1]["textRun"][ + "content" + ] + append_index = content[-1]["endIndex"] + if last_text.endswith("\n"): + append_index -= 1 + + requests = [ + {"insertText": {"location": {"index": append_index}, "text": text}} + ] + + docs_service.documents().batchUpdate( + documentId=document_id, body={"requests": requests} + ).execute() + return InfoArtifact("text appended successfully") + else: + return ErrorArtifact( + f"error appending to Google Doc, file not found for path {file_path}" + ) + + except Exception as e: + logging.error(e) + return ErrorArtifact( + f"error appending text to Google Doc with path {file_path}: {e}" + ) + + @activity( + config={ + "description": "Can be used to prepend text to a Google Doc", + "schema": Schema( + { + Literal( + "file_path", + description="Destination file path of Google Doc in the POSIX format. " + "For example, 'foo/bar/baz.txt'", + ): str, + Literal( + "text", description="Text to be prepended to the Google Doc." + ): str, + }, + ), + }, + ) + def prepend_text_to_google_doc(self, params: dict) -> InfoArtifact | ErrorArtifact: + values = params["values"] + file_path = values.get("file_path") + text = values.get("text") + + try: + docs_service = self._build_client( + scopes=self.DOCS_SCOPES, + service_name="docs", + version="v1", + ) + drive_service = self._build_client( + scopes=self.DRIVE_FILE_SCOPES, + service_name="drive", + version="v3", + ) + + document_id = self._convert_path_to_file_id(drive_service, file_path) + if document_id: + doc = docs_service.documents().get(documentId=document_id).execute() + + if len(doc["body"]["content"]) == 1: + requests = [ + {"insertText": {"location": {"index": 1}, "text": text}} + ] + else: + start_index = doc["body"]["content"][1]["startIndex"] + requests = [ + { + "insertText": { + "location": {"index": start_index}, + "text": text, + } + } + ] + + docs_service.documents().batchUpdate( + documentId=document_id, body={"requests": requests} + ).execute() + return InfoArtifact("text prepended successfully") + else: + return ErrorArtifact( + f"error prepending to google doc, file not found for path {file_path}" + ) + + except Exception as e: + logging.error(e) + return ErrorArtifact( + f"error prepending text to Google Doc with path {file_path}: {e}" + ) + + @activity( + config={ + "description": "Can be used to create a new Google Doc and optionally save content to it.", + "schema": Schema( + { + Literal( + "file_path", + description="Name of the file to be created, which will be used to save content in.", + ): str, + Optional( + "content", + default=None, + description="Optional content to be saved in Google Doc.", + ): str, + Optional( + "folder_path", + default=DEFAULT_FOLDER_PATH, + description="Path of the folder where the Google doc will be created.", + ): str, + }, + ), + }, + ) + def save_content_to_google_doc(self, params: dict) -> ErrorArtifact | InfoArtifact: + values = params["values"] + file_path = values.get("file_path") + content = values.get("content") + folder_path = values.get("folder_path", self.DEFAULT_FOLDER_PATH) + + try: + docs_service = self._build_client( + scopes=self.DOCS_SCOPES, + service_name="docs", + version="v1", + ) + drive_service = self._build_client( + scopes=self.DRIVE_FILE_SCOPES, + service_name="drive", + version="v3", + ) + + body = {"title": file_path} + + doc = docs_service.documents().create(body=body).execute() + doc_id = doc["documentId"] + + if folder_path.lower() != self.DEFAULT_FOLDER_PATH: + folder_id = self._convert_path_to_file_id(drive_service, folder_path) + if folder_id: + drive_service.files().update( + fileId=doc_id, addParents=folder_id, fields="id, parents" + ).execute() + else: + return ErrorArtifact( + f"Error: Folder not found for path {folder_path}" + ) + + if content: + save_content_params = {"document_id": doc_id, "content": content} + saved_document_id = self._save_to_doc(save_content_params) + return InfoArtifact( + f"Content has been successfully saved to Google Doc with ID: {saved_document_id}." + ) + else: + return InfoArtifact( + f"Google Doc '{file_path}' created with ID: {doc_id}" + ) + + except Exception as e: + logging.error(e) + return ErrorArtifact(f"Error creating/saving Google Doc: {e}") + + @activity( + config={ + "description": "Can be used to load content from memory and save it to a new Google Doc " + "in the specified folder.", + "schema": Schema( + { + "memory_name": str, + "artifact_namespace": str, + "file_name": str, + Optional( + "folder_path", + description="Path of the folder where the Google Doc should be saved.", + default=DEFAULT_FOLDER_PATH, + ): str, + }, + ), + }, + ) + def save_memory_artifacts_to_google_docs( + self, params: dict + ) -> ErrorArtifact | InfoArtifact: + values = params["values"] + memory = self.find_input_memory(values["memory_name"]) + + if memory: + artifacts = memory.load_artifacts(values["artifact_namespace"]) + + if artifacts: + try: + file_path = values["file_name"] + content = "\n".join([a.value for a in artifacts]) + + save_params = { + "file_path": file_path, + "content": content, + "folder_path": values.get( + "folder_path", self.DEFAULT_FOLDER_PATH + ), + } + + return self.save_content_to_google_doc(save_params) + + except Exception as e: + return ErrorArtifact(f"Error: {e}") + + else: + return ErrorArtifact("no artifacts found") + else: + return ErrorArtifact("memory not found") + + def _save_to_doc(self, params: dict) -> str: + service = self._build_client( + scopes=self.DOCS_SCOPES, + service_name="docs", + version="v1", + ) + + requests = [ + {"insertText": {"location": {"index": 1}, "text": params["content"]}} + ] + service.documents().batchUpdate( + documentId=params["document_id"], body={"requests": requests} + ).execute() + return params["document_id"] diff --git a/griptape/google/tools/google_drive/__init__.py b/griptape/google/tools/google_drive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/griptape/google/tools/google_drive/requirements.txt b/griptape/google/tools/google_drive/requirements.txt new file mode 100644 index 0000000..704bcd5 --- /dev/null +++ b/griptape/google/tools/google_drive/requirements.txt @@ -0,0 +1 @@ +google-api-python-client \ No newline at end of file diff --git a/griptape/google/tools/google_drive/tool.py b/griptape/google/tools/google_drive/tool.py new file mode 100644 index 0000000..9ba0f1b --- /dev/null +++ b/griptape/google/tools/google_drive/tool.py @@ -0,0 +1,431 @@ +from __future__ import annotations + +import logging +from io import BytesIO +from typing import Any + +import schema +from attrs import define +from schema import Literal, Or, Schema + +from griptape.artifacts import ( + BlobArtifact, + ErrorArtifact, + InfoArtifact, + ListArtifact, + TextArtifact, +) +from griptape.google.tools import BaseGoogleTool +from griptape.utils.decorators import activity + + +@define +class GoogleDriveTool(BaseGoogleTool): + LIST_FILES_SCOPES = ["https://www.googleapis.com/auth/drive.readonly"] + + GOOGLE_EXPORT_MIME_MAPPING = { + "application/vnd.google-apps.document": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.google-apps.spreadsheet": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.google-apps.presentation": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + } + + DEFAULT_FOLDER_PATH = "root" + + SERVICE_NAME = "drive" + + SERVICE_VERSION = "v3" + + @activity( + config={ + "description": "Can be used to list files in a specific Google Drive folder.", + "schema": Schema( + { + schema.Optional( + "folder_path", + default=DEFAULT_FOLDER_PATH, + description="Path of the Google Drive folder (like 'MainFolder/Subfolder1/Subfolder2') " + "from which files should be listed.", + ): str, + }, + ), + }, + ) + def list_files(self, params: dict) -> ListArtifact | ErrorArtifact: + values = params["values"] + from google.auth.exceptions import MalformedError # pyright: ignore[reportMissingImports] + + folder_path = values.get("folder_path", self.DEFAULT_FOLDER_PATH) + + try: + service = self._build_client( + self.LIST_FILES_SCOPES, + self.SERVICE_NAME, + self.SERVICE_VERSION, + ) + + if folder_path == self.DEFAULT_FOLDER_PATH: + query = "mimeType != 'application/vnd.google-apps.folder' and 'root' in parents and trashed=false" + else: + folder_id = self._convert_path_to_file_id(service, folder_path) + if folder_id: + query = f"'{folder_id}' in parents and trashed=false" + else: + return ErrorArtifact(f"Could not find folder: {folder_path}") + + items = self._list_files(service, query) + return ListArtifact([TextArtifact(i) for i in items]) + + except MalformedError: + return ErrorArtifact("error listing files due to malformed credentials") + except Exception as e: + return ErrorArtifact(f"error listing files from Google Drive: {e}") + + @activity( + config={ + "description": "Can be used to save memory artifacts to Google Drive using folder paths", + "schema": Schema( + { + "memory_name": str, + "artifact_namespace": str, + "file_name": str, + schema.Optional( + "folder_path", + description="Path of the Google Drive folder (like 'MainFolder/Subfolder1/Subfolder2') " + "where the file should be saved.", + default=DEFAULT_FOLDER_PATH, + ): str, + }, + ), + }, + ) + def save_memory_artifacts_to_drive( + self, params: dict + ) -> ErrorArtifact | InfoArtifact: + values = params["values"] + memory = self.find_input_memory(values["memory_name"]) + file_name = values["file_name"] + folder_path = values.get("folder_path", self.DEFAULT_FOLDER_PATH) + + if memory: + artifacts = memory.load_artifacts(values["artifact_namespace"]) + + if artifacts: + service = self._build_client( + self.DRIVE_FILE_SCOPES, + self.SERVICE_NAME, + self.SERVICE_VERSION, + ) + + if folder_path == self.DEFAULT_FOLDER_PATH: + folder_id = self.DEFAULT_FOLDER_PATH + else: + folder_id = self._convert_path_to_file_id(service, folder_path) + + if folder_id: + try: + if len(artifacts) == 1: + self._save_to_drive( + file_name, artifacts[0].value, folder_id + ) + else: + for a in artifacts: + self._save_to_drive( + f"{a.name}-{file_name}", a.value, folder_id + ) + + return InfoArtifact("saved successfully") + + except Exception as e: + return ErrorArtifact(f"error saving file to Google Drive: {e}") + else: + return ErrorArtifact(f"Could not find folder: {folder_path}") + else: + return ErrorArtifact("no artifacts found") + else: + return ErrorArtifact("memory not found") + + @activity( + config={ + "description": "Can be used to save content to a file on Google Drive", + "schema": Schema( + { + Literal( + "path", + description="Destination file path on Google Drive in the POSIX format. " + "For example, 'foo/bar/baz.txt'", + ): str, + "content": str, + }, + ), + }, + ) + def save_content_to_drive(self, params: dict) -> ErrorArtifact | InfoArtifact: + content = params["values"]["content"] + filename = params["values"]["path"] + + try: + self._save_to_drive(filename, content) + + return InfoArtifact("saved successfully") + except Exception as e: + return ErrorArtifact(f"error saving file to Google Drive: {e}") + + @activity( + config={ + "description": "Can be used to download multiple files from Google Drive based on a provided list of paths", + "schema": Schema( + { + Literal( + "paths", + description="List of paths to files to be loaded in the POSIX format. " + "For example, ['foo/bar/file1.txt', 'foo/bar/file2.txt']", + ): [str], + }, + ), + }, + ) + def download_files(self, params: dict) -> ListArtifact | ErrorArtifact: + from google.auth.exceptions import MalformedError + from googleapiclient.errors import HttpError # pyright: ignore[reportMissingImports] + + values = params["values"] + downloaded_files = [] + + try: + service = self._build_client( + self.LIST_FILES_SCOPES, + self.SERVICE_NAME, + self.SERVICE_VERSION, + ) + + for path in values["paths"]: + file_id = self._convert_path_to_file_id(service, path) + if file_id: + file_info = service.files().get(fileId=file_id).execute() + mime_type = file_info["mimeType"] + + if mime_type in self.GOOGLE_EXPORT_MIME_MAPPING: + export_mime = self.GOOGLE_EXPORT_MIME_MAPPING[mime_type] + request = service.files().export_media( + fileId=file_id, mimeType=export_mime + ) + else: + request = service.files().get_media(fileId=file_id) + + downloaded_files.append(BlobArtifact(request.execute())) + else: + logging.error("Could not find file: %s", path) + + return ListArtifact(downloaded_files) + except HttpError as e: + return ErrorArtifact(f"error downloading file in Google Drive: {e}") + except MalformedError: + return ErrorArtifact("error downloading file due to malformed credentials") + except Exception as e: + return ErrorArtifact(f"error downloading file to Google Drive: {e}") + + @activity( + config={ + "description": "Can search for files on Google Drive based on name or content", + "schema": Schema( + { + Literal( + "search_mode", + description="File search mode. Use 'name' to search in file name or " + "'content' to search in file content", + ): Or( + "name", # pyright: ignore [reportArgumentType] + "content", # pyright: ignore [reportArgumentType] + ), + Literal( + "search_query", + description="Query to search for. If search_mode is 'name', it's the file name. If 'content', " + "it's the text within files.", + ): str, + schema.Optional( + "folder_path", + description="Path of the Google Drive folder (like 'MainFolder/Subfolder1/Subfolder2') " + "where the search should be performed.", + default=DEFAULT_FOLDER_PATH, + ): str, + }, + ), + }, + ) + def search_files(self, params: dict) -> ListArtifact | ErrorArtifact: + from google.auth.exceptions import MalformedError + from googleapiclient.errors import HttpError # pyright: ignore[reportMissingImports] + + values = params["values"] + + search_mode = values["search_mode"] + folder_path = values.get("folder_path", self.DEFAULT_FOLDER_PATH) + + try: + service = self._build_client( + self.LIST_FILES_SCOPES, + self.SERVICE_NAME, + self.SERVICE_VERSION, + ) + + folder_id = None + if folder_path == self.DEFAULT_FOLDER_PATH: + folder_id = self.DEFAULT_FOLDER_PATH + else: + folder_id = self._convert_path_to_file_id(service, folder_path) + + if folder_id: + query = None + if search_mode == "name": + query = f"name='{values['search_query']}'" + elif search_mode == "content": + query = f"fullText contains '{values['search_query']}'" + else: + return ErrorArtifact(f"Invalid search mode: {search_mode}") + + query += " and trashed=false" + if folder_id != self.DEFAULT_FOLDER_PATH: + query += f" and '{folder_id}' in parents" + + results = service.files().list(q=query).execute() + items = results.get("files", []) + return ListArtifact([TextArtifact(i) for i in items]) + else: + return ErrorArtifact(f"Folder path {folder_path} not found") + + except HttpError as e: + return ErrorArtifact(f"error searching for file in Google Drive: {e}") + except MalformedError: + return ErrorArtifact( + "error searching for file due to malformed credentials" + ) + except Exception as e: + return ErrorArtifact(f"error searching file to Google Drive: {e}") + + @activity( + config={ + "description": "Can be used to share a file with a specified user.", + "schema": Schema( + { + Literal( + "file_path", description="The path of the file to share" + ): str, + Literal( + "email_address", + description="The email address of the user to share with", + ): str, + schema.Optional( + "role", + default="reader", + description="The role to give to the user, e.g., 'reader', 'writer', or 'commenter'", + ): Or( + "reader", # pyright: ignore [reportArgumentType] + "writer", # pyright: ignore [reportArgumentType] + "commenter", # pyright: ignore [reportArgumentType] + ), + }, + ), + }, + ) + def share_file(self, params: dict) -> InfoArtifact | ErrorArtifact: + from google.auth.exceptions import MalformedError + from googleapiclient.errors import HttpError # pyright: ignore [reportMissingImports] + + values = params["values"] + file_path = values.get("file_path") + email_address = values.get("email_address") + role = values.get("role", "reader") + + try: + service = self._build_client( + scopes=self.DRIVE_AUTH_SCOPES, + service_name="drive", + version="v3", + ) + + if file_path.lower() == self.DEFAULT_FOLDER_PATH: + file_id = self.DEFAULT_FOLDER_PATH + else: + file_id = self._convert_path_to_file_id(service, file_path) + + if file_id: + batch_update_permission_request_body = { + "role": role, + "type": "user", + "emailAddress": email_address, + } + request = service.permissions().create( + fileId=file_id, + body=batch_update_permission_request_body, + fields="id", + ) + request.execute() + return InfoArtifact( + f"File at {file_path} shared with {email_address} as a {role}" + ) + else: + return ErrorArtifact(f"error finding file at path: {file_path}") + except HttpError as e: + return ErrorArtifact(f"error sharing file due to http error: {e}") + except MalformedError as e: + return ErrorArtifact( + f"error sharing file due to malformed credentials: {e}" + ) + except Exception as e: + return ErrorArtifact(f"error sharing file: {e}") + + def _save_to_drive( + self, + filename: str, + value: Any, + parent_folder_id: str | None = None, + ) -> InfoArtifact | ErrorArtifact: + from googleapiclient.http import MediaIoBaseUpload # pyright: ignore[reportMissingImports] + + service = self._build_client( + self.DRIVE_FILE_SCOPES, + self.SERVICE_NAME, + self.SERVICE_VERSION, + ) + + if isinstance(value, str): + value = value.encode() + + parts = filename.split("/") + if len(parts) > 1: + directory = "/".join(parts[:-1]) + parent_folder_id = self._convert_path_to_file_id(service, directory) + if not parent_folder_id: + return ErrorArtifact(f"Could not find folder: {directory}") + filename = parts[-1] + + file_metadata = {"name": filename, "parents": []} + if parent_folder_id: + file_metadata["parents"] = [parent_folder_id] + + media = MediaIoBaseUpload( + BytesIO(value), mimetype="application/octet-stream", resumable=True + ) + + file = ( + service.files() + .create(body=file_metadata, media_body=media, fields="id") + .execute() + ) + return InfoArtifact(file) + + def _list_files(self, service: Any, query: str) -> list[dict]: + items = [] + next_page_token = None + + while True: + results = service.files().list(q=query, pageToken=next_page_token).execute() + + files = results.get("files", []) + items.extend(files) + + next_page_token = results.get("nextPageToken") + if not next_page_token: + break + + return items diff --git a/griptape/google/tools/google_gmail/__init__.py b/griptape/google/tools/google_gmail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/griptape/google/tools/google_gmail/requirements.txt b/griptape/google/tools/google_gmail/requirements.txt new file mode 100644 index 0000000..704bcd5 --- /dev/null +++ b/griptape/google/tools/google_gmail/requirements.txt @@ -0,0 +1 @@ +google-api-python-client \ No newline at end of file diff --git a/griptape/google/tools/google_gmail/tool.py b/griptape/google/tools/google_gmail/tool.py new file mode 100644 index 0000000..1534734 --- /dev/null +++ b/griptape/google/tools/google_gmail/tool.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import base64 +import logging +from email.message import EmailMessage + +from attrs import define +from schema import Literal, Schema + +from griptape.artifacts import ErrorArtifact, InfoArtifact +from griptape.google.tools import BaseGoogleTool +from griptape.utils.decorators import activity + + +@define +class GoogleGmailTool(BaseGoogleTool): + 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("body", description="body of the email"): str, + }, + ), + }, + ) + def create_draft_email(self, params: dict) -> InfoArtifact | ErrorArtifact: + values = params["values"] + + try: + service = self._build_client( + scopes=self.CREATE_DRAFT_EMAIL_SCOPES, + service_name="gmail", + version="v1", + ) + + message = EmailMessage() + message.set_content(values["body"]) + message["To"] = values["to"] + message["From"] = self.owner_email + 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/griptape/plugin_name/tools/reverse_string/__init__.py b/griptape/plugin_name/tools/reverse_string/__init__.py deleted file mode 100644 index cf36249..0000000 --- a/griptape/plugin_name/tools/reverse_string/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .tool import ReverseStringTool - -__all__ = ["ReverseStringTool"] diff --git a/griptape/plugin_name/tools/reverse_string/tool.py b/griptape/plugin_name/tools/reverse_string/tool.py deleted file mode 100644 index 61551b3..0000000 --- a/griptape/plugin_name/tools/reverse_string/tool.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations -from griptape.artifacts import TextArtifact, ErrorArtifact -from griptape.tools import BaseTool -from griptape.utils.decorators import activity -from schema import Schema, Literal -from attrs import define - - -@define -class ReverseStringTool(BaseTool): - @activity( - config={ - "description": "Can be used to reverse a string", - "schema": Schema( - {Literal("input", description="The string to be reversed"): str} - ), - } - ) - def reverse_string(self, params: dict) -> TextArtifact | ErrorArtifact: - input_value = params["values"].get("input") - - return TextArtifact(input_value[::-1]) diff --git a/poetry.lock b/poetry.lock index 761dfbf..b09ef27 100644 --- a/poetry.lock +++ b/poetry.lock @@ -51,6 +51,17 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + [[package]] name = "certifi" version = "2023.7.22" @@ -268,6 +279,106 @@ files = [ {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, ] +[[package]] +name = "google-api-core" +version = "2.23.0" +description = "Google API client core library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_api_core-2.23.0-py3-none-any.whl", hash = "sha256:c20100d4c4c41070cf365f1d8ddf5365915291b5eb11b83829fbd1c999b5122f"}, + {file = "google_api_core-2.23.0.tar.gz", hash = "sha256:2ceb087315e6af43f256704b871d99326b1f12a9d6ce99beaedec99ba26a0ace"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.dev0" +googleapis-common-protos = ">=1.56.2,<2.0.dev0" +proto-plus = [ + {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, +] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +requests = ">=2.18.0,<3.0.0.dev0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] + +[[package]] +name = "google-api-python-client" +version = "2.153.0" +description = "Google API Client Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_api_python_client-2.153.0-py2.py3-none-any.whl", hash = "sha256:6ff13bbfa92a57972e33ec3808e18309e5981b8ca1300e5da23bf2b4d6947384"}, + {file = "google_api_python_client-2.153.0.tar.gz", hash = "sha256:35cce8647f9c163fc04fb4d811fc91aae51954a2bdd74918decbe0e65d791dd2"}, +] + +[package.dependencies] +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-auth-httplib2 = ">=0.2.0,<1.0.0" +httplib2 = ">=0.19.0,<1.dev0" +uritemplate = ">=3.0.1,<5" + +[[package]] +name = "google-auth" +version = "2.36.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_auth-2.36.0-py2.py3-none-any.whl", hash = "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb"}, + {file = "google_auth-2.36.0.tar.gz", hash = "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +description = "Google Authentication Library: httplib2 transport" +optional = false +python-versions = "*" +files = [ + {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, + {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, +] + +[package.dependencies] +google-auth = "*" +httplib2 = ">=0.19.0" + +[[package]] +name = "googleapis-common-protos" +version = "1.66.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, + {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, +] + +[package.dependencies] +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] + [[package]] name = "griptape" version = "0.34.3" @@ -381,6 +492,20 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<0.23.0)"] +[[package]] +name = "httplib2" +version = "0.22.0" +description = "A comprehensive HTTP client library." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[package.dependencies] +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} + [[package]] name = "httpx" version = "0.25.1" @@ -732,6 +857,68 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "proto-plus" +version = "1.25.0" +description = "Beautiful, Pythonic protocol buffers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961"}, + {file = "proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<6.0.0dev" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "5.28.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24"}, + {file = "protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868"}, + {file = "protobuf-5.28.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687"}, + {file = "protobuf-5.28.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584"}, + {file = "protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135"}, + {file = "protobuf-5.28.3-cp38-cp38-win32.whl", hash = "sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548"}, + {file = "protobuf-5.28.3-cp38-cp38-win_amd64.whl", hash = "sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b"}, + {file = "protobuf-5.28.3-cp39-cp39-win32.whl", hash = "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535"}, + {file = "protobuf-5.28.3-cp39-cp39-win_amd64.whl", hash = "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36"}, + {file = "protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed"}, + {file = "protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.7.0" + [[package]] name = "pydantic" version = "2.5.1" @@ -882,6 +1069,20 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pyparsing" +version = "3.2.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pyright" version = "1.1.389" @@ -1161,6 +1362,20 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.1 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "ruff" version = "0.7.4" @@ -1343,6 +1558,17 @@ files = [ {file = "typos-1.27.3.tar.gz", hash = "sha256:971948dcb8658ca54a9540eeb3e98e70fde0567be270211ea777e6a48f097a0d"}, ] +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + [[package]] name = "urllib3" version = "2.1.0" @@ -1461,4 +1687,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "7a68f75dbd0a583b4aa8e88ec47e454e3b020eb9ebf899862a2c0ef2590ff4e5" +content-hash = "f35c7c5d8ed6582a90f853f3591d0f81f3667c5ef008aed1989fc332bb639c4b" diff --git a/pyproject.toml b/pyproject.toml index 422d8b4..dd146d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ packages = [ [tool.poetry.dependencies] python = "^3.9" griptape = "^0.34.0" +google-api-python-client = "^2.149.0" +google-auth-httplib2 = "^0.2.0" [tool.poetry.group.test] optional = true diff --git a/tests/unit/tools/test_google_calendar_tool.py b/tests/unit/tools/test_google_calendar_tool.py new file mode 100644 index 0000000..43024cb --- /dev/null +++ b/tests/unit/tools/test_google_calendar_tool.py @@ -0,0 +1,6 @@ +from griptape.google.tools import GoogleCalendarTool + + +class TestGoogleCalendarTool: + def test_init(self): + assert GoogleCalendarTool(owner_email="foo", service_account_credentials={}) diff --git a/tests/unit/tools/test_google_docs_tool.py b/tests/unit/tools/test_google_docs_tool.py new file mode 100644 index 0000000..ad9d2a2 --- /dev/null +++ b/tests/unit/tools/test_google_docs_tool.py @@ -0,0 +1,6 @@ +from griptape.google.tools import GoogleDocsTool + + +class TestGoogleDocsTool: + def test_init(self): + assert GoogleDocsTool(owner_email="foo", service_account_credentials={}) diff --git a/tests/unit/tools/test_google_drive_tool.py b/tests/unit/tools/test_google_drive_tool.py new file mode 100644 index 0000000..931bba0 --- /dev/null +++ b/tests/unit/tools/test_google_drive_tool.py @@ -0,0 +1,6 @@ +from griptape.google.tools import GoogleDriveTool + + +class TestGoogleDriveTool: + def test_init(self): + assert GoogleDriveTool(owner_email="foo", service_account_credentials={}) diff --git a/tests/unit/tools/test_google_gmail_tool.py b/tests/unit/tools/test_google_gmail_tool.py new file mode 100644 index 0000000..8d2318f --- /dev/null +++ b/tests/unit/tools/test_google_gmail_tool.py @@ -0,0 +1,6 @@ +from griptape.google.tools import GoogleGmailTool + + +class TestGoogleGmailTool: + def test_init(self): + assert GoogleGmailTool(owner_email="foo", service_account_credentials={}) diff --git a/tests/unit/tools/test_reverse_string_tool.py b/tests/unit/tools/test_reverse_string_tool.py deleted file mode 100644 index a21a29a..0000000 --- a/tests/unit/tools/test_reverse_string_tool.py +++ /dev/null @@ -1,15 +0,0 @@ -from griptape.artifacts import TextArtifact -from griptape.plugin_name.tools.reverse_string import ReverseStringTool - - -class TestReverseStringTool: - def test_reverse_string(self): - value = "some_value" - - tool = ReverseStringTool() - - params = {"values": {"input": value}} - result = tool.reverse_string(params) - - assert isinstance(result, TextArtifact), "Expected TextArtifact instance" - assert result.value == value[::-1]