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.
Files changed (102) hide show
  1. dhisana/__init__.py +1 -0
  2. dhisana/cli/__init__.py +1 -0
  3. dhisana/cli/cli.py +20 -0
  4. dhisana/cli/datasets.py +27 -0
  5. dhisana/cli/models.py +26 -0
  6. dhisana/cli/predictions.py +20 -0
  7. dhisana/schemas/__init__.py +1 -0
  8. dhisana/schemas/common.py +399 -0
  9. dhisana/schemas/sales.py +965 -0
  10. dhisana/ui/__init__.py +1 -0
  11. dhisana/ui/components.py +472 -0
  12. dhisana/utils/__init__.py +1 -0
  13. dhisana/utils/add_mapping.py +352 -0
  14. dhisana/utils/agent_tools.py +51 -0
  15. dhisana/utils/apollo_tools.py +1597 -0
  16. dhisana/utils/assistant_tool_tag.py +4 -0
  17. dhisana/utils/built_with_api_tools.py +282 -0
  18. dhisana/utils/cache_output_tools.py +98 -0
  19. dhisana/utils/cache_output_tools_local.py +78 -0
  20. dhisana/utils/check_email_validity_tools.py +717 -0
  21. dhisana/utils/check_for_intent_signal.py +107 -0
  22. dhisana/utils/check_linkedin_url_validity.py +209 -0
  23. dhisana/utils/clay_tools.py +43 -0
  24. dhisana/utils/clean_properties.py +135 -0
  25. dhisana/utils/company_utils.py +60 -0
  26. dhisana/utils/compose_salesnav_query.py +259 -0
  27. dhisana/utils/compose_search_query.py +759 -0
  28. dhisana/utils/compose_three_step_workflow.py +234 -0
  29. dhisana/utils/composite_tools.py +137 -0
  30. dhisana/utils/dataframe_tools.py +237 -0
  31. dhisana/utils/domain_parser.py +45 -0
  32. dhisana/utils/email_body_utils.py +72 -0
  33. dhisana/utils/email_parse_helpers.py +132 -0
  34. dhisana/utils/email_provider.py +375 -0
  35. dhisana/utils/enrich_lead_information.py +933 -0
  36. dhisana/utils/extract_email_content_for_llm.py +101 -0
  37. dhisana/utils/fetch_openai_config.py +129 -0
  38. dhisana/utils/field_validators.py +426 -0
  39. dhisana/utils/g2_tools.py +104 -0
  40. dhisana/utils/generate_content.py +41 -0
  41. dhisana/utils/generate_custom_message.py +271 -0
  42. dhisana/utils/generate_email.py +278 -0
  43. dhisana/utils/generate_email_response.py +465 -0
  44. dhisana/utils/generate_flow.py +102 -0
  45. dhisana/utils/generate_leads_salesnav.py +303 -0
  46. dhisana/utils/generate_linkedin_connect_message.py +224 -0
  47. dhisana/utils/generate_linkedin_response_message.py +317 -0
  48. dhisana/utils/generate_structured_output_internal.py +462 -0
  49. dhisana/utils/google_custom_search.py +267 -0
  50. dhisana/utils/google_oauth_tools.py +727 -0
  51. dhisana/utils/google_workspace_tools.py +1294 -0
  52. dhisana/utils/hubspot_clearbit.py +96 -0
  53. dhisana/utils/hubspot_crm_tools.py +2440 -0
  54. dhisana/utils/instantly_tools.py +149 -0
  55. dhisana/utils/linkedin_crawler.py +168 -0
  56. dhisana/utils/lusha_tools.py +333 -0
  57. dhisana/utils/mailgun_tools.py +156 -0
  58. dhisana/utils/mailreach_tools.py +123 -0
  59. dhisana/utils/microsoft365_tools.py +455 -0
  60. dhisana/utils/openai_assistant_and_file_utils.py +267 -0
  61. dhisana/utils/openai_helpers.py +977 -0
  62. dhisana/utils/openapi_spec_to_tools.py +45 -0
  63. dhisana/utils/openapi_tool/__init__.py +1 -0
  64. dhisana/utils/openapi_tool/api_models.py +633 -0
  65. dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
  66. dhisana/utils/openapi_tool/openapi_tool.py +319 -0
  67. dhisana/utils/parse_linkedin_messages_txt.py +100 -0
  68. dhisana/utils/profile.py +37 -0
  69. dhisana/utils/proxy_curl_tools.py +1226 -0
  70. dhisana/utils/proxycurl_search_leads.py +426 -0
  71. dhisana/utils/python_function_to_tools.py +83 -0
  72. dhisana/utils/research_lead.py +176 -0
  73. dhisana/utils/sales_navigator_crawler.py +1103 -0
  74. dhisana/utils/salesforce_crm_tools.py +477 -0
  75. dhisana/utils/search_router.py +131 -0
  76. dhisana/utils/search_router_jobs.py +51 -0
  77. dhisana/utils/sendgrid_tools.py +162 -0
  78. dhisana/utils/serarch_router_local_business.py +75 -0
  79. dhisana/utils/serpapi_additional_tools.py +290 -0
  80. dhisana/utils/serpapi_google_jobs.py +117 -0
  81. dhisana/utils/serpapi_google_search.py +188 -0
  82. dhisana/utils/serpapi_local_business_search.py +129 -0
  83. dhisana/utils/serpapi_search_tools.py +852 -0
  84. dhisana/utils/serperdev_google_jobs.py +125 -0
  85. dhisana/utils/serperdev_local_business.py +154 -0
  86. dhisana/utils/serperdev_search.py +233 -0
  87. dhisana/utils/smtp_email_tools.py +582 -0
  88. dhisana/utils/test_connect.py +2087 -0
  89. dhisana/utils/trasform_json.py +173 -0
  90. dhisana/utils/web_download_parse_tools.py +189 -0
  91. dhisana/utils/workflow_code_model.py +5 -0
  92. dhisana/utils/zoominfo_tools.py +357 -0
  93. dhisana/workflow/__init__.py +1 -0
  94. dhisana/workflow/agent.py +18 -0
  95. dhisana/workflow/flow.py +44 -0
  96. dhisana/workflow/task.py +43 -0
  97. dhisana/workflow/test.py +90 -0
  98. dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
  99. dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
  100. dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
  101. dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
  102. 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
@@ -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
@@ -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
@@ -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