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
|
@@ -3,14 +3,66 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
import aiohttp
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import backoff
|
|
6
9
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
7
10
|
from dhisana.utils.cache_output_tools import retrieve_output, cache_output
|
|
8
|
-
import backoff
|
|
9
11
|
|
|
12
|
+
logging.basicConfig(level=logging.INFO)
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_google_custom_search_keys(tool_config: Optional[List[Dict]] = None) -> (str, str):
|
|
17
|
+
"""
|
|
18
|
+
Retrieve the Google Custom Search API key (GOOGLE_SEARCH_KEY) and CX
|
|
19
|
+
from the provided tool_config or environment variables.
|
|
20
|
+
|
|
21
|
+
tool_config example:
|
|
22
|
+
[
|
|
23
|
+
{
|
|
24
|
+
"name": "google_custom_search",
|
|
25
|
+
"configuration": [
|
|
26
|
+
{"name": "apiKey", "value": "XXX"},
|
|
27
|
+
{"name": "cx", "value": "YYY"}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
(api_key, cx) tuple.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If the Google Custom Search integration has not been configured.
|
|
37
|
+
"""
|
|
38
|
+
api_key = None
|
|
39
|
+
cx = None
|
|
40
|
+
|
|
41
|
+
if tool_config:
|
|
42
|
+
logger.debug("Looking for Google Custom Search config in tool_config.")
|
|
43
|
+
gcs_config = next((cfg for cfg in tool_config if cfg.get("name") == "google_custom_search"), None)
|
|
44
|
+
if gcs_config:
|
|
45
|
+
config_map = {
|
|
46
|
+
item["name"]: item["value"]
|
|
47
|
+
for item in gcs_config.get("configuration", [])
|
|
48
|
+
if item
|
|
49
|
+
}
|
|
50
|
+
api_key = config_map.get("apiKey")
|
|
51
|
+
cx = config_map.get("cx")
|
|
52
|
+
else:
|
|
53
|
+
logger.debug("No 'google_custom_search' config item found in tool_config.")
|
|
54
|
+
|
|
55
|
+
# Fall back to environment variables if not found in tool_config
|
|
56
|
+
api_key = api_key or os.environ.get('GOOGLE_SEARCH_KEY')
|
|
57
|
+
cx = cx or os.environ.get('GOOGLE_SEARCH_CX')
|
|
58
|
+
|
|
59
|
+
if not api_key or not cx:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"Google Custom Search integration is not configured. Please configure the connection to Google Custom Search in Integrations."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return api_key, cx
|
|
10
65
|
|
|
11
|
-
# This is google Custom Search API (Not SERP API)
|
|
12
|
-
# We use SERP API currently has it handles spell corrections etc correctly.
|
|
13
|
-
# TODO: Enhance Custom search to handle corner cases. Its much lower cost than SERP API.
|
|
14
66
|
|
|
15
67
|
@assistant_tool
|
|
16
68
|
@backoff.on_exception(
|
|
@@ -20,62 +72,93 @@ import backoff
|
|
|
20
72
|
giveup=lambda e: e.status != 429,
|
|
21
73
|
factor=60,
|
|
22
74
|
)
|
|
23
|
-
async def
|
|
75
|
+
async def search_google_custom_search(
|
|
24
76
|
query: str,
|
|
25
|
-
number_of_results: int = 10
|
|
26
|
-
|
|
77
|
+
number_of_results: int = 10,
|
|
78
|
+
offset: int = 0,
|
|
79
|
+
tool_config: Optional[List[Dict]] = None,
|
|
80
|
+
as_oq: Optional[str] = None
|
|
81
|
+
) -> List[str]:
|
|
27
82
|
"""
|
|
28
|
-
Search Google using the Google Custom Search JSON API and return the results
|
|
29
|
-
|
|
83
|
+
Search Google using the Google Custom Search JSON API and return the results
|
|
84
|
+
as a list of JSON strings in SerpAPI-like format:
|
|
85
|
+
{ "position", "title", "link", "snippet" }
|
|
86
|
+
|
|
30
87
|
Parameters:
|
|
31
|
-
-
|
|
32
|
-
-
|
|
88
|
+
- query (str): The search query.
|
|
89
|
+
- number_of_results (int): The number of results to return. Default 10.
|
|
90
|
+
- offset (int): The index offset for results (pagination). Default 0.
|
|
91
|
+
- tool_config (Optional[List[Dict]]): Tool config containing "google_custom_search" keys.
|
|
92
|
+
- as_oq (Optional[str]): Optional additional query terms, appended to 'query'.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
- List[str]: A list of JSON-serialized objects each containing title/link/snippet/position.
|
|
33
96
|
"""
|
|
97
|
+
logger.info("Entering search_google_custom_search")
|
|
34
98
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
|
|
99
|
+
# Construct the final query
|
|
100
|
+
full_query = query
|
|
101
|
+
if as_oq:
|
|
102
|
+
full_query += f" {as_oq}"
|
|
39
103
|
|
|
40
|
-
|
|
104
|
+
# Retrieve keys from tool_config or environment
|
|
105
|
+
try:
|
|
106
|
+
api_key, cx = get_google_custom_search_keys(tool_config)
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
error_msg = str(e)
|
|
109
|
+
logger.error(error_msg)
|
|
110
|
+
return [json.dumps({"error": error_msg})]
|
|
111
|
+
|
|
112
|
+
# We apply pagination via "start" parameter in Google Custom Search
|
|
113
|
+
# "start" = 1 means the 1st result; offset means skip 'offset' results
|
|
114
|
+
# So if offset=0 => start=1, if offset=10 => start=11, etc.
|
|
115
|
+
start_index = offset + 1
|
|
41
116
|
|
|
117
|
+
url = "https://www.googleapis.com/customsearch/v1"
|
|
42
118
|
params = {
|
|
43
|
-
"key":
|
|
44
|
-
"cx":
|
|
45
|
-
"q":
|
|
46
|
-
"num": number_of_results
|
|
119
|
+
"key": api_key,
|
|
120
|
+
"cx": cx,
|
|
121
|
+
"q": full_query,
|
|
122
|
+
"num": number_of_results,
|
|
123
|
+
"start": start_index
|
|
47
124
|
}
|
|
48
125
|
|
|
126
|
+
cache_key = f"{full_query}_{number_of_results}_{offset}"
|
|
127
|
+
cached_response = retrieve_output("search_google_custom_search", cache_key)
|
|
128
|
+
if cached_response is not None:
|
|
129
|
+
logger.info("Cache hit for search_google_custom_search.")
|
|
130
|
+
return cached_response
|
|
131
|
+
|
|
49
132
|
try:
|
|
50
|
-
cached_response = retrieve_output("search_google_custom", query)
|
|
51
|
-
if cached_response is not None:
|
|
52
|
-
return cached_response
|
|
53
|
-
|
|
54
133
|
async with aiohttp.ClientSession() as session:
|
|
55
134
|
async with session.get(url, params=params) as response:
|
|
56
135
|
if response.status == 200:
|
|
57
136
|
result = await response.json()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Check for spelling corrections in the response
|
|
137
|
+
|
|
138
|
+
# Check for spelling suggestion
|
|
61
139
|
if "spelling" in result and "correctedQuery" in result["spelling"]:
|
|
62
140
|
corrected_query = result["spelling"]["correctedQuery"]
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
params["q"] = query # Explicitly force the original query
|
|
67
|
-
params["spell"] = query
|
|
141
|
+
logger.warning(f"Spelling suggestion detected ({corrected_query}). Retrying with original query.")
|
|
142
|
+
# Force original query (no changes).
|
|
143
|
+
params["q"] = full_query
|
|
68
144
|
async with session.get(url, params=params) as retry_response:
|
|
69
145
|
if retry_response.status == 200:
|
|
70
146
|
retry_result = await retry_response.json()
|
|
71
|
-
|
|
147
|
+
normalized_results = _normalize_google_items(retry_result.get('items', []))
|
|
148
|
+
else:
|
|
149
|
+
retry_content = await retry_response.text()
|
|
150
|
+
return [json.dumps({"error": retry_content})]
|
|
72
151
|
else:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
152
|
+
# Normal path
|
|
153
|
+
normalized_results = _normalize_google_items(result.get('items', []))
|
|
154
|
+
|
|
155
|
+
serialized_results = [json.dumps(item) for item in normalized_results]
|
|
156
|
+
cache_output("search_google_custom_search", cache_key, serialized_results)
|
|
157
|
+
await asyncio.sleep(1) # small delay (optional)
|
|
76
158
|
return serialized_results
|
|
159
|
+
|
|
77
160
|
elif response.status == 429:
|
|
78
|
-
|
|
161
|
+
logger.warning("search_google_custom_search: Rate limit (429).")
|
|
79
162
|
raise aiohttp.ClientResponseError(
|
|
80
163
|
request_info=response.request_info,
|
|
81
164
|
history=response.history,
|
|
@@ -84,13 +167,34 @@ async def search_google_custom(
|
|
|
84
167
|
headers=response.headers
|
|
85
168
|
)
|
|
86
169
|
else:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
|
|
91
|
-
|
|
170
|
+
error_json = await response.json()
|
|
171
|
+
logger.warning(f"search_google_custom_search request failed: {error_json}")
|
|
172
|
+
return [json.dumps({"error": error_json})]
|
|
173
|
+
|
|
174
|
+
except aiohttp.ClientResponseError:
|
|
175
|
+
# Let backoff handle the re-raise
|
|
176
|
+
raise
|
|
92
177
|
except Exception as e:
|
|
93
|
-
|
|
178
|
+
logger.exception("Exception during search_google_custom_search.")
|
|
179
|
+
return [json.dumps({"error": str(e)})]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _normalize_google_items(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
183
|
+
"""
|
|
184
|
+
Convert Google Custom Search 'items' into SerpAPI/Serper.dev-like format:
|
|
185
|
+
{ position, title, link, snippet }
|
|
186
|
+
"""
|
|
187
|
+
normalized = []
|
|
188
|
+
for i, item in enumerate(items):
|
|
189
|
+
normalized_item = {
|
|
190
|
+
"position": i + 1,
|
|
191
|
+
"title": item.get("title", ""),
|
|
192
|
+
"link": item.get("link", ""),
|
|
193
|
+
"snippet": item.get("snippet", "")
|
|
194
|
+
}
|
|
195
|
+
normalized.append(normalized_item)
|
|
196
|
+
return normalized
|
|
197
|
+
|
|
94
198
|
|
|
95
199
|
@assistant_tool
|
|
96
200
|
async def search_google_places(
|
|
@@ -108,7 +212,9 @@ async def search_google_places(
|
|
|
108
212
|
"""
|
|
109
213
|
GOOGLE_SEARCH_KEY = os.environ.get('GOOGLE_SEARCH_KEY')
|
|
110
214
|
if not GOOGLE_SEARCH_KEY:
|
|
111
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
'error': "Google Places integration is not configured. Please configure the connection to Google Places in Integrations."
|
|
217
|
+
}
|
|
112
218
|
|
|
113
219
|
url = "https://places.googleapis.com/v1/places:searchText"
|
|
114
220
|
headers = {
|