dhisana 0.0.1.dev243__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dhisana/__init__.py +1 -0
- dhisana/cli/__init__.py +1 -0
- dhisana/cli/cli.py +20 -0
- dhisana/cli/datasets.py +27 -0
- dhisana/cli/models.py +26 -0
- dhisana/cli/predictions.py +20 -0
- dhisana/schemas/__init__.py +1 -0
- dhisana/schemas/common.py +399 -0
- dhisana/schemas/sales.py +965 -0
- dhisana/ui/__init__.py +1 -0
- dhisana/ui/components.py +472 -0
- dhisana/utils/__init__.py +1 -0
- dhisana/utils/add_mapping.py +352 -0
- dhisana/utils/agent_tools.py +51 -0
- dhisana/utils/apollo_tools.py +1597 -0
- dhisana/utils/assistant_tool_tag.py +4 -0
- dhisana/utils/built_with_api_tools.py +282 -0
- dhisana/utils/cache_output_tools.py +98 -0
- dhisana/utils/cache_output_tools_local.py +78 -0
- dhisana/utils/check_email_validity_tools.py +717 -0
- dhisana/utils/check_for_intent_signal.py +107 -0
- dhisana/utils/check_linkedin_url_validity.py +209 -0
- dhisana/utils/clay_tools.py +43 -0
- dhisana/utils/clean_properties.py +135 -0
- dhisana/utils/company_utils.py +60 -0
- dhisana/utils/compose_salesnav_query.py +259 -0
- dhisana/utils/compose_search_query.py +759 -0
- dhisana/utils/compose_three_step_workflow.py +234 -0
- dhisana/utils/composite_tools.py +137 -0
- dhisana/utils/dataframe_tools.py +237 -0
- dhisana/utils/domain_parser.py +45 -0
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_parse_helpers.py +132 -0
- dhisana/utils/email_provider.py +375 -0
- dhisana/utils/enrich_lead_information.py +933 -0
- dhisana/utils/extract_email_content_for_llm.py +101 -0
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +426 -0
- dhisana/utils/g2_tools.py +104 -0
- dhisana/utils/generate_content.py +41 -0
- dhisana/utils/generate_custom_message.py +271 -0
- dhisana/utils/generate_email.py +278 -0
- dhisana/utils/generate_email_response.py +465 -0
- dhisana/utils/generate_flow.py +102 -0
- dhisana/utils/generate_leads_salesnav.py +303 -0
- dhisana/utils/generate_linkedin_connect_message.py +224 -0
- dhisana/utils/generate_linkedin_response_message.py +317 -0
- dhisana/utils/generate_structured_output_internal.py +462 -0
- dhisana/utils/google_custom_search.py +267 -0
- dhisana/utils/google_oauth_tools.py +727 -0
- dhisana/utils/google_workspace_tools.py +1294 -0
- dhisana/utils/hubspot_clearbit.py +96 -0
- dhisana/utils/hubspot_crm_tools.py +2440 -0
- dhisana/utils/instantly_tools.py +149 -0
- dhisana/utils/linkedin_crawler.py +168 -0
- dhisana/utils/lusha_tools.py +333 -0
- dhisana/utils/mailgun_tools.py +156 -0
- dhisana/utils/mailreach_tools.py +123 -0
- dhisana/utils/microsoft365_tools.py +455 -0
- dhisana/utils/openai_assistant_and_file_utils.py +267 -0
- dhisana/utils/openai_helpers.py +977 -0
- dhisana/utils/openapi_spec_to_tools.py +45 -0
- dhisana/utils/openapi_tool/__init__.py +1 -0
- dhisana/utils/openapi_tool/api_models.py +633 -0
- dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
- dhisana/utils/openapi_tool/openapi_tool.py +319 -0
- dhisana/utils/parse_linkedin_messages_txt.py +100 -0
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +1226 -0
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/python_function_to_tools.py +83 -0
- dhisana/utils/research_lead.py +176 -0
- dhisana/utils/sales_navigator_crawler.py +1103 -0
- dhisana/utils/salesforce_crm_tools.py +477 -0
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +162 -0
- dhisana/utils/serarch_router_local_business.py +75 -0
- dhisana/utils/serpapi_additional_tools.py +290 -0
- dhisana/utils/serpapi_google_jobs.py +117 -0
- dhisana/utils/serpapi_google_search.py +188 -0
- dhisana/utils/serpapi_local_business_search.py +129 -0
- dhisana/utils/serpapi_search_tools.py +852 -0
- dhisana/utils/serperdev_google_jobs.py +125 -0
- dhisana/utils/serperdev_local_business.py +154 -0
- dhisana/utils/serperdev_search.py +233 -0
- dhisana/utils/smtp_email_tools.py +582 -0
- dhisana/utils/test_connect.py +2087 -0
- dhisana/utils/trasform_json.py +173 -0
- dhisana/utils/web_download_parse_tools.py +189 -0
- dhisana/utils/workflow_code_model.py +5 -0
- dhisana/utils/zoominfo_tools.py +357 -0
- dhisana/workflow/__init__.py +1 -0
- dhisana/workflow/agent.py +18 -0
- dhisana/workflow/flow.py +44 -0
- dhisana/workflow/task.py +43 -0
- dhisana/workflow/test.py +90 -0
- dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
- dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
- dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
- dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
- dhisana-0.0.1.dev243.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
import backoff
|
|
8
|
+
|
|
9
|
+
from dhisana.utils.cache_output_tools import cache_output, retrieve_output
|
|
10
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_zoominfo_credentials_from_config(
|
|
14
|
+
tool_config: Optional[List[Dict]] = None
|
|
15
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
16
|
+
"""
|
|
17
|
+
Retrieve ZoomInfo API key and secret from tool_config (looking for 'name' == 'zoominfo'),
|
|
18
|
+
or fall back to environment variables if not found.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
tool_config (List[Dict], optional): Configuration list that may contain ZoomInfo credentials.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tuple[str, str]: (zoominfo_api_key, zoominfo_api_secret), either from tool_config or environment.
|
|
25
|
+
"""
|
|
26
|
+
zoominfo_api_key = None
|
|
27
|
+
zoominfo_api_secret = None
|
|
28
|
+
|
|
29
|
+
if tool_config:
|
|
30
|
+
zoominfo_config = next(
|
|
31
|
+
(item for item in tool_config if item.get("name") == "zoominfo"),
|
|
32
|
+
None
|
|
33
|
+
)
|
|
34
|
+
if zoominfo_config:
|
|
35
|
+
# Convert the list of dicts under 'configuration' to a simple map {name: value}
|
|
36
|
+
config_map = {
|
|
37
|
+
cfg["name"]: cfg["value"]
|
|
38
|
+
for cfg in zoominfo_config.get("configuration", [])
|
|
39
|
+
if cfg
|
|
40
|
+
}
|
|
41
|
+
zoominfo_api_key = config_map.get("apiKey")
|
|
42
|
+
zoominfo_api_secret = config_map.get("apiSecret")
|
|
43
|
+
|
|
44
|
+
# Fall back to environment variables if not found in tool_config
|
|
45
|
+
zoominfo_api_key = zoominfo_api_key or os.environ.get("ZOOMINFO_API_KEY")
|
|
46
|
+
zoominfo_api_secret = zoominfo_api_secret or os.environ.get("ZOOMINFO_API_SECRET")
|
|
47
|
+
|
|
48
|
+
return zoominfo_api_key, zoominfo_api_secret
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@backoff.on_exception(
|
|
52
|
+
backoff.expo,
|
|
53
|
+
(aiohttp.ClientResponseError, Exception),
|
|
54
|
+
max_tries=3,
|
|
55
|
+
# Give up if the exception isn't a 429 (rate limit)
|
|
56
|
+
giveup=lambda e: not (isinstance(e, aiohttp.ClientResponseError) and e.status == 429),
|
|
57
|
+
factor=2,
|
|
58
|
+
)
|
|
59
|
+
async def get_zoominfo_access_token(
|
|
60
|
+
tool_config: Optional[List[Dict]] = None
|
|
61
|
+
) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Obtain a ZoomInfo access token using credentials from the provided tool_config
|
|
64
|
+
or environment variables.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
tool_config (List[Dict], optional): Configuration list that may contain ZoomInfo credentials.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
EnvironmentError: If the ZoomInfo integration has not been configured.
|
|
71
|
+
Exception: If the ZoomInfo API authentication fails.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: The ZoomInfo JWT access token.
|
|
75
|
+
"""
|
|
76
|
+
zoominfo_api_key, zoominfo_api_secret = get_zoominfo_credentials_from_config(tool_config)
|
|
77
|
+
|
|
78
|
+
if not zoominfo_api_key or not zoominfo_api_secret:
|
|
79
|
+
raise EnvironmentError(
|
|
80
|
+
"ZoomInfo integration is not configured. Please configure the connection to ZoomInfo in Integrations."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
headers = {"Content-Type": "application/json"}
|
|
84
|
+
data = {"username": zoominfo_api_key, "password": zoominfo_api_secret}
|
|
85
|
+
url = "https://api.zoominfo.com/authenticate"
|
|
86
|
+
|
|
87
|
+
async with aiohttp.ClientSession() as session:
|
|
88
|
+
async with session.post(url, headers=headers, json=data) as response:
|
|
89
|
+
if response.status == 200:
|
|
90
|
+
json_result = await response.json()
|
|
91
|
+
return json_result.get("accessToken")
|
|
92
|
+
else:
|
|
93
|
+
error_result = await response.json()
|
|
94
|
+
raise Exception(f"Failed to authenticate with ZoomInfo API: {error_result}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@assistant_tool
|
|
98
|
+
@backoff.on_exception(
|
|
99
|
+
backoff.expo,
|
|
100
|
+
(aiohttp.ClientResponseError, Exception),
|
|
101
|
+
max_tries=3,
|
|
102
|
+
giveup=lambda e: not (isinstance(e, aiohttp.ClientResponseError) and e.status == 429),
|
|
103
|
+
factor=2,
|
|
104
|
+
)
|
|
105
|
+
async def enrich_person_info_from_zoominfo(
|
|
106
|
+
linkedin_url: Optional[str] = None,
|
|
107
|
+
email: Optional[str] = None,
|
|
108
|
+
phone: Optional[str] = None,
|
|
109
|
+
tool_config: Optional[List[Dict]] = None
|
|
110
|
+
) -> dict:
|
|
111
|
+
"""
|
|
112
|
+
Fetch a person's details from ZoomInfo using LinkedIn URL, email, or phone number.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
linkedin_url (str, optional): LinkedIn profile URL of the person.
|
|
116
|
+
email (str, optional): Email address of the person.
|
|
117
|
+
phone (str, optional): Phone number of the person.
|
|
118
|
+
tool_config (List[Dict], optional): Configuration list that may contain ZoomInfo credentials.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
dict: JSON response containing person information, or an error message.
|
|
122
|
+
"""
|
|
123
|
+
access_token = await get_zoominfo_access_token(tool_config)
|
|
124
|
+
if not access_token:
|
|
125
|
+
return {"error": "Failed to obtain ZoomInfo access token"}
|
|
126
|
+
|
|
127
|
+
if not linkedin_url and not email and not phone:
|
|
128
|
+
return {"error": "At least one of linkedin_url, email, or phone must be provided"}
|
|
129
|
+
|
|
130
|
+
headers = {
|
|
131
|
+
"Authorization": f"Bearer {access_token}",
|
|
132
|
+
"Content-Type": "application/json"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
data: Dict[str, List[str]] = {}
|
|
136
|
+
cache_key_value = None
|
|
137
|
+
|
|
138
|
+
# Build request and check cache
|
|
139
|
+
if linkedin_url:
|
|
140
|
+
data["personLinkedinUrls"] = [linkedin_url]
|
|
141
|
+
cache_key_value = linkedin_url
|
|
142
|
+
if email:
|
|
143
|
+
data["personEmails"] = [email]
|
|
144
|
+
if phone:
|
|
145
|
+
data["personPhones"] = [phone]
|
|
146
|
+
|
|
147
|
+
if cache_key_value:
|
|
148
|
+
cached_response = retrieve_output(
|
|
149
|
+
"enrich_person_info_from_zoominfo",
|
|
150
|
+
cache_key_value
|
|
151
|
+
)
|
|
152
|
+
if cached_response is not None:
|
|
153
|
+
return cached_response
|
|
154
|
+
|
|
155
|
+
url = "https://api.zoominfo.com/person/contact"
|
|
156
|
+
|
|
157
|
+
async with aiohttp.ClientSession() as session:
|
|
158
|
+
async with session.post(url, headers=headers, json=data) as response:
|
|
159
|
+
if response.status == 200:
|
|
160
|
+
json_result = await response.json()
|
|
161
|
+
# Cache if LinkedIn URL was used
|
|
162
|
+
if cache_key_value:
|
|
163
|
+
cache_output(
|
|
164
|
+
"enrich_person_info_from_zoominfo",
|
|
165
|
+
cache_key_value,
|
|
166
|
+
json_result
|
|
167
|
+
)
|
|
168
|
+
return json_result
|
|
169
|
+
elif response.status == 429:
|
|
170
|
+
logging.warning("enrich_person_info_from_zoominfo Rate limit hit")
|
|
171
|
+
raise aiohttp.ClientResponseError(
|
|
172
|
+
request_info=response.request_info,
|
|
173
|
+
history=response.history,
|
|
174
|
+
status=response.status,
|
|
175
|
+
message="Rate limit exceeded",
|
|
176
|
+
headers=response.headers
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
error_result = await response.json()
|
|
180
|
+
logging.warning(
|
|
181
|
+
f"enrich_person_info_from_zoominfo failed with status "
|
|
182
|
+
f"{response.status}: {error_result}"
|
|
183
|
+
)
|
|
184
|
+
return {"error": error_result}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@assistant_tool
|
|
188
|
+
@backoff.on_exception(
|
|
189
|
+
backoff.expo,
|
|
190
|
+
(aiohttp.ClientResponseError, Exception),
|
|
191
|
+
max_tries=3,
|
|
192
|
+
giveup=lambda e: not (isinstance(e, aiohttp.ClientResponseError) and e.status == 429),
|
|
193
|
+
factor=2,
|
|
194
|
+
)
|
|
195
|
+
async def enrich_organization_info_from_zoominfo(
|
|
196
|
+
organization_domain: Optional[str] = None,
|
|
197
|
+
tool_config: Optional[List[Dict]] = None
|
|
198
|
+
) -> dict:
|
|
199
|
+
"""
|
|
200
|
+
Fetch an organization's details from ZoomInfo using the organization domain.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
organization_domain (str, optional): Domain of the organization.
|
|
204
|
+
tool_config (List[Dict], optional): Configuration list that may contain ZoomInfo credentials.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
dict: JSON response containing organization information, or an error message.
|
|
208
|
+
"""
|
|
209
|
+
access_token = await get_zoominfo_access_token(tool_config)
|
|
210
|
+
if not access_token:
|
|
211
|
+
return {"error": "Failed to obtain ZoomInfo access token"}
|
|
212
|
+
|
|
213
|
+
if not organization_domain:
|
|
214
|
+
return {"error": "Organization domain must be provided"}
|
|
215
|
+
|
|
216
|
+
headers = {
|
|
217
|
+
"Authorization": f"Bearer {access_token}",
|
|
218
|
+
"Content-Type": "application/json"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
cached_response = retrieve_output(
|
|
222
|
+
"enrich_organization_info_from_zoominfo",
|
|
223
|
+
organization_domain
|
|
224
|
+
)
|
|
225
|
+
if cached_response is not None:
|
|
226
|
+
return cached_response
|
|
227
|
+
|
|
228
|
+
data = {"companyDomains": [organization_domain]}
|
|
229
|
+
url = "https://api.zoominfo.com/company/enrich"
|
|
230
|
+
|
|
231
|
+
async with aiohttp.ClientSession() as session:
|
|
232
|
+
async with session.post(url, headers=headers, json=data) as response:
|
|
233
|
+
if response.status == 200:
|
|
234
|
+
json_result = await response.json()
|
|
235
|
+
cache_output(
|
|
236
|
+
"enrich_organization_info_from_zoominfo",
|
|
237
|
+
organization_domain,
|
|
238
|
+
json_result
|
|
239
|
+
)
|
|
240
|
+
return json_result
|
|
241
|
+
elif response.status == 429:
|
|
242
|
+
logging.warning("enrich_organization_info_from_zoominfo Rate limit hit")
|
|
243
|
+
raise aiohttp.ClientResponseError(
|
|
244
|
+
request_info=response.request_info,
|
|
245
|
+
history=response.history,
|
|
246
|
+
status=response.status,
|
|
247
|
+
message="Rate limit exceeded",
|
|
248
|
+
headers=response.headers
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
error_result = await response.json()
|
|
252
|
+
logging.warning(
|
|
253
|
+
f"enrich_organization_info_from_zoominfo failed with status "
|
|
254
|
+
f"{response.status}: {error_result}"
|
|
255
|
+
)
|
|
256
|
+
return {"error": error_result}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def enrich_user_info_with_zoominfo(
|
|
260
|
+
input_user_properties: dict,
|
|
261
|
+
tool_config: Optional[List[Dict]]
|
|
262
|
+
) -> dict:
|
|
263
|
+
"""
|
|
264
|
+
Update user info using ZoomInfo data. Checks LinkedIn URL, fetches data, and updates
|
|
265
|
+
the user's properties accordingly.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
input_user_properties (dict): Existing properties about the user.
|
|
269
|
+
tool_config (List[Dict], optional): Configuration list that may contain ZoomInfo credentials.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
dict: Updated user properties dictionary with ZoomInfo data.
|
|
273
|
+
"""
|
|
274
|
+
linkedin_url = input_user_properties.get("user_linkedin_url", "")
|
|
275
|
+
if not linkedin_url:
|
|
276
|
+
input_user_properties["linkedin_url_match"] = False
|
|
277
|
+
return input_user_properties
|
|
278
|
+
|
|
279
|
+
linkedin_data = await enrich_person_info_from_zoominfo(
|
|
280
|
+
linkedin_url=linkedin_url,
|
|
281
|
+
tool_config=tool_config
|
|
282
|
+
)
|
|
283
|
+
if not linkedin_data:
|
|
284
|
+
input_user_properties["linkedin_url_match"] = False
|
|
285
|
+
return input_user_properties
|
|
286
|
+
|
|
287
|
+
# person_data is extracted from the top-level "person" key in the response
|
|
288
|
+
person_data = linkedin_data.get("person", {})
|
|
289
|
+
additional_props = input_user_properties.get("additional_properties") or {}
|
|
290
|
+
|
|
291
|
+
# Store the data under a "zoominfo_person_data" key instead of "apollo_person_data"
|
|
292
|
+
additional_props["zoominfo_person_data"] = json.dumps(person_data)
|
|
293
|
+
input_user_properties["additional_properties"] = additional_props
|
|
294
|
+
|
|
295
|
+
# Fill missing contact info
|
|
296
|
+
if not input_user_properties.get("email"):
|
|
297
|
+
input_user_properties["email"] = person_data.get("email", "")
|
|
298
|
+
if not input_user_properties.get("phone"):
|
|
299
|
+
input_user_properties["phone"] = person_data.get("contact", {}).get("sanitized_phone", "")
|
|
300
|
+
|
|
301
|
+
# Map fields
|
|
302
|
+
if person_data.get("name"):
|
|
303
|
+
input_user_properties["full_name"] = person_data["name"]
|
|
304
|
+
if person_data.get("first_name"):
|
|
305
|
+
input_user_properties["first_name"] = person_data["first_name"]
|
|
306
|
+
if person_data.get("last_name"):
|
|
307
|
+
input_user_properties["last_name"] = person_data["last_name"]
|
|
308
|
+
if person_data.get("linkedin_url"):
|
|
309
|
+
input_user_properties["user_linkedin_url"] = person_data["linkedin_url"]
|
|
310
|
+
if (
|
|
311
|
+
person_data.get("organization")
|
|
312
|
+
and person_data["organization"].get("primary_domain")
|
|
313
|
+
):
|
|
314
|
+
input_user_properties["primary_domain_of_organization"] = (
|
|
315
|
+
person_data["organization"]["primary_domain"]
|
|
316
|
+
)
|
|
317
|
+
if person_data.get("title"):
|
|
318
|
+
input_user_properties["job_title"] = person_data["title"]
|
|
319
|
+
if person_data.get("headline"):
|
|
320
|
+
input_user_properties["headline"] = person_data["headline"]
|
|
321
|
+
if (
|
|
322
|
+
person_data.get("organization")
|
|
323
|
+
and person_data["organization"].get("name")
|
|
324
|
+
):
|
|
325
|
+
input_user_properties["organization_name"] = person_data["organization"]["name"]
|
|
326
|
+
if (
|
|
327
|
+
person_data.get("organization")
|
|
328
|
+
and person_data["organization"].get("website_url")
|
|
329
|
+
):
|
|
330
|
+
input_user_properties["organization_website"] = person_data["organization"]["website_url"]
|
|
331
|
+
if person_data.get("headline") and not input_user_properties.get("summary_about_lead"):
|
|
332
|
+
input_user_properties["summary_about_lead"] = person_data["headline"]
|
|
333
|
+
if (
|
|
334
|
+
person_data.get("organization")
|
|
335
|
+
and person_data["organization"].get("keywords")
|
|
336
|
+
):
|
|
337
|
+
input_user_properties["keywords"] = ", ".join(person_data["organization"]["keywords"])
|
|
338
|
+
|
|
339
|
+
# Derive location
|
|
340
|
+
if person_data.get("city") or person_data.get("state"):
|
|
341
|
+
input_user_properties["lead_location"] = (
|
|
342
|
+
f"{person_data.get('city', '')}, {person_data.get('state', '')}".strip(", ")
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Match checks
|
|
346
|
+
first_matched = bool(
|
|
347
|
+
input_user_properties.get("first_name")
|
|
348
|
+
and person_data.get("first_name") == input_user_properties["first_name"]
|
|
349
|
+
)
|
|
350
|
+
last_matched = bool(
|
|
351
|
+
input_user_properties.get("last_name")
|
|
352
|
+
and person_data.get("last_name") == input_user_properties["last_name"]
|
|
353
|
+
)
|
|
354
|
+
if first_matched and last_matched:
|
|
355
|
+
input_user_properties["linkedin_url_match"] = True
|
|
356
|
+
|
|
357
|
+
return input_user_properties
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from dhisana.workflow.task import Task
|
|
4
|
+
|
|
5
|
+
class Agent:
|
|
6
|
+
def __init__(self, name: str):
|
|
7
|
+
self.name = name
|
|
8
|
+
|
|
9
|
+
async def perform_task(self, task: Task, inputs_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
10
|
+
if task.error:
|
|
11
|
+
print(f"Skipping task '{task.name}' due to previous error.")
|
|
12
|
+
return []
|
|
13
|
+
print(f"\nAgent {self.name} is performing task: {task.name}")
|
|
14
|
+
return await task.run(inputs_list)
|
|
15
|
+
|
|
16
|
+
def agent(cls):
|
|
17
|
+
cls.agent = Agent(name=cls.__name__)
|
|
18
|
+
return cls
|
dhisana/workflow/flow.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import Callable, Any, Dict, List
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from dhisana.workflow.task import Task
|
|
5
|
+
from dhisana.workflow.agent import Agent
|
|
6
|
+
|
|
7
|
+
class Flow:
|
|
8
|
+
def __init__(self, name: str):
|
|
9
|
+
self.name = name
|
|
10
|
+
self.tasks: Dict[str, Task] = {}
|
|
11
|
+
self.agents: List[Agent] = []
|
|
12
|
+
|
|
13
|
+
def add_task(self, task: Task) -> None:
|
|
14
|
+
self.tasks[task.name] = task
|
|
15
|
+
|
|
16
|
+
def add_agent(self, agent: Agent) -> None:
|
|
17
|
+
self.agents.append(agent)
|
|
18
|
+
|
|
19
|
+
def resolve_dependencies(self):
|
|
20
|
+
for task in self.tasks.values():
|
|
21
|
+
task.dependencies = [self.tasks[dep_name] for dep_name in task.dependency_names]
|
|
22
|
+
|
|
23
|
+
async def run(self, inputs_list: List[Dict[str, Any]] = [{}]) -> None:
|
|
24
|
+
self.resolve_dependencies()
|
|
25
|
+
for task in self.tasks.values():
|
|
26
|
+
if all(dep.results and dep.error is None for dep in task.dependencies):
|
|
27
|
+
for agent in self.agents:
|
|
28
|
+
results = await agent.perform_task(task, inputs_list)
|
|
29
|
+
if results:
|
|
30
|
+
print(f"Results of task '{task.name}': {results}")
|
|
31
|
+
else:
|
|
32
|
+
print(f"Task '{task.name}' failed.")
|
|
33
|
+
else:
|
|
34
|
+
print(f"Skipping task '{task.name}' due to unmet dependencies.")
|
|
35
|
+
|
|
36
|
+
def flow(name: str):
|
|
37
|
+
def decorator(func: Callable[..., Any]):
|
|
38
|
+
@wraps(func)
|
|
39
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
40
|
+
f = Flow(name=name)
|
|
41
|
+
await func(f, *args, **kwargs)
|
|
42
|
+
await f.run(kwargs.get('inputs_list', [{}]))
|
|
43
|
+
return wrapper
|
|
44
|
+
return decorator
|
dhisana/workflow/task.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import Callable, Any, Dict, List
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
class Task:
|
|
6
|
+
def __init__(self, name: str, description: str, label: str, function: Callable[..., Any]):
|
|
7
|
+
self.name = name
|
|
8
|
+
self.description = description
|
|
9
|
+
self.label = label
|
|
10
|
+
self.function = function
|
|
11
|
+
self.dependencies: List['Task'] = []
|
|
12
|
+
self.results: List[Dict[str, Any]] = []
|
|
13
|
+
self.error = None
|
|
14
|
+
|
|
15
|
+
async def run(self, inputs_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
16
|
+
try:
|
|
17
|
+
self.results = []
|
|
18
|
+
for inputs in inputs_list:
|
|
19
|
+
# Gather inputs from dependencies
|
|
20
|
+
dep_results = {dep.label: dep.results for dep in self.dependencies}
|
|
21
|
+
inputs.update(dep_results)
|
|
22
|
+
result = await self.function([inputs])
|
|
23
|
+
self.results.extend(result)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
self.error = e
|
|
26
|
+
print(f"Error in task '{self.name}': {e}")
|
|
27
|
+
return self.results
|
|
28
|
+
|
|
29
|
+
def set_dependencies(self, dependencies: List['Task']):
|
|
30
|
+
self.dependencies = dependencies
|
|
31
|
+
|
|
32
|
+
def task(name: str, description: str = "", label: str = "", dependencies: List[str] = None):
|
|
33
|
+
# Enforce naming convention: alphanumeric characters and underscores only
|
|
34
|
+
if not name.replace('_', '').isalnum():
|
|
35
|
+
raise ValueError("Task name must contain only alphanumeric characters and underscores.")
|
|
36
|
+
def decorator(func: Callable[..., Any]):
|
|
37
|
+
@wraps(func)
|
|
38
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
39
|
+
return await func(*args, **kwargs)
|
|
40
|
+
wrapper.task = Task(name=name, description=description, label=label or name, function=wrapper)
|
|
41
|
+
wrapper.task.dependency_names = dependencies or []
|
|
42
|
+
return wrapper
|
|
43
|
+
return decorator
|
dhisana/workflow/test.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from http.client import HTTPException
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
import openai
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from dhisana.workflow.task import task
|
|
8
|
+
from dhisana.workflow.flow import Flow, flow
|
|
9
|
+
from dhisana.workflow.agent import agent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Initialize OpenAI API
|
|
14
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
15
|
+
client = openai.OpenAI(api_key=api_key)
|
|
16
|
+
|
|
17
|
+
async def call_openai_api_test(system_content: str, user_content: str, max_tokens: int) -> str:
|
|
18
|
+
return "This is a test response."
|
|
19
|
+
|
|
20
|
+
async def call_openai_api(system_content: str, user_content: str, max_tokens: int) -> str:
|
|
21
|
+
try:
|
|
22
|
+
# Call the OpenAI API using the new client method
|
|
23
|
+
response = client.chat.completions.create(
|
|
24
|
+
model="gpt-5.1-chat",
|
|
25
|
+
messages=[
|
|
26
|
+
{"role": "system", "content": system_content},
|
|
27
|
+
{"role": "user", "content": user_content}
|
|
28
|
+
],
|
|
29
|
+
max_tokens=max_tokens
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Access the response content properly
|
|
33
|
+
reply = response.choices[0].message.content.strip()
|
|
34
|
+
return reply
|
|
35
|
+
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Error: {e}")
|
|
38
|
+
raise HTTPException(status_code=500, detail="An error occurred while processing your request.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@task(name="generate_poem", description="Generate a poem about a given topic", label="poem")
|
|
44
|
+
async def generate_poem(inputs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
45
|
+
results = []
|
|
46
|
+
for input_data in inputs:
|
|
47
|
+
topic = input_data.get('topic', 'life')
|
|
48
|
+
system_content = "You are a poet."
|
|
49
|
+
user_content = f"Write a poem about {topic}."
|
|
50
|
+
poem = await call_openai_api(system_content, user_content, max_tokens=100)
|
|
51
|
+
results.append({'topic': topic, 'poem': poem})
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
@task(name="summarize_poem", description="Summarize the given poem", label="summary", dependencies=["generate_poem"])
|
|
55
|
+
async def summarize_poem(inputs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
56
|
+
results = []
|
|
57
|
+
for input_data in inputs:
|
|
58
|
+
poem = input_data.get('poem', '')
|
|
59
|
+
system_content = "You are a literary critic."
|
|
60
|
+
user_content = f"Summarize the following poem:\n\n{poem}"
|
|
61
|
+
summary = await call_openai_api(system_content, user_content, max_tokens=50)
|
|
62
|
+
results.append({'poem': poem, 'summary': summary})
|
|
63
|
+
return results
|
|
64
|
+
|
|
65
|
+
@task(name="analyze_sentiment", description="Analyze the sentiment of the given text", label="sentiment", dependencies=["summarize_poem"])
|
|
66
|
+
async def analyze_sentiment(inputs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
67
|
+
results = []
|
|
68
|
+
for input_data in inputs:
|
|
69
|
+
summary = input_data.get('summary', '')
|
|
70
|
+
system_content = "You are a sentiment analyst."
|
|
71
|
+
user_content = f"Analyze the sentiment of the following text:\n\n{summary}"
|
|
72
|
+
sentiment = await call_openai_api(system_content, user_content, max_tokens=50)
|
|
73
|
+
results.append({'summary': summary, 'sentiment': sentiment})
|
|
74
|
+
return results
|
|
75
|
+
|
|
76
|
+
@agent
|
|
77
|
+
class AIAgent:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@flow(name="AI_Workflow")
|
|
81
|
+
async def ai_workflow(f: Flow, inputs_list: List[Dict[str, Any]]):
|
|
82
|
+
f.add_task(generate_poem.task)
|
|
83
|
+
f.add_task(summarize_poem.task)
|
|
84
|
+
f.add_task(analyze_sentiment.task)
|
|
85
|
+
f.add_agent(AIAgent.agent)
|
|
86
|
+
|
|
87
|
+
# Execute the workflow with a list of topics
|
|
88
|
+
topics = [{"topic": "artificial intelligence"}, {"topic": "quantum computing"}]
|
|
89
|
+
|
|
90
|
+
asyncio.run(ai_workflow(inputs_list=topics))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dhisana
|
|
3
|
+
Version: 0.0.1.dev243
|
|
4
|
+
Summary: A Python SDK for Dhisana AI Platform
|
|
5
|
+
Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
|
|
6
|
+
Author: Admin
|
|
7
|
+
Author-email: contact@dhisana.ai
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Requires-Dist: bs4
|
|
15
|
+
Requires-Dist: click>=7.0
|
|
16
|
+
Requires-Dist: fastapi
|
|
17
|
+
Requires-Dist: google-api-python-client
|
|
18
|
+
Requires-Dist: google-auth
|
|
19
|
+
Requires-Dist: openai
|
|
20
|
+
Requires-Dist: playwright
|
|
21
|
+
Requires-Dist: requests
|
|
22
|
+
Requires-Dist: uvicorn[standard]
|
|
23
|
+
Requires-Dist: aiohttp
|
|
24
|
+
Requires-Dist: openapi_pydantic
|
|
25
|
+
Requires-Dist: pandas
|
|
26
|
+
Requires-Dist: simple_salesforce
|
|
27
|
+
Requires-Dist: backoff
|
|
28
|
+
Requires-Dist: html2text
|
|
29
|
+
Requires-Dist: hubspot-api-client
|
|
30
|
+
Requires-Dist: tldextract
|
|
31
|
+
Requires-Dist: pyperclip
|
|
32
|
+
Requires-Dist: azure-storage-blob
|
|
33
|
+
Requires-Dist: email_validator
|
|
34
|
+
Requires-Dist: fqdn
|
|
35
|
+
Requires-Dist: json_repair
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: author-email
|
|
38
|
+
Dynamic: classifier
|
|
39
|
+
Dynamic: home-page
|
|
40
|
+
Dynamic: license
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|