arcade-google 0.1.5__py3-none-any.whl → 1.2.4__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.
@@ -1 +1,96 @@
1
- __all__ = ["gmail", "calendar", "drive", "docs"]
1
+ from arcade_google.tools.calendar import (
2
+ create_event,
3
+ delete_event,
4
+ find_time_slots_when_everyone_is_free,
5
+ list_calendars,
6
+ list_events,
7
+ update_event,
8
+ )
9
+ from arcade_google.tools.contacts import (
10
+ create_contact,
11
+ search_contacts_by_email,
12
+ search_contacts_by_name,
13
+ )
14
+ from arcade_google.tools.docs import (
15
+ create_blank_document,
16
+ create_document_from_text,
17
+ get_document_by_id,
18
+ insert_text_at_end_of_document,
19
+ )
20
+ from arcade_google.tools.drive import (
21
+ get_file_tree_structure,
22
+ search_and_retrieve_documents,
23
+ search_documents,
24
+ )
25
+ from arcade_google.tools.file_picker import generate_google_file_picker_url
26
+ from arcade_google.tools.gmail import (
27
+ change_email_labels,
28
+ create_label,
29
+ delete_draft_email,
30
+ get_thread,
31
+ list_draft_emails,
32
+ list_emails,
33
+ list_emails_by_header,
34
+ list_labels,
35
+ list_threads,
36
+ reply_to_email,
37
+ search_threads,
38
+ send_draft_email,
39
+ send_email,
40
+ trash_email,
41
+ update_draft_email,
42
+ write_draft_email,
43
+ write_draft_reply_email,
44
+ )
45
+ from arcade_google.tools.sheets import (
46
+ create_spreadsheet,
47
+ get_spreadsheet,
48
+ write_to_cell,
49
+ )
50
+
51
+ __all__ = [
52
+ # Google Calendar
53
+ create_event,
54
+ delete_event,
55
+ find_time_slots_when_everyone_is_free,
56
+ list_calendars,
57
+ list_events,
58
+ update_event,
59
+ # Google Contacts
60
+ create_contact,
61
+ search_contacts_by_email,
62
+ search_contacts_by_name,
63
+ # Google Docs
64
+ create_blank_document,
65
+ create_document_from_text,
66
+ get_document_by_id,
67
+ insert_text_at_end_of_document,
68
+ # Google Drive
69
+ "get_file_tree_structure",
70
+ "search_and_retrieve_documents",
71
+ "search_documents",
72
+ # Google File Picker
73
+ generate_google_file_picker_url,
74
+ # Google Gmail
75
+ change_email_labels,
76
+ create_label,
77
+ delete_draft_email,
78
+ get_thread,
79
+ list_draft_emails,
80
+ list_emails,
81
+ list_emails_by_header,
82
+ list_labels,
83
+ list_threads,
84
+ reply_to_email,
85
+ search_threads,
86
+ send_draft_email,
87
+ send_email,
88
+ trash_email,
89
+ update_draft_email,
90
+ write_draft_email,
91
+ write_draft_reply_email,
92
+ # Google Sheets
93
+ create_spreadsheet,
94
+ get_spreadsheet,
95
+ write_to_cell,
96
+ ]
@@ -1,15 +1,65 @@
1
+ import json
1
2
  from datetime import datetime, timedelta
2
- from typing import Annotated
3
+ from typing import Annotated, Any
4
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
3
5
 
4
- from google.oauth2.credentials import Credentials
5
- from googleapiclient.discovery import build
6
+ from arcade_tdk import ToolContext, tool
7
+ from arcade_tdk.auth import Google
8
+ from arcade_tdk.errors import RetryableToolError
6
9
  from googleapiclient.errors import HttpError
7
10
 
8
- from arcade.sdk import ToolContext, tool
9
- from arcade.sdk.auth import Google
10
- from arcade.sdk.errors import RetryableToolError
11
- from arcade_google.tools.models import EventVisibility, SendUpdatesOptions
12
- from arcade_google.tools.utils import parse_datetime
11
+ from arcade_google.models import EventVisibility, SendUpdatesOptions
12
+ from arcade_google.utils import (
13
+ build_calendar_service,
14
+ build_oauth_service,
15
+ compute_free_time_intersection,
16
+ parse_datetime,
17
+ )
18
+
19
+
20
+ @tool(
21
+ requires_auth=Google(
22
+ scopes=[
23
+ "https://www.googleapis.com/auth/calendar.readonly",
24
+ "https://www.googleapis.com/auth/calendar.events",
25
+ ]
26
+ )
27
+ )
28
+ async def list_calendars(
29
+ context: ToolContext,
30
+ max_results: Annotated[
31
+ int, "The maximum number of calendars to return. Up to 250 calendars, defaults to 10."
32
+ ] = 10,
33
+ show_deleted: Annotated[bool, "Whether to show deleted calendars. Defaults to False"] = False,
34
+ show_hidden: Annotated[bool, "Whether to show hidden calendars. Defaults to False"] = False,
35
+ next_page_token: Annotated[
36
+ str | None, "The token to retrieve the next page of calendars. Optional."
37
+ ] = None,
38
+ ) -> Annotated[dict, "A dictionary containing the calendars accessible by the end user"]:
39
+ """
40
+ List all calendars accessible by the user.
41
+ """
42
+ max_results = max(1, min(max_results, 250))
43
+ service = build_calendar_service(context.get_auth_token_or_empty())
44
+ calendars = (
45
+ service.calendarList()
46
+ .list(
47
+ pageToken=next_page_token,
48
+ showDeleted=show_deleted,
49
+ showHidden=show_hidden,
50
+ maxResults=max_results,
51
+ )
52
+ .execute()
53
+ )
54
+
55
+ items = calendars.get("items", [])
56
+ keys = ["description", "id", "summary", "timeZone"]
57
+ relevant_items = [{k: i.get(k) for k in keys if i.get(k)} for i in items]
58
+ return {
59
+ "next_page_token": calendars.get("nextPageToken"),
60
+ "num_calendars": len(relevant_items),
61
+ "calendars": relevant_items,
62
+ }
13
63
 
14
64
 
15
65
  @tool(
@@ -44,7 +94,7 @@ async def create_event(
44
94
  ) -> Annotated[dict, "A dictionary containing the created event details"]:
45
95
  """Create a new event/meeting/sync/meetup in the specified calendar."""
46
96
 
47
- service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
97
+ service = build_calendar_service(context.get_auth_token_or_empty())
48
98
 
49
99
  # Get the calendar's time zone
50
100
  calendar = service.calendars().get(calendarId=calendar_id).execute()
@@ -54,7 +104,7 @@ async def create_event(
54
104
  start_dt = parse_datetime(start_datetime, time_zone)
55
105
  end_dt = parse_datetime(end_datetime, time_zone)
56
106
 
57
- event = {
107
+ event: dict[str, Any] = {
58
108
  "summary": summary,
59
109
  "description": description,
60
110
  "location": location,
@@ -82,11 +132,13 @@ async def list_events(
82
132
  context: ToolContext,
83
133
  min_end_datetime: Annotated[
84
134
  str,
85
- "Filter by events that end on or after this datetime in ISO 8601 format, e.g., '2024-09-15T09:00:00'.",
135
+ "Filter by events that end on or after this datetime in ISO 8601 format, "
136
+ "e.g., '2024-09-15T09:00:00'.",
86
137
  ],
87
138
  max_start_datetime: Annotated[
88
139
  str,
89
- "Filter by events that start before this datetime in ISO 8601 format, e.g., '2024-09-16T17:00:00'.",
140
+ "Filter by events that start before this datetime in ISO 8601 format, "
141
+ "e.g., '2024-09-16T17:00:00'.",
90
142
  ],
91
143
  calendar_id: Annotated[str, "The ID of the calendar to list events from"] = "primary",
92
144
  max_results: Annotated[int, "The maximum number of events to return"] = 10,
@@ -98,14 +150,15 @@ async def list_events(
98
150
  max_start_datetime serves as the upper bound (exclusive) for an event's start time.
99
151
 
100
152
  For example:
101
- If min_end_datetime is set to 2024-09-15T09:00:00 and max_start_datetime is set to 2024-09-16T17:00:00,
102
- the function will return events that:
153
+ If min_end_datetime is set to 2024-09-15T09:00:00 and max_start_datetime
154
+ is set to 2024-09-16T17:00:00, the function will return events that:
103
155
  1. End after 09:00 on September 15, 2024 (exclusive)
104
156
  2. Start before 17:00 on September 16, 2024 (exclusive)
105
- This means an event starting at 08:00 on September 15 and ending at 10:00 on September 15 would be included,
106
- but an event starting at 17:00 on September 16 would not be included.
157
+ This means an event starting at 08:00 on September 15 and
158
+ ending at 10:00 on September 15 would be included, but an
159
+ event starting at 17:00 on September 16 would not be included.
107
160
  """
108
- service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
161
+ service = build_calendar_service(context.get_auth_token_or_empty())
109
162
 
110
163
  # Get the calendar's time zone
111
164
  calendar = service.calendars().get(calendarId=calendar_id).execute()
@@ -157,7 +210,7 @@ async def list_events(
157
210
 
158
211
  @tool(
159
212
  requires_auth=Google(
160
- scopes=["https://www.googleapis.com/auth/calendar"],
213
+ scopes=["https://www.googleapis.com/auth/calendar.events"],
161
214
  )
162
215
  )
163
216
  async def update_event(
@@ -165,7 +218,8 @@ async def update_event(
165
218
  event_id: Annotated[str, "The ID of the event to update"],
166
219
  updated_start_datetime: Annotated[
167
220
  str | None,
168
- "The updated datetime that the event starts in ISO 8601 format, e.g., '2024-12-31T15:30:00'.",
221
+ "The updated datetime that the event starts in ISO 8601 format, "
222
+ "e.g., '2024-12-31T15:30:00'.",
169
223
  ] = None,
170
224
  updated_end_datetime: Annotated[
171
225
  str | None,
@@ -180,26 +234,31 @@ async def update_event(
180
234
  updated_visibility: Annotated[EventVisibility | None, "The visibility of the event"] = None,
181
235
  attendee_emails_to_add: Annotated[
182
236
  list[str] | None,
183
- "The list of attendee emails to add. Must be valid email addresses e.g., username@domain.com.",
237
+ "The list of attendee emails to add. Must be valid email addresses "
238
+ "e.g., username@domain.com.",
184
239
  ] = None,
185
240
  attendee_emails_to_remove: Annotated[
186
241
  list[str] | None,
187
- "The list of attendee emails to remove. Must be valid email addresses e.g., username@domain.com.",
242
+ "The list of attendee emails to remove. Must be valid email addresses "
243
+ "e.g., username@domain.com.",
188
244
  ] = None,
189
245
  send_updates: Annotated[
190
- SendUpdatesOptions, "Should attendees be notified of the update? (none, all, external_only)"
246
+ SendUpdatesOptions,
247
+ "Should attendees be notified of the update? (none, all, external_only)",
191
248
  ] = SendUpdatesOptions.ALL,
192
249
  ) -> Annotated[
193
250
  str,
194
- "A string containing the updated event details, including the event ID, update timestamp, and a link to view the updated event.",
251
+ "A string containing the updated event details, including the event ID, update timestamp, "
252
+ "and a link to view the updated event.",
195
253
  ]:
196
254
  """
197
255
  Update an existing event in the specified calendar with the provided details.
198
256
  Only the provided fields will be updated; others will remain unchanged.
199
257
 
200
- `updated_start_datetime` and `updated_end_datetime` are independent and can be provided separately.
258
+ `updated_start_datetime` and `updated_end_datetime` are
259
+ independent and can be provided separately.
201
260
  """
202
- service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
261
+ service = build_calendar_service(context.get_auth_token_or_empty())
203
262
 
204
263
  calendar = service.calendars().get(calendarId="primary").execute()
205
264
  time_zone = calendar["timeZone"]
@@ -221,16 +280,21 @@ async def update_event(
221
280
  )
222
281
  raise RetryableToolError(
223
282
  f"Event with ID {event_id} not found.",
224
- additional_prompt_content=f"Here is a list of valid events. The event_id parameter must match one of these: {valid_events_with_id}",
283
+ additional_prompt_content=(
284
+ f"Here is a list of valid events. The event_id parameter must match one of these: "
285
+ f"{valid_events_with_id}"
286
+ ),
225
287
  retry_after_ms=1000,
226
- developer_message=f"Event with ID {event_id} not found. Please try again with a valid event ID.",
288
+ developer_message=(
289
+ f"Event with ID {event_id} not found. Please try again with a valid event ID."
290
+ ),
227
291
  )
228
292
 
229
293
  update_fields = {
230
- "start": {"dateTime": updated_start_datetime.isoformat(), "timeZone": time_zone}
294
+ "start": {"dateTime": updated_start_datetime, "timeZone": time_zone}
231
295
  if updated_start_datetime
232
296
  else None,
233
- "end": {"dateTime": updated_end_datetime.isoformat(), "timeZone": time_zone}
297
+ "end": {"dateTime": updated_end_datetime, "timeZone": time_zone}
234
298
  if updated_end_datetime
235
299
  else None,
236
300
  "calendarId": updated_calendar_id,
@@ -272,7 +336,10 @@ async def update_event(
272
336
  )
273
337
  .execute()
274
338
  )
275
- return f"Event with ID {event_id} successfully updated at {updated_event['updated']}. View updated event at {updated_event['htmlLink']}"
339
+ return (
340
+ f"Event with ID {event_id} successfully updated at {updated_event['updated']}. "
341
+ f"View updated event at {updated_event['htmlLink']}"
342
+ )
276
343
 
277
344
 
278
345
  @tool(
@@ -289,7 +356,7 @@ async def delete_event(
289
356
  ] = SendUpdatesOptions.ALL,
290
357
  ) -> Annotated[str, "A string containing the deletion confirmation message"]:
291
358
  """Delete an event from Google Calendar."""
292
- service = build("calendar", "v3", credentials=Credentials(context.authorization.token))
359
+ service = build_calendar_service(context.get_auth_token_or_empty())
293
360
 
294
361
  service.events().delete(
295
362
  calendarId=calendar_id, eventId=event_id, sendUpdates=send_updates.value
@@ -303,4 +370,141 @@ async def delete_event(
303
370
  elif send_updates == SendUpdatesOptions.NONE:
304
371
  notification_message = "No notifications were sent to attendees."
305
372
 
306
- return f"Event with ID '{event_id}' successfully deleted from calendar '{calendar_id}'. {notification_message}"
373
+ return (
374
+ f"Event with ID '{event_id}' successfully deleted from calendar '{calendar_id}'. "
375
+ f"{notification_message}"
376
+ )
377
+
378
+
379
+ # TODO: would be nice to have a "min_slot_duration" parameter
380
+ # TODO: find a way to have "include_weekends" parameter without confusing LLMs
381
+ @tool(
382
+ requires_auth=Google(
383
+ scopes=["https://www.googleapis.com/auth/calendar.readonly"],
384
+ ),
385
+ )
386
+ async def find_time_slots_when_everyone_is_free(
387
+ context: ToolContext,
388
+ email_addresses: Annotated[
389
+ list[str] | None,
390
+ "The list of email addresses from people in the same organization domain (apart from the "
391
+ "currently logged in user) to search for free time slots. Defaults to None, which will "
392
+ "return free time slots for the current user only.",
393
+ ] = None,
394
+ start_date: Annotated[
395
+ str | None,
396
+ "The start date to search for time slots in the format 'YYYY-MM-DD'. Defaults to today's "
397
+ "date. It will search starting from this date at the time 00:00:00.",
398
+ ] = None,
399
+ end_date: Annotated[
400
+ str | None,
401
+ "The end date to search for time slots in the format 'YYYY-MM-DD'. Defaults to seven days "
402
+ "from the start date. It will search until this date at the time 23:59:59.",
403
+ ] = None,
404
+ start_time_boundary: Annotated[
405
+ str,
406
+ "Will return free slots in any given day starting from this time in the format 'HH:MM'. "
407
+ "Defaults to '08:00', which is a usual business hour start time.",
408
+ ] = "08:00",
409
+ end_time_boundary: Annotated[
410
+ str,
411
+ "Will return free slots in any given day until this time in the format 'HH:MM'. "
412
+ "Defaults to '18:00', which is a usual business hour end time.",
413
+ ] = "18:00",
414
+ ) -> Annotated[
415
+ dict,
416
+ "A dictionary with the free slots and the timezone in which time slots are represented.",
417
+ ]:
418
+ """
419
+ Provides time slots when everyone is free within a given date range and time boundaries.
420
+ """
421
+
422
+ # Build google api services
423
+ oauth_service = build_oauth_service(context.get_auth_token_or_empty())
424
+ calendar_service = build_calendar_service(context.get_auth_token_or_empty())
425
+
426
+ email_addresses = email_addresses or []
427
+
428
+ if isinstance(email_addresses, str):
429
+ email_addresses = [email_addresses]
430
+
431
+ # Add the currently logged in user to the list of email addresses
432
+ user_info = oauth_service.userinfo().get().execute()
433
+ if user_info["email"] not in email_addresses:
434
+ email_addresses.append(user_info["email"])
435
+
436
+ # Get the timezone of the currently logged in user
437
+ calendar = calendar_service.calendars().get(calendarId="primary").execute()
438
+ timezone_name = calendar.get("timeZone")
439
+
440
+ try:
441
+ tz = ZoneInfo(timezone_name)
442
+ # If the calendar timezone name is not supported by Python's zoneinfo, use UTC
443
+ except ZoneInfoNotFoundError:
444
+ timezone_name = "UTC"
445
+ tz = ZoneInfo("UTC")
446
+
447
+ # Set default start and end dates, if not provided by the caller
448
+ start_date = start_date or datetime.now(tz=tz).date().isoformat()
449
+ end_date = end_date or (datetime.now(tz=tz).date() + timedelta(days=7)).isoformat()
450
+
451
+ # Parse start and end dates to datetime objects
452
+ start_datetime = datetime.strptime(start_date, "%Y-%m-%d").replace(
453
+ hour=0, minute=0, second=0, microsecond=0, tzinfo=tz
454
+ )
455
+ end_datetime = datetime.strptime(end_date, "%Y-%m-%d").replace(
456
+ hour=23, minute=59, second=59, microsecond=0, tzinfo=tz
457
+ )
458
+
459
+ # Get the busy slots from the calendars of the users
460
+ freebusy_response = (
461
+ calendar_service.freebusy()
462
+ .query(
463
+ body={
464
+ "timeMin": start_datetime.isoformat(),
465
+ "timeMax": end_datetime.isoformat(),
466
+ "timeZone": timezone_name,
467
+ "items": [{"id": email_address} for email_address in email_addresses],
468
+ }
469
+ )
470
+ .execute()
471
+ )
472
+ busy_slots = freebusy_response["calendars"]
473
+
474
+ response_errors = []
475
+
476
+ for email in email_addresses:
477
+ if "errors" not in busy_slots[email]:
478
+ continue
479
+ errors = busy_slots[email]["errors"]
480
+ for error in errors:
481
+ response_errors.append(
482
+ f"Error retrieving free slots from calendar of '{email}': "
483
+ f"{error.get('reason', 'not determined')}"
484
+ )
485
+
486
+ if response_errors:
487
+ raise RetryableToolError(
488
+ "Error retrieving free slots from calendars of one or more users.",
489
+ additional_prompt_content=json.dumps(response_errors),
490
+ retry_after_ms=1000,
491
+ developer_message="Error retrieving free slots from calendars of one or more users.",
492
+ )
493
+
494
+ # Compute the free slots
495
+ free_slots = compute_free_time_intersection(
496
+ busy_data=busy_slots,
497
+ global_start=start_datetime,
498
+ global_end=end_datetime,
499
+ start_time_boundary=datetime.strptime(start_time_boundary, "%H:%M")
500
+ .time()
501
+ .replace(tzinfo=tz),
502
+ end_time_boundary=datetime.strptime(end_time_boundary, "%H:%M").time().replace(tzinfo=tz),
503
+ include_weekends=True,
504
+ tz=tz,
505
+ )
506
+
507
+ return {
508
+ "free_slots": free_slots,
509
+ "timezone": timezone_name,
510
+ }
@@ -0,0 +1,96 @@
1
+ import asyncio
2
+ from typing import Annotated
3
+
4
+ from arcade_tdk import ToolContext, tool
5
+ from arcade_tdk.auth import Google
6
+
7
+ from arcade_google.constants import DEFAULT_SEARCH_CONTACTS_LIMIT
8
+ from arcade_google.utils import build_people_service, search_contacts
9
+
10
+
11
+ async def _warmup_cache(service) -> None: # type: ignore[no-untyped-def]
12
+ """
13
+ Warm-up the search cache for contacts by sending a request with an empty query.
14
+ This ensures that the lazy cache is updated for both primary contacts and other contacts.
15
+ This is unfortunately a real thing: https://developers.google.com/people/v1/contacts#search_the_users_contacts
16
+ """
17
+ service.people().searchContacts(query="", pageSize=1, readMask="names,emailAddresses").execute()
18
+ await asyncio.sleep(3) # TODO experiment with this value
19
+
20
+
21
+ @tool(requires_auth=Google(scopes=["https://www.googleapis.com/auth/contacts.readonly"]))
22
+ async def search_contacts_by_email(
23
+ context: ToolContext,
24
+ email: Annotated[str, "The email address to search for"],
25
+ limit: Annotated[
26
+ int | None,
27
+ "The maximum number of contacts to return (30 is the max allowed by Google API)",
28
+ ] = DEFAULT_SEARCH_CONTACTS_LIMIT,
29
+ ) -> Annotated[dict, "A dictionary containing the list of matching contacts"]:
30
+ """
31
+ Search the user's contacts in Google Contacts by email address.
32
+ """
33
+ service = build_people_service(context.get_auth_token_or_empty())
34
+ # Warm-up the cache before performing search.
35
+ # TODO: Ideally we should warmup only if this user (or google domain?) hasn't warmed up recently
36
+ await _warmup_cache(service)
37
+
38
+ return {"contacts": search_contacts(service, email, limit)}
39
+
40
+
41
+ @tool(requires_auth=Google(scopes=["https://www.googleapis.com/auth/contacts.readonly"]))
42
+ async def search_contacts_by_name(
43
+ context: ToolContext,
44
+ name: Annotated[str, "The full name to search for"],
45
+ limit: Annotated[
46
+ int | None,
47
+ "The maximum number of contacts to return (30 is the max allowed by Google API)",
48
+ ] = DEFAULT_SEARCH_CONTACTS_LIMIT,
49
+ ) -> Annotated[dict, "A dictionary containing the list of matching contacts"]:
50
+ """
51
+ Search the user's contacts in Google Contacts by name.
52
+ """
53
+ service = build_people_service(context.get_auth_token_or_empty())
54
+ # Warm-up the cache before performing search.
55
+ # TODO: Ideally we should warmup only if this user (or google domain?) hasn't warmed up recently
56
+ await _warmup_cache(service)
57
+ return {"contacts": search_contacts(service, name, limit)}
58
+
59
+
60
+ @tool(requires_auth=Google(scopes=["https://www.googleapis.com/auth/contacts"]))
61
+ async def create_contact(
62
+ context: ToolContext,
63
+ given_name: Annotated[str, "The given name of the contact"],
64
+ family_name: Annotated[str | None, "The optional family name of the contact"],
65
+ email: Annotated[str | None, "The optional email address of the contact"],
66
+ ) -> Annotated[dict, "A dictionary containing the details of the created contact"]:
67
+ """
68
+ Create a new contact record in Google Contacts.
69
+
70
+ Examples:
71
+ ```
72
+ create_contact(given_name="Alice")
73
+ create_contact(given_name="Alice", family_name="Smith")
74
+ create_contact(given_name="Alice", email="alice@example.com")
75
+ ```
76
+ """
77
+ # Build the People API service
78
+ service = build_people_service(context.get_auth_token_or_empty())
79
+
80
+ # Construct the person payload with the specified names
81
+ name_body = {"givenName": given_name}
82
+ if family_name:
83
+ name_body["familyName"] = family_name
84
+ contact_body = {"names": [name_body]}
85
+ if email:
86
+ contact_body["emailAddresses"] = [{"value": email, "type": "work"}]
87
+
88
+ # Create the contact. The personFields parameter specifies what information
89
+ # should be returned. Here, we return names and emailAddresses.
90
+ created_contact = (
91
+ service.people()
92
+ .createContact(body=contact_body, personFields="names,emailAddresses")
93
+ .execute()
94
+ )
95
+
96
+ return {"contact": created_contact}
@@ -1,8 +1,9 @@
1
1
  from typing import Annotated
2
2
 
3
- from arcade.sdk import ToolContext, tool
4
- from arcade.sdk.auth import Google
5
- from arcade_google.tools.utils import build_docs_service
3
+ from arcade_tdk import ToolContext, tool
4
+ from arcade_tdk.auth import Google
5
+
6
+ from arcade_google.utils import build_docs_service
6
7
 
7
8
 
8
9
  # Uses https://developers.google.com/docs/api/reference/rest/v1/documents/get
@@ -11,7 +12,7 @@ from arcade_google.tools.utils import build_docs_service
11
12
  @tool(
12
13
  requires_auth=Google(
13
14
  scopes=[
14
- "https://www.googleapis.com/auth/documents.readonly",
15
+ "https://www.googleapis.com/auth/drive.file",
15
16
  ],
16
17
  )
17
18
  )
@@ -22,13 +23,15 @@ async def get_document_by_id(
22
23
  """
23
24
  Get the latest version of the specified Google Docs document.
24
25
  """
25
- service = build_docs_service(context.authorization.token)
26
+ service = build_docs_service(
27
+ context.authorization.token if context.authorization and context.authorization.token else ""
28
+ )
26
29
 
27
30
  # Execute the documents().get() method. Returns a Document object
28
31
  # https://developers.google.com/docs/api/reference/rest/v1/documents#Document
29
32
  request = service.documents().get(documentId=document_id)
30
33
  response = request.execute()
31
- return response
34
+ return dict(response)
32
35
 
33
36
 
34
37
  # Uses https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate
@@ -36,7 +39,7 @@ async def get_document_by_id(
36
39
  @tool(
37
40
  requires_auth=Google(
38
41
  scopes=[
39
- "https://www.googleapis.com/auth/documents",
42
+ "https://www.googleapis.com/auth/drive.file",
40
43
  ],
41
44
  )
42
45
  )
@@ -52,7 +55,9 @@ async def insert_text_at_end_of_document(
52
55
 
53
56
  end_index = document["body"]["content"][-1]["endIndex"]
54
57
 
55
- service = build_docs_service(context.authorization.token)
58
+ service = build_docs_service(
59
+ context.authorization.token if context.authorization and context.authorization.token else ""
60
+ )
56
61
 
57
62
  requests = [
58
63
  {
@@ -72,7 +77,7 @@ async def insert_text_at_end_of_document(
72
77
  .execute()
73
78
  )
74
79
 
75
- return response
80
+ return dict(response)
76
81
 
77
82
 
78
83
  # Uses https://developers.google.com/docs/api/reference/rest/v1/documents/create
@@ -80,7 +85,7 @@ async def insert_text_at_end_of_document(
80
85
  @tool(
81
86
  requires_auth=Google(
82
87
  scopes=[
83
- "https://www.googleapis.com/auth/documents",
88
+ "https://www.googleapis.com/auth/drive.file",
84
89
  ],
85
90
  )
86
91
  )
@@ -90,7 +95,9 @@ async def create_blank_document(
90
95
  """
91
96
  Create a blank Google Docs document with the specified title.
92
97
  """
93
- service = build_docs_service(context.authorization.token)
98
+ service = build_docs_service(
99
+ context.authorization.token if context.authorization and context.authorization.token else ""
100
+ )
94
101
 
95
102
  body = {"title": title}
96
103
 
@@ -106,11 +113,12 @@ async def create_blank_document(
106
113
 
107
114
 
108
115
  # Uses https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate
109
- # Example `arcade chat` query: `create document with title "My New Document" and text content "Hello, World!"`
116
+ # Example `arcade chat` query:
117
+ # `create document with title "My New Document" and text content "Hello, World!"`
110
118
  @tool(
111
119
  requires_auth=Google(
112
120
  scopes=[
113
- "https://www.googleapis.com/auth/documents",
121
+ "https://www.googleapis.com/auth/drive.file",
114
122
  ],
115
123
  )
116
124
  )
@@ -125,7 +133,9 @@ async def create_document_from_text(
125
133
  # First, create a blank document
126
134
  document = await create_blank_document(context, title)
127
135
 
128
- service = build_docs_service(context.authorization.token)
136
+ service = build_docs_service(
137
+ context.authorization.token if context.authorization and context.authorization.token else ""
138
+ )
129
139
 
130
140
  requests = [
131
141
  {