dhisana 0.0.1.dev249__tar.gz → 0.0.1.dev251__tar.gz
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-0.0.1.dev249 → dhisana-0.0.1.dev251}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/setup.py +1 -1
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/apollo_tools.py +121 -52
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/cache_output_tools.py +22 -2
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/proxy_curl_tools.py +12 -9
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_apollo_company_search.py +41 -19
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/README.md +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/setup.cfg +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/email_body_utils.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_custom_message.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/google_oauth_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/mailgun_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/mailreach_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/microsoft365_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/smtp_email_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/test_connect.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana.egg-info/SOURCES.txt +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_apollo_lead_search.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_email_body_utils.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_mailreach.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -1287,62 +1287,131 @@ def fill_in_company_properties(company_data: dict) -> dict:
|
|
|
1287
1287
|
company_data: Raw company data from Apollo API
|
|
1288
1288
|
|
|
1289
1289
|
Returns:
|
|
1290
|
-
Dictionary
|
|
1290
|
+
Dictionary matching the SmartList `Account` schema shape.
|
|
1291
1291
|
"""
|
|
1292
|
-
|
|
1292
|
+
def _parse_keywords(value: Any) -> List[Any]:
|
|
1293
|
+
if value is None:
|
|
1294
|
+
return []
|
|
1295
|
+
if isinstance(value, list):
|
|
1296
|
+
return value
|
|
1297
|
+
if isinstance(value, str):
|
|
1298
|
+
text = value.strip()
|
|
1299
|
+
if not text:
|
|
1300
|
+
return []
|
|
1301
|
+
if "," in text:
|
|
1302
|
+
return [part.strip() for part in text.split(",") if part.strip()]
|
|
1303
|
+
return [text]
|
|
1304
|
+
return [value]
|
|
1293
1305
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1306
|
+
def _parse_compact_number(value: Any) -> Optional[float]:
|
|
1307
|
+
if value is None:
|
|
1308
|
+
return None
|
|
1309
|
+
if isinstance(value, (int, float)):
|
|
1310
|
+
return float(value)
|
|
1311
|
+
text = str(value).strip()
|
|
1312
|
+
if not text:
|
|
1313
|
+
return None
|
|
1314
|
+
text = text.replace("$", "").replace(",", "").strip()
|
|
1315
|
+
multiplier = 1.0
|
|
1316
|
+
suffix = text[-1:].upper()
|
|
1317
|
+
if suffix in ("K", "M", "B"):
|
|
1318
|
+
multiplier = {"K": 1e3, "M": 1e6, "B": 1e9}[suffix]
|
|
1319
|
+
text = text[:-1].strip()
|
|
1320
|
+
try:
|
|
1321
|
+
return float(text) * multiplier
|
|
1322
|
+
except ValueError:
|
|
1323
|
+
return None
|
|
1324
|
+
|
|
1325
|
+
annual_revenue = (
|
|
1326
|
+
company_data.get("organization_revenue")
|
|
1327
|
+
if company_data.get("organization_revenue") is not None
|
|
1328
|
+
else company_data.get("annual_revenue")
|
|
1329
|
+
)
|
|
1330
|
+
annual_revenue = _parse_compact_number(annual_revenue)
|
|
1331
|
+
if annual_revenue is None:
|
|
1332
|
+
annual_revenue = _parse_compact_number(company_data.get("organization_revenue_printed"))
|
|
1333
|
+
|
|
1334
|
+
company_size = company_data.get("estimated_num_employees")
|
|
1335
|
+
if company_size is not None:
|
|
1336
|
+
try:
|
|
1337
|
+
company_size = int(company_size)
|
|
1338
|
+
except (TypeError, ValueError):
|
|
1339
|
+
company_size = None
|
|
1340
|
+
|
|
1341
|
+
founded_year = company_data.get("founded_year")
|
|
1342
|
+
if founded_year is not None:
|
|
1343
|
+
try:
|
|
1344
|
+
founded_year = int(founded_year)
|
|
1345
|
+
except (TypeError, ValueError):
|
|
1346
|
+
founded_year = None
|
|
1347
|
+
|
|
1348
|
+
primary_phone = company_data.get("primary_phone")
|
|
1349
|
+
primary_phone_number = None
|
|
1350
|
+
if isinstance(primary_phone, dict):
|
|
1351
|
+
primary_phone_number = primary_phone.get("number") or primary_phone.get(
|
|
1352
|
+
"sanitized_number"
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
phone = (
|
|
1356
|
+
primary_phone_number
|
|
1357
|
+
or company_data.get("phone")
|
|
1358
|
+
or company_data.get("primary_phone_number")
|
|
1359
|
+
or company_data.get("sanitized_phone")
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
industry = company_data.get("industry")
|
|
1363
|
+
if not industry and isinstance(company_data.get("industries"), list):
|
|
1364
|
+
industries = [str(x).strip() for x in company_data["industries"] if str(x).strip()]
|
|
1365
|
+
industry = industries[0] if industries else None
|
|
1366
|
+
|
|
1367
|
+
billing_street = (
|
|
1368
|
+
company_data.get("street_address")
|
|
1369
|
+
or company_data.get("billing_street")
|
|
1370
|
+
or company_data.get("address")
|
|
1371
|
+
or company_data.get("raw_address")
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
account: Dict[str, Any] = {
|
|
1375
|
+
"name": company_data.get("name"),
|
|
1376
|
+
"domain": company_data.get("primary_domain"),
|
|
1377
|
+
"website": company_data.get("website_url"),
|
|
1378
|
+
"phone": phone,
|
|
1379
|
+
"fax": company_data.get("fax") or company_data.get("fax_number"),
|
|
1380
|
+
"industry": industry,
|
|
1381
|
+
"company_size": company_size,
|
|
1382
|
+
"founded_year": founded_year,
|
|
1383
|
+
"annual_revenue": annual_revenue,
|
|
1384
|
+
"type": company_data.get("type") or company_data.get("organization_type"),
|
|
1385
|
+
"ownership": company_data.get("ownership"),
|
|
1386
|
+
"organization_linkedin_url": company_data.get("linkedin_url"),
|
|
1387
|
+
"billing_street": billing_street,
|
|
1388
|
+
"billing_city": company_data.get("city"),
|
|
1389
|
+
"billing_state": company_data.get("state"),
|
|
1390
|
+
"billing_zip": company_data.get("postal_code")
|
|
1391
|
+
or company_data.get("zip")
|
|
1392
|
+
or company_data.get("zipcode"),
|
|
1393
|
+
"billing_country": company_data.get("country"),
|
|
1394
|
+
"description": company_data.get("description"),
|
|
1395
|
+
"keywords": _parse_keywords(company_data.get("keywords")),
|
|
1396
|
+
"tags": [],
|
|
1397
|
+
"notes": [],
|
|
1398
|
+
"additional_properties": {
|
|
1399
|
+
"apollo_organization_id": company_data.get("id"),
|
|
1400
|
+
"facebook_url": company_data.get("facebook_url"),
|
|
1401
|
+
"twitter_url": company_data.get("twitter_url"),
|
|
1402
|
+
"funding_stage": company_data.get("latest_funding_stage"),
|
|
1403
|
+
"total_funding": company_data.get("total_funding"),
|
|
1404
|
+
"technology_names": company_data.get("technology_names"),
|
|
1405
|
+
"primary_phone": primary_phone if isinstance(primary_phone, dict) else None,
|
|
1406
|
+
"raw_address": company_data.get("raw_address"),
|
|
1407
|
+
"organization_revenue_printed": company_data.get("organization_revenue_printed"),
|
|
1408
|
+
"apollo_organization_data": json.dumps(cleanup_properties(company_data)),
|
|
1409
|
+
},
|
|
1410
|
+
"research_summary": None,
|
|
1411
|
+
"enchrichment_status": None,
|
|
1343
1412
|
}
|
|
1344
1413
|
|
|
1345
|
-
return
|
|
1414
|
+
return account
|
|
1346
1415
|
|
|
1347
1416
|
|
|
1348
1417
|
@assistant_tool
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import hashlib
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
5
6
|
|
|
6
7
|
from azure.storage.blob import BlobServiceClient
|
|
7
8
|
from azure.core.exceptions import ResourceNotFoundError, AzureError
|
|
@@ -49,10 +50,11 @@ def cache_output(tool_name: str, key: str, value, ttl: int = None) -> bool:
|
|
|
49
50
|
# Construct the blob name using a virtual folder for the tool name
|
|
50
51
|
blob_name = f"{tool_name}/{key_hash}.json"
|
|
51
52
|
|
|
52
|
-
# Prepare the cache data
|
|
53
|
+
# Prepare the cache data with timestamp for TTL expiration checking
|
|
53
54
|
cache_data = {
|
|
54
55
|
"value": value,
|
|
55
|
-
"ttl": ttl
|
|
56
|
+
"ttl": ttl,
|
|
57
|
+
"cached_at": datetime.now(timezone.utc).isoformat()
|
|
56
58
|
}
|
|
57
59
|
data = json.dumps(cache_data)
|
|
58
60
|
|
|
@@ -88,6 +90,24 @@ def retrieve_output(tool_name: str, key: str):
|
|
|
88
90
|
download_stream = blob_client.download_blob()
|
|
89
91
|
content = download_stream.readall() # content is in bytes
|
|
90
92
|
cache_data = json.loads(content.decode("utf-8"))
|
|
93
|
+
|
|
94
|
+
# Check if TTL has expired
|
|
95
|
+
ttl = cache_data.get("ttl")
|
|
96
|
+
cached_at = cache_data.get("cached_at")
|
|
97
|
+
|
|
98
|
+
if ttl is not None and cached_at is not None:
|
|
99
|
+
try:
|
|
100
|
+
cached_time = datetime.fromisoformat(cached_at)
|
|
101
|
+
now = datetime.now(timezone.utc)
|
|
102
|
+
elapsed_seconds = (now - cached_time).total_seconds()
|
|
103
|
+
if elapsed_seconds > ttl:
|
|
104
|
+
logger.info(f"Cache expired for blob '{blob_name}' (elapsed: {elapsed_seconds}s, ttl: {ttl}s)")
|
|
105
|
+
return None
|
|
106
|
+
except (ValueError, TypeError) as e:
|
|
107
|
+
logger.warning(f"Error parsing cached_at timestamp: {e}")
|
|
108
|
+
# If we can't parse the timestamp, treat as expired for safety
|
|
109
|
+
return None
|
|
110
|
+
|
|
91
111
|
return cache_data.get("value")
|
|
92
112
|
except ResourceNotFoundError:
|
|
93
113
|
# Blob does not exist
|
|
@@ -16,6 +16,9 @@ from urllib.parse import urlparse, urlunparse
|
|
|
16
16
|
logging.basicConfig(level=logging.INFO)
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
+
# Cache TTL for Proxycurl responses: 14 days in seconds
|
|
20
|
+
PROXYCURL_CACHE_TTL = 14 * 24 * 60 * 60 # 1,209,600 seconds
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def get_proxycurl_access_token(tool_config: Optional[List[Dict]] = None) -> str:
|
|
21
24
|
"""
|
|
@@ -117,7 +120,7 @@ async def enrich_person_info_from_proxycurl(
|
|
|
117
120
|
if response.status == 200:
|
|
118
121
|
result = await response.json()
|
|
119
122
|
if linkedin_url:
|
|
120
|
-
cache_output("enrich_person_info_from_proxycurl", linkedin_url, result)
|
|
123
|
+
cache_output("enrich_person_info_from_proxycurl", linkedin_url, result, ttl=PROXYCURL_CACHE_TTL)
|
|
121
124
|
logger.info("Successfully retrieved person info from Proxycurl.")
|
|
122
125
|
return result
|
|
123
126
|
elif response.status == 404:
|
|
@@ -195,14 +198,14 @@ async def lookup_person_in_proxy_curl_by_name(
|
|
|
195
198
|
logger.debug(f"Received response status: {response.status}")
|
|
196
199
|
if response.status == 200:
|
|
197
200
|
result = await response.json()
|
|
198
|
-
cache_output("lookup_person_in_proxycurl_by_name", key, result)
|
|
201
|
+
cache_output("lookup_person_in_proxycurl_by_name", key, result, ttl=PROXYCURL_CACHE_TTL)
|
|
199
202
|
logger.info("Successfully retrieved person search info from Proxycurl.")
|
|
200
203
|
return result
|
|
201
204
|
elif response.status == 404:
|
|
202
205
|
msg = "Person not found"
|
|
203
206
|
logger.warning(msg)
|
|
204
207
|
if key:
|
|
205
|
-
cache_output("lookup_person_in_proxycurl_by_name", key, {'error': msg})
|
|
208
|
+
cache_output("lookup_person_in_proxycurl_by_name", key, {'error': msg}, ttl=PROXYCURL_CACHE_TTL)
|
|
206
209
|
return {'error': msg}
|
|
207
210
|
elif response.status == 429:
|
|
208
211
|
msg = "Rate limit exceeded"
|
|
@@ -415,7 +418,7 @@ async def enrich_organization_info_from_proxycurl(
|
|
|
415
418
|
if response.status == 200:
|
|
416
419
|
result = await response.json()
|
|
417
420
|
transformed_result = transform_company_data(result)
|
|
418
|
-
cache_output("enrich_organization_info_from_proxycurl", cache_key, transformed_result)
|
|
421
|
+
cache_output("enrich_organization_info_from_proxycurl", cache_key, transformed_result, ttl=PROXYCURL_CACHE_TTL)
|
|
419
422
|
logger.info("Successfully retrieved and transformed organization info from Proxycurl by LinkedIn URL.")
|
|
420
423
|
return transformed_result
|
|
421
424
|
elif response.status == 429:
|
|
@@ -429,7 +432,7 @@ async def enrich_organization_info_from_proxycurl(
|
|
|
429
432
|
f"Proxycurl organization profile not found for LinkedIn URL {standardized_url}: {error_text}"
|
|
430
433
|
)
|
|
431
434
|
cache_output(
|
|
432
|
-
"enrich_organization_info_from_proxycurl", cache_key, {}
|
|
435
|
+
"enrich_organization_info_from_proxycurl", cache_key, {}, ttl=PROXYCURL_CACHE_TTL
|
|
433
436
|
)
|
|
434
437
|
return {}
|
|
435
438
|
else:
|
|
@@ -483,7 +486,7 @@ async def enrich_organization_info_from_proxycurl(
|
|
|
483
486
|
if profile_response.status == 200:
|
|
484
487
|
result = await profile_response.json()
|
|
485
488
|
transformed_result = transform_company_data(result)
|
|
486
|
-
cache_output("enrich_organization_info_from_proxycurl", domain_cache_key, transformed_result)
|
|
489
|
+
cache_output("enrich_organization_info_from_proxycurl", domain_cache_key, transformed_result, ttl=PROXYCURL_CACHE_TTL)
|
|
487
490
|
logger.info("Successfully retrieved and transformed organization info from Proxycurl by domain.")
|
|
488
491
|
return transformed_result
|
|
489
492
|
elif profile_response.status == 429:
|
|
@@ -509,7 +512,7 @@ async def enrich_organization_info_from_proxycurl(
|
|
|
509
512
|
elif response.status == 404:
|
|
510
513
|
msg = "Item not found"
|
|
511
514
|
logger.warning(msg)
|
|
512
|
-
cache_output("enrich_organization_info_from_proxycurl", domain_cache_key, {})
|
|
515
|
+
cache_output("enrich_organization_info_from_proxycurl", domain_cache_key, {}, ttl=PROXYCURL_CACHE_TTL)
|
|
513
516
|
return {}
|
|
514
517
|
else:
|
|
515
518
|
error_text = await response.text()
|
|
@@ -572,7 +575,7 @@ async def enrich_job_info_from_proxycurl(
|
|
|
572
575
|
logger.debug(f"Received response status: {response.status}")
|
|
573
576
|
if response.status == 200:
|
|
574
577
|
result = await response.json()
|
|
575
|
-
cache_output("enrich_job_info_from_proxycurl", job_url, result)
|
|
578
|
+
cache_output("enrich_job_info_from_proxycurl", job_url, result, ttl=PROXYCURL_CACHE_TTL)
|
|
576
579
|
logger.info("Successfully retrieved job info from Proxycurl.")
|
|
577
580
|
return result
|
|
578
581
|
elif response.status == 429:
|
|
@@ -583,7 +586,7 @@ async def enrich_job_info_from_proxycurl(
|
|
|
583
586
|
elif response.status == 404:
|
|
584
587
|
msg = "Job not found"
|
|
585
588
|
logger.warning(msg)
|
|
586
|
-
cache_output("enrich_job_info_from_proxycurl", job_url, {'error': msg})
|
|
589
|
+
cache_output("enrich_job_info_from_proxycurl", job_url, {'error': msg}, ttl=PROXYCURL_CACHE_TTL)
|
|
587
590
|
return {'error': msg}
|
|
588
591
|
else:
|
|
589
592
|
error_text = await response.text()
|
|
@@ -84,31 +84,37 @@ class TestApolloCompanySearch(unittest.IsolatedAsyncioTestCase):
|
|
|
84
84
|
company_data = self.mock_apollo_response["organizations"][0]
|
|
85
85
|
result = fill_in_company_properties(company_data)
|
|
86
86
|
|
|
87
|
-
self.assertEqual(result["
|
|
88
|
-
self.assertEqual(result["
|
|
89
|
-
self.assertEqual(result["
|
|
90
|
-
self.assertEqual(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
self.assertEqual(result["
|
|
95
|
-
self.assertEqual(result["
|
|
96
|
-
self.assertEqual(result["
|
|
87
|
+
self.assertEqual(result["name"], "Example Corp")
|
|
88
|
+
self.assertEqual(result["domain"], "example.com")
|
|
89
|
+
self.assertEqual(result["website"], "https://example.com")
|
|
90
|
+
self.assertEqual(
|
|
91
|
+
result["organization_linkedin_url"],
|
|
92
|
+
"https://linkedin.com/company/example-corp",
|
|
93
|
+
)
|
|
94
|
+
self.assertEqual(result["billing_city"], "San Francisco")
|
|
95
|
+
self.assertEqual(result["billing_state"], "California")
|
|
96
|
+
self.assertEqual(result["billing_country"], "United States")
|
|
97
|
+
self.assertEqual(result["company_size"], 150)
|
|
98
|
+
self.assertEqual(result["annual_revenue"], 10000000.0)
|
|
97
99
|
self.assertEqual(result["industry"], "Technology")
|
|
98
|
-
self.assertEqual(result["keywords"], "saas, software")
|
|
100
|
+
self.assertEqual(result["keywords"], ["saas", "software"])
|
|
99
101
|
self.assertEqual(result["description"], "A technology company")
|
|
100
102
|
self.assertEqual(result["founded_year"], 2010)
|
|
101
|
-
self.assertEqual(result["funding_stage"], "Series B")
|
|
102
|
-
self.assertEqual(result["total_funding"], 5000000)
|
|
103
|
-
self.assertEqual(result["technology_stack"], "React, Python, AWS")
|
|
104
|
-
self.assertEqual(result["apollo_organization_id"], "123456")
|
|
105
103
|
self.assertEqual(result["phone"], "+1-555-123-4567")
|
|
106
|
-
self.assertEqual(result["facebook_url"], "https://facebook.com/example-corp")
|
|
107
|
-
self.assertEqual(result["twitter_url"], "https://twitter.com/example_corp")
|
|
108
104
|
|
|
109
105
|
# Check that additional_properties contains the raw data
|
|
110
106
|
self.assertIn("additional_properties", result)
|
|
111
107
|
self.assertIn("apollo_organization_data", result["additional_properties"])
|
|
108
|
+
self.assertEqual(result["additional_properties"]["apollo_organization_id"], "123456")
|
|
109
|
+
self.assertEqual(
|
|
110
|
+
result["additional_properties"]["facebook_url"], "https://facebook.com/example-corp"
|
|
111
|
+
)
|
|
112
|
+
self.assertEqual(
|
|
113
|
+
result["additional_properties"]["twitter_url"], "https://twitter.com/example_corp"
|
|
114
|
+
)
|
|
115
|
+
self.assertEqual(result["additional_properties"]["funding_stage"], "Series B")
|
|
116
|
+
self.assertEqual(result["additional_properties"]["total_funding"], 5000000)
|
|
117
|
+
self.assertEqual(result["additional_properties"]["technology_names"], ["React", "Python", "AWS"])
|
|
112
118
|
|
|
113
119
|
@patch('src.dhisana.utils.apollo_tools.fetch_apollo_data')
|
|
114
120
|
@patch('src.dhisana.utils.apollo_tools.get_apollo_access_token')
|
|
@@ -141,8 +147,8 @@ class TestApolloCompanySearch(unittest.IsolatedAsyncioTestCase):
|
|
|
141
147
|
|
|
142
148
|
# Check results
|
|
143
149
|
self.assertEqual(len(result["results"]), 2)
|
|
144
|
-
self.assertEqual(result["results"][0]["
|
|
145
|
-
self.assertEqual(result["results"][1]["
|
|
150
|
+
self.assertEqual(result["results"][0]["name"], "Example Corp")
|
|
151
|
+
self.assertEqual(result["results"][1]["name"], "Test Inc")
|
|
146
152
|
|
|
147
153
|
# Verify API call was made correctly
|
|
148
154
|
mock_fetch_data.assert_called_once()
|
|
@@ -207,6 +213,22 @@ class TestApolloCompanySearch(unittest.IsolatedAsyncioTestCase):
|
|
|
207
213
|
self.assertEqual(query.sort_by_field, "employee_count")
|
|
208
214
|
self.assertFalse(query.sort_ascending)
|
|
209
215
|
|
|
216
|
+
def test_fill_in_company_properties_prefers_primary_phone_and_org_revenue(self):
|
|
217
|
+
company_data = dict(self.mock_apollo_response["organizations"][0])
|
|
218
|
+
company_data["primary_phone"] = {
|
|
219
|
+
"number": "+971 4 433 2589",
|
|
220
|
+
"source": "Account",
|
|
221
|
+
"sanitized_number": "+97144332589",
|
|
222
|
+
}
|
|
223
|
+
company_data.pop("annual_revenue", None)
|
|
224
|
+
company_data["organization_revenue_printed"] = "35M"
|
|
225
|
+
company_data["organization_revenue"] = 35000000.0
|
|
226
|
+
|
|
227
|
+
result = fill_in_company_properties(company_data)
|
|
228
|
+
self.assertEqual(result["phone"], "+971 4 433 2589")
|
|
229
|
+
self.assertEqual(result["annual_revenue"], 35000000.0)
|
|
230
|
+
self.assertEqual(result["additional_properties"]["organization_revenue_printed"], "35M")
|
|
231
|
+
|
|
210
232
|
@patch('src.dhisana.utils.apollo_tools.fetch_apollo_data')
|
|
211
233
|
@patch('src.dhisana.utils.apollo_tools.get_apollo_access_token')
|
|
212
234
|
async def test_search_companies_with_apollo_basic(self, mock_get_token, mock_fetch_data):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/parse_linkedin_messages_txt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serarch_router_local_business.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev249 → dhisana-0.0.1.dev251}/src/dhisana/utils/serpapi_local_business_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|