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.
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/setup.py +1 -1
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/schemas/sales.py +2 -2
- dhisana-0.0.1.dev45/src/dhisana/utils/check_email_validity_tools.py +587 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/dataframe_tools.py +1 -1
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/enrich_lead_information.py +7 -5
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_email.py +1 -1
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_email_response.py +1 -1
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/serpapi_search_tools.py +3 -3
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/PKG-INFO +1 -1
- dhisana-0.0.1.dev44/src/dhisana/utils/check_email_validity_tools.py +0 -380
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/README.md +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/setup.cfg +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/agent_task.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_cadence.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/compose_workflow.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/SOURCES.txt +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev44 → dhisana-0.0.1.dev45}/tests/test_agent_tools.py +0 -0
|
@@ -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)
|
|
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
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
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.
|
|
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".
|