Add Google File Picker Tool (#361)
## Google File Picker The Google Picker lets users select or upload Google Drive files. Users can grant permission to your apps to access their Drive data, providing a secure and authorized way to interact with their files. The `generate_google_file_picker_url` returns a URL to a Google File Picker for the user.
This commit is contained in:
parent
edac6ab48d
commit
81ada1d9b9
19 changed files with 318 additions and 21 deletions
|
|
@ -1 +1,96 @@
|
|||
__all__ = ["gmail", "calendar", "drive", "docs"]
|
||||
from arcade_google.tools.calendar import (
|
||||
create_event,
|
||||
delete_event,
|
||||
find_time_slots_when_everyone_is_free,
|
||||
list_calendars,
|
||||
list_events,
|
||||
update_event,
|
||||
)
|
||||
from arcade_google.tools.contacts import (
|
||||
create_contact,
|
||||
search_contacts_by_email,
|
||||
search_contacts_by_name,
|
||||
)
|
||||
from arcade_google.tools.docs import (
|
||||
create_blank_document,
|
||||
create_document_from_text,
|
||||
get_document_by_id,
|
||||
insert_text_at_end_of_document,
|
||||
)
|
||||
from arcade_google.tools.drive import (
|
||||
get_file_tree_structure,
|
||||
search_and_retrieve_documents,
|
||||
search_documents,
|
||||
)
|
||||
from arcade_google.tools.file_picker import generate_google_file_picker_url
|
||||
from arcade_google.tools.gmail import (
|
||||
change_email_labels,
|
||||
create_label,
|
||||
delete_draft_email,
|
||||
get_thread,
|
||||
list_draft_emails,
|
||||
list_emails,
|
||||
list_emails_by_header,
|
||||
list_labels,
|
||||
list_threads,
|
||||
reply_to_email,
|
||||
search_threads,
|
||||
send_draft_email,
|
||||
send_email,
|
||||
trash_email,
|
||||
update_draft_email,
|
||||
write_draft_email,
|
||||
write_draft_reply_email,
|
||||
)
|
||||
from arcade_google.tools.sheets import (
|
||||
create_spreadsheet,
|
||||
get_spreadsheet,
|
||||
write_to_cell,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Google Calendar
|
||||
create_event,
|
||||
delete_event,
|
||||
find_time_slots_when_everyone_is_free,
|
||||
list_calendars,
|
||||
list_events,
|
||||
update_event,
|
||||
# Google Contacts
|
||||
create_contact,
|
||||
search_contacts_by_email,
|
||||
search_contacts_by_name,
|
||||
# Google Docs
|
||||
create_blank_document,
|
||||
create_document_from_text,
|
||||
get_document_by_id,
|
||||
insert_text_at_end_of_document,
|
||||
# Google Drive
|
||||
get_file_tree_structure, # type: ignore[has-type]
|
||||
search_and_retrieve_documents, # type: ignore[has-type]
|
||||
search_documents, # type: ignore[has-type]
|
||||
# Google File Picker
|
||||
generate_google_file_picker_url,
|
||||
# Google Gmail
|
||||
change_email_labels,
|
||||
create_label,
|
||||
delete_draft_email,
|
||||
get_thread,
|
||||
list_draft_emails,
|
||||
list_emails,
|
||||
list_emails_by_header,
|
||||
list_labels,
|
||||
list_threads,
|
||||
reply_to_email,
|
||||
search_threads,
|
||||
send_draft_email,
|
||||
send_email,
|
||||
trash_email,
|
||||
update_draft_email,
|
||||
write_draft_email,
|
||||
write_draft_reply_email,
|
||||
# Google Sheets
|
||||
create_spreadsheet,
|
||||
get_spreadsheet,
|
||||
write_to_cell,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -28,12 +28,10 @@ from arcade_google.utils import (
|
|||
async def list_calendars(
|
||||
context: ToolContext,
|
||||
max_results: Annotated[
|
||||
int, "The maximum number of calendars to return. " "Up to 250 calendars, defaults to 10."
|
||||
int, "The maximum number of calendars to return. Up to 250 calendars, defaults to 10."
|
||||
] = 10,
|
||||
show_deleted: Annotated[
|
||||
bool, "Whether to show deleted calendars." " Defaults to False"
|
||||
] = False,
|
||||
show_hidden: Annotated[bool, "Whether to show hidden calendars." " Defaults to False"] = False,
|
||||
show_deleted: Annotated[bool, "Whether to show deleted calendars. Defaults to False"] = False,
|
||||
show_hidden: Annotated[bool, "Whether to show hidden calendars. Defaults to False"] = False,
|
||||
next_page_token: Annotated[
|
||||
str | None, "The token to retrieve the next page of calendars. Optional."
|
||||
] = None,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from googleapiclient.errors import HttpError
|
|||
from arcade_google.doc_to_html import convert_document_to_html
|
||||
from arcade_google.doc_to_markdown import convert_document_to_markdown
|
||||
from arcade_google.models import DocumentFormat, OrderBy
|
||||
from arcade_google.tools.docs import get_document_by_id
|
||||
from arcade_google.tools import get_document_by_id
|
||||
from arcade_google.utils import (
|
||||
build_drive_service,
|
||||
build_file_tree,
|
||||
|
|
|
|||
54
toolkits/google/arcade_google/tools/file_picker.py
Normal file
54
toolkits/google/arcade_google/tools/file_picker.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import base64
|
||||
import json
|
||||
from typing import Annotated
|
||||
|
||||
from arcade.sdk import ToolContext, ToolMetadataKey, tool
|
||||
from arcade.sdk.auth import Google
|
||||
from arcade.sdk.errors import ToolExecutionError
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(),
|
||||
requires_metadata=[ToolMetadataKey.CLIENT_ID, ToolMetadataKey.COORDINATOR_URL],
|
||||
)
|
||||
def generate_google_file_picker_url(
|
||||
context: ToolContext,
|
||||
) -> Annotated[dict, "Google File Picker URL for user file selection and permission granting"]:
|
||||
"""Generate a Google File Picker URL for user-driven file selection and authorization.
|
||||
|
||||
This tool generates a URL that directs the end-user to a Google File Picker interface where
|
||||
where they can select or upload Google Drive files. Users can grant permission to access their
|
||||
Drive files, providing a secure and authorized way to interact with their files.
|
||||
|
||||
This is particularly useful when prior tools (e.g., those accessing or modifying
|
||||
Google Docs, Google Sheets, etc.) encountered failures due to file non-existence
|
||||
(Requested entity was not found) or permission errors. Once the user completes the file
|
||||
picker flow, the prior tool can be retried.
|
||||
"""
|
||||
client_id = context.get_metadata(ToolMetadataKey.CLIENT_ID)
|
||||
client_id_parts = client_id.split("-")
|
||||
if not client_id_parts:
|
||||
raise ToolExecutionError(
|
||||
message="Invalid Google Client ID",
|
||||
developer_message=f"Google Client ID '{client_id}' is not valid",
|
||||
)
|
||||
app_id = client_id_parts[0]
|
||||
cloud_coordinator_url = context.get_metadata(ToolMetadataKey.COORDINATOR_URL).strip("/")
|
||||
|
||||
config = {
|
||||
"auth": {
|
||||
"client_id": client_id,
|
||||
"app_id": app_id,
|
||||
},
|
||||
}
|
||||
config_json = json.dumps(config)
|
||||
config_base64 = base64.urlsafe_b64encode(config_json.encode("utf-8")).decode("utf-8")
|
||||
url = f"{cloud_coordinator_url}/google/drive_picker?config={config_base64}"
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"llm_instructions": (
|
||||
"Instruct the user to click the following link to open the Google Drive File Picker. "
|
||||
"This will allow them to select files and grant access permissions: {url}"
|
||||
),
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ from arcade.sdk.eval import (
|
|||
|
||||
import arcade_google
|
||||
from arcade_google.critics import AnyDatetimeCritic, DatetimeOrNoneCritic
|
||||
from arcade_google.tools.calendar import find_time_slots_when_everyone_is_free
|
||||
from arcade_google.tools import find_time_slots_when_everyone_is_free
|
||||
|
||||
rubric = EvalRubric(
|
||||
fail_threshold=0.9,
|
||||
|
|
|
|||
105
toolkits/google/evals/eval_file_picker.py
Normal file
105
toolkits/google/evals/eval_file_picker.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
from arcade.sdk import ToolCatalog
|
||||
from arcade.sdk.eval import (
|
||||
EvalRubric,
|
||||
EvalSuite,
|
||||
ExpectedToolCall,
|
||||
tool_eval,
|
||||
)
|
||||
|
||||
import arcade_google
|
||||
from arcade_google.tools import generate_google_file_picker_url
|
||||
|
||||
# Evaluation rubric
|
||||
rubric = EvalRubric(
|
||||
fail_threshold=0.9,
|
||||
warn_threshold=0.95,
|
||||
)
|
||||
|
||||
|
||||
catalog = ToolCatalog()
|
||||
catalog.add_module(arcade_google)
|
||||
|
||||
history_1 = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "get the contents of my spreadsheet named 'How to teach an old dog a new trick'",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_errutwjcpZLiHu8LKip6XKLq",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "Google_SearchDocuments",
|
||||
"arguments": '{"document_contains":["How to teach an old dog a new trick"],"limit":5}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": '{"documents":[],"documents_count":0}',
|
||||
"tool_call_id": "call_errutwjcpZLiHu8LKip6XKLq",
|
||||
"name": "Google_SearchDocuments",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "It seems that I don't have access to your spreadsheet named \"How to teach an old dog a new trick.\" If you need help retrieving the data, you can check your cloud storage or computer files where the spreadsheet might be saved, such as Google Sheets, Microsoft Excel, or other spreadsheet software. Let me know if there's anything specific you'd like assistance with regarding the content!",
|
||||
},
|
||||
]
|
||||
|
||||
history_2 = [
|
||||
{"role": "system", "content": "Today is 2025-04-15, Tuesday."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I got this error from you: \n❌ Arcade Chat failed with error: Error code: 500 - {'name':'non_retryable_tool_call_error', 'message': 'tool call failed - \nGoogle.InsertTextAtEndOfDocument failed: Error inexecution of GetDocumentById: Error in get_document_by_id: <HttpError 404 when\nrequestinghttps://docs.googleapis.com/v1/documents/192394932924?alt=json returned \"Requested entity was not found.\". Details: \n\"Requestedentity was not found.\">'}",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The error message indicates that there was an attempt to retrieve a document with a specific ID \\(`192394932924`) from Google Docs, but the document could not be found. The HTTP 404 error code means that the document doesn't \\exist or is not accessible with the current permissions. Here's what you can do:\n\n1. **Check Document ID:** Make sure the \\document ID (`192394932924`) is correct. A typo or incorrect ID will lead to this error.\n\n2. **Permissions:** Ensure that you \\have permission to access the document. If the document is owned by someone else or is in a shared drive with restricted access,\\you might not be able to view it.\n\n3. **Document Existence:** Double-check that the document hasn't been deleted or moved to \the trash or a location you don't have access to.\n\nLet me know if there's anything else you'd like to address or any other \assistance you need!",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@tool_eval()
|
||||
def file_picker_eval_suite() -> EvalSuite:
|
||||
"""Create an evaluation suite for Gmail tools."""
|
||||
suite = EvalSuite(
|
||||
name="File Picker Tools Evaluation",
|
||||
system_message="You are an AI assistant that can perform actions for the user using the provided tools.",
|
||||
catalog=catalog,
|
||||
rubric=rubric,
|
||||
)
|
||||
|
||||
suite.add_case(
|
||||
name="Get file picker url 1",
|
||||
user_message="I am certain that the spreadsheet exists",
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
func=generate_google_file_picker_url,
|
||||
args={},
|
||||
)
|
||||
],
|
||||
additional_messages=history_1,
|
||||
)
|
||||
|
||||
suite.add_case(
|
||||
name="Get file picker url 2",
|
||||
user_message="Please help in anyway you can. I'm literally looking at the document right now in the browser.",
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(func=generate_google_file_picker_url, args={}),
|
||||
],
|
||||
additional_messages=history_2,
|
||||
)
|
||||
|
||||
suite.add_case(
|
||||
name="Get file picker url 3",
|
||||
user_message="I want to give you permissions to more files in my Drive right now.",
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(func=generate_google_file_picker_url, args={}),
|
||||
],
|
||||
)
|
||||
|
||||
return suite
|
||||
|
|
@ -12,7 +12,7 @@ from arcade.sdk.eval import (
|
|||
|
||||
import arcade_google
|
||||
from arcade_google.models import EventVisibility, SendUpdatesOptions
|
||||
from arcade_google.tools.calendar import (
|
||||
from arcade_google.tools import (
|
||||
create_event,
|
||||
delete_event,
|
||||
list_calendars,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from arcade.sdk.eval import (
|
|||
from arcade.sdk.eval.critic import BinaryCritic
|
||||
|
||||
import arcade_google
|
||||
from arcade_google.tools.contacts import (
|
||||
from arcade_google.tools import (
|
||||
create_contact,
|
||||
search_contacts_by_email,
|
||||
search_contacts_by_name,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from arcade.sdk.eval import (
|
|||
)
|
||||
|
||||
import arcade_google
|
||||
from arcade_google.tools.docs import (
|
||||
from arcade_google.tools import (
|
||||
create_blank_document,
|
||||
create_document_from_text,
|
||||
get_document_by_id,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from arcade.sdk.eval import (
|
|||
|
||||
import arcade_google
|
||||
from arcade_google.models import DocumentFormat, OrderBy
|
||||
from arcade_google.tools.drive import (
|
||||
from arcade_google.tools import (
|
||||
get_file_tree_structure,
|
||||
search_and_retrieve_documents,
|
||||
search_documents,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from arcade.sdk.eval import (
|
|||
|
||||
import arcade_google
|
||||
from arcade_google.models import GmailReplyToWhom
|
||||
from arcade_google.tools.gmail import (
|
||||
from arcade_google.tools import (
|
||||
get_thread,
|
||||
list_emails_by_header,
|
||||
list_threads,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from arcade.sdk.eval import (
|
|||
)
|
||||
|
||||
import arcade_google
|
||||
from arcade_google.tools.sheets import (
|
||||
from arcade_google.tools import (
|
||||
create_spreadsheet,
|
||||
get_spreadsheet,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
[tool.poetry]
|
||||
name = "arcade_google"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
description = "Arcade.dev LLM tools for Google Workspace"
|
||||
authors = ["Arcade <dev@arcade.dev>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
arcade-ai = ">=1.0.5,<2.0"
|
||||
arcade-ai = ">=1.3.0,<2.0"
|
||||
google-api-core = "2.19.1"
|
||||
google-api-python-client = "2.137.0"
|
||||
google-auth = "2.32.0"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from arcade.sdk.errors import RetryableToolError, ToolExecutionError
|
|||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_google.models import EventVisibility, SendUpdatesOptions
|
||||
from arcade_google.tools.calendar import (
|
||||
from arcade_google.tools import (
|
||||
create_event,
|
||||
delete_event,
|
||||
find_time_slots_when_everyone_is_free,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
import pytest
|
||||
from arcade.sdk import ToolContext
|
||||
|
||||
from arcade_google.tools.contacts import create_contact
|
||||
from arcade_google.tools import create_contact
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import pytest
|
|||
from arcade.sdk.errors import ToolExecutionError
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_google.tools.docs import (
|
||||
from arcade_google.tools import (
|
||||
create_blank_document,
|
||||
create_document_from_text,
|
||||
get_document_by_id,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from arcade.sdk.errors import ToolExecutionError
|
|||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_google.models import Corpora, DocumentFormat, OrderBy
|
||||
from arcade_google.tools.drive import (
|
||||
from arcade_google.tools import (
|
||||
get_file_tree_structure,
|
||||
search_and_retrieve_documents,
|
||||
search_documents,
|
||||
|
|
|
|||
45
toolkits/google/tests/test_file_picker.py
Normal file
45
toolkits/google/tests/test_file_picker.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import base64
|
||||
import json
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
from arcade.core.schema import ToolMetadataItem
|
||||
from arcade.sdk import ToolContext, ToolMetadataKey
|
||||
|
||||
from arcade_google.tools import generate_google_file_picker_url
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context():
|
||||
context = ToolContext(
|
||||
metadata=[
|
||||
ToolMetadataItem(key=ToolMetadataKey.CLIENT_ID, value="1234-3444534323"),
|
||||
ToolMetadataItem(
|
||||
key=ToolMetadataKey.COORDINATOR_URL, value="https://mock_coordinator_url"
|
||||
),
|
||||
],
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_google_file_picker_url(mock_context):
|
||||
expected_decoded_config = {
|
||||
"auth": {
|
||||
"client_id": "1234-3444534323",
|
||||
"app_id": "1234",
|
||||
},
|
||||
}
|
||||
|
||||
result = generate_google_file_picker_url(mock_context)
|
||||
|
||||
assert result["url"].startswith("https://mock_coordinator_url/google/drive_picker?config=")
|
||||
|
||||
# Decode the config from the URL
|
||||
parsed_url = urlparse(result["url"])
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
encoded_config = query_params.get("config", [None])[0]
|
||||
assert encoded_config is not None
|
||||
|
||||
decoded_config = json.loads(base64.urlsafe_b64decode(encoded_config).decode("utf-8"))
|
||||
assert decoded_config == expected_decoded_config
|
||||
|
|
@ -8,7 +8,7 @@ from arcade.sdk.errors import ToolExecutionError
|
|||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_google.models import GmailReplyToWhom
|
||||
from arcade_google.tools.gmail import (
|
||||
from arcade_google.tools import (
|
||||
delete_draft_email,
|
||||
get_thread,
|
||||
list_draft_emails,
|
||||
|
|
|
|||
Loading…
Reference in a new issue