arcade-google 0.0.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arcade_google/__init__.py +0 -0
- arcade_google/tools/__init__.py +1 -0
- arcade_google/tools/calendar.py +307 -0
- arcade_google/tools/docs.py +151 -0
- arcade_google/tools/drive.py +80 -0
- arcade_google/tools/gmail.py +333 -0
- arcade_google/tools/models.py +294 -0
- arcade_google/tools/utils.py +280 -0
- arcade_google-0.0.13.dist-info/METADATA +20 -0
- arcade_google-0.0.13.dist-info/RECORD +11 -0
- arcade_google-0.0.13.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ["gmail", "calendar", "drive", "docs"]
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from google.oauth2.credentials import Credentials
|
|
5
|
+
from googleapiclient.discovery import build
|
|
6
|
+
from googleapiclient.errors import HttpError
|
|
7
|
+
|
|
8
|
+
from arcade.core.errors import RetryableToolError
|
|
9
|
+
from arcade.core.schema import ToolContext
|
|
10
|
+
from arcade.sdk import tool
|
|
11
|
+
from arcade.sdk.auth import Google
|
|
12
|
+
from arcade_google.tools.models import EventVisibility, SendUpdatesOptions
|
|
13
|
+
from arcade_google.tools.utils import parse_datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@tool(
|
|
17
|
+
requires_auth=Google(
|
|
18
|
+
scopes=[
|
|
19
|
+
"https://www.googleapis.com/auth/calendar.readonly",
|
|
20
|
+
"https://www.googleapis.com/auth/calendar.events",
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
async def create_event(
|
|
25
|
+
context: ToolContext,
|
|
26
|
+
summary: Annotated[str, "The title of the event"],
|
|
27
|
+
start_datetime: Annotated[
|
|
28
|
+
str,
|
|
29
|
+
"The datetime when the event starts in ISO 8601 format, e.g., '2024-12-31T15:30:00'.",
|
|
30
|
+
],
|
|
31
|
+
end_datetime: Annotated[
|
|
32
|
+
str,
|
|
33
|
+
"The datetime when the event ends in ISO 8601 format, e.g., '2024-12-31T17:30:00'.",
|
|
34
|
+
],
|
|
35
|
+
calendar_id: Annotated[
|
|
36
|
+
str, "The ID of the calendar to create the event in, usually 'primary'."
|
|
37
|
+
] = "primary",
|
|
38
|
+
description: Annotated[str | None, "The description of the event"] = None,
|
|
39
|
+
location: Annotated[str | None, "The location of the event"] = None,
|
|
40
|
+
visibility: Annotated[EventVisibility, "The visibility of the event"] = EventVisibility.DEFAULT,
|
|
41
|
+
attendee_emails: Annotated[
|
|
42
|
+
list[str] | None,
|
|
43
|
+
"The list of attendee emails. Must be valid email addresses e.g., username@domain.com.",
|
|
44
|
+
] = None,
|
|
45
|
+
) -> Annotated[dict, "A dictionary containing the created event details"]:
|
|
46
|
+
"""Create a new event/meeting/sync/meetup in the specified calendar."""
|
|
47
|
+
|
|
48
|
+
service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
|
|
49
|
+
|
|
50
|
+
# Get the calendar's time zone
|
|
51
|
+
calendar = service.calendars().get(calendarId=calendar_id).execute()
|
|
52
|
+
time_zone = calendar["timeZone"]
|
|
53
|
+
|
|
54
|
+
# Parse datetime strings
|
|
55
|
+
start_dt = parse_datetime(start_datetime, time_zone)
|
|
56
|
+
end_dt = parse_datetime(end_datetime, time_zone)
|
|
57
|
+
|
|
58
|
+
event = {
|
|
59
|
+
"summary": summary,
|
|
60
|
+
"description": description,
|
|
61
|
+
"location": location,
|
|
62
|
+
"start": {"dateTime": start_dt.isoformat(), "timeZone": time_zone},
|
|
63
|
+
"end": {"dateTime": end_dt.isoformat(), "timeZone": time_zone},
|
|
64
|
+
"visibility": visibility.value,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if attendee_emails:
|
|
68
|
+
event["attendees"] = [{"email": email} for email in attendee_emails]
|
|
69
|
+
|
|
70
|
+
created_event = service.events().insert(calendarId=calendar_id, body=event).execute()
|
|
71
|
+
return {"event": created_event}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@tool(
|
|
75
|
+
requires_auth=Google(
|
|
76
|
+
scopes=[
|
|
77
|
+
"https://www.googleapis.com/auth/calendar.readonly",
|
|
78
|
+
"https://www.googleapis.com/auth/calendar.events",
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
async def list_events(
|
|
83
|
+
context: ToolContext,
|
|
84
|
+
min_end_datetime: Annotated[
|
|
85
|
+
str,
|
|
86
|
+
"Filter by events that end on or after this datetime in ISO 8601 format, e.g., '2024-09-15T09:00:00'.",
|
|
87
|
+
],
|
|
88
|
+
max_start_datetime: Annotated[
|
|
89
|
+
str,
|
|
90
|
+
"Filter by events that start before this datetime in ISO 8601 format, e.g., '2024-09-16T17:00:00'.",
|
|
91
|
+
],
|
|
92
|
+
calendar_id: Annotated[str, "The ID of the calendar to list events from"] = "primary",
|
|
93
|
+
max_results: Annotated[int, "The maximum number of events to return"] = 10,
|
|
94
|
+
) -> Annotated[dict, "A dictionary containing the list of events"]:
|
|
95
|
+
"""
|
|
96
|
+
List events from the specified calendar within the given datetime range.
|
|
97
|
+
|
|
98
|
+
min_end_datetime serves as the lower bound (exclusive) for an event's end time.
|
|
99
|
+
max_start_datetime serves as the upper bound (exclusive) for an event's start time.
|
|
100
|
+
|
|
101
|
+
For example:
|
|
102
|
+
If min_end_datetime is set to 2024-09-15T09:00:00 and max_start_datetime is set to 2024-09-16T17:00:00,
|
|
103
|
+
the function will return events that:
|
|
104
|
+
1. End after 09:00 on September 15, 2024 (exclusive)
|
|
105
|
+
2. Start before 17:00 on September 16, 2024 (exclusive)
|
|
106
|
+
This means an event starting at 08:00 on September 15 and ending at 10:00 on September 15 would be included,
|
|
107
|
+
but an event starting at 17:00 on September 16 would not be included.
|
|
108
|
+
"""
|
|
109
|
+
service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
|
|
110
|
+
|
|
111
|
+
# Get the calendar's time zone
|
|
112
|
+
calendar = service.calendars().get(calendarId=calendar_id).execute()
|
|
113
|
+
time_zone = calendar["timeZone"]
|
|
114
|
+
|
|
115
|
+
# Parse datetime strings
|
|
116
|
+
min_end_dt = parse_datetime(min_end_datetime, time_zone)
|
|
117
|
+
max_start_dt = parse_datetime(max_start_datetime, time_zone)
|
|
118
|
+
|
|
119
|
+
if min_end_dt > max_start_dt:
|
|
120
|
+
min_end_dt, max_start_dt = max_start_dt, min_end_dt
|
|
121
|
+
|
|
122
|
+
events_result = (
|
|
123
|
+
service.events()
|
|
124
|
+
.list(
|
|
125
|
+
calendarId=calendar_id,
|
|
126
|
+
timeMin=min_end_dt.isoformat(),
|
|
127
|
+
timeMax=max_start_dt.isoformat(),
|
|
128
|
+
maxResults=max_results,
|
|
129
|
+
singleEvents=True,
|
|
130
|
+
orderBy="startTime",
|
|
131
|
+
)
|
|
132
|
+
.execute()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
items_keys = [
|
|
136
|
+
"attachments",
|
|
137
|
+
"attendees",
|
|
138
|
+
"creator",
|
|
139
|
+
"description",
|
|
140
|
+
"end",
|
|
141
|
+
"eventType",
|
|
142
|
+
"htmlLink",
|
|
143
|
+
"id",
|
|
144
|
+
"location",
|
|
145
|
+
"organizer",
|
|
146
|
+
"start",
|
|
147
|
+
"summary",
|
|
148
|
+
"visibility",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
events = [
|
|
152
|
+
{key: event[key] for key in items_keys if key in event}
|
|
153
|
+
for event in events_result.get("items", [])
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
return {"events_count": len(events), "events": events}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@tool(
|
|
160
|
+
requires_auth=Google(
|
|
161
|
+
scopes=["https://www.googleapis.com/auth/calendar"],
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
async def update_event(
|
|
165
|
+
context: ToolContext,
|
|
166
|
+
event_id: Annotated[str, "The ID of the event to update"],
|
|
167
|
+
updated_start_datetime: Annotated[
|
|
168
|
+
str | None,
|
|
169
|
+
"The updated datetime that the event starts in ISO 8601 format, e.g., '2024-12-31T15:30:00'.",
|
|
170
|
+
] = None,
|
|
171
|
+
updated_end_datetime: Annotated[
|
|
172
|
+
str | None,
|
|
173
|
+
"The updated datetime that the event ends in ISO 8601 format, e.g., '2024-12-31T17:30:00'.",
|
|
174
|
+
] = None,
|
|
175
|
+
updated_calendar_id: Annotated[
|
|
176
|
+
str | None, "The updated ID of the calendar containing the event."
|
|
177
|
+
] = None,
|
|
178
|
+
updated_summary: Annotated[str | None, "The updated title of the event"] = None,
|
|
179
|
+
updated_description: Annotated[str | None, "The updated description of the event"] = None,
|
|
180
|
+
updated_location: Annotated[str | None, "The updated location of the event"] = None,
|
|
181
|
+
updated_visibility: Annotated[EventVisibility | None, "The visibility of the event"] = None,
|
|
182
|
+
attendee_emails_to_add: Annotated[
|
|
183
|
+
list[str] | None,
|
|
184
|
+
"The list of attendee emails to add. Must be valid email addresses e.g., username@domain.com.",
|
|
185
|
+
] = None,
|
|
186
|
+
attendee_emails_to_remove: Annotated[
|
|
187
|
+
list[str] | None,
|
|
188
|
+
"The list of attendee emails to remove. Must be valid email addresses e.g., username@domain.com.",
|
|
189
|
+
] = None,
|
|
190
|
+
send_updates: Annotated[
|
|
191
|
+
SendUpdatesOptions, "Should attendees be notified of the update? (none, all, external_only)"
|
|
192
|
+
] = SendUpdatesOptions.ALL,
|
|
193
|
+
) -> Annotated[
|
|
194
|
+
str,
|
|
195
|
+
"A string containing the updated event details, including the event ID, update timestamp, and a link to view the updated event.",
|
|
196
|
+
]:
|
|
197
|
+
"""
|
|
198
|
+
Update an existing event in the specified calendar with the provided details.
|
|
199
|
+
Only the provided fields will be updated; others will remain unchanged.
|
|
200
|
+
|
|
201
|
+
`updated_start_datetime` and `updated_end_datetime` are independent and can be provided separately.
|
|
202
|
+
"""
|
|
203
|
+
service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
|
|
204
|
+
|
|
205
|
+
calendar = service.calendars().get(calendarId="primary").execute()
|
|
206
|
+
time_zone = calendar["timeZone"]
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
event = service.events().get(calendarId="primary", eventId=event_id).execute()
|
|
210
|
+
except HttpError:
|
|
211
|
+
valid_events_with_id = (
|
|
212
|
+
service.events()
|
|
213
|
+
.list(
|
|
214
|
+
calendarId="primary",
|
|
215
|
+
timeMin=(datetime.now() - timedelta(days=2)).isoformat(),
|
|
216
|
+
timeMax=(datetime.now() + timedelta(days=365)).isoformat(),
|
|
217
|
+
maxResults=50,
|
|
218
|
+
singleEvents=True,
|
|
219
|
+
orderBy="startTime",
|
|
220
|
+
)
|
|
221
|
+
.execute()
|
|
222
|
+
)
|
|
223
|
+
raise RetryableToolError(
|
|
224
|
+
f"Event with ID {event_id} not found.",
|
|
225
|
+
additional_prompt_content=f"Here is a list of valid events. The event_id parameter must match one of these: {valid_events_with_id}",
|
|
226
|
+
retry_after_ms=1000,
|
|
227
|
+
developer_message=f"Event with ID {event_id} not found. Please try again with a valid event ID.",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
update_fields = {
|
|
231
|
+
"start": {"dateTime": updated_start_datetime.isoformat(), "timeZone": time_zone}
|
|
232
|
+
if updated_start_datetime
|
|
233
|
+
else None,
|
|
234
|
+
"end": {"dateTime": updated_end_datetime.isoformat(), "timeZone": time_zone}
|
|
235
|
+
if updated_end_datetime
|
|
236
|
+
else None,
|
|
237
|
+
"calendarId": updated_calendar_id,
|
|
238
|
+
"sendUpdates": send_updates.value if send_updates else None,
|
|
239
|
+
"summary": updated_summary,
|
|
240
|
+
"description": updated_description,
|
|
241
|
+
"location": updated_location,
|
|
242
|
+
"visibility": updated_visibility.value if updated_visibility else None,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
event.update({k: v for k, v in update_fields.items() if v is not None})
|
|
246
|
+
|
|
247
|
+
if attendee_emails_to_remove:
|
|
248
|
+
event["attendees"] = [
|
|
249
|
+
attendee
|
|
250
|
+
for attendee in event.get("attendees", [])
|
|
251
|
+
if attendee.get("email", "").lower()
|
|
252
|
+
not in [email.lower() for email in attendee_emails_to_remove]
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
if attendee_emails_to_add:
|
|
256
|
+
existing_emails = {
|
|
257
|
+
attendee.get("email", "").lower() for attendee in event.get("attendees", [])
|
|
258
|
+
}
|
|
259
|
+
new_attendees = [
|
|
260
|
+
{"email": email}
|
|
261
|
+
for email in attendee_emails_to_add
|
|
262
|
+
if email.lower() not in existing_emails
|
|
263
|
+
]
|
|
264
|
+
event["attendees"] = event.get("attendees", []) + new_attendees
|
|
265
|
+
|
|
266
|
+
updated_event = (
|
|
267
|
+
service.events()
|
|
268
|
+
.update(
|
|
269
|
+
calendarId="primary",
|
|
270
|
+
eventId=event_id,
|
|
271
|
+
sendUpdates=send_updates.value,
|
|
272
|
+
body=event,
|
|
273
|
+
)
|
|
274
|
+
.execute()
|
|
275
|
+
)
|
|
276
|
+
return f"Event with ID {event_id} successfully updated at {updated_event['updated']}. View updated event at {updated_event['htmlLink']}"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@tool(
|
|
280
|
+
requires_auth=Google(
|
|
281
|
+
scopes=["https://www.googleapis.com/auth/calendar.events"],
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
async def delete_event(
|
|
285
|
+
context: ToolContext,
|
|
286
|
+
event_id: Annotated[str, "The ID of the event to delete"],
|
|
287
|
+
calendar_id: Annotated[str, "The ID of the calendar containing the event"] = "primary",
|
|
288
|
+
send_updates: Annotated[
|
|
289
|
+
SendUpdatesOptions, "Specifies which attendees to notify about the deletion"
|
|
290
|
+
] = SendUpdatesOptions.ALL,
|
|
291
|
+
) -> Annotated[str, "A string containing the deletion confirmation message"]:
|
|
292
|
+
"""Delete an event from Google Calendar."""
|
|
293
|
+
service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
|
|
294
|
+
|
|
295
|
+
service.events().delete(
|
|
296
|
+
calendarId=calendar_id, eventId=event_id, sendUpdates=send_updates.value
|
|
297
|
+
).execute()
|
|
298
|
+
|
|
299
|
+
notification_message = ""
|
|
300
|
+
if send_updates == SendUpdatesOptions.ALL:
|
|
301
|
+
notification_message = "Notifications were sent to all attendees."
|
|
302
|
+
elif send_updates == SendUpdatesOptions.EXTERNAL_ONLY:
|
|
303
|
+
notification_message = "Notifications were sent to external attendees only."
|
|
304
|
+
elif send_updates == SendUpdatesOptions.NONE:
|
|
305
|
+
notification_message = "No notifications were sent to attendees."
|
|
306
|
+
|
|
307
|
+
return f"Event with ID '{event_id}' successfully deleted from calendar '{calendar_id}'. {notification_message}"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from arcade.core.schema import ToolContext
|
|
4
|
+
from arcade.sdk import tool
|
|
5
|
+
from arcade.sdk.auth import Google
|
|
6
|
+
from arcade_google.tools.utils import build_docs_service
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Uses https://developers.google.com/docs/api/reference/rest/v1/documents/get
|
|
10
|
+
# Example `arcade chat` query: `get document with ID 1234567890`
|
|
11
|
+
# Note: Document IDs are returned in the response of the Google Drive's `list_documents` tool
|
|
12
|
+
@tool(
|
|
13
|
+
requires_auth=Google(
|
|
14
|
+
scopes=[
|
|
15
|
+
"https://www.googleapis.com/auth/documents.readonly",
|
|
16
|
+
],
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
async def get_document_by_id(
|
|
20
|
+
context: ToolContext,
|
|
21
|
+
document_id: Annotated[str, "The ID of the document to retrieve."],
|
|
22
|
+
) -> Annotated[dict, "The document contents as a dictionary"]:
|
|
23
|
+
"""
|
|
24
|
+
Get the latest version of the specified Google Docs document.
|
|
25
|
+
"""
|
|
26
|
+
service = build_docs_service(context.authorization.token)
|
|
27
|
+
|
|
28
|
+
# Execute the documents().get() method. Returns a Document object
|
|
29
|
+
# https://developers.google.com/docs/api/reference/rest/v1/documents#Document
|
|
30
|
+
request = service.documents().get(documentId=document_id)
|
|
31
|
+
response = request.execute()
|
|
32
|
+
return response
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Uses https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate
|
|
36
|
+
# Example `arcade chat` query: `insert "The END" at the end of document with ID 1234567890`
|
|
37
|
+
@tool(
|
|
38
|
+
requires_auth=Google(
|
|
39
|
+
scopes=[
|
|
40
|
+
"https://www.googleapis.com/auth/documents",
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
async def insert_text_at_end_of_document(
|
|
45
|
+
context: ToolContext,
|
|
46
|
+
document_id: Annotated[str, "The ID of the document to update."],
|
|
47
|
+
text_content: Annotated[str, "The text content to insert into the document"],
|
|
48
|
+
) -> Annotated[dict, "The response from the batchUpdate API as a dict."]:
|
|
49
|
+
"""
|
|
50
|
+
Updates an existing Google Docs document using the batchUpdate API endpoint.
|
|
51
|
+
"""
|
|
52
|
+
document = await get_document_by_id(context, document_id)
|
|
53
|
+
|
|
54
|
+
end_index = document["body"]["content"][-1]["endIndex"]
|
|
55
|
+
|
|
56
|
+
service = build_docs_service(context.authorization.token)
|
|
57
|
+
|
|
58
|
+
requests = [
|
|
59
|
+
{
|
|
60
|
+
"insertText": {
|
|
61
|
+
"location": {
|
|
62
|
+
"index": int(end_index) - 1,
|
|
63
|
+
},
|
|
64
|
+
"text": text_content,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Execute the documents().batchUpdate() method
|
|
70
|
+
response = (
|
|
71
|
+
service.documents()
|
|
72
|
+
.batchUpdate(documentId=document_id, body={"requests": requests})
|
|
73
|
+
.execute()
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return response
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Uses https://developers.google.com/docs/api/reference/rest/v1/documents/create
|
|
80
|
+
# Example `arcade chat` query: `create blank document with title "My New Document"`
|
|
81
|
+
@tool(
|
|
82
|
+
requires_auth=Google(
|
|
83
|
+
scopes=[
|
|
84
|
+
"https://www.googleapis.com/auth/documents",
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
async def create_blank_document(
|
|
89
|
+
context: ToolContext, title: Annotated[str, "The title of the blank document to create"]
|
|
90
|
+
) -> Annotated[dict, "The created document's title, documentId, and documentUrl in a dictionary"]:
|
|
91
|
+
"""
|
|
92
|
+
Create a blank Google Docs document with the specified title.
|
|
93
|
+
"""
|
|
94
|
+
service = build_docs_service(context.authorization.token)
|
|
95
|
+
|
|
96
|
+
body = {"title": title}
|
|
97
|
+
|
|
98
|
+
# Execute the documents().create() method. Returns a Document object https://developers.google.com/docs/api/reference/rest/v1/documents#Document
|
|
99
|
+
request = service.documents().create(body=body)
|
|
100
|
+
response = request.execute()
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"title": response["title"],
|
|
104
|
+
"documentId": response["documentId"],
|
|
105
|
+
"documentUrl": f"https://docs.google.com/document/d/{response['documentId']}/edit",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Uses https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate
|
|
110
|
+
# Example `arcade chat` query: `create document with title "My New Document" and text content "Hello, World!"`
|
|
111
|
+
@tool(
|
|
112
|
+
requires_auth=Google(
|
|
113
|
+
scopes=[
|
|
114
|
+
"https://www.googleapis.com/auth/documents",
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
async def create_document_from_text(
|
|
119
|
+
context: ToolContext,
|
|
120
|
+
title: Annotated[str, "The title of the document to create"],
|
|
121
|
+
text_content: Annotated[str, "The text content to insert into the document"],
|
|
122
|
+
) -> Annotated[dict, "The created document's title, documentId, and documentUrl in a dictionary"]:
|
|
123
|
+
"""
|
|
124
|
+
Create a Google Docs document with the specified title and text content.
|
|
125
|
+
"""
|
|
126
|
+
# First, create a blank document
|
|
127
|
+
document = await create_blank_document(context, title)
|
|
128
|
+
|
|
129
|
+
service = build_docs_service(context.authorization.token)
|
|
130
|
+
|
|
131
|
+
requests = [
|
|
132
|
+
{
|
|
133
|
+
"insertText": {
|
|
134
|
+
"location": {
|
|
135
|
+
"index": 1,
|
|
136
|
+
},
|
|
137
|
+
"text": text_content,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# Execute the batchUpdate method to insert text
|
|
143
|
+
service.documents().batchUpdate(
|
|
144
|
+
documentId=document["documentId"], body={"requests": requests}
|
|
145
|
+
).execute()
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"title": document["title"],
|
|
149
|
+
"documentId": document["documentId"],
|
|
150
|
+
"documentUrl": f"https://docs.google.com/document/d/{document['documentId']}/edit",
|
|
151
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Annotated, Optional
|
|
2
|
+
|
|
3
|
+
from arcade.core.schema import ToolContext
|
|
4
|
+
from arcade.sdk import tool
|
|
5
|
+
from arcade.sdk.auth import Google
|
|
6
|
+
from arcade_google.tools.utils import build_drive_service, remove_none_values
|
|
7
|
+
|
|
8
|
+
from .models import Corpora, OrderBy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Implements: https://googleapis.github.io/google-api-python-client/docs/dyn/drive_v3.files.html#list
|
|
12
|
+
# Example `arcade chat` query: `list my 5 most recently modified documents`
|
|
13
|
+
# TODO: Support query with natural language. Currently, the tool expects a fully formed query string as input with the syntax defined here: https://developers.google.com/drive/api/guides/search-files
|
|
14
|
+
@tool(
|
|
15
|
+
requires_auth=Google(
|
|
16
|
+
scopes=["https://www.googleapis.com/auth/drive.readonly"],
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
async def list_documents(
|
|
20
|
+
context: ToolContext,
|
|
21
|
+
corpora: Annotated[Optional[Corpora], "The source of files to list"] = Corpora.USER,
|
|
22
|
+
title_keywords: Annotated[
|
|
23
|
+
Optional[list[str]], "Keywords or phrases that must be in the document title"
|
|
24
|
+
] = None,
|
|
25
|
+
order_by: Annotated[
|
|
26
|
+
Optional[OrderBy],
|
|
27
|
+
"Sort order. Defaults to listing the most recently modified documents first",
|
|
28
|
+
] = OrderBy.MODIFIED_TIME_DESC,
|
|
29
|
+
supports_all_drives: Annotated[
|
|
30
|
+
Optional[bool],
|
|
31
|
+
"Whether the requesting application supports both My Drives and shared drives",
|
|
32
|
+
] = False,
|
|
33
|
+
limit: Annotated[Optional[int], "The number of documents to list"] = 50,
|
|
34
|
+
) -> Annotated[
|
|
35
|
+
dict,
|
|
36
|
+
"A dictionary containing 'documents_count' (number of documents returned) and 'documents' (a list of document details including 'kind', 'mimeType', 'id', and 'name' for each document)",
|
|
37
|
+
]:
|
|
38
|
+
"""
|
|
39
|
+
List documents in the user's Google Drive. Excludes documents that are in the trash.
|
|
40
|
+
"""
|
|
41
|
+
page_size = min(10, limit)
|
|
42
|
+
page_token = None # The page token is used for continuing a previous request on the next page
|
|
43
|
+
files = []
|
|
44
|
+
|
|
45
|
+
service = build_drive_service(context.authorization.token)
|
|
46
|
+
|
|
47
|
+
query = "mimeType = 'application/vnd.google-apps.document' and trashed = false"
|
|
48
|
+
if title_keywords:
|
|
49
|
+
# Escape single quotes in title_keywords
|
|
50
|
+
title_keywords = [keyword.replace("'", "\\'") for keyword in title_keywords]
|
|
51
|
+
# Only support logically ANDed keywords in query for now
|
|
52
|
+
keyword_queries = [f"name contains '{keyword}'" for keyword in title_keywords]
|
|
53
|
+
query += " and " + " and ".join(keyword_queries)
|
|
54
|
+
|
|
55
|
+
# Prepare the request parameters
|
|
56
|
+
params = {
|
|
57
|
+
"q": query,
|
|
58
|
+
"pageSize": page_size,
|
|
59
|
+
"orderBy": order_by.value,
|
|
60
|
+
"corpora": corpora.value,
|
|
61
|
+
"supportsAllDrives": supports_all_drives,
|
|
62
|
+
}
|
|
63
|
+
params = remove_none_values(params)
|
|
64
|
+
|
|
65
|
+
# Paginate through the results until the limit is reached
|
|
66
|
+
while len(files) < limit:
|
|
67
|
+
if page_token:
|
|
68
|
+
params["pageToken"] = page_token
|
|
69
|
+
else:
|
|
70
|
+
params.pop("pageToken", None)
|
|
71
|
+
|
|
72
|
+
results = service.files().list(**params).execute()
|
|
73
|
+
batch = results.get("files", [])
|
|
74
|
+
files.extend(batch[: limit - len(files)])
|
|
75
|
+
|
|
76
|
+
page_token = results.get("nextPageToken")
|
|
77
|
+
if not page_token or len(batch) < page_size:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
return {"documents_count": len(files), "documents": files}
|