dhisana 0.0.1.dev277__py3-none-any.whl → 0.0.1.dev279__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/utils/apollo_tools.py +405 -9
- dhisana/utils/enrich_lead_information.py +82 -16
- dhisana/utils/test_connect.py +197 -0
- {dhisana-0.0.1.dev277.dist-info → dhisana-0.0.1.dev279.dist-info}/METADATA +1 -1
- {dhisana-0.0.1.dev277.dist-info → dhisana-0.0.1.dev279.dist-info}/RECORD +8 -8
- {dhisana-0.0.1.dev277.dist-info → dhisana-0.0.1.dev279.dist-info}/WHEEL +0 -0
- {dhisana-0.0.1.dev277.dist-info → dhisana-0.0.1.dev279.dist-info}/entry_points.txt +0 -0
- {dhisana-0.0.1.dev277.dist-info → dhisana-0.0.1.dev279.dist-info}/top_level.txt +0 -0
dhisana/utils/apollo_tools.py
CHANGED
|
@@ -1345,7 +1345,14 @@ def fill_in_company_properties(company_data: dict) -> dict:
|
|
|
1345
1345
|
if annual_revenue is None:
|
|
1346
1346
|
annual_revenue = _parse_compact_number(company_data.get("organization_revenue_printed"))
|
|
1347
1347
|
|
|
1348
|
-
|
|
1348
|
+
# Try multiple fields for company size/employee count
|
|
1349
|
+
company_size = (
|
|
1350
|
+
company_data.get("estimated_num_employees")
|
|
1351
|
+
or company_data.get("num_employees")
|
|
1352
|
+
or company_data.get("employee_count")
|
|
1353
|
+
or company_data.get("employees_count")
|
|
1354
|
+
or company_data.get("headcount")
|
|
1355
|
+
)
|
|
1349
1356
|
if company_size is not None:
|
|
1350
1357
|
try:
|
|
1351
1358
|
company_size = int(company_size)
|
|
@@ -1373,10 +1380,14 @@ def fill_in_company_properties(company_data: dict) -> dict:
|
|
|
1373
1380
|
or company_data.get("sanitized_phone")
|
|
1374
1381
|
)
|
|
1375
1382
|
|
|
1383
|
+
# Try multiple fields for industry
|
|
1376
1384
|
industry = company_data.get("industry")
|
|
1377
1385
|
if not industry and isinstance(company_data.get("industries"), list):
|
|
1378
1386
|
industries = [str(x).strip() for x in company_data["industries"] if str(x).strip()]
|
|
1379
1387
|
industry = industries[0] if industries else None
|
|
1388
|
+
# Some Apollo responses have industry_tag_id but not industry name
|
|
1389
|
+
if not industry and company_data.get("industry_tag_id"):
|
|
1390
|
+
industry = company_data.get("industry_tag_id")
|
|
1380
1391
|
|
|
1381
1392
|
billing_street = (
|
|
1382
1393
|
company_data.get("street_address")
|
|
@@ -1385,19 +1396,42 @@ def fill_in_company_properties(company_data: dict) -> dict:
|
|
|
1385
1396
|
or company_data.get("raw_address")
|
|
1386
1397
|
)
|
|
1387
1398
|
|
|
1399
|
+
# Determine ownership from publicly traded info
|
|
1400
|
+
ownership = company_data.get("ownership")
|
|
1401
|
+
if not ownership:
|
|
1402
|
+
if company_data.get("publicly_traded_symbol") or company_data.get("publicly_traded_exchange"):
|
|
1403
|
+
ownership = "public"
|
|
1404
|
+
|
|
1405
|
+
# Parse market cap
|
|
1406
|
+
market_cap = _parse_compact_number(company_data.get("market_cap"))
|
|
1407
|
+
|
|
1408
|
+
# Build account dictionary with ProxyCurl-compatible field names
|
|
1388
1409
|
account: Dict[str, Any] = {
|
|
1389
|
-
|
|
1390
|
-
"
|
|
1391
|
-
"
|
|
1410
|
+
# Primary identifiers - use ProxyCurl-compatible names
|
|
1411
|
+
"name": company_data.get("name"), # Keep for backward compatibility
|
|
1412
|
+
"organization_name": company_data.get("name"), # ProxyCurl-compatible
|
|
1413
|
+
"domain": company_data.get("primary_domain"), # Keep for backward compatibility
|
|
1414
|
+
"primary_domain_of_organization": company_data.get("primary_domain"), # ProxyCurl-compatible
|
|
1415
|
+
"website": company_data.get("website_url"), # Keep for backward compatibility
|
|
1416
|
+
"organization_website": company_data.get("website_url"), # ProxyCurl-compatible
|
|
1417
|
+
"organization_linkedin_url": company_data.get("linkedin_url"),
|
|
1418
|
+
|
|
1419
|
+
# Contact info
|
|
1392
1420
|
"phone": phone,
|
|
1393
1421
|
"fax": company_data.get("fax") or company_data.get("fax_number"),
|
|
1394
|
-
|
|
1395
|
-
|
|
1422
|
+
|
|
1423
|
+
# Business details - use ProxyCurl-compatible names
|
|
1424
|
+
"industry": industry, # Keep for backward compatibility
|
|
1425
|
+
"organization_industry": industry, # ProxyCurl-compatible
|
|
1426
|
+
"company_size": company_size, # Keep for backward compatibility
|
|
1427
|
+
"organization_size": company_size, # ProxyCurl-compatible
|
|
1396
1428
|
"founded_year": founded_year,
|
|
1397
1429
|
"annual_revenue": annual_revenue,
|
|
1398
1430
|
"type": company_data.get("type") or company_data.get("organization_type"),
|
|
1399
|
-
"ownership":
|
|
1400
|
-
"
|
|
1431
|
+
"ownership": ownership,
|
|
1432
|
+
"description": company_data.get("description") or company_data.get("short_description"),
|
|
1433
|
+
|
|
1434
|
+
# Address info
|
|
1401
1435
|
"billing_street": billing_street,
|
|
1402
1436
|
"billing_city": company_data.get("city"),
|
|
1403
1437
|
"billing_state": company_data.get("state"),
|
|
@@ -1405,20 +1439,44 @@ def fill_in_company_properties(company_data: dict) -> dict:
|
|
|
1405
1439
|
or company_data.get("zip")
|
|
1406
1440
|
or company_data.get("zipcode"),
|
|
1407
1441
|
"billing_country": company_data.get("country"),
|
|
1408
|
-
|
|
1442
|
+
|
|
1443
|
+
# Build organization_hq_location like ProxyCurl does
|
|
1444
|
+
"organization_hq_location": ", ".join(filter(None, [
|
|
1445
|
+
company_data.get("city"),
|
|
1446
|
+
company_data.get("state"),
|
|
1447
|
+
company_data.get("country")
|
|
1448
|
+
])) or None,
|
|
1449
|
+
|
|
1450
|
+
# Other fields
|
|
1409
1451
|
"keywords": _parse_keywords(company_data.get("keywords")),
|
|
1410
1452
|
"tags": [],
|
|
1411
1453
|
"notes": [],
|
|
1412
1454
|
"additional_properties": {
|
|
1413
1455
|
"apollo_organization_id": company_data.get("id"),
|
|
1456
|
+
"logo_url": company_data.get("logo_url"),
|
|
1414
1457
|
"facebook_url": company_data.get("facebook_url"),
|
|
1415
1458
|
"twitter_url": company_data.get("twitter_url"),
|
|
1459
|
+
"angellist_url": company_data.get("angellist_url"),
|
|
1460
|
+
"crunchbase_url": company_data.get("crunchbase_url"),
|
|
1461
|
+
"blog_url": company_data.get("blog_url"),
|
|
1416
1462
|
"funding_stage": company_data.get("latest_funding_stage"),
|
|
1417
1463
|
"total_funding": company_data.get("total_funding"),
|
|
1418
1464
|
"technology_names": company_data.get("technology_names"),
|
|
1419
1465
|
"primary_phone": primary_phone if isinstance(primary_phone, dict) else None,
|
|
1420
1466
|
"raw_address": company_data.get("raw_address"),
|
|
1421
1467
|
"organization_revenue_printed": company_data.get("organization_revenue_printed"),
|
|
1468
|
+
"publicly_traded_symbol": company_data.get("publicly_traded_symbol"),
|
|
1469
|
+
"publicly_traded_exchange": company_data.get("publicly_traded_exchange"),
|
|
1470
|
+
"market_cap": market_cap,
|
|
1471
|
+
"market_cap_printed": company_data.get("market_cap"),
|
|
1472
|
+
"sic_codes": company_data.get("sic_codes"),
|
|
1473
|
+
"naics_codes": company_data.get("naics_codes"),
|
|
1474
|
+
"languages": company_data.get("languages"),
|
|
1475
|
+
"alexa_ranking": company_data.get("alexa_ranking"),
|
|
1476
|
+
"linkedin_uid": company_data.get("linkedin_uid"),
|
|
1477
|
+
"headcount_6_month_growth": company_data.get("organization_headcount_six_month_growth"),
|
|
1478
|
+
"headcount_12_month_growth": company_data.get("organization_headcount_twelve_month_growth"),
|
|
1479
|
+
"headcount_24_month_growth": company_data.get("organization_headcount_twenty_four_month_growth"),
|
|
1422
1480
|
"apollo_organization_data": json.dumps(cleanup_properties(company_data)),
|
|
1423
1481
|
},
|
|
1424
1482
|
"research_summary": None,
|
|
@@ -1726,3 +1784,341 @@ async def search_companies_with_apollo_page(
|
|
|
1726
1784
|
"next_page": current_page + 1 if has_next_page else None,
|
|
1727
1785
|
"results": companies
|
|
1728
1786
|
}
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def _extract_domain_from_url(url: str) -> Optional[str]:
|
|
1790
|
+
"""
|
|
1791
|
+
Extract domain from a URL.
|
|
1792
|
+
|
|
1793
|
+
Args:
|
|
1794
|
+
url: The URL to extract domain from
|
|
1795
|
+
|
|
1796
|
+
Returns:
|
|
1797
|
+
The extracted domain or None if extraction fails
|
|
1798
|
+
"""
|
|
1799
|
+
if not url:
|
|
1800
|
+
return None
|
|
1801
|
+
|
|
1802
|
+
try:
|
|
1803
|
+
# Handle URLs without scheme
|
|
1804
|
+
if not url.startswith(('http://', 'https://')):
|
|
1805
|
+
url = 'https://' + url
|
|
1806
|
+
|
|
1807
|
+
parsed = urlparse(url)
|
|
1808
|
+
domain = parsed.netloc or parsed.path.split('/')[0]
|
|
1809
|
+
|
|
1810
|
+
# Remove www. prefix if present
|
|
1811
|
+
if domain.startswith('www.'):
|
|
1812
|
+
domain = domain[4:]
|
|
1813
|
+
|
|
1814
|
+
return domain if domain else None
|
|
1815
|
+
except Exception:
|
|
1816
|
+
return None
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
def _extract_linkedin_company_identifier(linkedin_url: str) -> Optional[str]:
|
|
1820
|
+
"""
|
|
1821
|
+
Extract the company identifier from a LinkedIn company URL.
|
|
1822
|
+
|
|
1823
|
+
Args:
|
|
1824
|
+
linkedin_url: LinkedIn company URL (e.g., https://www.linkedin.com/company/microsoft)
|
|
1825
|
+
|
|
1826
|
+
Returns:
|
|
1827
|
+
The company identifier (e.g., 'microsoft') or None if extraction fails
|
|
1828
|
+
"""
|
|
1829
|
+
if not linkedin_url:
|
|
1830
|
+
return None
|
|
1831
|
+
|
|
1832
|
+
try:
|
|
1833
|
+
# Normalize the URL
|
|
1834
|
+
url = linkedin_url.strip().rstrip('/')
|
|
1835
|
+
|
|
1836
|
+
# Handle various LinkedIn URL formats
|
|
1837
|
+
# https://www.linkedin.com/company/microsoft
|
|
1838
|
+
# https://linkedin.com/company/microsoft/
|
|
1839
|
+
# linkedin.com/company/microsoft
|
|
1840
|
+
|
|
1841
|
+
if not url.startswith(('http://', 'https://')):
|
|
1842
|
+
url = 'https://' + url
|
|
1843
|
+
|
|
1844
|
+
parsed = urlparse(url)
|
|
1845
|
+
path_parts = [p for p in parsed.path.split('/') if p]
|
|
1846
|
+
|
|
1847
|
+
# Look for 'company' in path and get the next segment
|
|
1848
|
+
if 'company' in path_parts:
|
|
1849
|
+
company_idx = path_parts.index('company')
|
|
1850
|
+
if company_idx + 1 < len(path_parts):
|
|
1851
|
+
return path_parts[company_idx + 1]
|
|
1852
|
+
|
|
1853
|
+
return None
|
|
1854
|
+
except Exception:
|
|
1855
|
+
return None
|
|
1856
|
+
|
|
1857
|
+
|
|
1858
|
+
@assistant_tool
|
|
1859
|
+
@backoff.on_exception(
|
|
1860
|
+
backoff.expo,
|
|
1861
|
+
aiohttp.ClientResponseError,
|
|
1862
|
+
max_tries=2,
|
|
1863
|
+
giveup=lambda e: e.status != 429,
|
|
1864
|
+
factor=10,
|
|
1865
|
+
)
|
|
1866
|
+
async def search_organization_by_linkedin_or_domain(
|
|
1867
|
+
linkedin_url: Optional[str] = None,
|
|
1868
|
+
domain: Optional[str] = None,
|
|
1869
|
+
tool_config: Optional[List[Dict]] = None,
|
|
1870
|
+
) -> Dict[str, Any]:
|
|
1871
|
+
"""
|
|
1872
|
+
Search for an organization in Apollo using LinkedIn URL or domain and return
|
|
1873
|
+
standardized organization information.
|
|
1874
|
+
|
|
1875
|
+
This function uses Apollo's mixed_companies/search endpoint to find companies
|
|
1876
|
+
by their LinkedIn URL or domain, then transforms the result into a standardized
|
|
1877
|
+
organization information format.
|
|
1878
|
+
|
|
1879
|
+
Parameters:
|
|
1880
|
+
- **linkedin_url** (*str*, optional): LinkedIn company URL
|
|
1881
|
+
(e.g., https://www.linkedin.com/company/microsoft)
|
|
1882
|
+
- **domain** (*str*, optional): Company domain (e.g., microsoft.com)
|
|
1883
|
+
|
|
1884
|
+
At least one of linkedin_url or domain must be provided.
|
|
1885
|
+
|
|
1886
|
+
Returns:
|
|
1887
|
+
- **dict**: Standardized organization information containing:
|
|
1888
|
+
- name: Company name
|
|
1889
|
+
- domain: Primary domain
|
|
1890
|
+
- website: Company website URL
|
|
1891
|
+
- phone: Primary phone number
|
|
1892
|
+
- industry: Primary industry
|
|
1893
|
+
- company_size: Number of employees
|
|
1894
|
+
- founded_year: Year company was founded
|
|
1895
|
+
- annual_revenue: Annual revenue
|
|
1896
|
+
- organization_linkedin_url: LinkedIn company URL
|
|
1897
|
+
- billing_street, billing_city, billing_state, billing_zip, billing_country: Address info
|
|
1898
|
+
- description: Company description
|
|
1899
|
+
- keywords: List of keywords/tags
|
|
1900
|
+
- additional_properties: Additional Apollo-specific data
|
|
1901
|
+
- error: Error message if search fails
|
|
1902
|
+
"""
|
|
1903
|
+
logger.info("Entering search_organization_by_linkedin_or_domain")
|
|
1904
|
+
|
|
1905
|
+
if not linkedin_url and not domain:
|
|
1906
|
+
logger.warning("No linkedin_url or domain provided. At least one is required.")
|
|
1907
|
+
return {'error': "At least one of linkedin_url or domain must be provided"}
|
|
1908
|
+
|
|
1909
|
+
token, is_oauth = get_apollo_access_token(tool_config)
|
|
1910
|
+
|
|
1911
|
+
headers = {
|
|
1912
|
+
"Content-Type": "application/json",
|
|
1913
|
+
"Cache-Control": "no-cache",
|
|
1914
|
+
}
|
|
1915
|
+
if is_oauth:
|
|
1916
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
1917
|
+
else:
|
|
1918
|
+
headers["X-Api-Key"] = token
|
|
1919
|
+
|
|
1920
|
+
# Build the search payload
|
|
1921
|
+
payload: Dict[str, Any] = {
|
|
1922
|
+
"page": 1,
|
|
1923
|
+
"per_page": 25, # Get more results to improve matching
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
# Add LinkedIn URL filter if provided
|
|
1927
|
+
if linkedin_url:
|
|
1928
|
+
# Extract the company identifier for keyword search
|
|
1929
|
+
company_identifier = _extract_linkedin_company_identifier(linkedin_url)
|
|
1930
|
+
|
|
1931
|
+
# Normalize the LinkedIn URL for matching
|
|
1932
|
+
normalized_linkedin = linkedin_url.strip().rstrip('/')
|
|
1933
|
+
if not normalized_linkedin.startswith(('http://', 'https://')):
|
|
1934
|
+
normalized_linkedin = 'https://' + normalized_linkedin
|
|
1935
|
+
|
|
1936
|
+
# Use q_organization_name for better search results
|
|
1937
|
+
# The company identifier from LinkedIn URL is usually the company name
|
|
1938
|
+
if company_identifier:
|
|
1939
|
+
payload["q_organization_name"] = company_identifier
|
|
1940
|
+
|
|
1941
|
+
# Add domain filter if provided
|
|
1942
|
+
if domain:
|
|
1943
|
+
# Clean the domain (remove http://, https://, www., etc.)
|
|
1944
|
+
clean_domain = _extract_domain_from_url(domain) or domain
|
|
1945
|
+
payload["q_organization_domains_list"] = [clean_domain]
|
|
1946
|
+
|
|
1947
|
+
url = "https://api.apollo.io/api/v1/mixed_companies/search"
|
|
1948
|
+
logger.debug(f"Making POST request to Apollo organization search with payload: {json.dumps(payload, indent=2)}")
|
|
1949
|
+
|
|
1950
|
+
async with aiohttp.ClientSession() as session:
|
|
1951
|
+
try:
|
|
1952
|
+
async with session.post(url, headers=headers, json=payload) as response:
|
|
1953
|
+
logger.debug(f"Received response status: {response.status}")
|
|
1954
|
+
|
|
1955
|
+
if response.status == 200:
|
|
1956
|
+
result = await response.json()
|
|
1957
|
+
|
|
1958
|
+
# Extract organizations from response
|
|
1959
|
+
organizations = result.get("organizations", [])
|
|
1960
|
+
accounts = result.get("accounts", [])
|
|
1961
|
+
all_results = organizations + accounts
|
|
1962
|
+
|
|
1963
|
+
if not all_results:
|
|
1964
|
+
logger.info("No organizations found matching the criteria.")
|
|
1965
|
+
return {
|
|
1966
|
+
'error': 'No organizations found matching the provided criteria',
|
|
1967
|
+
'search_criteria': {
|
|
1968
|
+
'linkedin_url': linkedin_url,
|
|
1969
|
+
'domain': domain
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
# Get the best matching organization with confidence tracking
|
|
1974
|
+
best_match = None
|
|
1975
|
+
match_confidence = None
|
|
1976
|
+
match_reason = None
|
|
1977
|
+
|
|
1978
|
+
# If we have a domain, try to find exact match first (highest confidence)
|
|
1979
|
+
if domain:
|
|
1980
|
+
clean_domain = _extract_domain_from_url(domain) or domain
|
|
1981
|
+
for org in all_results:
|
|
1982
|
+
org_domain = org.get("primary_domain", "")
|
|
1983
|
+
if org_domain and org_domain.lower() == clean_domain.lower():
|
|
1984
|
+
best_match = org
|
|
1985
|
+
match_confidence = "high"
|
|
1986
|
+
match_reason = f"exact_domain_match: {org_domain}"
|
|
1987
|
+
logger.info(f"Found exact domain match: {org.get('name')} with domain {org_domain}")
|
|
1988
|
+
break
|
|
1989
|
+
|
|
1990
|
+
# If we have LinkedIn URL, try to find exact match
|
|
1991
|
+
if not best_match and linkedin_url:
|
|
1992
|
+
# Extract company identifier from the input URL
|
|
1993
|
+
input_company_id = _extract_linkedin_company_identifier(linkedin_url)
|
|
1994
|
+
|
|
1995
|
+
for org in all_results:
|
|
1996
|
+
org_linkedin = org.get("linkedin_url", "")
|
|
1997
|
+
org_name = org.get("name", "").lower()
|
|
1998
|
+
org_domain = org.get("primary_domain", "")
|
|
1999
|
+
|
|
2000
|
+
if org_linkedin:
|
|
2001
|
+
# Extract company identifier from org's LinkedIn URL
|
|
2002
|
+
org_company_id = _extract_linkedin_company_identifier(org_linkedin)
|
|
2003
|
+
|
|
2004
|
+
# Match by company identifier (e.g., 'walmart' matches 'walmart')
|
|
2005
|
+
if input_company_id and org_company_id:
|
|
2006
|
+
if input_company_id.lower() == org_company_id.lower():
|
|
2007
|
+
best_match = org
|
|
2008
|
+
match_confidence = "high"
|
|
2009
|
+
match_reason = f"linkedin_identifier_match: {org_company_id}"
|
|
2010
|
+
logger.info(f"Found LinkedIn identifier match: {org.get('name')} with identifier {org_company_id}")
|
|
2011
|
+
break
|
|
2012
|
+
|
|
2013
|
+
# Also try direct URL comparison
|
|
2014
|
+
normalized_input = linkedin_url.lower().rstrip('/').replace('www.', '')
|
|
2015
|
+
normalized_org = org_linkedin.lower().rstrip('/').replace('www.', '')
|
|
2016
|
+
if normalized_input in normalized_org or normalized_org in normalized_input:
|
|
2017
|
+
best_match = org
|
|
2018
|
+
match_confidence = "high"
|
|
2019
|
+
match_reason = f"linkedin_url_match: {org_linkedin}"
|
|
2020
|
+
logger.info(f"Found LinkedIn URL match: {org.get('name')}")
|
|
2021
|
+
break
|
|
2022
|
+
|
|
2023
|
+
# Secondary match: company name contains the identifier
|
|
2024
|
+
if not best_match and input_company_id:
|
|
2025
|
+
# Check if the org name contains the identifier or vice versa
|
|
2026
|
+
input_id_lower = input_company_id.lower().replace('-', ' ').replace('_', ' ')
|
|
2027
|
+
org_name_normalized = org_name.replace('-', ' ').replace('_', ' ')
|
|
2028
|
+
|
|
2029
|
+
if input_id_lower == org_name_normalized or input_id_lower in org_name_normalized:
|
|
2030
|
+
best_match = org
|
|
2031
|
+
match_confidence = "medium"
|
|
2032
|
+
match_reason = f"name_contains_identifier: {org_name}"
|
|
2033
|
+
logger.info(f"Found name match: {org.get('name')} matches identifier {input_company_id}")
|
|
2034
|
+
break
|
|
2035
|
+
|
|
2036
|
+
# If still no match and we searched by LinkedIn, return error if no exact match found
|
|
2037
|
+
if not best_match and linkedin_url and not domain:
|
|
2038
|
+
input_company_id = _extract_linkedin_company_identifier(linkedin_url)
|
|
2039
|
+
logger.warning(f"No organization found matching LinkedIn URL: {linkedin_url}")
|
|
2040
|
+
# Log what we did find for debugging
|
|
2041
|
+
found_orgs = [{"name": org.get("name"), "linkedin": org.get("linkedin_url"), "domain": org.get("primary_domain")} for org in all_results[:5]]
|
|
2042
|
+
logger.debug(f"Found organizations (first 5): {found_orgs}")
|
|
2043
|
+
return {
|
|
2044
|
+
'error': f'No organization found matching LinkedIn company: {input_company_id or linkedin_url}',
|
|
2045
|
+
'search_criteria': {
|
|
2046
|
+
'linkedin_url': linkedin_url,
|
|
2047
|
+
'domain': domain
|
|
2048
|
+
},
|
|
2049
|
+
'total_results_returned': len(all_results)
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
# Fall back to first result only if we have other criteria (domain was provided)
|
|
2053
|
+
if not best_match:
|
|
2054
|
+
best_match = all_results[0]
|
|
2055
|
+
match_confidence = "low"
|
|
2056
|
+
match_reason = "fallback_to_first_result"
|
|
2057
|
+
logger.warning(f"Using fallback match (first result): {best_match.get('name')}")
|
|
2058
|
+
|
|
2059
|
+
# Get the organization ID to fetch full details
|
|
2060
|
+
organization_id = best_match.get("id")
|
|
2061
|
+
full_org_details = best_match # Default to search result
|
|
2062
|
+
|
|
2063
|
+
# Fetch full organization details using the organization ID
|
|
2064
|
+
if organization_id:
|
|
2065
|
+
logger.info(f"Fetching full organization details for ID: {organization_id}")
|
|
2066
|
+
try:
|
|
2067
|
+
full_details = await get_organization_details_from_apollo(
|
|
2068
|
+
organization_id=organization_id,
|
|
2069
|
+
tool_config=tool_config,
|
|
2070
|
+
)
|
|
2071
|
+
if full_details and not full_details.get("error"):
|
|
2072
|
+
# Merge the full details with the search result
|
|
2073
|
+
# Full details from organization endpoint has more data
|
|
2074
|
+
full_org_details = full_details
|
|
2075
|
+
logger.info(f"Successfully fetched full organization details for {full_org_details.get('name')}")
|
|
2076
|
+
else:
|
|
2077
|
+
logger.warning(f"Could not fetch full organization details: {full_details.get('error', 'Unknown error')}")
|
|
2078
|
+
except Exception as e:
|
|
2079
|
+
logger.warning(f"Error fetching full organization details: {e}")
|
|
2080
|
+
|
|
2081
|
+
# Transform to standardized format using the full details
|
|
2082
|
+
standardized_org = fill_in_company_properties(full_org_details)
|
|
2083
|
+
|
|
2084
|
+
# Add logo_url to additional_properties if available
|
|
2085
|
+
if full_org_details.get("logo_url"):
|
|
2086
|
+
standardized_org["additional_properties"]["logo_url"] = full_org_details.get("logo_url")
|
|
2087
|
+
|
|
2088
|
+
# Add search metadata
|
|
2089
|
+
standardized_org['search_criteria'] = {
|
|
2090
|
+
'linkedin_url': linkedin_url,
|
|
2091
|
+
'domain': domain
|
|
2092
|
+
}
|
|
2093
|
+
standardized_org['total_matches_found'] = len(all_results)
|
|
2094
|
+
standardized_org['match_confidence'] = match_confidence
|
|
2095
|
+
standardized_org['match_reason'] = match_reason
|
|
2096
|
+
|
|
2097
|
+
# Log the matched organization details for verification
|
|
2098
|
+
logger.info(f"Successfully found organization: {standardized_org.get('name')} "
|
|
2099
|
+
f"(domain: {standardized_org.get('domain')}, "
|
|
2100
|
+
f"linkedin: {standardized_org.get('organization_linkedin_url')}, "
|
|
2101
|
+
f"confidence: {match_confidence})")
|
|
2102
|
+
return standardized_org
|
|
2103
|
+
|
|
2104
|
+
elif response.status == 429:
|
|
2105
|
+
msg = "Rate limit exceeded"
|
|
2106
|
+
logger.warning(msg)
|
|
2107
|
+
await asyncio.sleep(30)
|
|
2108
|
+
raise aiohttp.ClientResponseError(
|
|
2109
|
+
request_info=response.request_info,
|
|
2110
|
+
history=response.history,
|
|
2111
|
+
status=response.status,
|
|
2112
|
+
message=msg,
|
|
2113
|
+
headers=response.headers
|
|
2114
|
+
)
|
|
2115
|
+
else:
|
|
2116
|
+
result = await response.json()
|
|
2117
|
+
logger.warning(f"search_organization_by_linkedin_or_domain error: {result}")
|
|
2118
|
+
return {'error': result}
|
|
2119
|
+
|
|
2120
|
+
except aiohttp.ClientResponseError:
|
|
2121
|
+
raise
|
|
2122
|
+
except Exception as e:
|
|
2123
|
+
logger.exception("Exception occurred while searching for organization in Apollo.")
|
|
2124
|
+
return {'error': str(e)}
|
|
@@ -22,7 +22,7 @@ from dhisana.utils.field_validators import (
|
|
|
22
22
|
validation_organization_domain,
|
|
23
23
|
validate_website_url
|
|
24
24
|
)
|
|
25
|
-
from dhisana.utils.apollo_tools import enrich_user_info_with_apollo, enrich_person_info_from_apollo
|
|
25
|
+
from dhisana.utils.apollo_tools import enrich_user_info_with_apollo, enrich_person_info_from_apollo, search_organization_by_linkedin_or_domain
|
|
26
26
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
27
27
|
from dhisana.utils.domain_parser import get_domain_from_website, is_excluded_domain
|
|
28
28
|
from dhisana.utils.generate_structured_output_internal import get_structured_output_internal
|
|
@@ -804,25 +804,91 @@ async def enrich_organization_info_from_company_url(
|
|
|
804
804
|
) -> Dict[str, Any]:
|
|
805
805
|
"""
|
|
806
806
|
Given an organization LinkedIn URL, attempt to enrich its data (e.g. name, website)
|
|
807
|
-
via
|
|
807
|
+
first via Apollo API, then fallback to ProxyCurl if Apollo doesn't return results.
|
|
808
|
+
Additional Proxycurl Company API boolean flags (categories, funding_data, etc.)
|
|
808
809
|
can be supplied to control the returned payload (True -> "include"). If data is found,
|
|
809
810
|
set domain, then return the dict. Otherwise, return {}.
|
|
810
811
|
"""
|
|
812
|
+
company_data = None
|
|
813
|
+
apollo_website = None
|
|
814
|
+
apollo_domain = None
|
|
811
815
|
|
|
812
|
-
#
|
|
813
|
-
|
|
814
|
-
organization_linkedin_url
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
816
|
+
# First, try Apollo API to get company information
|
|
817
|
+
try:
|
|
818
|
+
logger.debug(f"Attempting Apollo lookup for organization LinkedIn URL: {organization_linkedin_url}")
|
|
819
|
+
apollo_result = await search_organization_by_linkedin_or_domain(
|
|
820
|
+
linkedin_url=organization_linkedin_url,
|
|
821
|
+
tool_config=tool_config,
|
|
822
|
+
)
|
|
823
|
+
if apollo_result and not apollo_result.get("error"):
|
|
824
|
+
logger.debug(f"Apollo returned company data: {apollo_result.get('organization_name')}")
|
|
825
|
+
# Store Apollo's website and domain for later use
|
|
826
|
+
apollo_website = apollo_result.get("organization_website")
|
|
827
|
+
apollo_domain = apollo_result.get("primary_domain_of_organization")
|
|
828
|
+
|
|
829
|
+
# If Apollo returned valid data, use it directly
|
|
830
|
+
# Apollo now returns ProxyCurl-compatible field names
|
|
831
|
+
if apollo_result.get("organization_name"):
|
|
832
|
+
company_data = {
|
|
833
|
+
# Primary identifiers
|
|
834
|
+
"organization_name": apollo_result.get("organization_name", ""),
|
|
835
|
+
"organization_linkedin_url": apollo_result.get("organization_linkedin_url", organization_linkedin_url),
|
|
836
|
+
"organization_website": apollo_result.get("organization_website", ""),
|
|
837
|
+
"primary_domain_of_organization": apollo_result.get("primary_domain_of_organization", ""),
|
|
838
|
+
|
|
839
|
+
# Contact info
|
|
840
|
+
"phone": apollo_result.get("phone", ""),
|
|
841
|
+
"fax": apollo_result.get("fax", ""),
|
|
842
|
+
|
|
843
|
+
# Business details - use ProxyCurl-compatible names
|
|
844
|
+
"organization_industry": apollo_result.get("organization_industry", ""),
|
|
845
|
+
"industry": apollo_result.get("industry", ""), # Keep for backward compatibility
|
|
846
|
+
"organization_size": apollo_result.get("organization_size"),
|
|
847
|
+
"company_size": apollo_result.get("company_size"), # Keep for backward compatibility
|
|
848
|
+
"founded_year": apollo_result.get("founded_year"),
|
|
849
|
+
"annual_revenue": apollo_result.get("annual_revenue"),
|
|
850
|
+
"type": apollo_result.get("type", ""),
|
|
851
|
+
"ownership": apollo_result.get("ownership", ""),
|
|
852
|
+
"description": apollo_result.get("description", ""),
|
|
853
|
+
|
|
854
|
+
# Location info
|
|
855
|
+
"organization_hq_location": apollo_result.get("organization_hq_location", ""),
|
|
856
|
+
"billing_street": apollo_result.get("billing_street", ""),
|
|
857
|
+
"billing_city": apollo_result.get("billing_city", ""),
|
|
858
|
+
"billing_state": apollo_result.get("billing_state", ""),
|
|
859
|
+
"billing_zip": apollo_result.get("billing_zip", ""),
|
|
860
|
+
"billing_country": apollo_result.get("billing_country", ""),
|
|
861
|
+
|
|
862
|
+
# Other fields
|
|
863
|
+
"keywords": apollo_result.get("keywords", []),
|
|
864
|
+
"additional_properties": apollo_result.get("additional_properties", {}),
|
|
865
|
+
}
|
|
866
|
+
except Exception as e:
|
|
867
|
+
logger.warning(f"Apollo lookup failed for {organization_linkedin_url}: {e}")
|
|
868
|
+
|
|
869
|
+
# If Apollo didn't return data, fallback to ProxyCurl
|
|
870
|
+
if not company_data:
|
|
871
|
+
logger.debug(f"Falling back to ProxyCurl for organization LinkedIn URL: {organization_linkedin_url}")
|
|
872
|
+
company_data = await enrich_organization_info_from_proxycurl(
|
|
873
|
+
organization_linkedin_url=organization_linkedin_url,
|
|
874
|
+
tool_config=tool_config,
|
|
875
|
+
categories=categories,
|
|
876
|
+
funding_data=funding_data,
|
|
877
|
+
exit_data=exit_data,
|
|
878
|
+
acquisitions=acquisitions,
|
|
879
|
+
extra=extra,
|
|
880
|
+
use_cache=use_cache,
|
|
881
|
+
fallback_to_cache=fallback_to_cache,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
# If ProxyCurl returned data but Apollo had better website/domain info, use Apollo's
|
|
885
|
+
if company_data and isinstance(company_data, dict):
|
|
886
|
+
if apollo_website and not company_data.get("organization_website"):
|
|
887
|
+
company_data["organization_website"] = apollo_website
|
|
888
|
+
if apollo_domain and not company_data.get("primary_domain_of_organization"):
|
|
889
|
+
company_data["primary_domain_of_organization"] = apollo_domain
|
|
890
|
+
|
|
891
|
+
# If we have company data, set domain and get research summary
|
|
826
892
|
if company_data and isinstance(company_data, dict):
|
|
827
893
|
await set_organization_domain(company_data, use_strict_check, tool_config)
|
|
828
894
|
summary = await research_company_with_full_info_ai(company_data, "", tool_config=tool_config)
|
dhisana/utils/test_connect.py
CHANGED
|
@@ -1869,6 +1869,151 @@ async def test_datagma(api_key: str) -> Dict[str, Any]:
|
|
|
1869
1869
|
return {"success": False, "status_code": 0, "error_message": str(e)}
|
|
1870
1870
|
|
|
1871
1871
|
|
|
1872
|
+
###############################################################################
|
|
1873
|
+
# MICROSOFT DATAVERSE CONNECTIVITY
|
|
1874
|
+
###############################################################################
|
|
1875
|
+
|
|
1876
|
+
async def test_dataverse(
|
|
1877
|
+
environment_url: str,
|
|
1878
|
+
tenant_id: str,
|
|
1879
|
+
client_id: str,
|
|
1880
|
+
client_secret: str,
|
|
1881
|
+
api_version: str = "v9.2",
|
|
1882
|
+
) -> Dict[str, Any]:
|
|
1883
|
+
"""
|
|
1884
|
+
Validate Microsoft Dataverse connectivity using client credentials OAuth.
|
|
1885
|
+
|
|
1886
|
+
Uses the OAuth 2.0 client credentials flow to obtain an access token from
|
|
1887
|
+
Microsoft Entra ID, then makes a test API call to fetch sample accounts.
|
|
1888
|
+
|
|
1889
|
+
Required:
|
|
1890
|
+
• environment_url (e.g. https://org12345.crm.dynamics.com)
|
|
1891
|
+
• tenant_id (Microsoft Entra tenant GUID)
|
|
1892
|
+
• client_id (Application/client ID from app registration)
|
|
1893
|
+
• client_secret (Client secret from app registration)
|
|
1894
|
+
|
|
1895
|
+
Optional:
|
|
1896
|
+
• api_version (default: v9.2)
|
|
1897
|
+
"""
|
|
1898
|
+
if not environment_url:
|
|
1899
|
+
return {
|
|
1900
|
+
"success": False,
|
|
1901
|
+
"status_code": 0,
|
|
1902
|
+
"error_message": "Missing environment_url for Dataverse.",
|
|
1903
|
+
}
|
|
1904
|
+
if not tenant_id:
|
|
1905
|
+
return {
|
|
1906
|
+
"success": False,
|
|
1907
|
+
"status_code": 0,
|
|
1908
|
+
"error_message": "Missing tenant_id for Dataverse.",
|
|
1909
|
+
}
|
|
1910
|
+
if not client_id:
|
|
1911
|
+
return {
|
|
1912
|
+
"success": False,
|
|
1913
|
+
"status_code": 0,
|
|
1914
|
+
"error_message": "Missing client_id for Dataverse.",
|
|
1915
|
+
}
|
|
1916
|
+
if not client_secret:
|
|
1917
|
+
return {
|
|
1918
|
+
"success": False,
|
|
1919
|
+
"status_code": 0,
|
|
1920
|
+
"error_message": "Missing client_secret for Dataverse.",
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
# Normalize environment URL
|
|
1924
|
+
environment_url = environment_url.rstrip("/")
|
|
1925
|
+
|
|
1926
|
+
# Token endpoint and scope for client credentials flow
|
|
1927
|
+
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
|
|
1928
|
+
scope = f"{environment_url}/.default"
|
|
1929
|
+
|
|
1930
|
+
try:
|
|
1931
|
+
timeout = aiohttp.ClientTimeout(total=15)
|
|
1932
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
1933
|
+
# Step 1: Get access token using client credentials
|
|
1934
|
+
token_data = {
|
|
1935
|
+
"client_id": client_id,
|
|
1936
|
+
"client_secret": client_secret,
|
|
1937
|
+
"grant_type": "client_credentials",
|
|
1938
|
+
"scope": scope,
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
async with session.post(
|
|
1942
|
+
token_url,
|
|
1943
|
+
data=token_data,
|
|
1944
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
1945
|
+
) as token_response:
|
|
1946
|
+
token_status = token_response.status
|
|
1947
|
+
token_json = await safe_json(token_response)
|
|
1948
|
+
|
|
1949
|
+
if token_status != 200:
|
|
1950
|
+
error_msg = None
|
|
1951
|
+
if isinstance(token_json, dict):
|
|
1952
|
+
error_msg = (
|
|
1953
|
+
token_json.get("error_description")
|
|
1954
|
+
or token_json.get("error")
|
|
1955
|
+
or token_json.get("message")
|
|
1956
|
+
)
|
|
1957
|
+
return {
|
|
1958
|
+
"success": False,
|
|
1959
|
+
"status_code": token_status,
|
|
1960
|
+
"error_message": error_msg or f"Token acquisition failed: {token_status}",
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
access_token = token_json.get("access_token") if token_json else None
|
|
1964
|
+
if not access_token:
|
|
1965
|
+
return {
|
|
1966
|
+
"success": False,
|
|
1967
|
+
"status_code": token_status,
|
|
1968
|
+
"error_message": "No access_token in token response.",
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
# Step 2: Test API access by fetching sample accounts
|
|
1972
|
+
api_url = f"{environment_url}/api/data/{api_version}/accounts"
|
|
1973
|
+
headers = {
|
|
1974
|
+
"Authorization": f"Bearer {access_token}",
|
|
1975
|
+
"Accept": "application/json",
|
|
1976
|
+
"OData-MaxVersion": "4.0",
|
|
1977
|
+
"OData-Version": "4.0",
|
|
1978
|
+
}
|
|
1979
|
+
params = {"$top": "5", "$select": "name,accountid"}
|
|
1980
|
+
|
|
1981
|
+
async with session.get(api_url, headers=headers, params=params) as api_response:
|
|
1982
|
+
api_status = api_response.status
|
|
1983
|
+
api_data = await safe_json(api_response)
|
|
1984
|
+
|
|
1985
|
+
if api_status != 200:
|
|
1986
|
+
error_msg = None
|
|
1987
|
+
if isinstance(api_data, dict):
|
|
1988
|
+
# Dataverse error format
|
|
1989
|
+
error_obj = api_data.get("error", {})
|
|
1990
|
+
if isinstance(error_obj, dict):
|
|
1991
|
+
error_msg = error_obj.get("message")
|
|
1992
|
+
else:
|
|
1993
|
+
error_msg = api_data.get("message") or api_data.get("error")
|
|
1994
|
+
return {
|
|
1995
|
+
"success": False,
|
|
1996
|
+
"status_code": api_status,
|
|
1997
|
+
"error_message": error_msg or f"Dataverse API error: {api_status}",
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
# Success - check if we got valid data
|
|
2001
|
+
if isinstance(api_data, dict) and "value" in api_data:
|
|
2002
|
+
record_count = len(api_data.get("value", []))
|
|
2003
|
+
return {
|
|
2004
|
+
"success": True,
|
|
2005
|
+
"status_code": api_status,
|
|
2006
|
+
"error_message": None,
|
|
2007
|
+
"message": f"Connected successfully. Found {record_count} sample accounts.",
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
return {"success": True, "status_code": api_status, "error_message": None}
|
|
2011
|
+
|
|
2012
|
+
except Exception as exc:
|
|
2013
|
+
logger.error(f"Dataverse connectivity test failed: {exc}")
|
|
2014
|
+
return {"success": False, "status_code": 0, "error_message": str(exc)}
|
|
2015
|
+
|
|
2016
|
+
|
|
1872
2017
|
###############################################################################
|
|
1873
2018
|
# MAIN CONNECTIVITY FUNCTION
|
|
1874
2019
|
###############################################################################
|
|
@@ -1902,6 +2047,7 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
|
|
|
1902
2047
|
"hunter": test_hunter,
|
|
1903
2048
|
"findymail": test_findyemail,
|
|
1904
2049
|
"datagma": test_datagma,
|
|
2050
|
+
"dataverse": test_dataverse,
|
|
1905
2051
|
"jinaai": test_jinaai,
|
|
1906
2052
|
"firefliesai": test_firefliesai,
|
|
1907
2053
|
"firecrawl": test_firecrawl,
|
|
@@ -2145,6 +2291,57 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
|
|
|
2145
2291
|
results[tool_name] = await test_twilio(account_sid, auth_token)
|
|
2146
2292
|
continue
|
|
2147
2293
|
|
|
2294
|
+
# ------------------------------------------------------------------ #
|
|
2295
|
+
# Special-case: Dataverse (client credentials OAuth)
|
|
2296
|
+
# ------------------------------------------------------------------ #
|
|
2297
|
+
if tool_name == "dataverse":
|
|
2298
|
+
environment_url = next(
|
|
2299
|
+
(c["value"] for c in config_entries if c["name"] in ("environment_url", "environmentUrl")),
|
|
2300
|
+
None,
|
|
2301
|
+
)
|
|
2302
|
+
tenant_id = next(
|
|
2303
|
+
(c["value"] for c in config_entries if c["name"] in ("tenant_id", "tenantId")),
|
|
2304
|
+
None,
|
|
2305
|
+
)
|
|
2306
|
+
client_id = next(
|
|
2307
|
+
(c["value"] for c in config_entries if c["name"] in ("client_id", "clientId")),
|
|
2308
|
+
None,
|
|
2309
|
+
)
|
|
2310
|
+
client_secret = next(
|
|
2311
|
+
(c["value"] for c in config_entries if c["name"] in ("client_secret", "clientSecret")),
|
|
2312
|
+
None,
|
|
2313
|
+
)
|
|
2314
|
+
api_version = next(
|
|
2315
|
+
(c["value"] for c in config_entries if c["name"] in ("api_version", "apiVersion")),
|
|
2316
|
+
"v9.2",
|
|
2317
|
+
)
|
|
2318
|
+
|
|
2319
|
+
if not all([environment_url, tenant_id, client_id, client_secret]):
|
|
2320
|
+
missing = []
|
|
2321
|
+
if not environment_url:
|
|
2322
|
+
missing.append("environment_url")
|
|
2323
|
+
if not tenant_id:
|
|
2324
|
+
missing.append("tenant_id")
|
|
2325
|
+
if not client_id:
|
|
2326
|
+
missing.append("client_id")
|
|
2327
|
+
if not client_secret:
|
|
2328
|
+
missing.append("client_secret")
|
|
2329
|
+
results[tool_name] = {
|
|
2330
|
+
"success": False,
|
|
2331
|
+
"status_code": 0,
|
|
2332
|
+
"error_message": f"Missing required fields: {', '.join(missing)}",
|
|
2333
|
+
}
|
|
2334
|
+
else:
|
|
2335
|
+
logger.info("Testing connectivity for Dataverse…")
|
|
2336
|
+
results[tool_name] = await test_dataverse(
|
|
2337
|
+
environment_url,
|
|
2338
|
+
tenant_id,
|
|
2339
|
+
client_id,
|
|
2340
|
+
client_secret,
|
|
2341
|
+
api_version,
|
|
2342
|
+
)
|
|
2343
|
+
continue
|
|
2344
|
+
|
|
2148
2345
|
# ------------------------------------------------------------------ #
|
|
2149
2346
|
# All other tools – expect an apiKey by default
|
|
2150
2347
|
# ------------------------------------------------------------------ #
|
|
@@ -12,7 +12,7 @@ dhisana/ui/components.py,sha256=4NXrAyl9tx2wWwoVYyABO-EOGnreGMvql1AkXWajIIo,1431
|
|
|
12
12
|
dhisana/utils/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
|
|
13
13
|
dhisana/utils/add_mapping.py,sha256=oq_QNqag86DhgdwINBRRXNx7SOb8Q9M-V0QLP6pTzr8,13837
|
|
14
14
|
dhisana/utils/agent_tools.py,sha256=pzBFvfhU4wfSB4zv1eiRzjmnteJnfhC5V32r_v1m38Y,2321
|
|
15
|
-
dhisana/utils/apollo_tools.py,sha256=
|
|
15
|
+
dhisana/utils/apollo_tools.py,sha256=o25JvQPo5__Uyv1PAWxD_PYbklof3cto5osHmnaccHE,90193
|
|
16
16
|
dhisana/utils/assistant_tool_tag.py,sha256=rYRl8ubLI7fUUIjg30XTefHBkFgRqNEVC12lF6U6Z-8,119
|
|
17
17
|
dhisana/utils/built_with_api_tools.py,sha256=TFNGhnPb2vFdveVCpjiCvE1WKe_eK95UPpR0Ha5NgMQ,10260
|
|
18
18
|
dhisana/utils/cache_output_tools.py,sha256=q-d-WR_pkIUQyCJk8T-u9sfTy1TvvWoD2kJlZfqY-vA,4392
|
|
@@ -32,7 +32,7 @@ dhisana/utils/domain_parser.py,sha256=Kw5MPP06wK2azWQzuSiOE-DffOezLqDyF-L9JEBsMS
|
|
|
32
32
|
dhisana/utils/email_body_utils.py,sha256=rlCVjdBlqNnEiUberJGXGcrYY1GQOkW0-aB6AEpS3L4,2302
|
|
33
33
|
dhisana/utils/email_parse_helpers.py,sha256=rl72ggS-yoB-w3ZHW2sevKJulQ-_8iLdpVTH6QnKPcs,6789
|
|
34
34
|
dhisana/utils/email_provider.py,sha256=ukW_0nHcjTQmpnE9pdJci78LrZcsK1_0v6kcgc2ChPY,14573
|
|
35
|
-
dhisana/utils/enrich_lead_information.py,sha256=
|
|
35
|
+
dhisana/utils/enrich_lead_information.py,sha256=9U6wkwe0REtmrfdMr7u6KW9N2SNg0Ei2cjyplVgdOh8,45411
|
|
36
36
|
dhisana/utils/extract_email_content_for_llm.py,sha256=SQmMZ3YJtm3ZI44XiWEVAItcAwrsSSy1QzDne7LTu_Q,3713
|
|
37
37
|
dhisana/utils/fetch_openai_config.py,sha256=LjWdFuUeTNeAW106pb7DLXZNElos2PlmXRe6bHZJ2hw,5159
|
|
38
38
|
dhisana/utils/field_validators.py,sha256=BZgNCpBG264aRqNUu_J67c6zfr15zlAaIw2XRy8J7DY,11809
|
|
@@ -81,7 +81,7 @@ dhisana/utils/serperdev_google_jobs.py,sha256=m5_2f_5y79FOFZz1A_go6m0hIUfbbAoZ0Y
|
|
|
81
81
|
dhisana/utils/serperdev_local_business.py,sha256=JoZfTg58Hojv61cyuwA2lcnPdLT1lawnWaBNrUYWnuQ,6447
|
|
82
82
|
dhisana/utils/serperdev_search.py,sha256=_iBKIfHMq4gFv5StYz58eArriygoi1zW6VnLlux8vto,9363
|
|
83
83
|
dhisana/utils/smtp_email_tools.py,sha256=peW0dKMUW5s_yso9uhLb6DGOM3Aj028zshqBWlQKviE,21990
|
|
84
|
-
dhisana/utils/test_connect.py,sha256=
|
|
84
|
+
dhisana/utils/test_connect.py,sha256=PFwCQ6ODzXGdUV0kXREed8U2FG_emizlDF-wG9r51wQ,100367
|
|
85
85
|
dhisana/utils/trasform_json.py,sha256=7V72XNDpuxUX0GHN5D83z4anj_gIf5zabaHeQm7b1_E,6979
|
|
86
86
|
dhisana/utils/web_download_parse_tools.py,sha256=ouXwH7CmjcRjoBfP5BWat86MvcGO-8rLCmWQe_eZKjc,7810
|
|
87
87
|
dhisana/utils/workflow_code_model.py,sha256=YPWse5vBb3O6Km2PvKh1Q3AB8qBkzLt1CrR5xOL9Mro,99
|
|
@@ -95,8 +95,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
|
|
|
95
95
|
dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
|
|
96
96
|
dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
|
|
97
97
|
dhisana/workflow/test.py,sha256=E7lRnXK0PguTNzyasHytLzTJdkqIPxG5_4qk4hMEeKc,3399
|
|
98
|
-
dhisana-0.0.1.
|
|
99
|
-
dhisana-0.0.1.
|
|
100
|
-
dhisana-0.0.1.
|
|
101
|
-
dhisana-0.0.1.
|
|
102
|
-
dhisana-0.0.1.
|
|
98
|
+
dhisana-0.0.1.dev279.dist-info/METADATA,sha256=6Zksvpdsjon8UlBdVEoWtlHjoRFVxfI8HwOyFWwmWoE,1190
|
|
99
|
+
dhisana-0.0.1.dev279.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
100
|
+
dhisana-0.0.1.dev279.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
|
|
101
|
+
dhisana-0.0.1.dev279.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
|
|
102
|
+
dhisana-0.0.1.dev279.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|