dhisana 0.0.1.dev243__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/__init__.py +1 -0
- dhisana/cli/__init__.py +1 -0
- dhisana/cli/cli.py +20 -0
- dhisana/cli/datasets.py +27 -0
- dhisana/cli/models.py +26 -0
- dhisana/cli/predictions.py +20 -0
- dhisana/schemas/__init__.py +1 -0
- dhisana/schemas/common.py +399 -0
- dhisana/schemas/sales.py +965 -0
- dhisana/ui/__init__.py +1 -0
- dhisana/ui/components.py +472 -0
- dhisana/utils/__init__.py +1 -0
- dhisana/utils/add_mapping.py +352 -0
- dhisana/utils/agent_tools.py +51 -0
- dhisana/utils/apollo_tools.py +1597 -0
- dhisana/utils/assistant_tool_tag.py +4 -0
- dhisana/utils/built_with_api_tools.py +282 -0
- dhisana/utils/cache_output_tools.py +98 -0
- dhisana/utils/cache_output_tools_local.py +78 -0
- dhisana/utils/check_email_validity_tools.py +717 -0
- dhisana/utils/check_for_intent_signal.py +107 -0
- dhisana/utils/check_linkedin_url_validity.py +209 -0
- dhisana/utils/clay_tools.py +43 -0
- dhisana/utils/clean_properties.py +135 -0
- dhisana/utils/company_utils.py +60 -0
- dhisana/utils/compose_salesnav_query.py +259 -0
- dhisana/utils/compose_search_query.py +759 -0
- dhisana/utils/compose_three_step_workflow.py +234 -0
- dhisana/utils/composite_tools.py +137 -0
- dhisana/utils/dataframe_tools.py +237 -0
- dhisana/utils/domain_parser.py +45 -0
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_parse_helpers.py +132 -0
- dhisana/utils/email_provider.py +375 -0
- dhisana/utils/enrich_lead_information.py +933 -0
- dhisana/utils/extract_email_content_for_llm.py +101 -0
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +426 -0
- dhisana/utils/g2_tools.py +104 -0
- dhisana/utils/generate_content.py +41 -0
- dhisana/utils/generate_custom_message.py +271 -0
- dhisana/utils/generate_email.py +278 -0
- dhisana/utils/generate_email_response.py +465 -0
- dhisana/utils/generate_flow.py +102 -0
- dhisana/utils/generate_leads_salesnav.py +303 -0
- dhisana/utils/generate_linkedin_connect_message.py +224 -0
- dhisana/utils/generate_linkedin_response_message.py +317 -0
- dhisana/utils/generate_structured_output_internal.py +462 -0
- dhisana/utils/google_custom_search.py +267 -0
- dhisana/utils/google_oauth_tools.py +727 -0
- dhisana/utils/google_workspace_tools.py +1294 -0
- dhisana/utils/hubspot_clearbit.py +96 -0
- dhisana/utils/hubspot_crm_tools.py +2440 -0
- dhisana/utils/instantly_tools.py +149 -0
- dhisana/utils/linkedin_crawler.py +168 -0
- dhisana/utils/lusha_tools.py +333 -0
- dhisana/utils/mailgun_tools.py +156 -0
- dhisana/utils/mailreach_tools.py +123 -0
- dhisana/utils/microsoft365_tools.py +455 -0
- dhisana/utils/openai_assistant_and_file_utils.py +267 -0
- dhisana/utils/openai_helpers.py +977 -0
- dhisana/utils/openapi_spec_to_tools.py +45 -0
- dhisana/utils/openapi_tool/__init__.py +1 -0
- dhisana/utils/openapi_tool/api_models.py +633 -0
- dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
- dhisana/utils/openapi_tool/openapi_tool.py +319 -0
- dhisana/utils/parse_linkedin_messages_txt.py +100 -0
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +1226 -0
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/python_function_to_tools.py +83 -0
- dhisana/utils/research_lead.py +176 -0
- dhisana/utils/sales_navigator_crawler.py +1103 -0
- dhisana/utils/salesforce_crm_tools.py +477 -0
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +162 -0
- 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 +852 -0
- 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 +582 -0
- dhisana/utils/test_connect.py +2087 -0
- dhisana/utils/trasform_json.py +173 -0
- dhisana/utils/web_download_parse_tools.py +189 -0
- dhisana/utils/workflow_code_model.py +5 -0
- dhisana/utils/zoominfo_tools.py +357 -0
- dhisana/workflow/__init__.py +1 -0
- dhisana/workflow/agent.py +18 -0
- dhisana/workflow/flow.py +44 -0
- dhisana/workflow/task.py +43 -0
- dhisana/workflow/test.py +90 -0
- dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
- dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
- dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
- dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
- dhisana-0.0.1.dev243.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import aiohttp
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import backoff
|
|
9
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
10
|
+
from dhisana.utils.cache_output_tools import retrieve_output, cache_output
|
|
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
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@assistant_tool
|
|
68
|
+
@backoff.on_exception(
|
|
69
|
+
backoff.expo,
|
|
70
|
+
aiohttp.ClientResponseError,
|
|
71
|
+
max_tries=3,
|
|
72
|
+
giveup=lambda e: e.status != 429,
|
|
73
|
+
factor=60,
|
|
74
|
+
)
|
|
75
|
+
async def search_google_custom_search(
|
|
76
|
+
query: str,
|
|
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]:
|
|
82
|
+
"""
|
|
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
|
+
|
|
87
|
+
Parameters:
|
|
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.
|
|
96
|
+
"""
|
|
97
|
+
logger.info("Entering search_google_custom_search")
|
|
98
|
+
|
|
99
|
+
# Construct the final query
|
|
100
|
+
full_query = query
|
|
101
|
+
if as_oq:
|
|
102
|
+
full_query += f" {as_oq}"
|
|
103
|
+
|
|
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
|
|
116
|
+
|
|
117
|
+
url = "https://www.googleapis.com/customsearch/v1"
|
|
118
|
+
params = {
|
|
119
|
+
"key": api_key,
|
|
120
|
+
"cx": cx,
|
|
121
|
+
"q": full_query,
|
|
122
|
+
"num": number_of_results,
|
|
123
|
+
"start": start_index
|
|
124
|
+
}
|
|
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
|
+
|
|
132
|
+
try:
|
|
133
|
+
async with aiohttp.ClientSession() as session:
|
|
134
|
+
async with session.get(url, params=params) as response:
|
|
135
|
+
if response.status == 200:
|
|
136
|
+
result = await response.json()
|
|
137
|
+
|
|
138
|
+
# Check for spelling suggestion
|
|
139
|
+
if "spelling" in result and "correctedQuery" in result["spelling"]:
|
|
140
|
+
corrected_query = result["spelling"]["correctedQuery"]
|
|
141
|
+
logger.warning(f"Spelling suggestion detected ({corrected_query}). Retrying with original query.")
|
|
142
|
+
# Force original query (no changes).
|
|
143
|
+
params["q"] = full_query
|
|
144
|
+
async with session.get(url, params=params) as retry_response:
|
|
145
|
+
if retry_response.status == 200:
|
|
146
|
+
retry_result = await retry_response.json()
|
|
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})]
|
|
151
|
+
else:
|
|
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)
|
|
158
|
+
return serialized_results
|
|
159
|
+
|
|
160
|
+
elif response.status == 429:
|
|
161
|
+
logger.warning("search_google_custom_search: Rate limit (429).")
|
|
162
|
+
raise aiohttp.ClientResponseError(
|
|
163
|
+
request_info=response.request_info,
|
|
164
|
+
history=response.history,
|
|
165
|
+
status=response.status,
|
|
166
|
+
message="Rate limit exceeded",
|
|
167
|
+
headers=response.headers
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
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
|
|
177
|
+
except Exception as e:
|
|
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
|
+
|
|
198
|
+
|
|
199
|
+
@assistant_tool
|
|
200
|
+
async def search_google_places(
|
|
201
|
+
query: str,
|
|
202
|
+
location_bias: dict = None,
|
|
203
|
+
number_of_results: int = 3
|
|
204
|
+
):
|
|
205
|
+
"""
|
|
206
|
+
Search Google Places API (New) and return the results as an array of serialized JSON strings.
|
|
207
|
+
|
|
208
|
+
Parameters:
|
|
209
|
+
- **query** (*str*): The search query.
|
|
210
|
+
- **location_bias** (*dict*): Optional. A dictionary with 'latitude', 'longitude', and 'radius' (in meters) to bias the search.
|
|
211
|
+
- **number_of_results** (*int*): The number of results to return.
|
|
212
|
+
"""
|
|
213
|
+
GOOGLE_SEARCH_KEY = os.environ.get('GOOGLE_SEARCH_KEY')
|
|
214
|
+
if not GOOGLE_SEARCH_KEY:
|
|
215
|
+
return {
|
|
216
|
+
'error': "Google Places integration is not configured. Please configure the connection to Google Places in Integrations."
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
url = "https://places.googleapis.com/v1/places:searchText"
|
|
220
|
+
headers = {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'X-Goog-Api-Key': GOOGLE_SEARCH_KEY,
|
|
223
|
+
'X-Goog-FieldMask': 'places.displayName,places.formattedAddress,places.location,places.websiteUri,places.rating,places.reviews'
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
request_body = {
|
|
227
|
+
"textQuery": query
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if location_bias:
|
|
231
|
+
request_body["locationBias"] = {
|
|
232
|
+
"circle": {
|
|
233
|
+
"center": {
|
|
234
|
+
"latitude": location_bias.get("latitude"),
|
|
235
|
+
"longitude": location_bias.get("longitude")
|
|
236
|
+
},
|
|
237
|
+
"radius": location_bias.get("radius", 5000) # Default to 5 km if radius not provided
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Create a cache key that includes query, number_of_results, and location_bias
|
|
242
|
+
location_bias_str = json.dumps(location_bias, sort_keys=True) if location_bias else "None"
|
|
243
|
+
cache_key = f"{query}:{number_of_results}:{location_bias_str}"
|
|
244
|
+
cached_response = retrieve_output("search_google_places", cache_key)
|
|
245
|
+
if cached_response is not None:
|
|
246
|
+
return cached_response
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
async with aiohttp.ClientSession() as session:
|
|
250
|
+
async with session.post(url, headers=headers, json=request_body) as response:
|
|
251
|
+
result = await response.json()
|
|
252
|
+
if response.status != 200:
|
|
253
|
+
return {'error': result.get('error', {}).get('message', 'Unknown error')}
|
|
254
|
+
|
|
255
|
+
# Extract the required number of results
|
|
256
|
+
places = result.get('places', [])[:number_of_results]
|
|
257
|
+
|
|
258
|
+
# Serialize each place result to JSON string
|
|
259
|
+
serialized_results = [json.dumps(place) for place in places]
|
|
260
|
+
|
|
261
|
+
# Cache the response
|
|
262
|
+
cache_output("search_google_places", cache_key, serialized_results)
|
|
263
|
+
|
|
264
|
+
return serialized_results
|
|
265
|
+
except Exception as e:
|
|
266
|
+
return {'error': str(e)}
|
|
267
|
+
|