dhisana 0.0.1.dev207__py3-none-any.whl → 0.0.1.dev209__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.
- dhisana/utils/email_provider.py +48 -13
- dhisana/utils/google_oauth_tools.py +455 -0
- {dhisana-0.0.1.dev207.dist-info → dhisana-0.0.1.dev209.dist-info}/METADATA +1 -1
- {dhisana-0.0.1.dev207.dist-info → dhisana-0.0.1.dev209.dist-info}/RECORD +7 -6
- {dhisana-0.0.1.dev207.dist-info → dhisana-0.0.1.dev209.dist-info}/WHEEL +0 -0
- {dhisana-0.0.1.dev207.dist-info → dhisana-0.0.1.dev209.dist-info}/entry_points.txt +0 -0
- {dhisana-0.0.1.dev207.dist-info → dhisana-0.0.1.dev209.dist-info}/top_level.txt +0 -0
dhisana/utils/email_provider.py
CHANGED
|
@@ -17,6 +17,11 @@ from dhisana.utils.google_workspace_tools import (
|
|
|
17
17
|
list_emails_in_time_range_async,
|
|
18
18
|
reply_to_email_async as gw_reply_to_email_async,
|
|
19
19
|
)
|
|
20
|
+
from dhisana.utils.google_oauth_tools import (
|
|
21
|
+
send_email_using_google_oauth_async,
|
|
22
|
+
list_emails_in_time_range_google_oauth_async,
|
|
23
|
+
reply_to_email_google_oauth_async,
|
|
24
|
+
)
|
|
20
25
|
from dhisana.utils.microsoft365_tools import (
|
|
21
26
|
send_email_using_microsoft_graph_async,
|
|
22
27
|
list_emails_in_time_range_m365_async,
|
|
@@ -105,7 +110,14 @@ async def send_email_async(
|
|
|
105
110
|
send_email_context: SendEmailContext,
|
|
106
111
|
tool_config: Optional[List[Dict]] = None,
|
|
107
112
|
*,
|
|
108
|
-
provider_order: Sequence[str] = (
|
|
113
|
+
provider_order: Sequence[str] = (
|
|
114
|
+
"mailgun",
|
|
115
|
+
"sendgrid",
|
|
116
|
+
"google", # Google OAuth (per-user token)
|
|
117
|
+
"smtpEmail",
|
|
118
|
+
"googleworkspace", # Google Workspace service account (DWD)
|
|
119
|
+
"microsoft365",
|
|
120
|
+
),
|
|
109
121
|
):
|
|
110
122
|
"""
|
|
111
123
|
Send an e-mail using the first *configured* provider in *provider_order*.
|
|
@@ -154,15 +166,12 @@ async def send_email_async(
|
|
|
154
166
|
continue
|
|
155
167
|
return await send_email_using_sendgrid_async(send_email_context, tool_config)
|
|
156
168
|
|
|
157
|
-
# 1d)
|
|
158
|
-
elif provider == "
|
|
159
|
-
|
|
160
|
-
if not
|
|
169
|
+
# 1d) Google (Gmail API via per-user OAuth)
|
|
170
|
+
elif provider == "google":
|
|
171
|
+
g_cfg = _find_provider_cfg(tool_config, "google")
|
|
172
|
+
if not g_cfg:
|
|
161
173
|
continue
|
|
162
|
-
|
|
163
|
-
return await send_email_using_microsoft_graph_async(
|
|
164
|
-
send_email_context, tool_config
|
|
165
|
-
)
|
|
174
|
+
return await send_email_using_google_oauth_async(send_email_context, tool_config)
|
|
166
175
|
|
|
167
176
|
# 1e) Google Workspace
|
|
168
177
|
elif provider == "googleworkspace":
|
|
@@ -174,6 +183,16 @@ async def send_email_async(
|
|
|
174
183
|
send_email_context, tool_config
|
|
175
184
|
)
|
|
176
185
|
|
|
186
|
+
# 1f) Microsoft 365 (Graph API)
|
|
187
|
+
elif provider == "microsoft365":
|
|
188
|
+
ms_cfg = _find_provider_cfg(tool_config, "microsoft365")
|
|
189
|
+
if not ms_cfg:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
return await send_email_using_microsoft_graph_async(
|
|
193
|
+
send_email_context, tool_config
|
|
194
|
+
)
|
|
195
|
+
|
|
177
196
|
# -- future providers slot --------------------------------------
|
|
178
197
|
|
|
179
198
|
# ------------------------------------------------------------------ #
|
|
@@ -230,7 +249,7 @@ async def list_emails_async(
|
|
|
230
249
|
query_email_context: QueryEmailContext,
|
|
231
250
|
tool_config: Optional[List[Dict]] = None,
|
|
232
251
|
*,
|
|
233
|
-
provider_order: Sequence[str] = ("smtpEmail", "googleworkspace", "microsoft365"),
|
|
252
|
+
provider_order: Sequence[str] = ("google", "smtpEmail", "googleworkspace", "microsoft365"),
|
|
234
253
|
) -> List[MessageItem]:
|
|
235
254
|
"""
|
|
236
255
|
List e-mails (see ``QueryEmailContext``) using the first configured provider.
|
|
@@ -255,6 +274,12 @@ async def list_emails_async(
|
|
|
255
274
|
password=creds["password"],
|
|
256
275
|
)
|
|
257
276
|
|
|
277
|
+
elif provider == "google":
|
|
278
|
+
g_cfg = _find_provider_cfg(tool_config, "google")
|
|
279
|
+
if not g_cfg:
|
|
280
|
+
continue
|
|
281
|
+
return await list_emails_in_time_range_google_oauth_async(query_email_context, tool_config)
|
|
282
|
+
|
|
258
283
|
elif provider == "googleworkspace":
|
|
259
284
|
gw_cfg = _find_provider_cfg(tool_config, "googleworkspace")
|
|
260
285
|
if not gw_cfg:
|
|
@@ -279,7 +304,7 @@ async def reply_email_async(
|
|
|
279
304
|
reply_email_context: ReplyEmailContext,
|
|
280
305
|
tool_config: Optional[List[Dict]] = None,
|
|
281
306
|
*,
|
|
282
|
-
provider_order: Sequence[str] = ("smtpEmail", "googleworkspace", "microsoft365"),
|
|
307
|
+
provider_order: Sequence[str] = ("google", "smtpEmail", "googleworkspace", "microsoft365"),
|
|
283
308
|
) -> Dict[str, Any]:
|
|
284
309
|
"""
|
|
285
310
|
Reply (reply-all) to an e-mail using the first *configured* provider
|
|
@@ -312,7 +337,17 @@ async def reply_email_async(
|
|
|
312
337
|
)
|
|
313
338
|
|
|
314
339
|
# ------------------------------------------------------------------
|
|
315
|
-
# 2) Google
|
|
340
|
+
# 2) Google OAuth (per-user)
|
|
341
|
+
# ------------------------------------------------------------------
|
|
342
|
+
elif provider == "google":
|
|
343
|
+
g_cfg = _find_provider_cfg(tool_config, "google")
|
|
344
|
+
if not g_cfg:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
return await reply_to_email_google_oauth_async(reply_email_context, tool_config)
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
# 3) Google Workspace service-account
|
|
316
351
|
# ------------------------------------------------------------------
|
|
317
352
|
elif provider == "googleworkspace":
|
|
318
353
|
gw_cfg = _find_provider_cfg(tool_config, "googleworkspace")
|
|
@@ -322,7 +357,7 @@ async def reply_email_async(
|
|
|
322
357
|
return await gw_reply_to_email_async(reply_email_context, tool_config)
|
|
323
358
|
|
|
324
359
|
# ------------------------------------------------------------------
|
|
325
|
-
#
|
|
360
|
+
# 4) Microsoft 365 (Graph)
|
|
326
361
|
# ------------------------------------------------------------------
|
|
327
362
|
elif provider == "microsoft365":
|
|
328
363
|
ms_cfg = _find_provider_cfg(tool_config, "microsoft365")
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from email.mime.text import MIMEText
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from dhisana.schemas.common import (
|
|
11
|
+
SendEmailContext,
|
|
12
|
+
QueryEmailContext,
|
|
13
|
+
ReplyEmailContext,
|
|
14
|
+
)
|
|
15
|
+
from dhisana.schemas.sales import MessageItem
|
|
16
|
+
from dhisana.utils.email_parse_helpers import (
|
|
17
|
+
find_header,
|
|
18
|
+
parse_single_address,
|
|
19
|
+
find_all_recipients_in_headers,
|
|
20
|
+
convert_date_to_iso,
|
|
21
|
+
extract_email_body_in_plain_text,
|
|
22
|
+
)
|
|
23
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
24
|
+
from dhisana.utils.cache_output_tools import retrieve_output, cache_output
|
|
25
|
+
from typing import Optional as _Optional # avoid name clash in wrappers
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_google_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Retrieve a Google OAuth2 access token from the 'google' integration config.
|
|
31
|
+
|
|
32
|
+
Expected tool_config shape:
|
|
33
|
+
{
|
|
34
|
+
"name": "google",
|
|
35
|
+
"configuration": [
|
|
36
|
+
{"name": "oauth_tokens", "value": {"access_token": "..."} }
|
|
37
|
+
# or {"name": "access_token", "value": "..."}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
If provided as a JSON string under oauth_tokens, it is parsed.
|
|
42
|
+
"""
|
|
43
|
+
access_token: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
if tool_config:
|
|
46
|
+
g_cfg = next((c for c in tool_config if c.get("name") == "google"), None)
|
|
47
|
+
if g_cfg:
|
|
48
|
+
cfg_map = {f["name"]: f.get("value") for f in g_cfg.get("configuration", []) if f}
|
|
49
|
+
raw_oauth = cfg_map.get("oauth_tokens")
|
|
50
|
+
# oauth_tokens might be a JSON string or a dict
|
|
51
|
+
if isinstance(raw_oauth, str):
|
|
52
|
+
try:
|
|
53
|
+
raw_oauth = json.loads(raw_oauth)
|
|
54
|
+
except Exception:
|
|
55
|
+
raw_oauth = None
|
|
56
|
+
if isinstance(raw_oauth, dict):
|
|
57
|
+
access_token = raw_oauth.get("access_token") or raw_oauth.get("token")
|
|
58
|
+
if not access_token:
|
|
59
|
+
access_token = cfg_map.get("access_token")
|
|
60
|
+
|
|
61
|
+
if not access_token:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"Google integration is not configured. Please connect Google and supply an OAuth access token."
|
|
64
|
+
)
|
|
65
|
+
return access_token
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def send_email_using_google_oauth_async(
|
|
69
|
+
send_email_context: SendEmailContext,
|
|
70
|
+
tool_config: Optional[List[Dict]] = None,
|
|
71
|
+
) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Send an email using Gmail API with a per-user OAuth2 token.
|
|
74
|
+
|
|
75
|
+
Returns the Gmail message id of the sent message when available.
|
|
76
|
+
"""
|
|
77
|
+
token = get_google_access_token(tool_config)
|
|
78
|
+
|
|
79
|
+
message = MIMEText(send_email_context.body, _subtype="html")
|
|
80
|
+
message["to"] = send_email_context.recipient
|
|
81
|
+
message["from"] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
82
|
+
message["subject"] = send_email_context.subject
|
|
83
|
+
|
|
84
|
+
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
|
85
|
+
|
|
86
|
+
payload: Dict[str, Any] = {"raw": raw_message}
|
|
87
|
+
if send_email_context.labels:
|
|
88
|
+
payload["labelIds"] = send_email_context.labels
|
|
89
|
+
|
|
90
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
91
|
+
url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
|
|
92
|
+
|
|
93
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
94
|
+
resp = await client.post(url, headers=headers, json=payload)
|
|
95
|
+
resp.raise_for_status()
|
|
96
|
+
data = resp.json() or {}
|
|
97
|
+
return data.get("id", "")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def list_emails_in_time_range_google_oauth_async(
|
|
101
|
+
context: QueryEmailContext,
|
|
102
|
+
tool_config: Optional[List[Dict]] = None,
|
|
103
|
+
) -> List[MessageItem]:
|
|
104
|
+
"""
|
|
105
|
+
List Gmail messages for the connected user in a time range using OAuth2.
|
|
106
|
+
Returns a list of MessageItem.
|
|
107
|
+
"""
|
|
108
|
+
if context.labels is None:
|
|
109
|
+
context.labels = []
|
|
110
|
+
|
|
111
|
+
token = get_google_access_token(tool_config)
|
|
112
|
+
base_url = "https://gmail.googleapis.com/gmail/v1/users/me/messages"
|
|
113
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
114
|
+
|
|
115
|
+
# Convert RFC3339 times to unix timestamps for Gmail search query
|
|
116
|
+
# Expecting context.start_time and context.end_time as ISO 8601; Gmail q uses epoch seconds
|
|
117
|
+
from datetime import datetime
|
|
118
|
+
start_dt = datetime.fromisoformat(context.start_time.replace("Z", "+00:00"))
|
|
119
|
+
end_dt = datetime.fromisoformat(context.end_time.replace("Z", "+00:00"))
|
|
120
|
+
after_ts = int(start_dt.timestamp())
|
|
121
|
+
before_ts = int(end_dt.timestamp())
|
|
122
|
+
|
|
123
|
+
q_parts: List[str] = [f"after:{after_ts}", f"before:{before_ts}"]
|
|
124
|
+
if context.unread_only:
|
|
125
|
+
q_parts.append("is:unread")
|
|
126
|
+
if context.labels:
|
|
127
|
+
q_parts.extend([f"label:{lbl}" for lbl in context.labels])
|
|
128
|
+
query = " ".join(q_parts)
|
|
129
|
+
|
|
130
|
+
params = {"q": query}
|
|
131
|
+
|
|
132
|
+
items: List[MessageItem] = []
|
|
133
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
134
|
+
list_resp = await client.get(base_url, headers=headers, params=params)
|
|
135
|
+
list_resp.raise_for_status()
|
|
136
|
+
list_data = list_resp.json() or {}
|
|
137
|
+
for m in list_data.get("messages", []) or []:
|
|
138
|
+
mid = m.get("id")
|
|
139
|
+
tid = m.get("threadId")
|
|
140
|
+
if not mid:
|
|
141
|
+
continue
|
|
142
|
+
get_url = f"{base_url}/{mid}"
|
|
143
|
+
get_resp = await client.get(get_url, headers=headers)
|
|
144
|
+
get_resp.raise_for_status()
|
|
145
|
+
mdata = get_resp.json() or {}
|
|
146
|
+
|
|
147
|
+
headers_list = (mdata.get("payload") or {}).get("headers", [])
|
|
148
|
+
from_header = find_header(headers_list, "From") or ""
|
|
149
|
+
subject_header = find_header(headers_list, "Subject") or ""
|
|
150
|
+
date_header = find_header(headers_list, "Date") or ""
|
|
151
|
+
|
|
152
|
+
iso_dt = convert_date_to_iso(date_header)
|
|
153
|
+
s_name, s_email = parse_single_address(from_header)
|
|
154
|
+
r_name, r_email = find_all_recipients_in_headers(headers_list)
|
|
155
|
+
|
|
156
|
+
items.append(
|
|
157
|
+
MessageItem(
|
|
158
|
+
message_id=mdata.get("id", ""),
|
|
159
|
+
thread_id=tid or "",
|
|
160
|
+
sender_name=s_name,
|
|
161
|
+
sender_email=s_email,
|
|
162
|
+
receiver_name=r_name,
|
|
163
|
+
receiver_email=r_email,
|
|
164
|
+
iso_datetime=iso_dt,
|
|
165
|
+
subject=subject_header,
|
|
166
|
+
body=extract_email_body_in_plain_text(mdata),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return items
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def reply_to_email_google_oauth_async(
|
|
174
|
+
reply_email_context: ReplyEmailContext,
|
|
175
|
+
tool_config: Optional[List[Dict]] = None,
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
"""
|
|
178
|
+
Reply-all to a Gmail message for the connected user using OAuth2.
|
|
179
|
+
Returns a metadata dictionary similar to other providers.
|
|
180
|
+
"""
|
|
181
|
+
if reply_email_context.add_labels is None:
|
|
182
|
+
reply_email_context.add_labels = []
|
|
183
|
+
|
|
184
|
+
token = get_google_access_token(tool_config)
|
|
185
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
186
|
+
base = "https://gmail.googleapis.com/gmail/v1/users/me"
|
|
187
|
+
|
|
188
|
+
# 1) Fetch original message
|
|
189
|
+
get_url = f"{base}/messages/{reply_email_context.message_id}"
|
|
190
|
+
params = {"format": "full"}
|
|
191
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
192
|
+
get_resp = await client.get(get_url, headers=headers, params=params)
|
|
193
|
+
get_resp.raise_for_status()
|
|
194
|
+
original = get_resp.json() or {}
|
|
195
|
+
|
|
196
|
+
headers_list = (original.get("payload") or {}).get("headers", [])
|
|
197
|
+
headers_map = {h.get("name"): h.get("value") for h in headers_list if isinstance(h, dict)}
|
|
198
|
+
thread_id = original.get("threadId")
|
|
199
|
+
|
|
200
|
+
subject = headers_map.get("Subject", "") or ""
|
|
201
|
+
if not subject.startswith("Re:"):
|
|
202
|
+
subject = f"Re: {subject}"
|
|
203
|
+
to_addresses = headers_map.get("From", "") or ""
|
|
204
|
+
cc_addresses = headers_map.get("Cc", "") or ""
|
|
205
|
+
message_id_header = headers_map.get("Message-ID", "") or ""
|
|
206
|
+
|
|
207
|
+
# 2) Build reply MIME
|
|
208
|
+
msg = MIMEText(reply_email_context.reply_body, _subtype="html")
|
|
209
|
+
msg["To"] = to_addresses
|
|
210
|
+
if cc_addresses:
|
|
211
|
+
msg["Cc"] = cc_addresses
|
|
212
|
+
msg["From"] = f"{reply_email_context.sender_name} <{reply_email_context.sender_email}>"
|
|
213
|
+
msg["Subject"] = subject
|
|
214
|
+
if message_id_header:
|
|
215
|
+
msg["In-Reply-To"] = message_id_header
|
|
216
|
+
msg["References"] = message_id_header
|
|
217
|
+
|
|
218
|
+
raw_message = base64.urlsafe_b64encode(msg.as_bytes()).decode()
|
|
219
|
+
payload = {"raw": raw_message}
|
|
220
|
+
if thread_id:
|
|
221
|
+
payload["threadId"] = thread_id
|
|
222
|
+
|
|
223
|
+
# 3) Send the reply
|
|
224
|
+
send_url = f"{base}/messages/send"
|
|
225
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
226
|
+
send_resp = await client.post(send_url, headers=headers, json=payload)
|
|
227
|
+
send_resp.raise_for_status()
|
|
228
|
+
sent = send_resp.json() or {}
|
|
229
|
+
|
|
230
|
+
# 4) Optional: mark as read
|
|
231
|
+
if str(reply_email_context.mark_as_read).lower() == "true" and thread_id:
|
|
232
|
+
modify_url = f"{base}/threads/{thread_id}/modify"
|
|
233
|
+
modify_payload = {"removeLabelIds": ["UNREAD"]}
|
|
234
|
+
try:
|
|
235
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
236
|
+
await client.post(modify_url, headers=headers, json=modify_payload)
|
|
237
|
+
except Exception:
|
|
238
|
+
logging.exception("Gmail: failed to mark thread as read (best-effort)")
|
|
239
|
+
|
|
240
|
+
# 5) Optional: add labels
|
|
241
|
+
if reply_email_context.add_labels and thread_id:
|
|
242
|
+
modify_url = f"{base}/threads/{thread_id}/modify"
|
|
243
|
+
modify_payload = {"addLabelIds": reply_email_context.add_labels}
|
|
244
|
+
try:
|
|
245
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
246
|
+
await client.post(modify_url, headers=headers, json=modify_payload)
|
|
247
|
+
except Exception:
|
|
248
|
+
logging.exception("Gmail: failed to add labels to thread (best-effort)")
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"mailbox_email_id": sent.get("id"),
|
|
252
|
+
"message_id": (sent.get("threadId") or thread_id or ""),
|
|
253
|
+
"email_subject": subject,
|
|
254
|
+
"email_sender": reply_email_context.sender_email,
|
|
255
|
+
"email_recipients": [to_addresses] + ([cc_addresses] if cc_addresses else []),
|
|
256
|
+
"read_email_status": "READ" if str(reply_email_context.mark_as_read).lower() == "true" else "UNREAD",
|
|
257
|
+
"email_labels": sent.get("labelIds", []),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Google Calendar (OAuth per-user)
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
@assistant_tool
|
|
266
|
+
async def get_calendar_events_using_google_oauth_async(
|
|
267
|
+
start_date: str,
|
|
268
|
+
end_date: str,
|
|
269
|
+
tool_config: Optional[List[Dict]] = None,
|
|
270
|
+
) -> List[Dict[str, Any]]:
|
|
271
|
+
"""
|
|
272
|
+
Retrieve events from the user's primary Google Calendar using a per-user OAuth token.
|
|
273
|
+
|
|
274
|
+
start_date, end_date: 'YYYY-MM-DD' strings (inclusive start, inclusive end day as 23:59:59Z).
|
|
275
|
+
Returns a list of event dicts from the Calendar API.
|
|
276
|
+
"""
|
|
277
|
+
token = get_google_access_token(tool_config)
|
|
278
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
279
|
+
url = "https://www.googleapis.com/calendar/v3/calendars/primary/events"
|
|
280
|
+
|
|
281
|
+
time_min = f"{start_date}T00:00:00Z"
|
|
282
|
+
time_max = f"{end_date}T23:59:59Z"
|
|
283
|
+
params = {
|
|
284
|
+
"timeMin": time_min,
|
|
285
|
+
"timeMax": time_max,
|
|
286
|
+
"maxResults": 10,
|
|
287
|
+
"singleEvents": True,
|
|
288
|
+
"orderBy": "startTime",
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
292
|
+
resp = await client.get(url, headers=headers, params=params)
|
|
293
|
+
resp.raise_for_status()
|
|
294
|
+
data = resp.json() or {}
|
|
295
|
+
events = data.get("items", [])
|
|
296
|
+
if not events:
|
|
297
|
+
logging.info("No upcoming events found within the specified range (OAuth).")
|
|
298
|
+
return events
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Google Sheets and Docs (OAuth per-user)
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
def _get_sheet_id_from_url(sheet_url: str) -> str:
|
|
306
|
+
match = re.search(r"/d/([a-zA-Z0-9-_]+)/", sheet_url)
|
|
307
|
+
if not match:
|
|
308
|
+
raise ValueError("Could not extract spreadsheet ID from the provided URL.")
|
|
309
|
+
return match.group(1)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _get_document_id_from_url(doc_url: str) -> str:
|
|
313
|
+
match = re.search(r"/d/([a-zA-Z0-9-_]+)/", doc_url)
|
|
314
|
+
if not match:
|
|
315
|
+
raise ValueError("Could not extract document ID from the provided URL.")
|
|
316
|
+
return match.group(1)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@assistant_tool
|
|
320
|
+
async def read_google_sheet_using_google_oauth(
|
|
321
|
+
sheet_url: str,
|
|
322
|
+
range_name: str,
|
|
323
|
+
tool_config: Optional[List[Dict]] = None,
|
|
324
|
+
) -> List[List[str]]:
|
|
325
|
+
"""
|
|
326
|
+
Read data from a Google Sheet using the connected user's OAuth token.
|
|
327
|
+
|
|
328
|
+
If range_name is empty, reads the first sheet tab by fetching spreadsheet metadata.
|
|
329
|
+
"""
|
|
330
|
+
token = get_google_access_token(tool_config)
|
|
331
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
332
|
+
|
|
333
|
+
spreadsheet_id = _get_sheet_id_from_url(sheet_url)
|
|
334
|
+
|
|
335
|
+
# Default range to first sheet title if not supplied
|
|
336
|
+
if not range_name:
|
|
337
|
+
meta_url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}"
|
|
338
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
339
|
+
meta_resp = await client.get(meta_url, headers=headers)
|
|
340
|
+
meta_resp.raise_for_status()
|
|
341
|
+
meta = meta_resp.json() or {}
|
|
342
|
+
sheets = meta.get("sheets", [])
|
|
343
|
+
if not sheets:
|
|
344
|
+
return []
|
|
345
|
+
range_name = (sheets[0].get("properties") or {}).get("title") or "Sheet1"
|
|
346
|
+
|
|
347
|
+
values_url = f"https://sheets.googleapis.com/v4/spreadsheets/{spreadsheet_id}/values/{range_name}"
|
|
348
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
349
|
+
val_resp = await client.get(values_url, headers=headers)
|
|
350
|
+
val_resp.raise_for_status()
|
|
351
|
+
data = val_resp.json() or {}
|
|
352
|
+
return data.get("values", [])
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@assistant_tool
|
|
356
|
+
async def read_google_document_using_google_oauth(
|
|
357
|
+
doc_url: str,
|
|
358
|
+
tool_config: Optional[List[Dict]] = None,
|
|
359
|
+
) -> str:
|
|
360
|
+
"""
|
|
361
|
+
Read text content from a Google Doc using the connected user's OAuth token.
|
|
362
|
+
Concatenates all text runs in the document body.
|
|
363
|
+
"""
|
|
364
|
+
token = get_google_access_token(tool_config)
|
|
365
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
366
|
+
|
|
367
|
+
document_id = _get_document_id_from_url(doc_url)
|
|
368
|
+
url = f"https://docs.googleapis.com/v1/documents/{document_id}"
|
|
369
|
+
|
|
370
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
371
|
+
resp = await client.get(url, headers=headers)
|
|
372
|
+
resp.raise_for_status()
|
|
373
|
+
doc = resp.json() or {}
|
|
374
|
+
|
|
375
|
+
content = (doc.get("body") or {}).get("content", [])
|
|
376
|
+
parts: List[str] = []
|
|
377
|
+
for element in content:
|
|
378
|
+
paragraph = element.get("paragraph")
|
|
379
|
+
if not paragraph:
|
|
380
|
+
continue
|
|
381
|
+
for elem in paragraph.get("elements", []) or []:
|
|
382
|
+
text_run = elem.get("textRun")
|
|
383
|
+
if text_run:
|
|
384
|
+
parts.append(text_run.get("content", ""))
|
|
385
|
+
|
|
386
|
+
return "".join(parts)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@assistant_tool
|
|
390
|
+
async def search_google_places_using_google_oauth(
|
|
391
|
+
query: str,
|
|
392
|
+
location_bias: dict = None,
|
|
393
|
+
number_of_results: int = 3,
|
|
394
|
+
tool_config: _Optional[List[Dict]] = None,
|
|
395
|
+
) -> List[str]:
|
|
396
|
+
"""
|
|
397
|
+
Search Google Places (New) with a per-user OAuth token.
|
|
398
|
+
|
|
399
|
+
- Requires that the OAuth token has Maps/Places access enabled for the project.
|
|
400
|
+
- Returns a list of JSON strings, each being a place object.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
token = get_google_access_token(tool_config)
|
|
405
|
+
url = "https://places.googleapis.com/v1/places:searchText"
|
|
406
|
+
headers = {
|
|
407
|
+
"Content-Type": "application/json",
|
|
408
|
+
"Authorization": f"Bearer {token}",
|
|
409
|
+
# Field mask is required to limit returned fields
|
|
410
|
+
"X-Goog-FieldMask": (
|
|
411
|
+
"places.displayName,places.formattedAddress,places.location,"
|
|
412
|
+
"places.websiteUri,places.rating,places.reviews"
|
|
413
|
+
),
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
body: Dict[str, Any] = {"textQuery": query}
|
|
417
|
+
if location_bias:
|
|
418
|
+
body["locationBias"] = {
|
|
419
|
+
"circle": {
|
|
420
|
+
"center": {
|
|
421
|
+
"latitude": location_bias.get("latitude"),
|
|
422
|
+
"longitude": location_bias.get("longitude"),
|
|
423
|
+
},
|
|
424
|
+
"radius": location_bias.get("radius", 5000),
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# Cache key based on query, count and bias
|
|
429
|
+
bias_str = json.dumps(location_bias, sort_keys=True) if location_bias else "None"
|
|
430
|
+
cache_key = f"oauth_places:{query}:{number_of_results}:{bias_str}"
|
|
431
|
+
cached = retrieve_output("search_google_places_oauth", cache_key)
|
|
432
|
+
if cached is not None:
|
|
433
|
+
return cached
|
|
434
|
+
|
|
435
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
436
|
+
try:
|
|
437
|
+
resp = await client.post(url, headers=headers, json=body)
|
|
438
|
+
if resp.status_code == 429:
|
|
439
|
+
return [json.dumps({"error": "Rate limit exceeded (429)"})]
|
|
440
|
+
resp.raise_for_status()
|
|
441
|
+
data = resp.json() or {}
|
|
442
|
+
places = (data.get("places") or [])[: max(0, int(number_of_results))]
|
|
443
|
+
out = [json.dumps(p) for p in places]
|
|
444
|
+
cache_output("search_google_places_oauth", cache_key, out)
|
|
445
|
+
return out
|
|
446
|
+
except httpx.HTTPStatusError as exc:
|
|
447
|
+
try:
|
|
448
|
+
err_json = exc.response.json()
|
|
449
|
+
except Exception:
|
|
450
|
+
err_json = {"status": exc.response.status_code, "text": exc.response.text}
|
|
451
|
+
logging.warning(f"Places OAuth request failed: {err_json}")
|
|
452
|
+
return [json.dumps({"error": err_json})]
|
|
453
|
+
except Exception as e:
|
|
454
|
+
logging.exception("Places OAuth request failed")
|
|
455
|
+
return [json.dumps({"error": str(e)})]
|
|
@@ -30,7 +30,7 @@ dhisana/utils/composite_tools.py,sha256=ZlwHCp7PXjYFUWUEeR_fTF0Z4Wg-4F6eBi1reE3F
|
|
|
30
30
|
dhisana/utils/dataframe_tools.py,sha256=jxyvyXAMKxccST_W6o6FnBqAsvp7mGNOD6HV5V6xgeA,9242
|
|
31
31
|
dhisana/utils/domain_parser.py,sha256=Kw5MPP06wK2azWQzuSiOE-DffOezLqDyF-L9JEBsMSU,1206
|
|
32
32
|
dhisana/utils/email_parse_helpers.py,sha256=LIdm1B1IyGSW50y8EkxOk6YRjvxO2SJTgTKPLxYls_o,4613
|
|
33
|
-
dhisana/utils/email_provider.py,sha256=
|
|
33
|
+
dhisana/utils/email_provider.py,sha256=oYjk-9yIgRzcPoPwnYjPvChPn0J78T5p6CoqXFPI-zk,14264
|
|
34
34
|
dhisana/utils/enrich_lead_information.py,sha256=hZxSstIErqxXG40j5YyzTYTCSsTEf8YxRF25DU5-s2k,37302
|
|
35
35
|
dhisana/utils/extract_email_content_for_llm.py,sha256=SQmMZ3YJtm3ZI44XiWEVAItcAwrsSSy1QzDne7LTu_Q,3713
|
|
36
36
|
dhisana/utils/fetch_openai_config.py,sha256=LjWdFuUeTNeAW106pb7DLXZNElos2PlmXRe6bHZJ2hw,5159
|
|
@@ -45,6 +45,7 @@ dhisana/utils/generate_linkedin_connect_message.py,sha256=eL4RV2B1ByyMnjoGohdTA0
|
|
|
45
45
|
dhisana/utils/generate_linkedin_response_message.py,sha256=udAt4V_vNuieyyfhrtTFWA1CiBx63jfac9E35wunS5k,14404
|
|
46
46
|
dhisana/utils/generate_structured_output_internal.py,sha256=83SaThDAa_fANJEZ5CSCMcPpD_MN5zMI9NU1uEtQO2E,20705
|
|
47
47
|
dhisana/utils/google_custom_search.py,sha256=5rQ4uAF-hjFpd9ooJkd6CjRvSmhZHhqM0jfHItsbpzk,10071
|
|
48
|
+
dhisana/utils/google_oauth_tools.py,sha256=KoiNiC8iLh2CGslgQi5drHgjjO-lswLkZmkPfmtJL2U,17238
|
|
48
49
|
dhisana/utils/google_workspace_tools.py,sha256=22mmJ2KyJAx4Ewo_AK2qDLF5veYcB7sWcb00vw21qhM,44727
|
|
49
50
|
dhisana/utils/hubspot_clearbit.py,sha256=keNX1F_RnDl9AOPxYEOTMdukV_A9g8v9j1fZyT4tuP4,3440
|
|
50
51
|
dhisana/utils/hubspot_crm_tools.py,sha256=lbXFCeq690_TDLjDG8Gm5E-2f1P5EuDqNf5j8PYpMm8,99298
|
|
@@ -91,8 +92,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
|
|
|
91
92
|
dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
|
|
92
93
|
dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
|
|
93
94
|
dhisana/workflow/test.py,sha256=kwW8jWqSBNcRmoyaxlTuZCMOpGJpTbJQgHI7gSjwdzM,3399
|
|
94
|
-
dhisana-0.0.1.
|
|
95
|
-
dhisana-0.0.1.
|
|
96
|
-
dhisana-0.0.1.
|
|
97
|
-
dhisana-0.0.1.
|
|
98
|
-
dhisana-0.0.1.
|
|
95
|
+
dhisana-0.0.1.dev209.dist-info/METADATA,sha256=RLMggAC3XyqdtE58kUr7gC1pBpv2TEN_RGTXfOjRUh4,1190
|
|
96
|
+
dhisana-0.0.1.dev209.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
97
|
+
dhisana-0.0.1.dev209.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
|
|
98
|
+
dhisana-0.0.1.dev209.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
|
|
99
|
+
dhisana-0.0.1.dev209.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|