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.
Files changed (64) hide show
  1. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/PKG-INFO +3 -1
  2. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/setup.py +4 -2
  3. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/ui/components.py +29 -7
  4. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/agent_tools.py +4 -1
  5. dhisana-0.0.1.dev15/src/dhisana/utils/apollo_tools.py +469 -0
  6. dhisana-0.0.1.dev15/src/dhisana/utils/built_with_api_tools.py +242 -0
  7. dhisana-0.0.1.dev15/src/dhisana/utils/cache_output_tools.py +78 -0
  8. dhisana-0.0.1.dev15/src/dhisana/utils/clay_tools.py +42 -0
  9. dhisana-0.0.1.dev15/src/dhisana/utils/company_utils.py +46 -0
  10. dhisana-0.0.1.dev15/src/dhisana/utils/composite_tools.py +138 -0
  11. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/dataframe_tools.py +68 -33
  12. dhisana-0.0.1.dev15/src/dhisana/utils/g2_tools.py +105 -0
  13. dhisana-0.0.1.dev15/src/dhisana/utils/generate_flow.py +116 -0
  14. dhisana-0.0.1.dev15/src/dhisana/utils/google_custom_search.py +694 -0
  15. dhisana-0.0.1.dev15/src/dhisana/utils/hubspot_clearbit.py +92 -0
  16. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/hubspot_crm_tools.py +133 -121
  17. dhisana-0.0.1.dev15/src/dhisana/utils/instantly_tools.py +145 -0
  18. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/linkedin_crawler.py +8 -13
  19. dhisana-0.0.1.dev15/src/dhisana/utils/lusha_tools.py +306 -0
  20. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openai_helpers.py +342 -115
  21. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_spec_to_tools.py +0 -1
  22. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/api_models.py +1 -1
  23. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +1 -1
  24. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/openapi_tool.py +11 -7
  25. dhisana-0.0.1.dev15/src/dhisana/utils/proxy_curl_tools.py +398 -0
  26. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/python_function_to_tools.py +3 -4
  27. dhisana-0.0.1.dev15/src/dhisana/utils/sales_navigator_crawler.py +381 -0
  28. dhisana-0.0.1.dev15/src/dhisana/utils/sendgrid_tools.py +46 -0
  29. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/serpapi_search_tools.py +10 -5
  30. dhisana-0.0.1.dev15/src/dhisana/utils/trasform_json.py +94 -0
  31. dhisana-0.0.1.dev15/src/dhisana/utils/web_download_parse_tools.py +189 -0
  32. dhisana-0.0.1.dev15/src/dhisana/utils/zoominfo_tools.py +452 -0
  33. dhisana-0.0.1.dev15/src/dhisana/workflow/agent.py +18 -0
  34. dhisana-0.0.1.dev15/src/dhisana/workflow/flow.py +44 -0
  35. dhisana-0.0.1.dev15/src/dhisana/workflow/task.py +43 -0
  36. dhisana-0.0.1.dev15/src/dhisana/workflow/test.py +90 -0
  37. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/PKG-INFO +3 -1
  38. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/SOURCES.txt +19 -0
  39. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/requires.txt +2 -0
  40. dhisana-0.0.1.dev13/src/dhisana/utils/apollo_tools.py +0 -121
  41. dhisana-0.0.1.dev13/src/dhisana/utils/web_download_parse_tools.py +0 -46
  42. dhisana-0.0.1.dev13/src/dhisana/workflow/task.py +0 -60
  43. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/README.md +0 -0
  44. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/pyproject.toml +0 -0
  45. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/setup.cfg +0 -0
  46. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/__init__.py +0 -0
  47. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/__init__.py +0 -0
  48. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/cli.py +0 -0
  49. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/datasets.py +0 -0
  50. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/models.py +0 -0
  51. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/cli/predictions.py +0 -0
  52. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/ui/__init__.py +0 -0
  53. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/__init__.py +0 -0
  54. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  55. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  56. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/google_workspace_tools.py +0 -0
  57. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  58. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  59. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/utils/tools_json.py +0 -0
  60. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana/workflow/__init__.py +0 -0
  61. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/dependency_links.txt +0 -0
  62. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/entry_points.txt +0 -0
  63. {dhisana-0.0.1.dev13 → dhisana-0.0.1.dev15}/src/dhisana.egg-info/top_level.txt +0 -0
  64. {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.dev13
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-dev13',
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(Component):
212
- def __init__(self, label: str, button_type: str = 'button', disabled: bool = False, on_click: Optional[str] = None, style: Optional[Dict[str, Any]] = None):
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.on_click = on_click
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}