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,282 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import aiohttp
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import os
|
|
6
|
+
import aiohttp
|
|
7
|
+
import backoff
|
|
8
|
+
from dhisana.utils.cache_output_tools import cache_output,retrieve_output
|
|
9
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from typing import Optional, List, Dict
|
|
12
|
+
|
|
13
|
+
def get_builtwith_api_key(tool_config: Optional[List[Dict]] = None) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Retrieves the BUILTWITH_API_KEY access token from the provided tool configuration.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
tool_config (list): A list of dictionaries containing the tool configuration.
|
|
19
|
+
Each dictionary should have a "name" key and a "configuration" key,
|
|
20
|
+
where "configuration" is a list of dictionaries containing "name" and "value" keys.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str: The BUILTWITH_API_KEY access token.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If the BuiltWith integration has not been configured.
|
|
27
|
+
"""
|
|
28
|
+
if tool_config:
|
|
29
|
+
builtwith_config = next(
|
|
30
|
+
(item for item in tool_config if item.get("name") == "builtwith"), None
|
|
31
|
+
)
|
|
32
|
+
if builtwith_config:
|
|
33
|
+
config_map = {
|
|
34
|
+
item["name"]: item["value"]
|
|
35
|
+
for item in builtwith_config.get("configuration", [])
|
|
36
|
+
if item
|
|
37
|
+
}
|
|
38
|
+
BUILTWITH_API_KEY = config_map.get("apiKey")
|
|
39
|
+
else:
|
|
40
|
+
BUILTWITH_API_KEY = None
|
|
41
|
+
else:
|
|
42
|
+
BUILTWITH_API_KEY = None
|
|
43
|
+
|
|
44
|
+
BUILTWITH_API_KEY = BUILTWITH_API_KEY or os.getenv("BUILTWITH_API_KEY")
|
|
45
|
+
if not BUILTWITH_API_KEY:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"BuiltWith integration is not configured. Please configure the connection to BuiltWith in Integrations."
|
|
48
|
+
)
|
|
49
|
+
return BUILTWITH_API_KEY
|
|
50
|
+
|
|
51
|
+
# Use BuiltWith API to find tech stack and financials of a company
|
|
52
|
+
@assistant_tool
|
|
53
|
+
@backoff.on_exception(
|
|
54
|
+
backoff.expo,
|
|
55
|
+
aiohttp.ClientResponseError,
|
|
56
|
+
max_tries=2,
|
|
57
|
+
giveup=lambda e: e.status != 429,
|
|
58
|
+
factor=10,
|
|
59
|
+
)
|
|
60
|
+
async def get_company_info_from_builtwith(
|
|
61
|
+
company_domain: Optional[str] = None,
|
|
62
|
+
tool_config: Optional[List[Dict]] = None,
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Fetch a company's technology details from BuiltWith using the company domain.
|
|
66
|
+
|
|
67
|
+
Parameters:
|
|
68
|
+
- **company_domain** (*str*, optional): Domain of the company.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
- **dict**: JSON response containing technology information.
|
|
72
|
+
"""
|
|
73
|
+
BUILTWITH_API_KEY = get_builtwith_api_key(tool_config=tool_config)
|
|
74
|
+
|
|
75
|
+
if not company_domain:
|
|
76
|
+
return {'error': "Company domain must be provided"}
|
|
77
|
+
|
|
78
|
+
headers = {
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
"Cache-Control": "no-cache",
|
|
81
|
+
"accept": "application/json"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
cached_response = retrieve_output("get_company_info_from_builtwith", company_domain) # Replace with your caching logic if needed
|
|
85
|
+
if cached_response is not None:
|
|
86
|
+
return cached_response
|
|
87
|
+
|
|
88
|
+
url = f'https://api.builtwith.com/v19/api.json?KEY={BUILTWITH_API_KEY}&LOOKUP={company_domain}'
|
|
89
|
+
|
|
90
|
+
async with aiohttp.ClientSession() as session:
|
|
91
|
+
async with session.get(url, headers=headers) as response:
|
|
92
|
+
if response.status == 200:
|
|
93
|
+
result = await response.json()
|
|
94
|
+
cache_output("get_company_info_from_builtwith", company_domain, result) # Replace with your caching logic if needed
|
|
95
|
+
return result
|
|
96
|
+
elif response.status == 429:
|
|
97
|
+
raise aiohttp.ClientResponseError(
|
|
98
|
+
request_info=response.request_info,
|
|
99
|
+
history=response.history,
|
|
100
|
+
status=response.status,
|
|
101
|
+
message="Rate limit exceeded",
|
|
102
|
+
headers=response.headers
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
try:
|
|
106
|
+
result = await response.json()
|
|
107
|
+
return {'error': result}
|
|
108
|
+
except Exception as e:
|
|
109
|
+
return {'error': f"Unexpected error: {str(e)}"}
|
|
110
|
+
|
|
111
|
+
@assistant_tool
|
|
112
|
+
@backoff.on_exception(
|
|
113
|
+
backoff.expo,
|
|
114
|
+
aiohttp.ClientResponseError,
|
|
115
|
+
max_tries=2,
|
|
116
|
+
giveup=lambda e: e.status != 429,
|
|
117
|
+
factor=10,
|
|
118
|
+
)
|
|
119
|
+
async def get_company_financials_from_builtwith(
|
|
120
|
+
company_domain: Optional[str] = None,
|
|
121
|
+
tool_config: Optional[List[Dict]] = None,
|
|
122
|
+
):
|
|
123
|
+
"""
|
|
124
|
+
Fetch a company's financial details from BuiltWith using the company domain.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
- **company_domain** (*str*, optional): Domain of the company.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
- **dict**: JSON response containing financial information.
|
|
131
|
+
"""
|
|
132
|
+
BUILTWITH_API_KEY = get_builtwith_api_key(tool_config=tool_config)
|
|
133
|
+
|
|
134
|
+
if not company_domain:
|
|
135
|
+
return {'error': "Company domain must be provided"}
|
|
136
|
+
|
|
137
|
+
headers = {
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
"Cache-Control": "no-cache",
|
|
140
|
+
"accept": "application/json"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
cached_response = retrieve_output("get_company_financials_from_builtwith", company_domain) # Replace with your caching logic if needed
|
|
144
|
+
if cached_response is not None:
|
|
145
|
+
return cached_response
|
|
146
|
+
|
|
147
|
+
url = f'https://api.builtwith.com/v19/financial.json?KEY={BUILTWITH_API_KEY}&LOOKUP={company_domain}'
|
|
148
|
+
|
|
149
|
+
async with aiohttp.ClientSession() as session:
|
|
150
|
+
async with session.get(url, headers=headers) as response:
|
|
151
|
+
if response.status == 200:
|
|
152
|
+
result = await response.json()
|
|
153
|
+
cache_output("get_company_financials_from_builtwith", company_domain, result) # Replace with your caching logic if needed
|
|
154
|
+
return result
|
|
155
|
+
elif response.status == 429:
|
|
156
|
+
raise aiohttp.ClientResponseError(
|
|
157
|
+
request_info=response.request_info,
|
|
158
|
+
history=response.history,
|
|
159
|
+
status=response.status,
|
|
160
|
+
message="Rate limit exceeded",
|
|
161
|
+
headers=response.headers
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
try:
|
|
165
|
+
result = await response.json()
|
|
166
|
+
return {'error': result}
|
|
167
|
+
except Exception as e:
|
|
168
|
+
return {'error': f"Unexpected error: {str(e)}"}
|
|
169
|
+
|
|
170
|
+
@assistant_tool
|
|
171
|
+
@backoff.on_exception(
|
|
172
|
+
backoff.expo,
|
|
173
|
+
aiohttp.ClientResponseError,
|
|
174
|
+
max_tries=3,
|
|
175
|
+
giveup=lambda e: e.status != 429,
|
|
176
|
+
factor=10,
|
|
177
|
+
)
|
|
178
|
+
async def get_company_info_from_builtwith_by_name(company_name: str, tool_config: Optional[List[Dict]] = None):
|
|
179
|
+
"""
|
|
180
|
+
Fetch a company's technology details from BuiltWith using the company name.
|
|
181
|
+
|
|
182
|
+
Parameters:
|
|
183
|
+
- company_name (str): Name of the company.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
- dict: JSON response containing technology information or error details.
|
|
187
|
+
"""
|
|
188
|
+
BUILTWITH_API_KEY = get_builtwith_api_key(tool_config=tool_config)
|
|
189
|
+
|
|
190
|
+
if not company_name:
|
|
191
|
+
return {'error': "Company name must be provided"}
|
|
192
|
+
|
|
193
|
+
headers = {
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
"Cache-Control": "no-cache",
|
|
196
|
+
"Accept": "application/json"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Step 1: Use Company To URL API to get the company's domain
|
|
200
|
+
company_to_url_api = f'https://ctu.builtwith.com/ctu2/api.json?KEY={BUILTWITH_API_KEY}&COMPANY={company_name}'
|
|
201
|
+
company_info = retrieve_output("get_company_info_from_builtwith_by_name", company_name)
|
|
202
|
+
if company_info:
|
|
203
|
+
company_domain = company_info.get('Domain')
|
|
204
|
+
return await get_company_info_from_builtwith(company_domain)
|
|
205
|
+
|
|
206
|
+
async with aiohttp.ClientSession() as session:
|
|
207
|
+
async with session.get(company_to_url_api, headers=headers) as response:
|
|
208
|
+
if response.status == 200:
|
|
209
|
+
company_data = await response.json()
|
|
210
|
+
if isinstance(company_data, list) and company_data:
|
|
211
|
+
company_info = company_data[0]
|
|
212
|
+
company_domain = company_info.get('Domain')
|
|
213
|
+
if not company_domain:
|
|
214
|
+
return {'error': "Domain not found for the given company name"}
|
|
215
|
+
cache_output("get_company_info_from_builtwith_by_name", company_name, company_info)
|
|
216
|
+
return await get_company_info_from_builtwith(company_domain)
|
|
217
|
+
else:
|
|
218
|
+
return {'error': "No results found for the given company name"}
|
|
219
|
+
elif response.status == 429:
|
|
220
|
+
raise aiohttp.ClientResponseError(
|
|
221
|
+
request_info=response.request_info,
|
|
222
|
+
history=response.history,
|
|
223
|
+
status=response.status,
|
|
224
|
+
message="Rate limit exceeded",
|
|
225
|
+
headers=response.headers
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
try:
|
|
229
|
+
error_data = await response.json()
|
|
230
|
+
return {'error': error_data}
|
|
231
|
+
except Exception as e:
|
|
232
|
+
return {'error': f"Unexpected error: {str(e)}"}
|
|
233
|
+
|
|
234
|
+
@assistant_tool
|
|
235
|
+
@backoff.on_exception(
|
|
236
|
+
backoff.expo,
|
|
237
|
+
aiohttp.ClientResponseError,
|
|
238
|
+
max_tries=2,
|
|
239
|
+
giveup=lambda e: e.status != 429,
|
|
240
|
+
factor=10,
|
|
241
|
+
)
|
|
242
|
+
async def is_tech_used_in_company(
|
|
243
|
+
company_domain: Optional[str] = None,
|
|
244
|
+
company_name: Optional[str] = None,
|
|
245
|
+
keyword: Optional[str] = None,
|
|
246
|
+
tool_config: Optional[List[Dict]] = None
|
|
247
|
+
) -> bool:
|
|
248
|
+
if not company_domain and not company_name:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
if not keyword:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
if company_domain:
|
|
255
|
+
company_data_buildwith = await get_company_info_from_builtwith(company_domain, tool_config=tool_config)
|
|
256
|
+
elif company_name:
|
|
257
|
+
company_data_buildwith = await get_company_info_from_builtwith_by_name(company_name, tool_config=tool_config)
|
|
258
|
+
company_domain = company_data_buildwith.get('Lookup', '')
|
|
259
|
+
|
|
260
|
+
data_str = json.dumps(company_data_buildwith).lower()
|
|
261
|
+
keyword_lower = keyword.lower()
|
|
262
|
+
return keyword_lower in data_str
|
|
263
|
+
|
|
264
|
+
@assistant_tool
|
|
265
|
+
@backoff.on_exception(
|
|
266
|
+
backoff.expo,
|
|
267
|
+
aiohttp.ClientResponseError,
|
|
268
|
+
max_tries=2,
|
|
269
|
+
giveup=lambda e: e.status != 429,
|
|
270
|
+
factor=10,
|
|
271
|
+
)
|
|
272
|
+
async def get_company_domain_from_name(
|
|
273
|
+
company_name: Optional[str] = None,
|
|
274
|
+
tool_config: Optional[List[Dict]] = None,
|
|
275
|
+
) -> str:
|
|
276
|
+
if not company_name:
|
|
277
|
+
return ''
|
|
278
|
+
|
|
279
|
+
company_data_buildwith = await get_company_info_from_builtwith_by_name(company_name, tool_config=tool_config)
|
|
280
|
+
company_domain = company_data_buildwith.get('Lookup', '')
|
|
281
|
+
return company_domain
|
|
282
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from azure.storage.blob import BlobServiceClient
|
|
7
|
+
from azure.core.exceptions import ResourceNotFoundError, AzureError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
logging.getLogger("azure").setLevel(logging.CRITICAL)
|
|
11
|
+
|
|
12
|
+
CONTAINER_NAME = "cacheoutputs"
|
|
13
|
+
|
|
14
|
+
def _get_container_client():
|
|
15
|
+
"""
|
|
16
|
+
Returns the container client for the cache container.
|
|
17
|
+
Ensures that the container is created if it doesn't exist.
|
|
18
|
+
"""
|
|
19
|
+
connection_string = os.environ.get("AZURE_BLOB_CONNECTION_STRING")
|
|
20
|
+
if not connection_string:
|
|
21
|
+
raise ValueError("AZURE_BLOB_CONNECTION_STRING environment variable is not set")
|
|
22
|
+
|
|
23
|
+
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
|
|
24
|
+
container_client = blob_service_client.get_container_client(CONTAINER_NAME)
|
|
25
|
+
|
|
26
|
+
# Ensure the container exists; if already created, the AzureError is ignored.
|
|
27
|
+
try:
|
|
28
|
+
container_client.create_container()
|
|
29
|
+
except AzureError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
return container_client
|
|
33
|
+
|
|
34
|
+
def cache_output(tool_name: str, key: str, value, ttl: int = None) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Cache the output of a function using Azure Blob Storage.
|
|
37
|
+
|
|
38
|
+
Parameters:
|
|
39
|
+
tool_name (str): Name of the tool whose output is being cached.
|
|
40
|
+
key (str): The cache key.
|
|
41
|
+
value (Any): The value to be cached.
|
|
42
|
+
ttl (int, optional): The time-to-live (TTL) for the cached value in seconds.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bool: True if the value was successfully cached, False otherwise.
|
|
46
|
+
"""
|
|
47
|
+
# Create a hash of the key for a consistent blob name
|
|
48
|
+
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
|
49
|
+
# Construct the blob name using a virtual folder for the tool name
|
|
50
|
+
blob_name = f"{tool_name}/{key_hash}.json"
|
|
51
|
+
|
|
52
|
+
# Prepare the cache data
|
|
53
|
+
cache_data = {
|
|
54
|
+
"value": value,
|
|
55
|
+
"ttl": ttl
|
|
56
|
+
}
|
|
57
|
+
data = json.dumps(cache_data)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
container_client = _get_container_client()
|
|
61
|
+
blob_client = container_client.get_blob_client(blob=blob_name)
|
|
62
|
+
# Upload the blob content (overwrite if the blob already exists)
|
|
63
|
+
blob_client.upload_blob(data, overwrite=True)
|
|
64
|
+
return True
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Error uploading blob '{blob_name}': {e}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def retrieve_output(tool_name: str, key: str):
|
|
70
|
+
"""
|
|
71
|
+
Retrieve the cached output for a given tool and cache key from Azure Blob Storage.
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
tool_name (str): Name of the tool whose output is being retrieved.
|
|
75
|
+
key (str): The cache key.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Any: The cached value if found, None otherwise.
|
|
79
|
+
"""
|
|
80
|
+
# Create a hash of the key to locate the blob
|
|
81
|
+
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
|
82
|
+
# Construct the blob name using the tool name folder
|
|
83
|
+
blob_name = f"{tool_name}/{key_hash}.json"
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
container_client = _get_container_client()
|
|
87
|
+
blob_client = container_client.get_blob_client(blob=blob_name)
|
|
88
|
+
download_stream = blob_client.download_blob()
|
|
89
|
+
content = download_stream.readall() # content is in bytes
|
|
90
|
+
cache_data = json.loads(content.decode("utf-8"))
|
|
91
|
+
return cache_data.get("value")
|
|
92
|
+
except ResourceNotFoundError:
|
|
93
|
+
# Blob does not exist
|
|
94
|
+
logger.info(f"Blob '{blob_name}' not found.")
|
|
95
|
+
return None
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error retrieving blob '{blob_name}': {e}")
|
|
98
|
+
return None
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
CACHE_PATH = os.environ.get('AGENT_CACHE_PATH', '/tmp/dhisana_ai/cache_run_outputs/')
|
|
7
|
+
|
|
8
|
+
def cache_output(tool_name, key, value, ttl=None):
|
|
9
|
+
"""
|
|
10
|
+
Cache the output of a function using the provided key and value.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
tool_name (str): Name of the tool whose output is being cached.
|
|
14
|
+
key (str): The cache key.
|
|
15
|
+
value (Any): The value to be cached.
|
|
16
|
+
ttl (int, optional): The time-to-live (TTL) for the cached value in seconds.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
bool: True if the value was successfully cached, False otherwise.
|
|
20
|
+
"""
|
|
21
|
+
# Ensure the cache directory exists
|
|
22
|
+
if not os.path.exists(CACHE_PATH):
|
|
23
|
+
os.makedirs(CACHE_PATH)
|
|
24
|
+
|
|
25
|
+
# Create a hash of the key
|
|
26
|
+
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
|
27
|
+
|
|
28
|
+
# Create the cache file path
|
|
29
|
+
cache_file_path = os.path.join(CACHE_PATH, f"{tool_name}_{key_hash}.json")
|
|
30
|
+
|
|
31
|
+
# Create the cache data
|
|
32
|
+
cache_data = {
|
|
33
|
+
'value': value,
|
|
34
|
+
'ttl': ttl
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Write the cache data to the file
|
|
38
|
+
try:
|
|
39
|
+
os.makedirs(os.path.dirname(cache_file_path), exist_ok=True)
|
|
40
|
+
with open(cache_file_path, 'w') as cache_file:
|
|
41
|
+
json.dump(cache_data, cache_file)
|
|
42
|
+
return True
|
|
43
|
+
except IOError as e:
|
|
44
|
+
logging.error(f"IOError while writing to cache file {cache_file_path}: {e}")
|
|
45
|
+
return False
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logging.error(f"Unexpected error while writing to cache file {cache_file_path}: {e}")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def retrieve_output(tool_name, key):
|
|
52
|
+
"""
|
|
53
|
+
Retrieve the cached output for a given tool and cache key.
|
|
54
|
+
|
|
55
|
+
Parameters:
|
|
56
|
+
tool_name (str): Name of the tool whose output is being retrieved.
|
|
57
|
+
key (str): The cache key.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Any: The cached value if found, None otherwise.
|
|
61
|
+
"""
|
|
62
|
+
# Create a hash of the key
|
|
63
|
+
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
|
64
|
+
|
|
65
|
+
# Create the cache file path
|
|
66
|
+
cache_file_path = os.path.join(CACHE_PATH, f"{tool_name}_{key_hash}.json")
|
|
67
|
+
|
|
68
|
+
# Read the cache data from the file
|
|
69
|
+
if os.path.exists(cache_file_path):
|
|
70
|
+
try:
|
|
71
|
+
with open(cache_file_path, 'r') as cache_file:
|
|
72
|
+
cache_data = json.load(cache_file)
|
|
73
|
+
return cache_data['value']
|
|
74
|
+
except IOError as e:
|
|
75
|
+
logging.error(f"Error retrieving cache for tool '{tool_name}' with key '{key}': {e}")
|
|
76
|
+
return None
|
|
77
|
+
else:
|
|
78
|
+
return None
|