dhisana 0.0.1.dev13__tar.gz → 0.0.1.dev15__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.dev13 → dhisana-0.0.1.dev15}/PKG-INFO +3 -1
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/setup.py +4 -2
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/ui/components.py +29 -7
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/agent_tools.py +4 -1
- dhisana-0.0.1.dev15/src/dhisana/utils/apollo_tools.py +469 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/built_with_api_tools.py +242 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/cache_output_tools.py +78 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/clay_tools.py +42 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/company_utils.py +46 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/composite_tools.py +138 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/dataframe_tools.py +68 -33
- dhisana-0.0.1.dev15/src/dhisana/utils/g2_tools.py +105 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/generate_flow.py +116 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/google_custom_search.py +694 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/hubspot_clearbit.py +92 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/hubspot_crm_tools.py +133 -121
- dhisana-0.0.1.dev15/src/dhisana/utils/instantly_tools.py +145 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/linkedin_crawler.py +8 -13
- dhisana-0.0.1.dev15/src/dhisana/utils/lusha_tools.py +306 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openai_helpers.py +342 -115
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_spec_to_tools.py +0 -1
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/api_models.py +1 -1
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +1 -1
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/openapi_tool.py +11 -7
- dhisana-0.0.1.dev15/src/dhisana/utils/proxy_curl_tools.py +398 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/python_function_to_tools.py +3 -4
- dhisana-0.0.1.dev15/src/dhisana/utils/sales_navigator_crawler.py +381 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/sendgrid_tools.py +46 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/serpapi_search_tools.py +10 -5
- dhisana-0.0.1.dev15/src/dhisana/utils/trasform_json.py +94 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/web_download_parse_tools.py +189 -0
- dhisana-0.0.1.dev15/src/dhisana/utils/zoominfo_tools.py +452 -0
- dhisana-0.0.1.dev15/src/dhisana/workflow/agent.py +18 -0
- dhisana-0.0.1.dev15/src/dhisana/workflow/flow.py +44 -0
- dhisana-0.0.1.dev15/src/dhisana/workflow/task.py +43 -0
- dhisana-0.0.1.dev15/src/dhisana/workflow/test.py +90 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/PKG-INFO +3 -1
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/SOURCES.txt +19 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/requires.txt +2 -0
- dhisana-0.0.1.dev13/src/dhisana/utils/apollo_tools.py +0 -121
- dhisana-0.0.1.dev13/src/dhisana/utils/web_download_parse_tools.py +0 -46
- dhisana-0.0.1.dev13/src/dhisana/workflow/task.py +0 -60
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/README.md +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/setup.cfg +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/tools_json.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/tests/test_agent_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dhisana
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev15
|
|
4
4
|
Summary: A Python SDK for Dhisana AI Platform
|
|
5
5
|
Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
|
|
6
6
|
Author: Admin
|
|
@@ -25,3 +25,5 @@ Requires-Dist: openapi_pydantic
|
|
|
25
25
|
Requires-Dist: pandas
|
|
26
26
|
Requires-Dist: simple_salesforce
|
|
27
27
|
Requires-Dist: backoff
|
|
28
|
+
Requires-Dist: html2text
|
|
29
|
+
Requires-Dist: hubspot-api-client
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name='dhisana',
|
|
5
|
-
version='0.0.1-
|
|
5
|
+
version='0.0.1-dev15',
|
|
6
6
|
description='A Python SDK for Dhisana AI Platform',
|
|
7
7
|
author='Admin',
|
|
8
8
|
author_email='contact@dhisana.ai',
|
|
@@ -24,7 +24,9 @@ setup(
|
|
|
24
24
|
'openapi_pydantic',
|
|
25
25
|
'pandas',
|
|
26
26
|
'simple_salesforce',
|
|
27
|
-
'backoff'
|
|
27
|
+
'backoff',
|
|
28
|
+
'html2text',
|
|
29
|
+
'hubspot-api-client'
|
|
28
30
|
],
|
|
29
31
|
entry_points={
|
|
30
32
|
'console_scripts': [
|
|
@@ -208,24 +208,46 @@ class Upload(Component):
|
|
|
208
208
|
},
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
class Button
|
|
212
|
-
def __init__(
|
|
211
|
+
class Button:
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
label: str = '',
|
|
215
|
+
on_click: str = None,
|
|
216
|
+
button_type: str = 'button',
|
|
217
|
+
condition: str = None,
|
|
218
|
+
confirm: bool = False,
|
|
219
|
+
confirmMessage: str = '',
|
|
220
|
+
style: dict = None,
|
|
221
|
+
icon: str = None,
|
|
222
|
+
disabled: bool = False,
|
|
223
|
+
btn_class: str = 'btn btn-secondary',
|
|
224
|
+
):
|
|
213
225
|
self.label = label
|
|
226
|
+
self.on_click = on_click
|
|
214
227
|
self.button_type = button_type
|
|
228
|
+
self.condition = condition
|
|
229
|
+
self.confirm = confirm
|
|
230
|
+
self.confirmMessage = confirmMessage
|
|
231
|
+
self.style = style or {}
|
|
232
|
+
self.icon = icon
|
|
215
233
|
self.disabled = disabled
|
|
216
|
-
self.
|
|
217
|
-
self.style = style
|
|
234
|
+
self.btn_class = btn_class
|
|
218
235
|
|
|
219
236
|
def to_dict(self):
|
|
220
237
|
return {
|
|
221
238
|
'type': 'button',
|
|
222
239
|
'properties': {
|
|
223
240
|
'label': self.label,
|
|
224
|
-
'type': self.button_type,
|
|
225
|
-
'disabled': self.disabled,
|
|
226
241
|
'onClick': self.on_click,
|
|
242
|
+
'buttonType': self.button_type,
|
|
243
|
+
'condition': self.condition,
|
|
244
|
+
'confirm': self.confirm,
|
|
245
|
+
'confirmMessage': self.confirmMessage,
|
|
227
246
|
'style': self.style,
|
|
228
|
-
|
|
247
|
+
'icon': self.icon,
|
|
248
|
+
'disabled': self.disabled,
|
|
249
|
+
'btnClass': self.btn_class,
|
|
250
|
+
},
|
|
229
251
|
}
|
|
230
252
|
|
|
231
253
|
class Text(Component):
|
|
@@ -45,4 +45,7 @@ if 'GLOBAL_TOOLS_FUNCTIONS' not in globals():
|
|
|
45
45
|
|
|
46
46
|
# Ensure GLOBAL_OPENAI_ASSISTANT_TOOLS is only initialized once
|
|
47
47
|
if 'GLOBAL_OPENAI_ASSISTANT_TOOLS' not in globals():
|
|
48
|
-
GLOBAL_OPENAI_ASSISTANT_TOOLS = []
|
|
48
|
+
GLOBAL_OPENAI_ASSISTANT_TOOLS = []
|
|
49
|
+
|
|
50
|
+
if 'GLOBAL_TOOLS_CACHE_PATH' not in globals():
|
|
51
|
+
GLOBAL_TOOLS_CACHE_PATH = '/tmp/dhisana_ai'
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import aiohttp
|
|
7
|
+
import backoff
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from dhisana.utils.cache_output_tools import cache_output,retrieve_output
|
|
11
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
12
|
+
# Utility functions to work with Apollo API.
|
|
13
|
+
|
|
14
|
+
@assistant_tool
|
|
15
|
+
@backoff.on_exception(
|
|
16
|
+
backoff.expo,
|
|
17
|
+
aiohttp.ClientResponseError,
|
|
18
|
+
max_tries=2,
|
|
19
|
+
giveup=lambda e: e.status != 429,
|
|
20
|
+
factor=10,
|
|
21
|
+
)
|
|
22
|
+
async def enrich_person_info_from_apollo(
|
|
23
|
+
linkedin_url: Optional[str] = None,
|
|
24
|
+
email: Optional[str] = None,
|
|
25
|
+
phone: Optional[str] = None,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Fetch a person's details from Apollo using LinkedIn URL, email, or phone number.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
- **linkedin_url** (*str*, optional): LinkedIn profile URL of the person.
|
|
32
|
+
- **email** (*str*, optional): Email address of the person.
|
|
33
|
+
- **phone** (*str*, optional): Phone number of the person.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
- **dict**: JSON response containing person information.
|
|
37
|
+
"""
|
|
38
|
+
APOLLO_API_KEY = os.environ.get('APOLLO_API_KEY')
|
|
39
|
+
if not APOLLO_API_KEY:
|
|
40
|
+
return {'error': "Apollo API key not found in environment variables"}
|
|
41
|
+
|
|
42
|
+
if not linkedin_url and not email and not phone:
|
|
43
|
+
return {'error': "At least one of linkedin_url, email, or phone must be provided"}
|
|
44
|
+
|
|
45
|
+
headers = {
|
|
46
|
+
"X-Api-Key": f"{APOLLO_API_KEY}",
|
|
47
|
+
"Content-Type": "application/json"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
data = {}
|
|
51
|
+
if linkedin_url:
|
|
52
|
+
data['linkedin_url'] = linkedin_url
|
|
53
|
+
cached_response = retrieve_output("enrich_person_info_from_apollo", linkedin_url)
|
|
54
|
+
if cached_response is not None:
|
|
55
|
+
return cached_response
|
|
56
|
+
if email:
|
|
57
|
+
data['email'] = email
|
|
58
|
+
if phone:
|
|
59
|
+
data['phone_numbers'] = [phone] # Apollo expects a list for phone numbers
|
|
60
|
+
|
|
61
|
+
url = 'https://api.apollo.io/v1/people/match'
|
|
62
|
+
|
|
63
|
+
async with aiohttp.ClientSession() as session:
|
|
64
|
+
async with session.post(url, headers=headers, json=data) as response:
|
|
65
|
+
if response.status == 200:
|
|
66
|
+
result = await response.json()
|
|
67
|
+
if linkedin_url:
|
|
68
|
+
cache_output("enrich_person_info_from_apollo", linkedin_url, result)
|
|
69
|
+
return result
|
|
70
|
+
elif response.status == 429:
|
|
71
|
+
logging.warning("enrich_person_info_from_apollo Rate limit hit")
|
|
72
|
+
await asyncio.sleep(30)
|
|
73
|
+
raise aiohttp.ClientResponseError(
|
|
74
|
+
request_info=response.request_info,
|
|
75
|
+
history=response.history,
|
|
76
|
+
status=response.status,
|
|
77
|
+
message="Rate limit exceeded",
|
|
78
|
+
headers=response.headers
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
result = await response.json()
|
|
82
|
+
logging.warning(f"enrich_person_info_from_apollo Failed to run assistant: ${result}")
|
|
83
|
+
return {'error': result}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@assistant_tool
|
|
87
|
+
@backoff.on_exception(
|
|
88
|
+
backoff.expo,
|
|
89
|
+
aiohttp.ClientResponseError,
|
|
90
|
+
max_tries=2,
|
|
91
|
+
giveup=lambda e: e.status != 429,
|
|
92
|
+
factor=30,
|
|
93
|
+
)
|
|
94
|
+
async def enrich_organization_info_from_apollo(
|
|
95
|
+
organization_domain: Optional[str] = None,
|
|
96
|
+
):
|
|
97
|
+
"""
|
|
98
|
+
Fetch a organization's details from Apollo using the organization domain.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
- **organization_domain** (*str*, optional): Domain of the organization.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
- **dict**: JSON response containing organization information.
|
|
105
|
+
"""
|
|
106
|
+
APOLLO_API_KEY = os.environ.get('APOLLO_API_KEY')
|
|
107
|
+
if not APOLLO_API_KEY:
|
|
108
|
+
return {'error': "Apollo API key not found in environment variables"}
|
|
109
|
+
|
|
110
|
+
if not organization_domain:
|
|
111
|
+
return {'error': "organization domain must be provided"}
|
|
112
|
+
|
|
113
|
+
headers = {
|
|
114
|
+
"X-Api-Key": f"{APOLLO_API_KEY}",
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"Cache-Control": "no-cache",
|
|
117
|
+
"accept": "application/json"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cached_response = retrieve_output("enrich_organization_info_from_apollo", organization_domain)
|
|
121
|
+
if cached_response is not None:
|
|
122
|
+
return cached_response
|
|
123
|
+
|
|
124
|
+
url = f'https://api.apollo.io/api/v1/organizations/enrich?domain={organization_domain}'
|
|
125
|
+
|
|
126
|
+
async with aiohttp.ClientSession() as session:
|
|
127
|
+
async with session.get(url, headers=headers) as response:
|
|
128
|
+
if response.status == 200:
|
|
129
|
+
result = await response.json()
|
|
130
|
+
cache_output("enrich_organization_info_from_apollo", organization_domain, result)
|
|
131
|
+
return result
|
|
132
|
+
elif response.status == 429:
|
|
133
|
+
raise aiohttp.ClientResponseError(
|
|
134
|
+
request_info=response.request_info,
|
|
135
|
+
history=response.history,
|
|
136
|
+
status=response.status,
|
|
137
|
+
message="Rate limit exceeded",
|
|
138
|
+
headers=response.headers
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
result = await response.json()
|
|
142
|
+
return {'error': result}
|
|
143
|
+
|
|
144
|
+
@assistant_tool
|
|
145
|
+
async def get_enriched_customer_information(
|
|
146
|
+
linkedin_url: Optional[str] = None,
|
|
147
|
+
email: Optional[str] = None,
|
|
148
|
+
phone: Optional[str] = None,
|
|
149
|
+
required_fields: Optional[List[str]] = None,
|
|
150
|
+
data_sources: Optional[List[str]] = None,
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
Fetch a person's details from specified data sources using LinkedIn URL, email, or phone number.
|
|
154
|
+
|
|
155
|
+
Parameters:
|
|
156
|
+
- **linkedin_url** (*str*, optional): LinkedIn profile URL of the person.
|
|
157
|
+
- **email** (*str*, optional): Email address of the person.
|
|
158
|
+
- **phone** (*str*, optional): Phone number of the person.
|
|
159
|
+
- **required_fields** (*List[str]*, optional): Properties of the customer to fetch (e.g., 'job_history', 'education_history', 'skills', etc.).
|
|
160
|
+
- **data_sources** (*List[str]*, optional): Data sources to fetch from (e.g., 'apollo', 'zoominfo', 'websearch', 'linkedin'). Defaults to all sources.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
- **dict**: JSON response containing person information.
|
|
164
|
+
"""
|
|
165
|
+
# Set default values if not provided
|
|
166
|
+
if required_fields is None:
|
|
167
|
+
required_fields = [
|
|
168
|
+
'job_history',
|
|
169
|
+
'education_history',
|
|
170
|
+
'skills',
|
|
171
|
+
'headline',
|
|
172
|
+
'summary',
|
|
173
|
+
'experiences',
|
|
174
|
+
'projects',
|
|
175
|
+
'certifications',
|
|
176
|
+
'publications',
|
|
177
|
+
'languages',
|
|
178
|
+
'volunteer_work',
|
|
179
|
+
]
|
|
180
|
+
if data_sources is None:
|
|
181
|
+
data_sources = ['apollo', 'zoominfo', 'websearch', 'linkedin']
|
|
182
|
+
|
|
183
|
+
data = await enrich_person_info_from_apollo(
|
|
184
|
+
linkedin_url=linkedin_url,
|
|
185
|
+
email=email,
|
|
186
|
+
phone=phone,
|
|
187
|
+
)
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
@assistant_tool
|
|
191
|
+
async def get_enriched_organization_information(
|
|
192
|
+
organization_domain: Optional[str] = None,
|
|
193
|
+
required_fields: Optional[List[str]] = None,
|
|
194
|
+
data_sources: Optional[List[str]] = None,
|
|
195
|
+
):
|
|
196
|
+
"""
|
|
197
|
+
Fetch a organization's details from specified data sources using the organization domain.
|
|
198
|
+
|
|
199
|
+
Parameters:
|
|
200
|
+
- **organization_domain** (*str*, optional): Domain of the organization.
|
|
201
|
+
- **required_fields** (*List[str]*, optional): Properties of the organization to fetch (e.g., 'technographics', 'firmographics', 'employee_count', etc.).
|
|
202
|
+
- **data_sources** (*List[str]*, optional): Data sources to fetch from (e.g., 'apollo', 'zoominfo', 'builtwith', 'linkedin'). Defaults to all sources.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
- **dict**: JSON response containing organization information.
|
|
206
|
+
"""
|
|
207
|
+
return await enrich_organization_info_from_apollo(organization_domain=organization_domain)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Define the backoff strategy for handling rate limiting
|
|
212
|
+
@backoff.on_exception(
|
|
213
|
+
backoff.expo,
|
|
214
|
+
aiohttp.ClientResponseError,
|
|
215
|
+
max_tries=5,
|
|
216
|
+
giveup=lambda e: e.status != 429,
|
|
217
|
+
factor=2,
|
|
218
|
+
)
|
|
219
|
+
async def fetch_apollo_data(session, url, headers, payload):
|
|
220
|
+
key_data = f"{url}_{json.dumps(payload, sort_keys=True)}"
|
|
221
|
+
key_hash = hashlib.sha256(key_data.encode()).hexdigest()
|
|
222
|
+
cached_response = retrieve_output("fetch_apollo_data", key_hash)
|
|
223
|
+
if cached_response is not None:
|
|
224
|
+
return cached_response
|
|
225
|
+
|
|
226
|
+
async with session.post(url, headers=headers, json=payload) as response:
|
|
227
|
+
if response.status == 200:
|
|
228
|
+
result = await response.json()
|
|
229
|
+
cache_output("fetch_apollo_data", key_hash, result)
|
|
230
|
+
return result
|
|
231
|
+
elif response.status == 429:
|
|
232
|
+
raise aiohttp.ClientResponseError(
|
|
233
|
+
request_info=response.request_info,
|
|
234
|
+
history=response.history,
|
|
235
|
+
status=response.status,
|
|
236
|
+
message="Rate limit exceeded",
|
|
237
|
+
headers=response.headers
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
response.raise_for_status()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@assistant_tool
|
|
245
|
+
async def search_people_with_apollo(
|
|
246
|
+
job_titles: List[str],
|
|
247
|
+
locations: List[str],
|
|
248
|
+
min_number_of_employees: int,
|
|
249
|
+
max_number_of_employees: int,
|
|
250
|
+
filter_by_signals: List[str],
|
|
251
|
+
max_number_of_items_to_return: int,
|
|
252
|
+
industries: List[str]
|
|
253
|
+
) -> List[Dict]:
|
|
254
|
+
"""
|
|
255
|
+
Search for individuals on Apollo based on specified criteria.
|
|
256
|
+
|
|
257
|
+
Parameters:
|
|
258
|
+
- **job_titles** (*List[str]*): Job titles to include in the search.
|
|
259
|
+
- **locations** (*List[str]*): Locations to filter the search.
|
|
260
|
+
- **min_number_of_employees** (*int*): Minimum number of employees in the organization.
|
|
261
|
+
Must be >= 1. Default is 1
|
|
262
|
+
- **max_number_of_employees** (*int*): Maximum number of employees in the organization.
|
|
263
|
+
Must be <= 100000 and greater than min_number_of_employees. Default 1000
|
|
264
|
+
- **filter_by_signals** (*List[str]*): Signals to filter by. Valid options:
|
|
265
|
+
- "RECENT_JOB_CHANGE"
|
|
266
|
+
- "RAPID_EXPANSION"
|
|
267
|
+
- **max_number_of_items_to_return** (*int*): Maximum number of results to return. Defaults to 100,
|
|
268
|
+
with a maximum allowed value of 5000.
|
|
269
|
+
- **industries** (*List[str]*): A list of industries to filter by. Defaults to [] for no filter. Default empty []
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
- **List[Dict]**: A list of individual records (dictionaries) matching the specified criteria,
|
|
273
|
+
or a single-item list containing a JSON-encoded error dictionary if an error occurs.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
# Validate min and max employees
|
|
277
|
+
if min_number_of_employees < 1:
|
|
278
|
+
raise ValueError("Minimum number of employees must be at least 1.")
|
|
279
|
+
if max_number_of_employees > 100000:
|
|
280
|
+
raise ValueError("Maximum number of employees must not exceed 100,000.")
|
|
281
|
+
if min_number_of_employees >= max_number_of_employees:
|
|
282
|
+
raise ValueError("Minimum number of employees must be less than the maximum number.")
|
|
283
|
+
|
|
284
|
+
APOLLO_API_KEY = os.getenv('APOLLO_API_KEY')
|
|
285
|
+
if not APOLLO_API_KEY:
|
|
286
|
+
raise EnvironmentError("Apollo API key not found in environment variables.")
|
|
287
|
+
|
|
288
|
+
# Log the search parameters for debugging
|
|
289
|
+
logging.info(
|
|
290
|
+
f"Initiating search with parameters: "
|
|
291
|
+
f"Job titles: {job_titles}, Locations: {locations}, "
|
|
292
|
+
f"Employee range: {min_number_of_employees}-{max_number_of_employees}, "
|
|
293
|
+
f"Signals: {filter_by_signals}, "
|
|
294
|
+
f"Industries: {industries}, "
|
|
295
|
+
f"Max items: {max_number_of_items_to_return}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Ensure a positive number of items to return
|
|
299
|
+
if max_number_of_items_to_return <= 0:
|
|
300
|
+
max_number_of_items_to_return = 10
|
|
301
|
+
|
|
302
|
+
headers = {
|
|
303
|
+
"Cache-Control": "no-cache",
|
|
304
|
+
"Content-Type": "application/json",
|
|
305
|
+
"X-Api-Key": APOLLO_API_KEY,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Apollo endpoint for searching
|
|
309
|
+
url = "https://api.apollo.io/v1/mixed_people/search"
|
|
310
|
+
|
|
311
|
+
# Mapping of filter signals to their Apollo-specific IDs
|
|
312
|
+
signal_mapping = {
|
|
313
|
+
"RECENT_JOB_CHANGE": "643daa349293c1cdaa4d00f8",
|
|
314
|
+
"RAPID_EXPANSION": "643daa3f9293c1cdaa4d00fa"
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Translate requested signals into Apollo signal IDs
|
|
318
|
+
search_signal_ids = [signal_mapping[s] for s in filter_by_signals if s in signal_mapping]
|
|
319
|
+
|
|
320
|
+
async with aiohttp.ClientSession() as session:
|
|
321
|
+
results = []
|
|
322
|
+
page = 1
|
|
323
|
+
# Apollo API allows up to 100 items per page
|
|
324
|
+
per_page = min(max_number_of_items_to_return, 100)
|
|
325
|
+
|
|
326
|
+
while len(results) < max_number_of_items_to_return:
|
|
327
|
+
payload = {
|
|
328
|
+
"person_titles": job_titles,
|
|
329
|
+
"person_locations": locations,
|
|
330
|
+
"search_signal_ids": search_signal_ids,
|
|
331
|
+
"organization_num_employees_ranges": [f"{min_number_of_employees},{max_number_of_employees}"],
|
|
332
|
+
"page": page,
|
|
333
|
+
"per_page": per_page
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Include industries filter if provided
|
|
337
|
+
if industries:
|
|
338
|
+
payload["organization_industries"] = industries
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
data = await fetch_apollo_data(session, url, headers, payload)
|
|
342
|
+
people = data.get('people', [])
|
|
343
|
+
contacts = data.get('contacts', [])
|
|
344
|
+
|
|
345
|
+
# If no results found, stop the loop
|
|
346
|
+
if not people and not contacts:
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
# Add the retrieved results
|
|
350
|
+
results.extend(people + contacts)
|
|
351
|
+
|
|
352
|
+
# Handle pagination
|
|
353
|
+
pagination = data.get('pagination', {})
|
|
354
|
+
current_page = pagination.get('page', 1)
|
|
355
|
+
total_pages = pagination.get('total_pages', 1)
|
|
356
|
+
|
|
357
|
+
if current_page >= total_pages:
|
|
358
|
+
# No more pages to fetch
|
|
359
|
+
break
|
|
360
|
+
|
|
361
|
+
page += 1
|
|
362
|
+
|
|
363
|
+
except aiohttp.ClientResponseError as e:
|
|
364
|
+
# If rate-limited, wait and retry
|
|
365
|
+
if e.status == 429:
|
|
366
|
+
await asyncio.sleep(30)
|
|
367
|
+
else:
|
|
368
|
+
# Return error details as a JSON string in a list
|
|
369
|
+
error_details = {
|
|
370
|
+
'status': e.status,
|
|
371
|
+
'message': str(e),
|
|
372
|
+
'url': str(e.request_info.url),
|
|
373
|
+
'headers': dict(e.headers),
|
|
374
|
+
}
|
|
375
|
+
return [json.dumps(error_details)]
|
|
376
|
+
|
|
377
|
+
# Return only up to the requested number of items
|
|
378
|
+
return results[:max_number_of_items_to_return]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@assistant_tool
|
|
382
|
+
async def get_organization_domain_from_apollo(
|
|
383
|
+
organization_id: str,
|
|
384
|
+
):
|
|
385
|
+
"""
|
|
386
|
+
Fetch an organization's domain from Apollo using the organization ID.
|
|
387
|
+
|
|
388
|
+
Parameters:
|
|
389
|
+
- organization_id (str): ID of the organization.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
- dict: Contains the organization's ID and domain, or an error message.
|
|
393
|
+
"""
|
|
394
|
+
result = await get_organization_details_from_apollo(organization_id)
|
|
395
|
+
if 'error' in result:
|
|
396
|
+
return result
|
|
397
|
+
domain = result.get('primary_domain')
|
|
398
|
+
if domain:
|
|
399
|
+
return {'organization_id': organization_id, 'domain': domain}
|
|
400
|
+
else:
|
|
401
|
+
return {'error': 'Domain not found in the organization details'}
|
|
402
|
+
|
|
403
|
+
@assistant_tool
|
|
404
|
+
@backoff.on_exception(
|
|
405
|
+
backoff.expo,
|
|
406
|
+
aiohttp.ClientResponseError,
|
|
407
|
+
max_tries=3,
|
|
408
|
+
giveup=lambda e: e.status != 429,
|
|
409
|
+
factor=60,
|
|
410
|
+
)
|
|
411
|
+
async def get_organization_details_from_apollo(
|
|
412
|
+
organization_id: str,
|
|
413
|
+
):
|
|
414
|
+
"""
|
|
415
|
+
Fetch an organization's details from Apollo using the organization ID.
|
|
416
|
+
|
|
417
|
+
Parameters:
|
|
418
|
+
- organization_id (str): ID of the organization.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
- dict: Organization details or an error message.
|
|
422
|
+
"""
|
|
423
|
+
APOLLO_API_KEY = os.environ.get('APOLLO_API_KEY')
|
|
424
|
+
if not APOLLO_API_KEY:
|
|
425
|
+
return {'error': "Apollo API key not found in environment variables"}
|
|
426
|
+
|
|
427
|
+
if not organization_id:
|
|
428
|
+
return {'error': "Organization ID must be provided"}
|
|
429
|
+
|
|
430
|
+
headers = {
|
|
431
|
+
"X-Api-Key": APOLLO_API_KEY,
|
|
432
|
+
"Content-Type": "application/json",
|
|
433
|
+
"Cache-Control": "no-cache",
|
|
434
|
+
"Accept": "application/json"
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
cached_response = retrieve_output("get_organization_details_from_apollo", organization_id)
|
|
438
|
+
if cached_response is not None:
|
|
439
|
+
return cached_response
|
|
440
|
+
|
|
441
|
+
url = f'https://api.apollo.io/v1/organizations/{organization_id}'
|
|
442
|
+
|
|
443
|
+
async with aiohttp.ClientSession() as session:
|
|
444
|
+
async with session.get(url, headers=headers) as response:
|
|
445
|
+
if response.status == 200:
|
|
446
|
+
result = await response.json()
|
|
447
|
+
org_details = result.get('organization', {})
|
|
448
|
+
if org_details:
|
|
449
|
+
cache_output("get_organization_details_from_apollo", organization_id, org_details)
|
|
450
|
+
return org_details
|
|
451
|
+
else:
|
|
452
|
+
return {'error': 'Organization details not found in the response'}
|
|
453
|
+
elif response.status == 429:
|
|
454
|
+
limit_minute = response.headers.get('x-rate-limit-minute')
|
|
455
|
+
limit_hourly = response.headers.get('x-rate-limit-hourly')
|
|
456
|
+
limit_daily = response.headers.get('x-rate-limit-daily')
|
|
457
|
+
logging.info(f" get_organization_details_from_apollo x-rate-limit-minute: {limit_minute}")
|
|
458
|
+
logging.info(f"get_organization_details_from_apollo x-rate-limit-hourly: {limit_hourly}")
|
|
459
|
+
logging.info(f"get_organization_details_from_apollo x-rate-limit-daily: {limit_daily}")
|
|
460
|
+
raise aiohttp.ClientResponseError(
|
|
461
|
+
request_info=response.request_info,
|
|
462
|
+
history=response.history,
|
|
463
|
+
status=response.status,
|
|
464
|
+
message="Rate limit exceeded",
|
|
465
|
+
headers=response.headers
|
|
466
|
+
)
|
|
467
|
+
else:
|
|
468
|
+
result = await response.json()
|
|
469
|
+
return {'error': result}
|