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.
Files changed (102) hide show
  1. dhisana/__init__.py +1 -0
  2. dhisana/cli/__init__.py +1 -0
  3. dhisana/cli/cli.py +20 -0
  4. dhisana/cli/datasets.py +27 -0
  5. dhisana/cli/models.py +26 -0
  6. dhisana/cli/predictions.py +20 -0
  7. dhisana/schemas/__init__.py +1 -0
  8. dhisana/schemas/common.py +399 -0
  9. dhisana/schemas/sales.py +965 -0
  10. dhisana/ui/__init__.py +1 -0
  11. dhisana/ui/components.py +472 -0
  12. dhisana/utils/__init__.py +1 -0
  13. dhisana/utils/add_mapping.py +352 -0
  14. dhisana/utils/agent_tools.py +51 -0
  15. dhisana/utils/apollo_tools.py +1597 -0
  16. dhisana/utils/assistant_tool_tag.py +4 -0
  17. dhisana/utils/built_with_api_tools.py +282 -0
  18. dhisana/utils/cache_output_tools.py +98 -0
  19. dhisana/utils/cache_output_tools_local.py +78 -0
  20. dhisana/utils/check_email_validity_tools.py +717 -0
  21. dhisana/utils/check_for_intent_signal.py +107 -0
  22. dhisana/utils/check_linkedin_url_validity.py +209 -0
  23. dhisana/utils/clay_tools.py +43 -0
  24. dhisana/utils/clean_properties.py +135 -0
  25. dhisana/utils/company_utils.py +60 -0
  26. dhisana/utils/compose_salesnav_query.py +259 -0
  27. dhisana/utils/compose_search_query.py +759 -0
  28. dhisana/utils/compose_three_step_workflow.py +234 -0
  29. dhisana/utils/composite_tools.py +137 -0
  30. dhisana/utils/dataframe_tools.py +237 -0
  31. dhisana/utils/domain_parser.py +45 -0
  32. dhisana/utils/email_body_utils.py +72 -0
  33. dhisana/utils/email_parse_helpers.py +132 -0
  34. dhisana/utils/email_provider.py +375 -0
  35. dhisana/utils/enrich_lead_information.py +933 -0
  36. dhisana/utils/extract_email_content_for_llm.py +101 -0
  37. dhisana/utils/fetch_openai_config.py +129 -0
  38. dhisana/utils/field_validators.py +426 -0
  39. dhisana/utils/g2_tools.py +104 -0
  40. dhisana/utils/generate_content.py +41 -0
  41. dhisana/utils/generate_custom_message.py +271 -0
  42. dhisana/utils/generate_email.py +278 -0
  43. dhisana/utils/generate_email_response.py +465 -0
  44. dhisana/utils/generate_flow.py +102 -0
  45. dhisana/utils/generate_leads_salesnav.py +303 -0
  46. dhisana/utils/generate_linkedin_connect_message.py +224 -0
  47. dhisana/utils/generate_linkedin_response_message.py +317 -0
  48. dhisana/utils/generate_structured_output_internal.py +462 -0
  49. dhisana/utils/google_custom_search.py +267 -0
  50. dhisana/utils/google_oauth_tools.py +727 -0
  51. dhisana/utils/google_workspace_tools.py +1294 -0
  52. dhisana/utils/hubspot_clearbit.py +96 -0
  53. dhisana/utils/hubspot_crm_tools.py +2440 -0
  54. dhisana/utils/instantly_tools.py +149 -0
  55. dhisana/utils/linkedin_crawler.py +168 -0
  56. dhisana/utils/lusha_tools.py +333 -0
  57. dhisana/utils/mailgun_tools.py +156 -0
  58. dhisana/utils/mailreach_tools.py +123 -0
  59. dhisana/utils/microsoft365_tools.py +455 -0
  60. dhisana/utils/openai_assistant_and_file_utils.py +267 -0
  61. dhisana/utils/openai_helpers.py +977 -0
  62. dhisana/utils/openapi_spec_to_tools.py +45 -0
  63. dhisana/utils/openapi_tool/__init__.py +1 -0
  64. dhisana/utils/openapi_tool/api_models.py +633 -0
  65. dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
  66. dhisana/utils/openapi_tool/openapi_tool.py +319 -0
  67. dhisana/utils/parse_linkedin_messages_txt.py +100 -0
  68. dhisana/utils/profile.py +37 -0
  69. dhisana/utils/proxy_curl_tools.py +1226 -0
  70. dhisana/utils/proxycurl_search_leads.py +426 -0
  71. dhisana/utils/python_function_to_tools.py +83 -0
  72. dhisana/utils/research_lead.py +176 -0
  73. dhisana/utils/sales_navigator_crawler.py +1103 -0
  74. dhisana/utils/salesforce_crm_tools.py +477 -0
  75. dhisana/utils/search_router.py +131 -0
  76. dhisana/utils/search_router_jobs.py +51 -0
  77. dhisana/utils/sendgrid_tools.py +162 -0
  78. dhisana/utils/serarch_router_local_business.py +75 -0
  79. dhisana/utils/serpapi_additional_tools.py +290 -0
  80. dhisana/utils/serpapi_google_jobs.py +117 -0
  81. dhisana/utils/serpapi_google_search.py +188 -0
  82. dhisana/utils/serpapi_local_business_search.py +129 -0
  83. dhisana/utils/serpapi_search_tools.py +852 -0
  84. dhisana/utils/serperdev_google_jobs.py +125 -0
  85. dhisana/utils/serperdev_local_business.py +154 -0
  86. dhisana/utils/serperdev_search.py +233 -0
  87. dhisana/utils/smtp_email_tools.py +582 -0
  88. dhisana/utils/test_connect.py +2087 -0
  89. dhisana/utils/trasform_json.py +173 -0
  90. dhisana/utils/web_download_parse_tools.py +189 -0
  91. dhisana/utils/workflow_code_model.py +5 -0
  92. dhisana/utils/zoominfo_tools.py +357 -0
  93. dhisana/workflow/__init__.py +1 -0
  94. dhisana/workflow/agent.py +18 -0
  95. dhisana/workflow/flow.py +44 -0
  96. dhisana/workflow/task.py +43 -0
  97. dhisana/workflow/test.py +90 -0
  98. dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
  99. dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
  100. dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
  101. dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
  102. 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
+