dhisana 0.0.1.dev44__tar.gz → 0.0.1.dev45__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 (86) hide show
  1. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/setup.py +1 -1
  3. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/schemas/sales.py +2 -2
  4. dhisana-0.0.1.dev45/src/dhisana/utils/check_email_validity_tools.py +587 -0
  5. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/dataframe_tools.py +1 -1
  6. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/enrich_lead_information.py +7 -5
  7. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_email.py +1 -1
  8. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_email_response.py +1 -1
  9. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/serpapi_search_tools.py +3 -3
  10. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/PKG-INFO +1 -1
  11. dhisana-0.0.1.dev44/src/dhisana/utils/check_email_validity_tools.py +0 -380
  12. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/README.md +0 -0
  13. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/pyproject.toml +0 -0
  14. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/setup.cfg +0 -0
  15. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/__init__.py +0 -0
  16. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/__init__.py +0 -0
  17. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/cli.py +0 -0
  18. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/datasets.py +0 -0
  19. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/models.py +0 -0
  20. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/predictions.py +0 -0
  21. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/schemas/__init__.py +0 -0
  22. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/schemas/common.py +0 -0
  23. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/ui/__init__.py +0 -0
  24. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/ui/components.py +0 -0
  25. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/__init__.py +0 -0
  26. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/agent_task.py +0 -0
  27. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/agent_tools.py +0 -0
  28. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/apollo_tools.py +0 -0
  29. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  30. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/built_with_api_tools.py +0 -0
  31. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/cache_output_tools.py +0 -0
  32. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  33. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  34. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  35. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/clay_tools.py +0 -0
  36. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/company_utils.py +0 -0
  37. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_cadence.py +0 -0
  38. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  39. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_search_query.py +0 -0
  40. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  41. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_workflow.py +0 -0
  42. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/composite_tools.py +0 -0
  43. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/domain_parser.py +0 -0
  44. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  45. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/g2_tools.py +0 -0
  46. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_content.py +0 -0
  47. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_flow.py +0 -0
  48. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  49. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  50. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  51. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  52. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/google_custom_search.py +0 -0
  53. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/google_workspace_tools.py +0 -0
  54. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  55. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  56. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/instantly_tools.py +0 -0
  57. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/linkedin_crawler.py +0 -0
  58. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/lusha_tools.py +0 -0
  59. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  60. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openai_helpers.py +0 -0
  61. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  62. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  63. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  64. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  65. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  66. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  67. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/python_function_to_tools.py +0 -0
  68. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/research_lead.py +0 -0
  69. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  70. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  71. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/sendgrid_tools.py +0 -0
  72. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/trasform_json.py +0 -0
  73. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  74. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/workflow_code_model.py +0 -0
  75. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/zoominfo_tools.py +0 -0
  76. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/__init__.py +0 -0
  77. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/agent.py +0 -0
  78. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/flow.py +0 -0
  79. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/task.py +0 -0
  80. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/test.py +0 -0
  81. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/SOURCES.txt +0 -0
  82. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/dependency_links.txt +0 -0
  83. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/entry_points.txt +0 -0
  84. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/requires.txt +0 -0
  85. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/top_level.txt +0 -0
  86. {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/tests/test_agent_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dhisana
3
- Version: 0.0.1.dev44
3
+ Version: 0.0.1.dev45
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
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='dhisana',
5
- version='0.0.1-dev44',
5
+ version='0.0.1-dev45',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -280,14 +280,14 @@ class ExternalDataSources(BaseModel):
280
280
  class ContentGenerationContext(BaseModel):
281
281
  """
282
282
  Consolidates all relevant data needed for generating content:
283
- 1) campaing_context: Details about the current campaign or marketing approach.
283
+ 1) campaign_context: Details about the current campaign or marketing approach.
284
284
  2) lead_info: The lead's basic information.
285
285
  3) sender_info: Who is sending the message.
286
286
  4) external_known_data: Any references to external data sources (e.g., vector store IDs).
287
287
  5) current_conversation_context: Current or ongoing conversation threads (email or LinkedIn).
288
288
  6) target_channel_type: Which channel we are generating content for (email, LinkedIn, etc.).
289
289
  """
290
- campaing_context: Optional[CampaingContext] = None
290
+ campaign_context: Optional[CampaingContext] = None
291
291
  lead_info: Optional[Lead] = None
292
292
  sender_info: Optional[SenderInfo] = None
293
293
  external_known_data: Optional[ExternalDataSources] = None
@@ -0,0 +1,587 @@
1
+ import os
2
+ import json
3
+ import logging
4
+ from typing import Dict, List, Optional, Any
5
+ import aiohttp
6
+
7
+ logger = logging.getLogger(__name__)
8
+ from dhisana.utils.assistant_tool_tag import assistant_tool
9
+ from dhisana.utils.cache_output_tools import cache_output, retrieve_output
10
+
11
+
12
+ # --------------------------------------------------------------------------------
13
+ # 1. Access Token Helpers
14
+ # --------------------------------------------------------------------------------
15
+
16
+ def get_zero_bounce_access_token(tool_config: Optional[List[Dict]] = None) -> str:
17
+ """
18
+ Retrieves the ZeroBounce access token from the provided tool configuration or environment.
19
+ """
20
+ if tool_config:
21
+ zerobounce_config = next(
22
+ (item for item in tool_config if item.get("name") == "zerobounce"), None
23
+ )
24
+ if zerobounce_config:
25
+ config_map = {
26
+ c["name"]: c["value"]
27
+ for c in zerobounce_config.get("configuration", [])
28
+ if c
29
+ }
30
+ ZERO_BOUNCE_API_KEY = config_map.get("apiKey")
31
+ else:
32
+ logger.warning("ZeroBounce config not provided or missing 'apiKey'.")
33
+ ZERO_BOUNCE_API_KEY = None
34
+ else:
35
+ logger.warning("ZeroBounce config not provided or missing 'apiKey'.")
36
+ ZERO_BOUNCE_API_KEY = None
37
+
38
+ ZERO_BOUNCE_API_KEY = ZERO_BOUNCE_API_KEY or os.getenv("ZERO_BOUNCE_API_KEY")
39
+ if not ZERO_BOUNCE_API_KEY:
40
+ logger.warning("ZERO_BOUNCE_API_KEY not found in config or env.")
41
+ return "" # Return empty so we don't break
42
+
43
+ return ZERO_BOUNCE_API_KEY
44
+
45
+
46
+ def get_hunter_access_token(tool_config: Optional[List[Dict]] = None) -> str:
47
+ """
48
+ Retrieves the Hunter.io access token from the provided tool configuration or environment.
49
+ """
50
+ if tool_config:
51
+ hunter_config = next(
52
+ (item for item in tool_config if item.get("name") == "hunter"), None
53
+ )
54
+ if hunter_config:
55
+ config_map = {
56
+ c["name"]: c["value"]
57
+ for c in hunter_config.get("configuration", [])
58
+ if c
59
+ }
60
+ HUNTER_API_KEY = config_map.get("apiKey")
61
+ else:
62
+ logger.warning("Hunter config not provided or missing 'apiKey'.")
63
+ HUNTER_API_KEY = None
64
+ else:
65
+ logger.warning("Hunter config not provided or missing 'apiKey'.")
66
+ HUNTER_API_KEY = None
67
+
68
+ HUNTER_API_KEY = HUNTER_API_KEY or os.getenv("HUNTER_API_KEY")
69
+ if not HUNTER_API_KEY:
70
+ logger.warning("HUNTER_API_KEY not found in config or env.")
71
+ return "" # Return empty so we don't break
72
+
73
+ return HUNTER_API_KEY
74
+
75
+
76
+ # --------------------------------------------------------------------------------
77
+ # 2. Provider-Specific Validation Functions
78
+ # --------------------------------------------------------------------------------
79
+
80
+ def _map_zerobounce_status_to_confidence(status: str) -> str:
81
+ """
82
+ Map ZeroBounce's status string to "high", "medium", or "low" confidence.
83
+ """
84
+ status = status.lower()
85
+ if status == "valid":
86
+ return "high"
87
+ elif status in ["catch-all", "unknown"]:
88
+ return "medium"
89
+ elif status in ["spamtrap", "invalid"]:
90
+ return "low"
91
+ return "low"
92
+
93
+
94
+ @assistant_tool
95
+ async def check_email_validity_with_zero_bounce(
96
+ email_id: str,
97
+ tool_config: Optional[List[Dict]] = None
98
+ ) -> Dict[str, Any]:
99
+ """
100
+ Validate a single email address using the ZeroBounce API, with caching.
101
+ Returns: {
102
+ "email": str,
103
+ "confidence": "high"|"medium"|"low",
104
+ "is_valid": bool
105
+ }
106
+ """
107
+ logger.info("Entering check_email_validity_with_zero_bounce for email_id: %s", email_id)
108
+ import re
109
+ if not email_id or not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email_id):
110
+ return {
111
+ "email": email_id,
112
+ "confidence": "low",
113
+ "is_valid": False
114
+ }
115
+
116
+ cache_key = f"{email_id}"
117
+ cached_response = retrieve_output("zerobounce_validate", cache_key)
118
+ if cached_response is not None:
119
+ logger.info("Cache hit for ZeroBounce validate.")
120
+ if not cached_response:
121
+ return {
122
+ "email": email_id,
123
+ "confidence": "low",
124
+ "is_valid": False
125
+ }
126
+ return json.loads(cached_response[0])
127
+
128
+ # Get API key
129
+ ZERO_BOUNCE_API_KEY = get_zero_bounce_access_token(tool_config)
130
+ if not ZERO_BOUNCE_API_KEY:
131
+ logger.warning("No ZeroBounce API key available. Returning low confidence.")
132
+ return {
133
+ "email": email_id,
134
+ "confidence": "low",
135
+ "is_valid": False
136
+ }
137
+
138
+ url = (
139
+ "https://api.zerobounce.net/v2/validate"
140
+ f"?api_key={ZERO_BOUNCE_API_KEY}&email={email_id}"
141
+ )
142
+
143
+ try:
144
+ async with aiohttp.ClientSession() as session:
145
+ async with session.get(url) as response:
146
+ if response.status != 200:
147
+ content = await safe_read_json_or_text(response)
148
+ logger.warning(
149
+ f"[ZeroBounce] Non-200 status: {response.status} => {content}"
150
+ )
151
+ # Return fallback instead of raising
152
+ final_response = {
153
+ "email": email_id,
154
+ "confidence": "low",
155
+ "is_valid": False
156
+ }
157
+ cache_output("zerobounce_validate", cache_key, [json.dumps(final_response)])
158
+ return final_response
159
+
160
+ result = await response.json()
161
+ except Exception as ex:
162
+ logger.warning(f"[ZeroBounce] Exception occurred => {ex}")
163
+ return {
164
+ "email": email_id,
165
+ "confidence": "low",
166
+ "is_valid": False
167
+ }
168
+
169
+ zb_status = result.get("status", "").lower() # e.g. "valid", "invalid"
170
+ confidence = _map_zerobounce_status_to_confidence(zb_status)
171
+ is_valid = (confidence == "high")
172
+
173
+ final_response = {
174
+ "email": email_id,
175
+ "confidence": confidence,
176
+ "is_valid": is_valid
177
+ }
178
+ cache_output("zerobounce_validate", cache_key, [json.dumps(final_response)])
179
+ logger.info("Exiting check_email_validity_with_zero_bounce.")
180
+ return final_response
181
+
182
+
183
+ def _map_hunter_status_to_confidence(hunter_result: str) -> str:
184
+ """
185
+ Map Hunter's email verifier result to "high", "medium", or "low" confidence.
186
+ Possible results: deliverable, undeliverable, risky, unknown, accept_all.
187
+ """
188
+ val = hunter_result.lower()
189
+ if val == "deliverable":
190
+ return "high"
191
+ elif val in ["risky", "unknown", "accept_all"]:
192
+ return "medium"
193
+ elif val == "undeliverable":
194
+ return "low"
195
+ return "low"
196
+
197
+
198
+ @assistant_tool
199
+ async def check_email_validity_with_hunter(
200
+ email_id: str,
201
+ tool_config: Optional[List[Dict]] = None
202
+ ) -> Dict[str, Any]:
203
+ """
204
+ Validate a single email address using Hunter.io's email verification API.
205
+ Returns: {
206
+ "email": str,
207
+ "confidence": "high"|"medium"|"low",
208
+ "is_valid": bool
209
+ }
210
+ """
211
+ logger.info("Entering check_email_validity_with_hunter for email_id: %s", email_id)
212
+ import re
213
+ if not email_id or not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email_id):
214
+ return {
215
+ "email": email_id,
216
+ "confidence": "low",
217
+ "is_valid": False
218
+ }
219
+
220
+ HUNTER_API_KEY = get_hunter_access_token(tool_config)
221
+ if not HUNTER_API_KEY:
222
+ logger.warning("No Hunter API key available. Returning low confidence.")
223
+ return {
224
+ "email": email_id,
225
+ "confidence": "low",
226
+ "is_valid": False
227
+ }
228
+
229
+ url = (
230
+ "https://api.hunter.io/v2/email-verifier"
231
+ f"?email={email_id}&api_key={HUNTER_API_KEY}"
232
+ )
233
+
234
+ try:
235
+ async with aiohttp.ClientSession() as session:
236
+ async with session.get(url) as response:
237
+ if response.status != 200:
238
+ content = await safe_read_json_or_text(response)
239
+ logger.warning(f"[Hunter] Non-200 status: {response.status} => {content}")
240
+ return {
241
+ "email": email_id,
242
+ "confidence": "low",
243
+ "is_valid": False
244
+ }
245
+
246
+ result = await response.json()
247
+ except Exception as ex:
248
+ logger.warning(f"[Hunter] Exception occurred => {ex}")
249
+ return {
250
+ "email": email_id,
251
+ "confidence": "low",
252
+ "is_valid": False
253
+ }
254
+
255
+ data = result.get("data", {})
256
+ verifier_result = data.get("result", "") # "deliverable", "undeliverable", etc.
257
+ confidence = _map_hunter_status_to_confidence(verifier_result)
258
+ is_valid = (confidence == "high")
259
+
260
+ final_response = {
261
+ "email": email_id,
262
+ "confidence": confidence,
263
+ "is_valid": is_valid
264
+ }
265
+ logger.info("Exiting check_email_validity_with_hunter.")
266
+ return final_response
267
+
268
+
269
+ # --------------------------------------------------------------------------------
270
+ # 3. Provider-Specific Guessing Functions
271
+ # --------------------------------------------------------------------------------
272
+
273
+ @assistant_tool
274
+ async def guess_email_with_zero_bounce(
275
+ first_name: str,
276
+ last_name: str,
277
+ domain: str,
278
+ middle_name: Optional[str] = None,
279
+ tool_config: Optional[List[Dict]] = None
280
+ ) -> Dict[str, Any]:
281
+ """
282
+ Attempt to guess the email using ZeroBounce's guessFormat endpoint, with caching.
283
+ We assume the API returns an "email" and an "email_confidence" field.
284
+ """
285
+ logger.info("Entering guess_email_with_zero_bounce.")
286
+ if not first_name or not last_name or not domain:
287
+ logger.error("Required parameters first_name, last_name, and domain must be provided.")
288
+ return {"email": "", "email_confidence": "low"}
289
+
290
+ cache_key = f"{first_name}_{last_name}_{domain}_{middle_name or ''}"
291
+ cached_response = retrieve_output("zerobounce_guess", cache_key)
292
+ if cached_response is not None:
293
+ logger.info("Cache hit for ZeroBounce guess.")
294
+ return json.loads(cached_response[0]) if cached_response else {"email": "", "email_confidence": "low"}
295
+
296
+ ZERO_BOUNCE_API_KEY = get_zero_bounce_access_token(tool_config)
297
+ if not ZERO_BOUNCE_API_KEY:
298
+ logger.warning("No ZeroBounce API key available. Returning low confidence guess.")
299
+ return {"email": "", "email_confidence": "low"}
300
+
301
+ base_url = "https://api.zerobounce.net/v2/guessformat"
302
+ query_params = (
303
+ f"?api_key={ZERO_BOUNCE_API_KEY}"
304
+ f"&domain={domain}"
305
+ f"&first_name={first_name}"
306
+ f"&middle_name={middle_name or ''}"
307
+ f"&last_name={last_name}"
308
+ )
309
+ url = base_url + query_params
310
+
311
+ try:
312
+ async with aiohttp.ClientSession() as session:
313
+ async with session.get(url) as response:
314
+ if response.status != 200:
315
+ content = await safe_read_json_or_text(response)
316
+ logger.warning(f"[ZeroBounce] guessFormat error: {response.status} => {content}")
317
+ return {"email": "", "email_confidence": "low"}
318
+ result = await response.json()
319
+ except Exception as ex:
320
+ logger.warning(f"[ZeroBounce] Exception => {ex}")
321
+ return {"email": "", "email_confidence": "low"}
322
+
323
+ # If the API doesn't provide "email_confidence", you can supply a fallback:
324
+ if "email_confidence" not in result:
325
+ result["email_confidence"] = "medium" if result.get("email") else "low"
326
+
327
+ cache_output("zerobounce_guess", cache_key, [json.dumps(result)])
328
+ logger.info("Exiting guess_email_with_zero_bounce.")
329
+ return result
330
+
331
+
332
+ @assistant_tool
333
+ async def guess_email_with_hunter(
334
+ first_name: str,
335
+ last_name: str,
336
+ domain: str,
337
+ middle_name: Optional[str] = None,
338
+ tool_config: Optional[List[Dict]] = None
339
+ ) -> Dict[str, Any]:
340
+ """
341
+ Attempt to guess the email using Hunter.io's email-finder endpoint.
342
+ We'll interpret the "score" (0-100) from the response and map it to "email_confidence".
343
+
344
+ Fix applied: We handle the case where score might be None or not numeric, defaulting to 0.
345
+ """
346
+ logger.info("Entering guess_email_with_hunter.")
347
+ if not first_name or not last_name or not domain:
348
+ logger.error("Required parameters first_name, last_name, and domain must be provided.")
349
+ return {"email": "", "email_confidence": "low"}
350
+
351
+ HUNTER_API_KEY = get_hunter_access_token(tool_config)
352
+ if not HUNTER_API_KEY:
353
+ logger.warning("No Hunter API key available. Returning low-confidence guess.")
354
+ return {"email": "", "email_confidence": "low"}
355
+
356
+ url = (
357
+ "https://api.hunter.io/v2/email-finder"
358
+ f"?domain={domain}"
359
+ f"&first_name={first_name}"
360
+ f"&last_name={last_name}"
361
+ f"&api_key={HUNTER_API_KEY}"
362
+ )
363
+ # If you want to handle middle_name and the API supports it, you could append it:
364
+ # f"&middle_name={middle_name}"
365
+
366
+ try:
367
+ async with aiohttp.ClientSession() as session:
368
+ async with session.get(url) as response:
369
+ if response.status != 200:
370
+ content = await safe_read_json_or_text(response)
371
+ logger.warning(f"[Hunter] email-finder error: {response.status} => {content}")
372
+ return {"email": "", "email_confidence": "low"}
373
+
374
+ result = await response.json()
375
+ except Exception as ex:
376
+ logger.warning(f"[Hunter] Exception => {ex}")
377
+ return {"email": "", "email_confidence": "low"}
378
+
379
+ data = result.get("data", {})
380
+ found_email = data.get("email", "")
381
+
382
+ # Safely parse numeric score
383
+ raw_score = data.get("score") # might be int, float, None, or not present
384
+ try:
385
+ score = float(raw_score) if raw_score is not None else 0.0
386
+ except (ValueError, TypeError):
387
+ score = 0.0
388
+
389
+ if score >= 80:
390
+ confidence = "high"
391
+ elif score >= 50:
392
+ confidence = "medium"
393
+ else:
394
+ confidence = "low"
395
+
396
+ output = {
397
+ "email": found_email,
398
+ "email_confidence": confidence
399
+ }
400
+ logger.info("Exiting guess_email_with_hunter.")
401
+ return output
402
+
403
+
404
+ GUESS_EMAIL_TOOL_MAP = {
405
+ "zerobounce": guess_email_with_zero_bounce,
406
+ "hunter": guess_email_with_hunter
407
+ }
408
+
409
+
410
+ # --------------------------------------------------------------------------------
411
+ # 4. Aggregators (High-Level Validation + Guess)
412
+ # --------------------------------------------------------------------------------
413
+
414
+ @assistant_tool
415
+ async def check_email_validity(
416
+ email_id: str,
417
+ tool_config: Optional[List[Dict]] = None
418
+ ) -> Dict[str, Any]:
419
+ """
420
+ Validate an email address by checking each provider in priority order:
421
+ 1) ZeroBounce
422
+ 2) Hunter
423
+ If a provider returns high confidence, we stop. Otherwise we continue.
424
+ """
425
+ logger.info("Entering check_email_validity method.")
426
+ if not tool_config:
427
+ logger.warning("No tool configuration found; returning low confidence.")
428
+ return {
429
+ "email": email_id,
430
+ "confidence": "low",
431
+ "is_valid": False
432
+ }
433
+
434
+ # Identify which providers are available
435
+ provider_names = [item.get("name") for item in tool_config if item.get("name")]
436
+ priority = ["zerobounce", "hunter"]
437
+
438
+ final_result = {
439
+ "email": email_id,
440
+ "confidence": "low",
441
+ "is_valid": False
442
+ }
443
+
444
+ for provider in priority:
445
+ if provider in provider_names:
446
+ if provider == "zerobounce":
447
+ result = await check_email_validity_with_zero_bounce(email_id, tool_config)
448
+ elif provider == "hunter":
449
+ result = await check_email_validity_with_hunter(email_id, tool_config)
450
+ else:
451
+ continue
452
+
453
+ # Overwrite final_result with the latest
454
+ final_result = result
455
+ # If we get "high" confidence, stop.
456
+ if result["confidence"] == "high":
457
+ logger.info(f"{provider} gave high confidence. Stopping further checks.")
458
+ break
459
+
460
+ logger.info("Exiting check_email_validity method with result: %s", final_result)
461
+ return final_result
462
+
463
+
464
+ @assistant_tool
465
+ async def guess_email(
466
+ first_name: str,
467
+ last_name: str,
468
+ domain: str,
469
+ middle_name: Optional[str],
470
+ tool_config: Optional[List[Dict]] = None
471
+ ) -> Dict[str, Any]:
472
+ """
473
+ Attempt to guess the email using the provider(s) indicated in the tool_config, in priority order.
474
+ If the first guess is "high" confidence, we stop. Otherwise we move on.
475
+ """
476
+ logger.info("Entering guess_email method.")
477
+ if not tool_config:
478
+ logger.warning("No tool configuration found; returning low-confidence guess.")
479
+ return {"email": "", "email_confidence": "low"}
480
+
481
+ provider_names = [item.get("name") for item in tool_config if item.get("name")]
482
+ priority = ["hunter", "zerobounce"]
483
+ final_result = {"email": "", "email_confidence": "low"}
484
+
485
+ for provider in priority:
486
+ if provider in provider_names:
487
+ guess_func = GUESS_EMAIL_TOOL_MAP[provider]
488
+ result = await guess_func(first_name, last_name, domain, middle_name, tool_config)
489
+ final_result = result
490
+ if result.get("email_confidence") == "high":
491
+ logger.info(f"{provider} gave high confidence on guess. Stopping further guesses.")
492
+ break
493
+
494
+ logger.info("Exiting guess_email method with result: %s", final_result)
495
+ return final_result
496
+
497
+
498
+ # --------------------------------------------------------------------------------
499
+ # 5. Orchestrating everything in a single function
500
+ # --------------------------------------------------------------------------------
501
+
502
+ @assistant_tool
503
+ async def process_email_properties(
504
+ input_properties: Dict[str, Any],
505
+ tool_config: Optional[List[Dict]] = None
506
+ ) -> None:
507
+ """
508
+ 1. If input_properties["email"] is present, validate it.
509
+ - If invalid or medium/low confidence, guess a new one.
510
+ 2. If email is empty, guess a new one using domain + name fields.
511
+ 3. If guessed email has "HIGH" confidence, replace input_properties["email"].
512
+ Otherwise, store guess in `additional_properties["guessed_email"]`.
513
+ """
514
+ logger.info("Entering process_email_properties.")
515
+ first_name = input_properties.get("first_name", "")
516
+ last_name = input_properties.get("last_name", "")
517
+ email = input_properties.get("email", "")
518
+ additional_properties = input_properties.get("additional_properties", {})
519
+
520
+ if email:
521
+ val_result = await check_email_validity(email, tool_config)
522
+ # If not valid or confidence < "high", we attempt to guess
523
+ if not val_result["is_valid"]:
524
+ logger.info("Email invalid or low/medium confidence; attempting to guess new one.")
525
+ guessed_result = await guess_email(
526
+ first_name,
527
+ last_name,
528
+ extract_domain(email),
529
+ "",
530
+ tool_config
531
+ )
532
+ if is_guess_usable(guessed_result):
533
+ input_properties["email"] = guessed_result["email"]
534
+ else:
535
+ additional_properties["guessed_email"] = guessed_result.get("email", "")
536
+ input_properties["email"] = ""
537
+ else:
538
+ # No existing email => must guess a new one
539
+ domain = input_properties.get("primary_domain_of_organization", "")
540
+ if not domain:
541
+ logger.info("No primary domain found; cannot guess.")
542
+ additional_properties["guessed_email"] = ""
543
+ input_properties["email"] = ""
544
+ else:
545
+ guessed_result = await guess_email(first_name, last_name, domain, "", tool_config)
546
+ if is_guess_usable(guessed_result):
547
+ input_properties["email"] = guessed_result["email"]
548
+ else:
549
+ additional_properties["guessed_email"] = guessed_result.get("email", "")
550
+ input_properties["email"] = ""
551
+
552
+ input_properties["additional_properties"] = additional_properties
553
+ logger.info("Exiting process_email_properties.")
554
+ return input_properties
555
+
556
+
557
+ # --------------------------------------------------------------------------------
558
+ # 6. Helper Functions
559
+ # --------------------------------------------------------------------------------
560
+
561
+ async def safe_read_json_or_text(response: aiohttp.ClientResponse) -> Any:
562
+ """
563
+ Safely attempts to parse an aiohttp response as JSON, else returns text.
564
+ """
565
+ try:
566
+ return await response.json()
567
+ except Exception:
568
+ return await response.text()
569
+
570
+
571
+ def extract_domain(email: str) -> str:
572
+ """Extract domain from email ( user@domain.com -> domain.com )."""
573
+ if "@" not in email:
574
+ return ""
575
+ return email.split("@")[-1].strip()
576
+
577
+
578
+ def is_guess_usable(guess_result: Dict[str, Any]) -> bool:
579
+ """
580
+ Decide if a guess is "usable".
581
+ Here we treat "high" or "medium" as usable.
582
+ Adjust as needed.
583
+ """
584
+ if not guess_result:
585
+ return False
586
+ email_confidence = guess_result.get("email_confidence", "").lower()
587
+ return email_confidence in ["high", "medium"]
@@ -52,7 +52,7 @@ async def get_structured_output(message: str, response_type, model: str = "o3-mi
52
52
 
53
53
  # Generate the cache key
54
54
  cache_key = f"{message_hash}:{response_type_hash}"
55
- cached_response = retrieve_output(get_structured_output, cache_key)
55
+ cached_response = retrieve_output("get_structured_output", cache_key)
56
56
  if cached_response is not None:
57
57
  parsed_cached_response = response_type.parse_raw(cached_response)
58
58
  return parsed_cached_response, 'SUCCESS'
@@ -129,11 +129,13 @@ async def enrich_with_provider(
129
129
  raise ValueError("No tool configuration found.")
130
130
 
131
131
  chosen_tool_func = None
132
- for item in tool_config:
133
- logger.debug("Selected tool: %s", item.get("name"))
134
- name = item.get("name")
135
- if name in USER_LOOKUP_TOOL_NAME_TO_FUNCTION_MAP and name in ALLOWED_ENRICHMENT_TOOLS:
136
- chosen_tool_func = USER_LOOKUP_TOOL_NAME_TO_FUNCTION_MAP[name]
132
+ for allowed_tool_name in ALLOWED_ENRICHMENT_TOOLS:
133
+ for item in tool_config:
134
+ logger.debug("Selected tool: %s", item.get("name"))
135
+ if item.get("name") == allowed_tool_name and allowed_tool_name in USER_LOOKUP_TOOL_NAME_TO_FUNCTION_MAP:
136
+ chosen_tool_func = USER_LOOKUP_TOOL_NAME_TO_FUNCTION_MAP[allowed_tool_name]
137
+ break
138
+ if chosen_tool_func:
137
139
  break
138
140
 
139
141
  if not chosen_tool_func:
@@ -87,7 +87,7 @@ async def generate_personalized_email_copy(
87
87
  # Pull out fields or fallback to empty if None
88
88
  lead_data = cleaned_context.lead_info or Lead()
89
89
  sender_data = cleaned_context.sender_info or SenderInfo()
90
- campaign_data = cleaned_context.campaing_context or CampaingContext()
90
+ campaign_data = cleaned_context.campaign_context or CampaingContext()
91
91
  conversation_data = cleaned_context.current_conversation_context or ConversationContext()
92
92
 
93
93
  # Construct the consolidated prompt
@@ -83,7 +83,7 @@ async def generate_inbound_email_response_copy(
83
83
  {cleaned_context.model_dump()}
84
84
 
85
85
  3. Triage Guidelines:
86
- {cleaned_context.campaing_context.email_triage_guidelines}
86
+ {cleaned_context.campaign_context.email_triage_guidelines}
87
87
 
88
88
  - If the request is standard, simple, or obviously handled by standard processes,
89
89
  set triage_status to "AUTOMATIC".