arcade-google 0.1.6__py3-none-any.whl → 2.0.0__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/constants.py +24 -0
- arcade_google/critics.py +41 -0
- arcade_google/doc_to_html.py +99 -0
- arcade_google/doc_to_markdown.py +64 -0
- arcade_google/enums.py +0 -0
- arcade_google/exceptions.py +70 -0
- arcade_google/models.py +654 -0
- arcade_google/tools/__init__.py +96 -1
- arcade_google/tools/calendar.py +236 -32
- arcade_google/tools/contacts.py +96 -0
- arcade_google/tools/docs.py +24 -14
- arcade_google/tools/drive.py +256 -48
- arcade_google/tools/file_picker.py +54 -0
- arcade_google/tools/gmail.py +336 -116
- arcade_google/tools/sheets.py +144 -0
- arcade_google/utils.py +1564 -0
- arcade_google-2.0.0.dist-info/METADATA +27 -0
- arcade_google-2.0.0.dist-info/RECORD +21 -0
- {arcade_google-0.1.6.dist-info → arcade_google-2.0.0.dist-info}/WHEEL +1 -1
- arcade_google-2.0.0.dist-info/licenses/LICENSE +21 -0
- arcade_google/tools/models.py +0 -296
- arcade_google/tools/utils.py +0 -282
- arcade_google-0.1.6.dist-info/METADATA +0 -20
- arcade_google-0.1.6.dist-info/RECORD +0 -11
arcade_google/tools/gmail.py
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
from email.message import EmailMessage
|
|
3
2
|
from email.mime.text import MIMEText
|
|
4
|
-
from typing import Annotated,
|
|
3
|
+
from typing import Annotated, Any
|
|
5
4
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
5
|
+
from arcade_tdk import ToolContext, tool
|
|
6
|
+
from arcade_tdk.auth import Google
|
|
7
|
+
from arcade_tdk.errors import RetryableToolError
|
|
8
8
|
from googleapiclient.errors import HttpError
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from arcade_google.
|
|
10
|
+
from arcade_google.constants import GMAIL_DEFAULT_REPLY_TO
|
|
11
|
+
from arcade_google.exceptions import GmailToolError
|
|
12
|
+
from arcade_google.models import GmailAction, GmailReplyToWhom
|
|
13
|
+
from arcade_google.utils import (
|
|
14
14
|
DateRange,
|
|
15
|
-
|
|
15
|
+
_build_gmail_service,
|
|
16
|
+
build_email_message,
|
|
17
|
+
build_gmail_query_string,
|
|
18
|
+
build_reply_recipients,
|
|
16
19
|
fetch_messages,
|
|
17
20
|
get_draft_url,
|
|
21
|
+
get_email_details,
|
|
18
22
|
get_email_in_trash_url,
|
|
23
|
+
get_label_ids,
|
|
19
24
|
get_sent_email_url,
|
|
20
25
|
parse_draft_email,
|
|
21
|
-
|
|
26
|
+
parse_multipart_email,
|
|
27
|
+
parse_plain_text_email,
|
|
22
28
|
remove_none_values,
|
|
23
29
|
)
|
|
24
30
|
|
|
@@ -34,35 +40,18 @@ async def send_email(
|
|
|
34
40
|
subject: Annotated[str, "The subject of the email"],
|
|
35
41
|
body: Annotated[str, "The body of the email"],
|
|
36
42
|
recipient: Annotated[str, "The recipient of the email"],
|
|
37
|
-
cc: Annotated[
|
|
38
|
-
bcc: Annotated[
|
|
43
|
+
cc: Annotated[list[str] | None, "CC recipients of the email"] = None,
|
|
44
|
+
bcc: Annotated[list[str] | None, "BCC recipients of the email"] = None,
|
|
39
45
|
) -> Annotated[dict, "A dictionary containing the sent email details"]:
|
|
40
46
|
"""
|
|
41
47
|
Send an email using the Gmail API.
|
|
42
48
|
"""
|
|
49
|
+
service = _build_gmail_service(context)
|
|
50
|
+
email = build_email_message(recipient, subject, body, cc, bcc)
|
|
43
51
|
|
|
44
|
-
# Set up the Gmail API client
|
|
45
|
-
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
|
46
|
-
|
|
47
|
-
message = EmailMessage()
|
|
48
|
-
message.set_content(body)
|
|
49
|
-
message["To"] = recipient
|
|
50
|
-
message["Subject"] = subject
|
|
51
|
-
if cc:
|
|
52
|
-
message["Cc"] = ", ".join(cc)
|
|
53
|
-
if bcc:
|
|
54
|
-
message["Bcc"] = ", ".join(bcc)
|
|
55
|
-
|
|
56
|
-
# Encode the message in base64
|
|
57
|
-
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
|
58
|
-
|
|
59
|
-
# Create the email
|
|
60
|
-
email = {"raw": encoded_message}
|
|
61
|
-
|
|
62
|
-
# Send the email
|
|
63
52
|
sent_message = service.users().messages().send(userId="me", body=email).execute()
|
|
64
53
|
|
|
65
|
-
email =
|
|
54
|
+
email = parse_plain_text_email(sent_message)
|
|
66
55
|
email["url"] = get_sent_email_url(sent_message["id"])
|
|
67
56
|
return email
|
|
68
57
|
|
|
@@ -79,13 +68,79 @@ async def send_draft_email(
|
|
|
79
68
|
Send a draft email using the Gmail API.
|
|
80
69
|
"""
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
|
71
|
+
service = _build_gmail_service(context)
|
|
84
72
|
|
|
85
73
|
# Send the draft email
|
|
86
74
|
sent_message = service.users().drafts().send(userId="me", body={"id": email_id}).execute()
|
|
87
75
|
|
|
88
|
-
email =
|
|
76
|
+
email = parse_plain_text_email(sent_message)
|
|
77
|
+
email["url"] = get_sent_email_url(sent_message["id"])
|
|
78
|
+
return email
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Note: in the Gmail UI, a user can customize the recipient and cc fields before replying.
|
|
82
|
+
# We decided not to support this feature, since we'd need a way for LLMs to tell apart between
|
|
83
|
+
# adding or removing recipients/cc, or replacing with an entirely new list of addresses,
|
|
84
|
+
# which would make the tool more complex to call.
|
|
85
|
+
@tool(
|
|
86
|
+
requires_auth=Google(
|
|
87
|
+
scopes=["https://www.googleapis.com/auth/gmail.send"],
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
async def reply_to_email(
|
|
91
|
+
context: ToolContext,
|
|
92
|
+
body: Annotated[str, "The body of the email"],
|
|
93
|
+
reply_to_message_id: Annotated[str, "The ID of the message to reply to"],
|
|
94
|
+
reply_to_whom: Annotated[
|
|
95
|
+
GmailReplyToWhom,
|
|
96
|
+
"Whether to reply to every recipient (including cc) or only to the original sender. "
|
|
97
|
+
f"Defaults to '{GMAIL_DEFAULT_REPLY_TO}'.",
|
|
98
|
+
] = GMAIL_DEFAULT_REPLY_TO,
|
|
99
|
+
bcc: Annotated[list[str] | None, "BCC recipients of the email"] = None,
|
|
100
|
+
) -> Annotated[dict, "A dictionary containing the sent email details"]:
|
|
101
|
+
"""
|
|
102
|
+
Send a reply to an email message.
|
|
103
|
+
"""
|
|
104
|
+
if isinstance(reply_to_whom, str):
|
|
105
|
+
reply_to_whom = GmailReplyToWhom(reply_to_whom)
|
|
106
|
+
|
|
107
|
+
service = _build_gmail_service(context)
|
|
108
|
+
|
|
109
|
+
current_user = service.users().getProfile(userId="me").execute()
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
replying_to_email = (
|
|
113
|
+
service.users().messages().get(userId="me", id=reply_to_message_id).execute()
|
|
114
|
+
)
|
|
115
|
+
except HttpError as e:
|
|
116
|
+
raise RetryableToolError(
|
|
117
|
+
message=f"Could not retrieve the message with id {reply_to_message_id}.",
|
|
118
|
+
developer_message=(
|
|
119
|
+
f"Could not retrieve the message with id {reply_to_message_id}. "
|
|
120
|
+
f"Reason: '{e.reason}'. Error details: '{e.error_details}'"
|
|
121
|
+
),
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
replying_to_email = parse_multipart_email(replying_to_email)
|
|
125
|
+
|
|
126
|
+
recipients = build_reply_recipients(
|
|
127
|
+
replying_to_email, current_user["emailAddress"], reply_to_whom
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
email = build_email_message(
|
|
131
|
+
recipient=recipients,
|
|
132
|
+
subject=f"Re: {replying_to_email['subject']}",
|
|
133
|
+
body=body,
|
|
134
|
+
cc=None
|
|
135
|
+
if reply_to_whom == GmailReplyToWhom.ONLY_THE_SENDER
|
|
136
|
+
else replying_to_email["cc"].split(","),
|
|
137
|
+
bcc=bcc,
|
|
138
|
+
replying_to=replying_to_email,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
sent_message = service.users().messages().send(userId="me", body=email).execute()
|
|
142
|
+
|
|
143
|
+
email = parse_plain_text_email(sent_message)
|
|
89
144
|
email["url"] = get_sent_email_url(sent_message["id"])
|
|
90
145
|
return email
|
|
91
146
|
|
|
@@ -101,28 +156,18 @@ async def write_draft_email(
|
|
|
101
156
|
subject: Annotated[str, "The subject of the draft email"],
|
|
102
157
|
body: Annotated[str, "The body of the draft email"],
|
|
103
158
|
recipient: Annotated[str, "The recipient of the draft email"],
|
|
104
|
-
cc: Annotated[
|
|
105
|
-
bcc: Annotated[
|
|
159
|
+
cc: Annotated[list[str] | None, "CC recipients of the draft email"] = None,
|
|
160
|
+
bcc: Annotated[list[str] | None, "BCC recipients of the draft email"] = None,
|
|
106
161
|
) -> Annotated[dict, "A dictionary containing the created draft email details"]:
|
|
107
162
|
"""
|
|
108
163
|
Compose a new email draft using the Gmail API.
|
|
109
164
|
"""
|
|
110
165
|
# Set up the Gmail API client
|
|
111
|
-
service =
|
|
112
|
-
|
|
113
|
-
message = MIMEText(body)
|
|
114
|
-
message["to"] = recipient
|
|
115
|
-
message["subject"] = subject
|
|
116
|
-
if cc:
|
|
117
|
-
message["Cc"] = ", ".join(cc)
|
|
118
|
-
if bcc:
|
|
119
|
-
message["Bcc"] = ", ".join(bcc)
|
|
120
|
-
|
|
121
|
-
# Encode the message in base64
|
|
122
|
-
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
|
166
|
+
service = _build_gmail_service(context)
|
|
123
167
|
|
|
124
|
-
|
|
125
|
-
|
|
168
|
+
draft = {
|
|
169
|
+
"message": build_email_message(recipient, subject, body, cc, bcc, action=GmailAction.DRAFT)
|
|
170
|
+
}
|
|
126
171
|
|
|
127
172
|
draft_message = service.users().drafts().create(userId="me", body=draft).execute()
|
|
128
173
|
email = parse_draft_email(draft_message)
|
|
@@ -130,6 +175,76 @@ async def write_draft_email(
|
|
|
130
175
|
return email
|
|
131
176
|
|
|
132
177
|
|
|
178
|
+
# Note: in the Gmail UI, a user can customize the recipient and cc fields before replying.
|
|
179
|
+
# We decided not to support this feature, since we'd need a way for LLMs to tell apart between
|
|
180
|
+
# adding or removing recipients/cc, or replacing with an entirely new list of addresses,
|
|
181
|
+
# which would make the tool more complex to call.
|
|
182
|
+
@tool(
|
|
183
|
+
requires_auth=Google(
|
|
184
|
+
scopes=["https://www.googleapis.com/auth/gmail.compose"],
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
async def write_draft_reply_email(
|
|
188
|
+
context: ToolContext,
|
|
189
|
+
body: Annotated[str, "The body of the draft reply email"],
|
|
190
|
+
reply_to_message_id: Annotated[str, "The Gmail message ID of the message to draft a reply to"],
|
|
191
|
+
reply_to_whom: Annotated[
|
|
192
|
+
GmailReplyToWhom,
|
|
193
|
+
"Whether to reply to every recipient (including cc) or only to the original sender. "
|
|
194
|
+
f"Defaults to '{GMAIL_DEFAULT_REPLY_TO}'.",
|
|
195
|
+
] = GMAIL_DEFAULT_REPLY_TO,
|
|
196
|
+
bcc: Annotated[list[str] | None, "BCC recipients of the draft reply email"] = None,
|
|
197
|
+
) -> Annotated[dict, "A dictionary containing the created draft reply email details"]:
|
|
198
|
+
"""
|
|
199
|
+
Compose a draft reply to an email message.
|
|
200
|
+
"""
|
|
201
|
+
if isinstance(reply_to_whom, str):
|
|
202
|
+
reply_to_whom = GmailReplyToWhom(reply_to_whom)
|
|
203
|
+
|
|
204
|
+
service = _build_gmail_service(context)
|
|
205
|
+
|
|
206
|
+
current_user = service.users().getProfile(userId="me").execute()
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
replying_to_email = (
|
|
210
|
+
service.users().messages().get(userId="me", id=reply_to_message_id).execute()
|
|
211
|
+
)
|
|
212
|
+
except HttpError as e:
|
|
213
|
+
raise RetryableToolError(
|
|
214
|
+
message="Could not retrieve the message to respond to.",
|
|
215
|
+
developer_message=(
|
|
216
|
+
"Could not retrieve the message to respond to. "
|
|
217
|
+
f"Reason: '{e.reason}'. Error details: '{e.error_details}'"
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
replying_to_email = parse_multipart_email(replying_to_email)
|
|
222
|
+
|
|
223
|
+
recipients = build_reply_recipients(
|
|
224
|
+
replying_to_email, current_user["emailAddress"], reply_to_whom
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
draft_message = {
|
|
228
|
+
"message": build_email_message(
|
|
229
|
+
recipient=recipients,
|
|
230
|
+
subject=f"Re: {replying_to_email['subject']}",
|
|
231
|
+
body=body,
|
|
232
|
+
cc=None
|
|
233
|
+
if reply_to_whom == GmailReplyToWhom.ONLY_THE_SENDER
|
|
234
|
+
else replying_to_email["cc"].split(","),
|
|
235
|
+
bcc=bcc,
|
|
236
|
+
replying_to=replying_to_email,
|
|
237
|
+
action=GmailAction.DRAFT,
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
draft = service.users().drafts().create(userId="me", body=draft_message).execute()
|
|
242
|
+
|
|
243
|
+
email = parse_draft_email(draft)
|
|
244
|
+
email["url"] = get_draft_url(draft["id"])
|
|
245
|
+
return email
|
|
246
|
+
|
|
247
|
+
|
|
133
248
|
@tool(
|
|
134
249
|
requires_auth=Google(
|
|
135
250
|
scopes=["https://www.googleapis.com/auth/gmail.compose"],
|
|
@@ -141,15 +256,13 @@ async def update_draft_email(
|
|
|
141
256
|
subject: Annotated[str, "The subject of the draft email"],
|
|
142
257
|
body: Annotated[str, "The body of the draft email"],
|
|
143
258
|
recipient: Annotated[str, "The recipient of the draft email"],
|
|
144
|
-
cc: Annotated[
|
|
145
|
-
bcc: Annotated[
|
|
259
|
+
cc: Annotated[list[str] | None, "CC recipients of the draft email"] = None,
|
|
260
|
+
bcc: Annotated[list[str] | None, "BCC recipients of the draft email"] = None,
|
|
146
261
|
) -> Annotated[dict, "A dictionary containing the updated draft email details"]:
|
|
147
262
|
"""
|
|
148
263
|
Update an existing email draft using the Gmail API.
|
|
149
264
|
"""
|
|
150
|
-
|
|
151
|
-
# Set up the Gmail API client
|
|
152
|
-
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
|
265
|
+
service = _build_gmail_service(context)
|
|
153
266
|
|
|
154
267
|
message = MIMEText(body)
|
|
155
268
|
message["to"] = recipient
|
|
@@ -171,6 +284,7 @@ async def update_draft_email(
|
|
|
171
284
|
|
|
172
285
|
email = parse_draft_email(updated_draft_message)
|
|
173
286
|
email["url"] = get_draft_url(updated_draft_message["id"])
|
|
287
|
+
|
|
174
288
|
return email
|
|
175
289
|
|
|
176
290
|
|
|
@@ -186,9 +300,7 @@ async def delete_draft_email(
|
|
|
186
300
|
"""
|
|
187
301
|
Delete a draft email using the Gmail API.
|
|
188
302
|
"""
|
|
189
|
-
|
|
190
|
-
# Set up the Gmail API client
|
|
191
|
-
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
|
303
|
+
service = _build_gmail_service(context)
|
|
192
304
|
|
|
193
305
|
# Delete the draft
|
|
194
306
|
service.users().drafts().delete(userId="me", id=draft_email_id).execute()
|
|
@@ -208,13 +320,12 @@ async def trash_email(
|
|
|
208
320
|
Move an email to the trash folder using the Gmail API.
|
|
209
321
|
"""
|
|
210
322
|
|
|
211
|
-
|
|
212
|
-
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
|
323
|
+
service = _build_gmail_service(context)
|
|
213
324
|
|
|
214
325
|
# Trash the email
|
|
215
326
|
trashed_email = service.users().messages().trash(userId="me", id=email_id).execute()
|
|
216
327
|
|
|
217
|
-
email =
|
|
328
|
+
email = parse_plain_text_email(trashed_email)
|
|
218
329
|
email["url"] = get_email_in_trash_url(trashed_email["id"])
|
|
219
330
|
return email
|
|
220
331
|
|
|
@@ -232,8 +343,7 @@ async def list_draft_emails(
|
|
|
232
343
|
"""
|
|
233
344
|
Lists draft emails in the user's draft mailbox using the Gmail API.
|
|
234
345
|
"""
|
|
235
|
-
|
|
236
|
-
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
|
346
|
+
service = _build_gmail_service(context)
|
|
237
347
|
|
|
238
348
|
listed_drafts = service.users().drafts().list(userId="me").execute()
|
|
239
349
|
|
|
@@ -250,12 +360,13 @@ async def list_draft_emails(
|
|
|
250
360
|
if draft_details:
|
|
251
361
|
emails.append(draft_details)
|
|
252
362
|
except Exception as e:
|
|
253
|
-
|
|
363
|
+
raise GmailToolError(
|
|
364
|
+
message=f"Error reading draft email {draft_id}.", developer_message=str(e)
|
|
365
|
+
)
|
|
254
366
|
|
|
255
367
|
return {"emails": emails}
|
|
256
368
|
|
|
257
369
|
|
|
258
|
-
# Email Search Tools
|
|
259
370
|
@tool(
|
|
260
371
|
requires_auth=Google(
|
|
261
372
|
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
|
@@ -263,47 +374,64 @@ async def list_draft_emails(
|
|
|
263
374
|
)
|
|
264
375
|
async def list_emails_by_header(
|
|
265
376
|
context: ToolContext,
|
|
266
|
-
sender: Annotated[
|
|
267
|
-
recipient: Annotated[
|
|
268
|
-
subject: Annotated[
|
|
269
|
-
body: Annotated[
|
|
270
|
-
date_range: Annotated[
|
|
271
|
-
|
|
377
|
+
sender: Annotated[str | None, "The name or email address of the sender of the email"] = None,
|
|
378
|
+
recipient: Annotated[str | None, "The name or email address of the recipient"] = None,
|
|
379
|
+
subject: Annotated[str | None, "Words to find in the subject of the email"] = None,
|
|
380
|
+
body: Annotated[str | None, "Words to find in the body of the email"] = None,
|
|
381
|
+
date_range: Annotated[DateRange | None, "The date range of the email"] = None,
|
|
382
|
+
label: Annotated[str | None, "The label name to filter by"] = None,
|
|
383
|
+
max_results: Annotated[int, "The maximum number of emails to return"] = 25,
|
|
272
384
|
) -> Annotated[
|
|
273
385
|
dict, "A dictionary containing a list of email details matching the search criteria"
|
|
274
386
|
]:
|
|
275
387
|
"""
|
|
276
388
|
Search for emails by header using the Gmail API.
|
|
277
|
-
|
|
389
|
+
|
|
390
|
+
At least one of the following parameters MUST be provided: sender, recipient,
|
|
391
|
+
subject, date_range, label, or body.
|
|
278
392
|
"""
|
|
279
|
-
|
|
393
|
+
service = _build_gmail_service(context)
|
|
394
|
+
# Ensure at least one search parameter is provided
|
|
395
|
+
if not any([sender, recipient, subject, body, label, date_range]):
|
|
280
396
|
raise RetryableToolError(
|
|
281
|
-
message=
|
|
282
|
-
|
|
397
|
+
message=(
|
|
398
|
+
"At least one of sender, recipient, subject, body, label, query, "
|
|
399
|
+
"or date_range must be provided."
|
|
400
|
+
),
|
|
401
|
+
developer_message=(
|
|
402
|
+
"At least one of sender, recipient, subject, body, label, query, "
|
|
403
|
+
"or date_range must be provided."
|
|
404
|
+
),
|
|
283
405
|
)
|
|
284
406
|
|
|
285
|
-
|
|
407
|
+
# Check if label is valid
|
|
408
|
+
if label:
|
|
409
|
+
label_ids = get_label_ids(service, [label])
|
|
410
|
+
|
|
411
|
+
if not label_ids:
|
|
412
|
+
labels = service.users().labels().list(userId="me").execute().get("labels", [])
|
|
413
|
+
label_names = [label["name"] for label in labels]
|
|
414
|
+
raise RetryableToolError(
|
|
415
|
+
message=f"Invalid label: {label}",
|
|
416
|
+
developer_message=f"Invalid label: {label}",
|
|
417
|
+
additional_prompt_content=f"List of valid labels: {label_names}",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Build a Gmail-style query string based on the filters
|
|
421
|
+
query = build_gmail_query_string(sender, recipient, subject, body, date_range, label)
|
|
286
422
|
|
|
287
|
-
|
|
288
|
-
messages = fetch_messages(service, query,
|
|
423
|
+
# Fetch matching messages. This fetches message metadata from Gmail
|
|
424
|
+
messages = fetch_messages(service, query, max_results)
|
|
289
425
|
|
|
426
|
+
# If no messages found, return an empty list
|
|
290
427
|
if not messages:
|
|
291
428
|
return {"emails": []}
|
|
292
429
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
430
|
+
# Process each message into a structured email object
|
|
431
|
+
emails = get_email_details(service, messages)
|
|
296
432
|
|
|
297
|
-
|
|
298
|
-
emails
|
|
299
|
-
for msg in messages:
|
|
300
|
-
try:
|
|
301
|
-
email_data = service.users().messages().get(userId="me", id=msg["id"]).execute()
|
|
302
|
-
email_details = parse_email(email_data)
|
|
303
|
-
emails += [email_details] if email_details else []
|
|
304
|
-
except HttpError as e:
|
|
305
|
-
print(f"Error reading email {msg['id']}: {e}")
|
|
306
|
-
return emails
|
|
433
|
+
# Return the list of emails in a dictionary with key "emails"
|
|
434
|
+
return {"emails": emails}
|
|
307
435
|
|
|
308
436
|
|
|
309
437
|
@tool(
|
|
@@ -318,8 +446,7 @@ async def list_emails(
|
|
|
318
446
|
"""
|
|
319
447
|
Read emails from a Gmail account and extract plain text content.
|
|
320
448
|
"""
|
|
321
|
-
|
|
322
|
-
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
|
449
|
+
service = _build_gmail_service(context)
|
|
323
450
|
|
|
324
451
|
messages = service.users().messages().list(userId="me").execute().get("messages", [])
|
|
325
452
|
|
|
@@ -330,12 +457,13 @@ async def list_emails(
|
|
|
330
457
|
for msg in messages[:n_emails]:
|
|
331
458
|
try:
|
|
332
459
|
email_data = service.users().messages().get(userId="me", id=msg["id"]).execute()
|
|
333
|
-
email_details =
|
|
460
|
+
email_details = parse_plain_text_email(email_data)
|
|
334
461
|
if email_details:
|
|
335
462
|
emails.append(email_details)
|
|
336
463
|
except Exception as e:
|
|
337
|
-
|
|
338
|
-
|
|
464
|
+
raise GmailToolError(
|
|
465
|
+
message=f"Error reading email {msg['id']}.", developer_message=str(e)
|
|
466
|
+
)
|
|
339
467
|
return {"emails": emails}
|
|
340
468
|
|
|
341
469
|
|
|
@@ -347,22 +475,22 @@ async def list_emails(
|
|
|
347
475
|
async def search_threads(
|
|
348
476
|
context: ToolContext,
|
|
349
477
|
page_token: Annotated[
|
|
350
|
-
|
|
478
|
+
str | None, "Page token to retrieve a specific page of results in the list"
|
|
351
479
|
] = None,
|
|
352
480
|
max_results: Annotated[int, "The maximum number of threads to return"] = 10,
|
|
353
481
|
include_spam_trash: Annotated[bool, "Whether to include spam and trash in the results"] = False,
|
|
354
|
-
label_ids: Annotated[
|
|
355
|
-
sender: Annotated[
|
|
356
|
-
recipient: Annotated[
|
|
357
|
-
subject: Annotated[
|
|
358
|
-
body: Annotated[
|
|
359
|
-
date_range: Annotated[
|
|
482
|
+
label_ids: Annotated[list[str] | None, "The IDs of labels to filter by"] = None,
|
|
483
|
+
sender: Annotated[str | None, "The name or email address of the sender of the email"] = None,
|
|
484
|
+
recipient: Annotated[str | None, "The name or email address of the recipient"] = None,
|
|
485
|
+
subject: Annotated[str | None, "Words to find in the subject of the email"] = None,
|
|
486
|
+
body: Annotated[str | None, "Words to find in the body of the email"] = None,
|
|
487
|
+
date_range: Annotated[DateRange | None, "The date range of the email"] = None,
|
|
360
488
|
) -> Annotated[dict, "A dictionary containing a list of thread details"]:
|
|
361
489
|
"""Search for threads in the user's mailbox"""
|
|
362
|
-
service =
|
|
490
|
+
service = _build_gmail_service(context)
|
|
363
491
|
|
|
364
492
|
query = (
|
|
365
|
-
|
|
493
|
+
build_gmail_query_string(sender, recipient, subject, body, date_range)
|
|
366
494
|
if any([sender, recipient, subject, body, date_range])
|
|
367
495
|
else None
|
|
368
496
|
)
|
|
@@ -377,7 +505,7 @@ async def search_threads(
|
|
|
377
505
|
}
|
|
378
506
|
params = remove_none_values(params)
|
|
379
507
|
|
|
380
|
-
threads = []
|
|
508
|
+
threads: list[dict[str, Any]] = []
|
|
381
509
|
next_page_token = None
|
|
382
510
|
# Paginate through thread pages until we have the desired number of threads
|
|
383
511
|
while len(threads) < max_results:
|
|
@@ -407,13 +535,16 @@ async def search_threads(
|
|
|
407
535
|
async def list_threads(
|
|
408
536
|
context: ToolContext,
|
|
409
537
|
page_token: Annotated[
|
|
410
|
-
|
|
538
|
+
str | None, "Page token to retrieve a specific page of results in the list"
|
|
411
539
|
] = None,
|
|
412
540
|
max_results: Annotated[int, "The maximum number of threads to return"] = 10,
|
|
413
541
|
include_spam_trash: Annotated[bool, "Whether to include spam and trash in the results"] = False,
|
|
414
542
|
) -> Annotated[dict, "A dictionary containing a list of thread details"]:
|
|
415
543
|
"""List threads in the user's mailbox."""
|
|
416
|
-
|
|
544
|
+
threads: dict[str, Any] = await search_threads(
|
|
545
|
+
context, page_token, max_results, include_spam_trash
|
|
546
|
+
)
|
|
547
|
+
return threads
|
|
417
548
|
|
|
418
549
|
|
|
419
550
|
@tool(
|
|
@@ -424,21 +555,110 @@ async def list_threads(
|
|
|
424
555
|
async def get_thread(
|
|
425
556
|
context: ToolContext,
|
|
426
557
|
thread_id: Annotated[str, "The ID of the thread to retrieve"],
|
|
427
|
-
metadata_headers: Annotated[
|
|
428
|
-
Optional[list[str]], "When given and format is METADATA, only include headers specified."
|
|
429
|
-
] = None,
|
|
430
558
|
) -> Annotated[dict, "A dictionary containing the thread details"]:
|
|
431
559
|
"""Get the specified thread by ID."""
|
|
432
560
|
params = {
|
|
433
561
|
"userId": "me",
|
|
434
562
|
"id": thread_id,
|
|
435
563
|
"format": "full",
|
|
436
|
-
"metadataHeaders": metadata_headers,
|
|
437
564
|
}
|
|
438
565
|
params = remove_none_values(params)
|
|
439
566
|
|
|
440
|
-
service =
|
|
567
|
+
service = _build_gmail_service(context)
|
|
568
|
+
|
|
441
569
|
thread = service.users().threads().get(**params).execute()
|
|
442
|
-
thread["messages"] = [
|
|
570
|
+
thread["messages"] = [parse_plain_text_email(message) for message in thread.get("messages", [])]
|
|
571
|
+
|
|
572
|
+
return dict(thread)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@tool(
|
|
576
|
+
requires_auth=Google(
|
|
577
|
+
scopes=["https://www.googleapis.com/auth/gmail.modify"],
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
async def change_email_labels(
|
|
581
|
+
context: ToolContext,
|
|
582
|
+
email_id: Annotated[str, "The ID of the email to modify labels for"],
|
|
583
|
+
labels_to_add: Annotated[list[str], "List of label names to add"],
|
|
584
|
+
labels_to_remove: Annotated[list[str], "List of label names to remove"],
|
|
585
|
+
) -> Annotated[dict, "List of labels that were added, removed, and not found"]:
|
|
586
|
+
"""
|
|
587
|
+
Add and remove labels from an email using the Gmail API.
|
|
588
|
+
"""
|
|
589
|
+
service = _build_gmail_service(context)
|
|
590
|
+
|
|
591
|
+
add_labels = get_label_ids(service, labels_to_add)
|
|
592
|
+
remove_labels = get_label_ids(service, labels_to_remove)
|
|
593
|
+
|
|
594
|
+
invalid_labels = (
|
|
595
|
+
set(labels_to_add + labels_to_remove) - set(add_labels.keys()) - set(remove_labels.keys())
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
if invalid_labels:
|
|
599
|
+
# prepare the list of valid labels
|
|
600
|
+
labels = service.users().labels().list(userId="me").execute().get("labels", [])
|
|
601
|
+
label_names = [label["name"] for label in labels]
|
|
602
|
+
|
|
603
|
+
# raise a retryable error with the list of valid labels
|
|
604
|
+
raise RetryableToolError(
|
|
605
|
+
message=f"Invalid labels: {invalid_labels}",
|
|
606
|
+
developer_message=f"Invalid labels: {invalid_labels}",
|
|
607
|
+
additional_prompt_content=f"List of valid labels: {label_names}",
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Prepare the modification body with label IDs.
|
|
611
|
+
body = {
|
|
612
|
+
"addLabelIds": list(add_labels.values()),
|
|
613
|
+
"removeLabelIds": list(remove_labels.values()),
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try: # Modify the email labels.
|
|
617
|
+
service.users().messages().modify(userId="me", id=email_id, body=body).execute()
|
|
618
|
+
|
|
619
|
+
except Exception as e:
|
|
620
|
+
raise GmailToolError(
|
|
621
|
+
message=f"Error modifying labels for email {email_id}", developer_message=str(e)
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Confirmation JSON with lists for added and removed labels.
|
|
625
|
+
confirmation = {
|
|
626
|
+
"addedLabels": list(add_labels.keys()),
|
|
627
|
+
"removedLabels": list(remove_labels.keys()),
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return {"confirmation": dict(confirmation)}
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@tool(
|
|
634
|
+
requires_auth=Google(
|
|
635
|
+
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
async def list_labels(
|
|
639
|
+
context: ToolContext,
|
|
640
|
+
) -> Annotated[dict, "A dictionary containing a list of label details"]:
|
|
641
|
+
"""List all the labels in the user's mailbox."""
|
|
642
|
+
|
|
643
|
+
service = _build_gmail_service(context)
|
|
644
|
+
|
|
645
|
+
labels = service.users().labels().list(userId="me").execute().get("labels", [])
|
|
646
|
+
|
|
647
|
+
return {"labels": labels}
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
@tool(
|
|
651
|
+
requires_auth=Google(
|
|
652
|
+
scopes=["https://www.googleapis.com/auth/gmail.labels"],
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
async def create_label(
|
|
656
|
+
context: ToolContext,
|
|
657
|
+
label_name: Annotated[str, "The name of the label to create"],
|
|
658
|
+
) -> Annotated[dict, "The details of the created label"]:
|
|
659
|
+
"""Create a new label in the user's mailbox."""
|
|
660
|
+
|
|
661
|
+
service = _build_gmail_service(context)
|
|
662
|
+
label = service.users().labels().create(userId="me", body={"name": label_name}).execute()
|
|
443
663
|
|
|
444
|
-
return
|
|
664
|
+
return {"label": label}
|