dhisana 0.0.1.dev116__py3-none-any.whl → 0.0.1.dev236__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/schemas/common.py +10 -1
- dhisana/schemas/sales.py +203 -22
- dhisana/utils/add_mapping.py +0 -2
- dhisana/utils/apollo_tools.py +739 -119
- dhisana/utils/built_with_api_tools.py +4 -2
- dhisana/utils/check_email_validity_tools.py +35 -18
- dhisana/utils/check_for_intent_signal.py +1 -2
- dhisana/utils/check_linkedin_url_validity.py +34 -8
- dhisana/utils/clay_tools.py +3 -2
- dhisana/utils/clean_properties.py +1 -4
- dhisana/utils/compose_salesnav_query.py +0 -1
- dhisana/utils/compose_search_query.py +7 -3
- dhisana/utils/composite_tools.py +0 -1
- dhisana/utils/dataframe_tools.py +2 -2
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_provider.py +174 -35
- dhisana/utils/enrich_lead_information.py +183 -53
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +1 -1
- dhisana/utils/g2_tools.py +0 -1
- dhisana/utils/generate_content.py +0 -1
- dhisana/utils/generate_email.py +68 -23
- dhisana/utils/generate_email_response.py +294 -46
- dhisana/utils/generate_flow.py +0 -1
- dhisana/utils/generate_linkedin_connect_message.py +9 -2
- dhisana/utils/generate_linkedin_response_message.py +137 -66
- dhisana/utils/generate_structured_output_internal.py +317 -164
- dhisana/utils/google_custom_search.py +150 -44
- dhisana/utils/google_oauth_tools.py +721 -0
- dhisana/utils/google_workspace_tools.py +278 -54
- dhisana/utils/hubspot_clearbit.py +3 -1
- dhisana/utils/hubspot_crm_tools.py +718 -272
- dhisana/utils/instantly_tools.py +3 -1
- dhisana/utils/lusha_tools.py +10 -7
- dhisana/utils/mailgun_tools.py +150 -0
- dhisana/utils/microsoft365_tools.py +447 -0
- dhisana/utils/openai_assistant_and_file_utils.py +121 -177
- dhisana/utils/openai_helpers.py +8 -6
- dhisana/utils/parse_linkedin_messages_txt.py +1 -3
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +377 -76
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/research_lead.py +3 -3
- dhisana/utils/sales_navigator_crawler.py +1 -6
- dhisana/utils/salesforce_crm_tools.py +323 -50
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +126 -91
- dhisana/utils/serarch_router_local_business.py +75 -0
- dhisana/utils/serpapi_additional_tools.py +290 -0
- dhisana/utils/serpapi_google_jobs.py +117 -0
- dhisana/utils/serpapi_google_search.py +188 -0
- dhisana/utils/serpapi_local_business_search.py +129 -0
- dhisana/utils/serpapi_search_tools.py +360 -432
- dhisana/utils/serperdev_google_jobs.py +125 -0
- dhisana/utils/serperdev_local_business.py +154 -0
- dhisana/utils/serperdev_search.py +233 -0
- dhisana/utils/smtp_email_tools.py +178 -18
- dhisana/utils/test_connect.py +1603 -130
- dhisana/utils/trasform_json.py +3 -3
- dhisana/utils/web_download_parse_tools.py +0 -1
- dhisana/utils/zoominfo_tools.py +2 -3
- dhisana/workflow/test.py +1 -1
- {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +1 -1
- dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
- {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
- dhisana-0.0.1.dev116.dist-info/RECORD +0 -83
- {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
- {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import csv
|
|
3
3
|
import datetime
|
|
4
|
+
import html as html_lib
|
|
4
5
|
import io
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
9
10
|
import uuid
|
|
11
|
+
from email.mime.multipart import MIMEMultipart
|
|
10
12
|
from email.mime.text import MIMEText
|
|
11
13
|
from typing import Any, Dict, List, Optional
|
|
12
14
|
|
|
@@ -22,8 +24,9 @@ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
|
|
22
24
|
from dhisana.schemas.sales import MessageItem
|
|
23
25
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
24
26
|
from dhisana.utils.email_parse_helpers import *
|
|
27
|
+
from dhisana.utils.email_body_utils import body_variants
|
|
25
28
|
import asyncio
|
|
26
|
-
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext)
|
|
29
|
+
from dhisana.schemas.common import (SendEmailContext, QueryEmailContext, ReplyEmailContext, BodyFormat)
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
################################################################################
|
|
@@ -43,7 +46,7 @@ def get_google_workspace_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
|
43
46
|
str: The base64-encoded JSON string for the service account credentials.
|
|
44
47
|
|
|
45
48
|
Raises:
|
|
46
|
-
ValueError: If the
|
|
49
|
+
ValueError: If the Google Workspace integration has not been configured.
|
|
47
50
|
"""
|
|
48
51
|
if tool_config:
|
|
49
52
|
google_workspace_config = next(
|
|
@@ -61,12 +64,14 @@ def get_google_workspace_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
|
61
64
|
else:
|
|
62
65
|
GOOGLE_SERVICE_KEY = None
|
|
63
66
|
|
|
64
|
-
if not GOOGLE_SERVICE_KEY:
|
|
67
|
+
if not GOOGLE_SERVICE_KEY:
|
|
65
68
|
env_service_key = os.getenv("GOOGLE_SERVICE_KEY")
|
|
66
|
-
if env_service_key:
|
|
69
|
+
if env_service_key:
|
|
67
70
|
GOOGLE_SERVICE_KEY = base64.b64decode(env_service_key).decode("utf-8")
|
|
68
71
|
if not GOOGLE_SERVICE_KEY:
|
|
69
|
-
raise ValueError(
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"Google Workspace integration is not configured. Please configure the connection to Google Workspace in Integrations."
|
|
74
|
+
)
|
|
70
75
|
return GOOGLE_SERVICE_KEY
|
|
71
76
|
|
|
72
77
|
|
|
@@ -107,6 +112,28 @@ def get_google_credentials(
|
|
|
107
112
|
return credentials
|
|
108
113
|
|
|
109
114
|
|
|
115
|
+
def _looks_like_html(text: str) -> bool:
|
|
116
|
+
"""Heuristically determine whether the body contains HTML markup."""
|
|
117
|
+
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _html_to_plain_text(html: str) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Produce a very lightweight plain-text version of an HTML fragment.
|
|
123
|
+
This keeps newlines on block boundaries and strips tags.
|
|
124
|
+
"""
|
|
125
|
+
if not html:
|
|
126
|
+
return ""
|
|
127
|
+
text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", html)
|
|
128
|
+
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
129
|
+
text = re.sub(r"(?i)</(p|div|li|h[1-6])\s*>", "\n", text)
|
|
130
|
+
text = re.sub(r"(?is)<.*?>", "", text)
|
|
131
|
+
text = html_lib.unescape(text)
|
|
132
|
+
text = re.sub(r"\s+\n", "\n", text)
|
|
133
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
134
|
+
return text.strip()
|
|
135
|
+
|
|
136
|
+
|
|
110
137
|
|
|
111
138
|
@assistant_tool
|
|
112
139
|
async def send_email_using_service_account_async(
|
|
@@ -135,8 +162,19 @@ async def send_email_using_service_account_async(
|
|
|
135
162
|
|
|
136
163
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
137
164
|
|
|
138
|
-
|
|
139
|
-
|
|
165
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
166
|
+
send_email_context.body,
|
|
167
|
+
getattr(send_email_context, "body_format", None),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if resolved_fmt == "text":
|
|
171
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
172
|
+
else:
|
|
173
|
+
# Gmail prefers multipart/alternative when HTML is present.
|
|
174
|
+
message = MIMEMultipart("alternative")
|
|
175
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
176
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
177
|
+
|
|
140
178
|
message['to'] = send_email_context.recipient
|
|
141
179
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
142
180
|
message['subject'] = send_email_context.subject
|
|
@@ -203,47 +241,64 @@ async def list_emails_in_time_range_async(
|
|
|
203
241
|
query += f' {label_query}'
|
|
204
242
|
|
|
205
243
|
headers = {'Authorization': f'Bearer {access_token}'}
|
|
206
|
-
params = {'q': query}
|
|
244
|
+
params = {'q': query, 'maxResults': 100}
|
|
207
245
|
|
|
208
246
|
message_items: List[MessageItem] = []
|
|
247
|
+
max_fetch = 500 # defensive cap
|
|
209
248
|
async with httpx.AsyncClient() as client:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
next_page_token = None
|
|
250
|
+
while True:
|
|
251
|
+
page_params = dict(params)
|
|
252
|
+
if next_page_token:
|
|
253
|
+
page_params["pageToken"] = next_page_token
|
|
213
254
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
255
|
+
response = await client.get(gmail_api_url, headers=headers, params=page_params)
|
|
256
|
+
response.raise_for_status()
|
|
257
|
+
resp_json = response.json() or {}
|
|
258
|
+
messages = resp_json.get('messages', [])
|
|
259
|
+
|
|
260
|
+
for msg in messages:
|
|
261
|
+
if len(message_items) >= max_fetch:
|
|
262
|
+
break
|
|
263
|
+
message_id = msg['id']
|
|
264
|
+
thread_id = msg.get('threadId', "")
|
|
265
|
+
message_url = f'{gmail_api_url}/{message_id}'
|
|
266
|
+
message_response = await client.get(message_url, headers=headers)
|
|
267
|
+
message_response.raise_for_status()
|
|
268
|
+
message_data = message_response.json()
|
|
269
|
+
|
|
270
|
+
headers_list = message_data['payload']['headers']
|
|
271
|
+
from_header = find_header(headers_list, 'From') or ""
|
|
272
|
+
subject_header = find_header(headers_list, 'Subject') or ""
|
|
273
|
+
date_header = find_header(headers_list, 'Date') or ""
|
|
274
|
+
|
|
275
|
+
iso_datetime_str = convert_date_to_iso(date_header)
|
|
276
|
+
|
|
277
|
+
# Parse the "From" into (sender_name, sender_email)
|
|
278
|
+
s_name, s_email = parse_single_address(from_header)
|
|
279
|
+
|
|
280
|
+
# Parse the recipients
|
|
281
|
+
r_name, r_email = find_all_recipients_in_headers(headers_list)
|
|
282
|
+
|
|
283
|
+
msg_item = MessageItem(
|
|
284
|
+
message_id=message_data['id'],
|
|
285
|
+
thread_id=thread_id,
|
|
286
|
+
sender_name=s_name,
|
|
287
|
+
sender_email=s_email,
|
|
288
|
+
receiver_name=r_name,
|
|
289
|
+
receiver_email=r_email,
|
|
290
|
+
iso_datetime=iso_datetime_str,
|
|
291
|
+
subject=subject_header,
|
|
292
|
+
body=extract_email_body_in_plain_text(message_data)
|
|
293
|
+
)
|
|
294
|
+
message_items.append(msg_item)
|
|
231
295
|
|
|
232
|
-
|
|
233
|
-
|
|
296
|
+
if len(message_items) >= max_fetch:
|
|
297
|
+
break
|
|
234
298
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
sender_name=s_name,
|
|
239
|
-
sender_email=s_email,
|
|
240
|
-
receiver_name=r_name,
|
|
241
|
-
receiver_email=r_email,
|
|
242
|
-
iso_datetime=iso_datetime_str,
|
|
243
|
-
subject=subject_header,
|
|
244
|
-
body=extract_email_body_in_plain_text(message_data)
|
|
245
|
-
)
|
|
246
|
-
message_items.append(msg_item)
|
|
299
|
+
next_page_token = resp_json.get("nextPageToken")
|
|
300
|
+
if not next_page_token:
|
|
301
|
+
break
|
|
247
302
|
|
|
248
303
|
return message_items
|
|
249
304
|
|
|
@@ -475,6 +530,7 @@ class SendEmailContext(BaseModel):
|
|
|
475
530
|
sender_name: str
|
|
476
531
|
sender_email: str
|
|
477
532
|
labels: Optional[List[str]]
|
|
533
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
478
534
|
|
|
479
535
|
@assistant_tool
|
|
480
536
|
async def send_email_using_service_account_async(
|
|
@@ -503,8 +559,18 @@ async def send_email_using_service_account_async(
|
|
|
503
559
|
|
|
504
560
|
gmail_api_url = 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'
|
|
505
561
|
|
|
562
|
+
plain_body, html_body, resolved_fmt = body_variants(
|
|
563
|
+
send_email_context.body,
|
|
564
|
+
getattr(send_email_context, "body_format", None),
|
|
565
|
+
)
|
|
566
|
+
|
|
506
567
|
# Construct the MIME text message
|
|
507
|
-
|
|
568
|
+
if resolved_fmt == "text":
|
|
569
|
+
message = MIMEText(plain_body, _subtype="plain", _charset="utf-8")
|
|
570
|
+
else:
|
|
571
|
+
message = MIMEMultipart("alternative")
|
|
572
|
+
message.attach(MIMEText(plain_body, "plain", _charset="utf-8"))
|
|
573
|
+
message.attach(MIMEText(html_body, "html", _charset="utf-8"))
|
|
508
574
|
message['to'] = send_email_context.recipient
|
|
509
575
|
message['from'] = f"{send_email_context.sender_name} <{send_email_context.sender_email}>"
|
|
510
576
|
message['subject'] = send_email_context.subject
|
|
@@ -846,20 +912,55 @@ async def reply_to_email_async(
|
|
|
846
912
|
original_message = response.json()
|
|
847
913
|
|
|
848
914
|
headers_list = original_message.get('payload', {}).get('headers', [])
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
# 2. Prepare reply headers
|
|
853
|
-
subject = headers_dict.get('Subject', '')
|
|
915
|
+
# Case-insensitive header lookup and resilient recipient fallback to avoid Gmail 400s.
|
|
916
|
+
subject = find_header(headers_list, 'Subject') or ''
|
|
854
917
|
if not subject.startswith('Re:'):
|
|
855
918
|
subject = f'Re: {subject}'
|
|
919
|
+
reply_to_header = find_header(headers_list, 'Reply-To') or ''
|
|
920
|
+
from_header = find_header(headers_list, 'From') or ''
|
|
921
|
+
to_header = find_header(headers_list, 'To') or ''
|
|
922
|
+
cc_header = find_header(headers_list, 'Cc') or ''
|
|
923
|
+
message_id_header = find_header(headers_list, 'Message-ID') or ''
|
|
924
|
+
thread_id = original_message.get('threadId')
|
|
856
925
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
926
|
+
sender_email_lc = (reply_email_context.sender_email or '').lower()
|
|
927
|
+
|
|
928
|
+
def _is_self(addr: str) -> bool:
|
|
929
|
+
return bool(sender_email_lc) and sender_email_lc in addr.lower()
|
|
930
|
+
|
|
931
|
+
cc_addresses = cc_header or ''
|
|
932
|
+
if reply_to_header and not _is_self(reply_to_header):
|
|
933
|
+
to_addresses = reply_to_header
|
|
934
|
+
elif from_header and not _is_self(from_header):
|
|
935
|
+
to_addresses = from_header
|
|
936
|
+
elif to_header and not _is_self(to_header):
|
|
937
|
+
to_addresses = to_header
|
|
938
|
+
else:
|
|
939
|
+
combined = ", ".join([v for v in (to_header, cc_header, from_header) if v])
|
|
940
|
+
to_addresses = combined
|
|
941
|
+
cc_addresses = ''
|
|
942
|
+
|
|
943
|
+
if (not to_addresses or _is_self(to_addresses)) and reply_email_context.fallback_recipient:
|
|
944
|
+
if not _is_self(reply_email_context.fallback_recipient):
|
|
945
|
+
to_addresses = reply_email_context.fallback_recipient
|
|
946
|
+
cc_addresses = ''
|
|
947
|
+
|
|
948
|
+
if not to_addresses or _is_self(to_addresses):
|
|
949
|
+
raise ValueError(
|
|
950
|
+
"No valid recipient found in the original message; refusing to reply to sender."
|
|
951
|
+
)
|
|
860
952
|
|
|
861
953
|
# 3. Create the reply email message
|
|
862
|
-
|
|
954
|
+
plain_reply, html_reply, resolved_reply_fmt = body_variants(
|
|
955
|
+
reply_email_context.reply_body,
|
|
956
|
+
getattr(reply_email_context, "reply_body_format", None),
|
|
957
|
+
)
|
|
958
|
+
if resolved_reply_fmt == "text":
|
|
959
|
+
msg = MIMEText(plain_reply, _subtype="plain", _charset="utf-8")
|
|
960
|
+
else:
|
|
961
|
+
msg = MIMEMultipart("alternative")
|
|
962
|
+
msg.attach(MIMEText(plain_reply, "plain", _charset="utf-8"))
|
|
963
|
+
msg.attach(MIMEText(html_reply, "html", _charset="utf-8"))
|
|
863
964
|
msg['To'] = to_addresses
|
|
864
965
|
if cc_addresses:
|
|
865
966
|
msg['Cc'] = cc_addresses
|
|
@@ -978,6 +1079,34 @@ async def get_calendar_events_using_service_account_async(
|
|
|
978
1079
|
|
|
979
1080
|
return events
|
|
980
1081
|
|
|
1082
|
+
def get_google_sheet_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
1083
|
+
"""
|
|
1084
|
+
Retrieves the Google Sheets API key from the provided tool configuration or
|
|
1085
|
+
the environment variable ``GOOGLE_SHEETS_API_KEY``.
|
|
1086
|
+
|
|
1087
|
+
Raises:
|
|
1088
|
+
ValueError: If the Google Sheets integration has not been configured.
|
|
1089
|
+
"""
|
|
1090
|
+
GOOGLE_SHEETS_API_KEY = None
|
|
1091
|
+
if tool_config:
|
|
1092
|
+
google_sheet_config = next(
|
|
1093
|
+
(item for item in tool_config if item.get("name") == "google_sheets"), None
|
|
1094
|
+
)
|
|
1095
|
+
if google_sheet_config:
|
|
1096
|
+
config_map = {
|
|
1097
|
+
item["name"]: item["value"]
|
|
1098
|
+
for item in google_sheet_config.get("configuration", [])
|
|
1099
|
+
if item
|
|
1100
|
+
}
|
|
1101
|
+
GOOGLE_SHEETS_API_KEY = config_map.get("apiKey")
|
|
1102
|
+
|
|
1103
|
+
GOOGLE_SHEETS_API_KEY = GOOGLE_SHEETS_API_KEY or os.getenv("GOOGLE_SHEETS_API_KEY")
|
|
1104
|
+
if not GOOGLE_SHEETS_API_KEY:
|
|
1105
|
+
raise ValueError(
|
|
1106
|
+
"Google Sheets integration is not configured. Please configure the connection to Google Sheets in Integrations."
|
|
1107
|
+
)
|
|
1108
|
+
return GOOGLE_SHEETS_API_KEY
|
|
1109
|
+
|
|
981
1110
|
def get_sheet_id_from_url(sheet_url: str) -> str:
|
|
982
1111
|
"""
|
|
983
1112
|
Extract the spreadsheet ID from a typical Google Sheets URL.
|
|
@@ -990,6 +1119,53 @@ def get_sheet_id_from_url(sheet_url: str) -> str:
|
|
|
990
1119
|
raise ValueError("Could not extract spreadsheet ID from the provided URL.")
|
|
991
1120
|
return match.group(1)
|
|
992
1121
|
|
|
1122
|
+
|
|
1123
|
+
def get_document_id_from_url(doc_url: str) -> str:
|
|
1124
|
+
"""Extract the document ID from a typical Google Docs URL.
|
|
1125
|
+
|
|
1126
|
+
Example URL format:
|
|
1127
|
+
https://docs.google.com/document/d/<DOCUMENT_ID>/edit
|
|
1128
|
+
"""
|
|
1129
|
+
match = re.search(r"/d/([a-zA-Z0-9-_]+)/", doc_url)
|
|
1130
|
+
if not match:
|
|
1131
|
+
raise ValueError("Could not extract document ID from the provided URL.")
|
|
1132
|
+
return match.group(1)
|
|
1133
|
+
|
|
1134
|
+
async def read_google_sheet_with_api_token(
|
|
1135
|
+
sheet_url: str,
|
|
1136
|
+
range_name: str,
|
|
1137
|
+
sender_email: str, # kept for signature compatibility – not used
|
|
1138
|
+
tool_config: Optional[List[Dict]] = None
|
|
1139
|
+
) -> List[List[str]]:
|
|
1140
|
+
"""
|
|
1141
|
+
Read data from a *public* Google Sheet (shared “Anyone with the link → Viewer”)
|
|
1142
|
+
using an API key instead of OAuth credentials.
|
|
1143
|
+
"""
|
|
1144
|
+
|
|
1145
|
+
# 1️⃣ Spreadsheet ID from the URL
|
|
1146
|
+
spreadsheet_id = get_sheet_id_from_url(sheet_url)
|
|
1147
|
+
|
|
1148
|
+
# 2️⃣ Grab the API key (tool_config ➜ googlesheet › apiKey, or env var)
|
|
1149
|
+
api_key = get_google_sheet_token(tool_config)
|
|
1150
|
+
|
|
1151
|
+
# 3️⃣ Build the Sheets service with the key
|
|
1152
|
+
service = build("sheets", "v4", developerKey=api_key)
|
|
1153
|
+
sheet = service.spreadsheets()
|
|
1154
|
+
|
|
1155
|
+
# 4️⃣ Default range to the first sheet if none supplied
|
|
1156
|
+
if not range_name:
|
|
1157
|
+
metadata = sheet.get(spreadsheetId=spreadsheet_id).execute()
|
|
1158
|
+
range_name = metadata["sheets"][0]["properties"]["title"]
|
|
1159
|
+
|
|
1160
|
+
# 5️⃣ Fetch the values
|
|
1161
|
+
result = sheet.values().get(
|
|
1162
|
+
spreadsheetId=spreadsheet_id,
|
|
1163
|
+
range=range_name
|
|
1164
|
+
).execute()
|
|
1165
|
+
|
|
1166
|
+
return result.get("values", [])
|
|
1167
|
+
|
|
1168
|
+
|
|
993
1169
|
async def read_google_sheet(
|
|
994
1170
|
sheet_url: str,
|
|
995
1171
|
range_name: str,
|
|
@@ -1038,7 +1214,56 @@ async def read_google_sheet(
|
|
|
1038
1214
|
except HttpError as e:
|
|
1039
1215
|
logging.error(f"An error occurred while reading the Google Sheet: {e}")
|
|
1040
1216
|
raise
|
|
1041
|
-
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
async def read_google_document(
|
|
1220
|
+
doc_url: str,
|
|
1221
|
+
sender_email: str,
|
|
1222
|
+
tool_config: Optional[List[Dict]] = None,
|
|
1223
|
+
) -> str:
|
|
1224
|
+
"""Read text content from a Google Doc using a service account.
|
|
1225
|
+
|
|
1226
|
+
Args:
|
|
1227
|
+
doc_url (str): Full URL of the Google Document.
|
|
1228
|
+
sender_email (str): The email address to impersonate.
|
|
1229
|
+
tool_config (Optional[List[Dict]]): Tool configuration for credentials.
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
str: The concatenated text content of the document.
|
|
1233
|
+
|
|
1234
|
+
Raises:
|
|
1235
|
+
HttpError: If there's an error calling the Docs API.
|
|
1236
|
+
"""
|
|
1237
|
+
|
|
1238
|
+
# --- 1. Extract Document ID from URL ---
|
|
1239
|
+
document_id = get_document_id_from_url(doc_url)
|
|
1240
|
+
|
|
1241
|
+
# --- 2. Set up credentials ---
|
|
1242
|
+
SCOPES = ['https://www.googleapis.com/auth/documents.readonly']
|
|
1243
|
+
credentials = get_google_credentials(sender_email, SCOPES, tool_config)
|
|
1244
|
+
|
|
1245
|
+
# --- 3. Build the Docs service and fetch the document ---
|
|
1246
|
+
try:
|
|
1247
|
+
service = build('docs', 'v1', credentials=credentials)
|
|
1248
|
+
document = service.documents().get(documentId=document_id).execute()
|
|
1249
|
+
|
|
1250
|
+
content = document.get('body', {}).get('content', [])
|
|
1251
|
+
text_parts: List[str] = []
|
|
1252
|
+
for element in content:
|
|
1253
|
+
paragraph = element.get('paragraph')
|
|
1254
|
+
if not paragraph:
|
|
1255
|
+
continue
|
|
1256
|
+
for elem in paragraph.get('elements', []):
|
|
1257
|
+
text_run = elem.get('textRun')
|
|
1258
|
+
if text_run:
|
|
1259
|
+
text_parts.append(text_run.get('content', ''))
|
|
1260
|
+
|
|
1261
|
+
return ''.join(text_parts)
|
|
1262
|
+
|
|
1263
|
+
except HttpError as e:
|
|
1264
|
+
logging.error(f"An error occurred while reading the Google Document: {e}")
|
|
1265
|
+
raise
|
|
1266
|
+
|
|
1042
1267
|
def save_values_to_csv(values: List[List[str]], output_filename: str) -> str:
|
|
1043
1268
|
"""
|
|
1044
1269
|
Saves a list of row values (list of lists) to a CSV file.
|
|
@@ -1060,4 +1285,3 @@ def save_values_to_csv(values: List[List[str]], output_filename: str) -> str:
|
|
|
1060
1285
|
writer.writerows(values)
|
|
1061
1286
|
|
|
1062
1287
|
return local_file_path
|
|
1063
|
-
|
|
@@ -27,7 +27,9 @@ async def get_company_domain_from_breeze(company_name: str):
|
|
|
27
27
|
"""
|
|
28
28
|
HUBSPOT_API_KEY = os.environ.get('HUBSPOT_API_KEY')
|
|
29
29
|
if not HUBSPOT_API_KEY:
|
|
30
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
'error': "HubSpot integration is not configured. Please configure the connection to HubSpot in Integrations."
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
if not company_name:
|
|
33
35
|
return {'error': "Company name must be provided"}
|