dhisana 0.0.1.dev273__tar.gz → 0.0.1.dev275__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.dev273 → dhisana-0.0.1.dev275}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/setup.py +1 -1
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/email_parse_helpers.py +78 -7
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_email_response.py +19 -14
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openai_assistant_and_file_utils.py +21 -11
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/smtp_email_tools.py +7 -13
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/test_connect.py +3 -3
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/README.md +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/setup.cfg +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/email_body_utils.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_custom_message.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/google_oauth_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/mailgun_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/mailreach_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/microsoft365_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/SOURCES.txt +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_apollo_company_search.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_apollo_lead_search.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_email_body_utils.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_mailreach.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_structured_output_with_mcp.py +0 -0
|
@@ -1,19 +1,86 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import email.utils
|
|
3
|
+
import quopri
|
|
4
|
+
from email.message import Message
|
|
3
5
|
from email.utils import parseaddr
|
|
4
6
|
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
5
8
|
from bs4 import BeautifulSoup
|
|
6
9
|
|
|
7
|
-
def decode_base64_url(data: str) ->
|
|
10
|
+
def decode_base64_url(data: str) -> bytes:
|
|
8
11
|
"""
|
|
9
|
-
|
|
12
|
+
Decode a Base64-url-encoded string (Gmail API uses URL-safe Base64).
|
|
10
13
|
"""
|
|
11
14
|
data = data.replace('-', '+').replace('_', '/')
|
|
12
15
|
# Fix padding
|
|
13
16
|
missing_padding = len(data) % 4
|
|
14
17
|
if missing_padding:
|
|
15
18
|
data += '=' * (4 - missing_padding)
|
|
16
|
-
return base64.b64decode(data)
|
|
19
|
+
return base64.b64decode(data)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_charset(headers_list: List[Dict[str, str]], mime_type: str) -> Optional[str]:
|
|
23
|
+
content_type = find_header(headers_list, "Content-Type") or mime_type or ""
|
|
24
|
+
if not content_type:
|
|
25
|
+
return None
|
|
26
|
+
msg = Message()
|
|
27
|
+
msg["Content-Type"] = content_type
|
|
28
|
+
return msg.get_content_charset()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _decode_transfer_encoding(payload: bytes, transfer_encoding: str) -> bytes:
|
|
32
|
+
encoding = (transfer_encoding or "").lower()
|
|
33
|
+
if "quoted-printable" in encoding:
|
|
34
|
+
return quopri.decodestring(payload)
|
|
35
|
+
if "base64" in encoding:
|
|
36
|
+
try:
|
|
37
|
+
return base64.b64decode(payload, validate=False)
|
|
38
|
+
except Exception:
|
|
39
|
+
return payload
|
|
40
|
+
return payload
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_MOJIBAKE_MARKERS = (
|
|
44
|
+
"\u00e2\u0080\u0099",
|
|
45
|
+
"\u00e2\u0080\u0093",
|
|
46
|
+
"\u00e2\u0080\u0094",
|
|
47
|
+
"\u00e2\u0080\u009c",
|
|
48
|
+
"\u00e2\u0080\u009d",
|
|
49
|
+
"\u00e2\u0080\u00a6",
|
|
50
|
+
"\u00c3\u00a9",
|
|
51
|
+
"\u00c3\u00a0",
|
|
52
|
+
"\u00c3\u00b6",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _repair_mojibake(text: str) -> str:
|
|
57
|
+
if not text or not any(marker in text for marker in _MOJIBAKE_MARKERS):
|
|
58
|
+
return text
|
|
59
|
+
try:
|
|
60
|
+
repaired = text.encode("latin-1").decode("utf-8")
|
|
61
|
+
except UnicodeError:
|
|
62
|
+
return text
|
|
63
|
+
if any(marker in repaired for marker in _MOJIBAKE_MARKERS):
|
|
64
|
+
return text
|
|
65
|
+
return repaired
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _decode_part_text(
|
|
69
|
+
data: str, headers_list: List[Dict[str, str]], mime_type: str
|
|
70
|
+
) -> str:
|
|
71
|
+
raw_bytes = decode_base64_url(data)
|
|
72
|
+
transfer_encoding = find_header(headers_list, "Content-Transfer-Encoding") or ""
|
|
73
|
+
decoded_bytes = _decode_transfer_encoding(raw_bytes, transfer_encoding)
|
|
74
|
+
|
|
75
|
+
charset = _get_charset(headers_list, mime_type)
|
|
76
|
+
tried = [charset, "utf-8", "windows-1252", "latin-1"]
|
|
77
|
+
for enc in [e for e in tried if e]:
|
|
78
|
+
try:
|
|
79
|
+
text = decoded_bytes.decode(enc)
|
|
80
|
+
return _repair_mojibake(text)
|
|
81
|
+
except (LookupError, UnicodeDecodeError):
|
|
82
|
+
continue
|
|
83
|
+
return _repair_mojibake(decoded_bytes.decode("utf-8", errors="replace"))
|
|
17
84
|
|
|
18
85
|
|
|
19
86
|
def parse_plain_text_from_parts(parts: List[Dict[str, Any]]) -> str:
|
|
@@ -30,7 +97,9 @@ def parse_plain_text_from_parts(parts: List[Dict[str, Any]]) -> str:
|
|
|
30
97
|
mime_type = part.get('mimeType', '')
|
|
31
98
|
data = part.get('body', {}).get('data', '')
|
|
32
99
|
if data:
|
|
33
|
-
decoded_data =
|
|
100
|
+
decoded_data = _decode_part_text(
|
|
101
|
+
data, part.get("headers", []), mime_type
|
|
102
|
+
)
|
|
34
103
|
if 'text/plain' in mime_type:
|
|
35
104
|
text_chunks.append(decoded_data)
|
|
36
105
|
elif 'text/html' in mime_type:
|
|
@@ -49,7 +118,9 @@ def extract_email_body_in_plain_text(message_data: Dict[str, Any]) -> str:
|
|
|
49
118
|
# If top-level body has data (i.e. single-part message)
|
|
50
119
|
if payload.get('body', {}).get('data'):
|
|
51
120
|
raw_data = payload['body']['data']
|
|
52
|
-
decoded_data =
|
|
121
|
+
decoded_data = _decode_part_text(
|
|
122
|
+
raw_data, payload.get("headers", []), payload.get("mimeType", "")
|
|
123
|
+
)
|
|
53
124
|
# Check if it might be HTML
|
|
54
125
|
mime_type = payload.get('mimeType', '')
|
|
55
126
|
if 'text/html' in mime_type:
|
|
@@ -85,7 +156,7 @@ def find_header(headers_list: List[Dict[str, str]], header_name: str) -> Optiona
|
|
|
85
156
|
return None
|
|
86
157
|
|
|
87
158
|
|
|
88
|
-
def parse_single_address(display_str: str) ->
|
|
159
|
+
def parse_single_address(display_str: str) -> Tuple[str, str]:
|
|
89
160
|
"""
|
|
90
161
|
Parses a single display string like "Alice <alice@example.com>"
|
|
91
162
|
returning (name, email).
|
|
@@ -112,7 +183,7 @@ def parse_address_list(display_str: str) -> List[Tuple[str, str]]:
|
|
|
112
183
|
return addresses
|
|
113
184
|
|
|
114
185
|
|
|
115
|
-
def find_all_recipients_in_headers(headers_list: List[Dict[str, str]]) ->
|
|
186
|
+
def find_all_recipients_in_headers(headers_list: List[Dict[str, str]]) -> Tuple[str, str]:
|
|
116
187
|
"""
|
|
117
188
|
Collect 'To', 'Cc', 'Bcc' headers, parse each address, and return:
|
|
118
189
|
(comma-separated receiver names, comma-separated receiver emails)
|
|
@@ -71,7 +71,6 @@ async def get_inbound_email_triage_action(
|
|
|
71
71
|
"SCHEDULE_MEETING",
|
|
72
72
|
"SEND_REPLY",
|
|
73
73
|
"OOF_MESSAGE",
|
|
74
|
-
"NEED_MORE_INFO",
|
|
75
74
|
"FORWARD_TO_OTHER_USER",
|
|
76
75
|
"NO_MORE_IN_ORGANIZATION",
|
|
77
76
|
"OBJECTION_RAISED",
|
|
@@ -93,6 +92,8 @@ async def get_inbound_email_triage_action(
|
|
|
93
92
|
allowed_actions =
|
|
94
93
|
{allowed_actions}
|
|
95
94
|
|
|
95
|
+
If you need more info, use SEND_REPLY and ask a short clarifying question.
|
|
96
|
+
|
|
96
97
|
1. Email thread or conversation:
|
|
97
98
|
{[thread_item.model_dump() for thread_item in cleaned_context.current_conversation_context.current_email_thread]}
|
|
98
99
|
|
|
@@ -129,8 +130,7 @@ async def get_inbound_email_triage_action(
|
|
|
129
130
|
Handling interest & objections
|
|
130
131
|
------------------------------
|
|
131
132
|
• If the prospect asks for **pricing, docs, case studies, or more info**
|
|
132
|
-
→ **
|
|
133
|
-
the material (or includes it if ≤ 150 words fits).
|
|
133
|
+
→ **SEND_REPLY** and ask a short clarifying question or promise a follow-up.
|
|
134
134
|
|
|
135
135
|
• If they mention **budget, timing, or competitor concerns**
|
|
136
136
|
→ **OBJECTION_RAISED** and reply with a brief acknowledgement
|
|
@@ -183,7 +183,7 @@ async def get_inbound_email_triage_action(
|
|
|
183
183
|
"triage_status": "...",
|
|
184
184
|
"triage_reason": null or "<reason>",
|
|
185
185
|
"response_action_to_take": "one of {allowed_actions}",
|
|
186
|
-
"response_message": "<only if SEND_REPLY/SCHEDULE_MEETING, else empty>"
|
|
186
|
+
"response_message": "<only if SEND_REPLY/SCHEDULE_MEETING/OBJECTION_RAISED, else empty>"
|
|
187
187
|
}}
|
|
188
188
|
|
|
189
189
|
Current date is: {current_date_iso}.
|
|
@@ -251,7 +251,6 @@ async def generate_inbound_email_response_copy(
|
|
|
251
251
|
"UNSUBSCRIBE",
|
|
252
252
|
"OOF_MESSAGE",
|
|
253
253
|
"NOT_INTERESTED",
|
|
254
|
-
"NEED_MORE_INFO",
|
|
255
254
|
"FORWARD_TO_OTHER_USER",
|
|
256
255
|
"NO_MORE_IN_ORGANIZATION",
|
|
257
256
|
"OBJECTION_RAISED",
|
|
@@ -282,7 +281,7 @@ async def generate_inbound_email_response_copy(
|
|
|
282
281
|
if cleaned_context.current_conversation_context.current_email_thread else []}
|
|
283
282
|
|
|
284
283
|
2) Lead information:
|
|
285
|
-
{lead_data.
|
|
284
|
+
{lead_data.model_dump()}
|
|
286
285
|
|
|
287
286
|
Sender information:
|
|
288
287
|
- Full name: {sender_data.sender_full_name or ''}
|
|
@@ -316,16 +315,17 @@ async def generate_inbound_email_response_copy(
|
|
|
316
315
|
Choose exactly ONE action from:
|
|
317
316
|
{allowed_actions}
|
|
318
317
|
|
|
318
|
+
If you need more info, use SEND_REPLY and ask a short clarifying question.
|
|
319
|
+
|
|
319
320
|
Priority order:
|
|
320
321
|
1. UNSUBSCRIBE
|
|
321
322
|
2. NOT_INTERESTED
|
|
322
323
|
3. OOF_MESSAGE
|
|
323
324
|
4. SCHEDULE_MEETING
|
|
324
325
|
5. FORWARD_TO_OTHER_USER
|
|
325
|
-
6.
|
|
326
|
-
7.
|
|
327
|
-
8.
|
|
328
|
-
9. END_CONVERSATION
|
|
326
|
+
6. OBJECTION_RAISED
|
|
327
|
+
7. SEND_REPLY
|
|
328
|
+
8. END_CONVERSATION
|
|
329
329
|
|
|
330
330
|
=====================================================
|
|
331
331
|
HOW THE RESPONSE SHOULD SOUND
|
|
@@ -391,7 +391,7 @@ async def generate_inbound_email_response_copy(
|
|
|
391
391
|
"triage_status": "AUTOMATIC" or "END_CONVERSATION",
|
|
392
392
|
"triage_reason": "<string if END_CONVERSATION, otherwise null>",
|
|
393
393
|
"response_action_to_take": "one of {allowed_actions}",
|
|
394
|
-
"response_message": "<reply body only if SEND_REPLY
|
|
394
|
+
"response_message": "<reply body only if SEND_REPLY/SCHEDULE_MEETING/OBJECTION_RAISED, otherwise empty>"
|
|
395
395
|
}}
|
|
396
396
|
|
|
397
397
|
=====================================================
|
|
@@ -446,6 +446,12 @@ async def generate_inbound_email_response_copy(
|
|
|
446
446
|
f"Campaign ID: {campaign_id}"
|
|
447
447
|
)
|
|
448
448
|
|
|
449
|
+
response_action = initial_response.response_action_to_take
|
|
450
|
+
if response_action == "NEED_MORE_INFO":
|
|
451
|
+
response_action = "SEND_REPLY"
|
|
452
|
+
|
|
453
|
+
response_message = initial_response.response_message or ""
|
|
454
|
+
|
|
449
455
|
response_item = MessageItem(
|
|
450
456
|
message_id="", # or generate one if appropriate
|
|
451
457
|
thread_id="",
|
|
@@ -455,7 +461,7 @@ async def generate_inbound_email_response_copy(
|
|
|
455
461
|
receiver_email=campaign_context.lead_info.email or "",
|
|
456
462
|
iso_datetime=datetime.datetime.utcnow().isoformat(),
|
|
457
463
|
subject="", # or set some triage subject if needed
|
|
458
|
-
body=
|
|
464
|
+
body=response_message
|
|
459
465
|
)
|
|
460
466
|
|
|
461
467
|
# Build a MessageResponse that includes triage metadata plus your message item
|
|
@@ -463,9 +469,8 @@ async def generate_inbound_email_response_copy(
|
|
|
463
469
|
triage_status=initial_response.triage_status,
|
|
464
470
|
triage_reason=initial_response.triage_reason,
|
|
465
471
|
message_item=response_item,
|
|
466
|
-
response_action_to_take=
|
|
472
|
+
response_action_to_take=response_action
|
|
467
473
|
)
|
|
468
|
-
print(response_message.model_dump())
|
|
469
474
|
return response_message.model_dump()
|
|
470
475
|
|
|
471
476
|
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
11
|
import traceback
|
|
12
|
-
from typing import Any, Dict, List, Optional
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from fastapi import HTTPException
|
|
15
15
|
|
|
@@ -52,6 +52,14 @@ async def delete_vector_store(
|
|
|
52
52
|
client = create_openai_client(tool_config)
|
|
53
53
|
try:
|
|
54
54
|
client.vector_stores.delete(vector_store_id=vector_store_id)
|
|
55
|
+
except openai.NotFoundError:
|
|
56
|
+
logging.warning(f"Vector store not found during delete: {vector_store_id}")
|
|
57
|
+
except openai.APIStatusError as e:
|
|
58
|
+
if getattr(e, "status_code", None) == 404:
|
|
59
|
+
logging.warning(f"Vector store not found during delete: {vector_store_id}")
|
|
60
|
+
return
|
|
61
|
+
logging.error(f"Error deleting vector store {vector_store_id}: {e}")
|
|
62
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
55
63
|
except Exception as e:
|
|
56
64
|
logging.error(f"Error deleting vector store {vector_store_id}: {e}")
|
|
57
65
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -74,10 +82,11 @@ async def upload_file_openai_and_vector_store(
|
|
|
74
82
|
|
|
75
83
|
try:
|
|
76
84
|
if isinstance(file_path_or_bytes, str):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
with open(file_path_or_bytes, "rb") as f:
|
|
86
|
+
file_upload = client.files.create(
|
|
87
|
+
file=f,
|
|
88
|
+
purpose=purpose,
|
|
89
|
+
)
|
|
81
90
|
elif isinstance(file_path_or_bytes, bytes):
|
|
82
91
|
file_upload = client.files.create(
|
|
83
92
|
file=(file_name, file_path_or_bytes, mime_type),
|
|
@@ -108,10 +117,11 @@ async def upload_file_openai(
|
|
|
108
117
|
|
|
109
118
|
try:
|
|
110
119
|
if isinstance(file_path_or_bytes, str):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
with open(file_path_or_bytes, "rb") as f:
|
|
121
|
+
file_upload = client.files.create(
|
|
122
|
+
file=f,
|
|
123
|
+
purpose=purpose,
|
|
124
|
+
)
|
|
115
125
|
else:
|
|
116
126
|
file_upload = client.files.create(
|
|
117
127
|
file=(file_name, file_path_or_bytes, mime_type),
|
|
@@ -217,7 +227,7 @@ async def run_response_text(
|
|
|
217
227
|
max_tokens: int = 2048,
|
|
218
228
|
store: bool = True,
|
|
219
229
|
tool_config: Optional[List[Dict]] = None,
|
|
220
|
-
) ->
|
|
230
|
+
) -> Tuple[str, str]:
|
|
221
231
|
"""Plain text completion via the Responses API."""
|
|
222
232
|
client = create_openai_client(tool_config)
|
|
223
233
|
|
|
@@ -241,7 +251,7 @@ async def run_response_structured(
|
|
|
241
251
|
max_tokens: int = 1024,
|
|
242
252
|
store: bool = True,
|
|
243
253
|
tool_config: Optional[List[Dict]] = None,
|
|
244
|
-
) ->
|
|
254
|
+
) -> Tuple[Any, str]:
|
|
245
255
|
"""Structured JSON output via Responses API."""
|
|
246
256
|
client = create_openai_client(tool_config)
|
|
247
257
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
# dhisana/
|
|
1
|
+
# dhisana/smtp_email_tools.py
|
|
2
2
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
3
|
# Standard library
|
|
4
4
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
import asyncio
|
|
6
|
-
import datetime
|
|
7
6
|
import email
|
|
8
7
|
import email.utils
|
|
9
8
|
import hashlib
|
|
@@ -105,11 +104,6 @@ def _to_datetime(val: Union[datetime, str]) -> datetime:
|
|
|
105
104
|
) from exc
|
|
106
105
|
|
|
107
106
|
|
|
108
|
-
def _looks_like_html(text: str) -> bool:
|
|
109
|
-
"""Heuristically determine whether the body contains HTML markup."""
|
|
110
|
-
return bool(text and re.search(r"<[a-zA-Z][^>]*>", text))
|
|
111
|
-
|
|
112
|
-
|
|
113
107
|
def _html_to_plain_text(html: str) -> str:
|
|
114
108
|
"""
|
|
115
109
|
Produce a very lightweight plain-text version of an HTML fragment.
|
|
@@ -186,18 +180,18 @@ async def send_email_via_smtp_async(
|
|
|
186
180
|
username=username,
|
|
187
181
|
password=password,
|
|
188
182
|
)
|
|
189
|
-
# Decide whether to use STARTTLS or TLS
|
|
183
|
+
# Decide whether to use STARTTLS or implicit TLS; otherwise connect plaintext.
|
|
190
184
|
if use_starttls:
|
|
191
185
|
smtp_kwargs["start_tls"] = True
|
|
192
|
-
|
|
193
|
-
|
|
186
|
+
elif smtp_port == 465:
|
|
187
|
+
# aiosmtplib expects `use_tls` for implicit TLS (e.g., port 465)
|
|
188
|
+
smtp_kwargs["use_tls"] = True
|
|
194
189
|
|
|
195
190
|
try:
|
|
196
191
|
# aiosmtplib.send returns a (code, response) tuple, but no server message ID.
|
|
197
192
|
# We rely on the real Message-ID we have just set.
|
|
198
193
|
await aiosmtplib.send(msg, **smtp_kwargs)
|
|
199
194
|
logging.info("SMTP send OK – msg id %s", generated_id)
|
|
200
|
-
await asyncio.sleep(20)
|
|
201
195
|
return generated_id
|
|
202
196
|
except Exception:
|
|
203
197
|
logging.exception("SMTP send failed")
|
|
@@ -557,8 +551,8 @@ async def reply_to_email_via_smtp_async(
|
|
|
557
551
|
)
|
|
558
552
|
if use_starttls_smtp:
|
|
559
553
|
smtp_kwargs["start_tls"] = True
|
|
560
|
-
|
|
561
|
-
smtp_kwargs["
|
|
554
|
+
elif smtp_port == 465:
|
|
555
|
+
smtp_kwargs["use_tls"] = True
|
|
562
556
|
|
|
563
557
|
await aiosmtplib.send(msg, **smtp_kwargs)
|
|
564
558
|
|
|
@@ -17,7 +17,6 @@ from googleapiclient.discovery import build
|
|
|
17
17
|
import imaplib
|
|
18
18
|
import aiosmtplib
|
|
19
19
|
from simple_salesforce import Salesforce
|
|
20
|
-
from urllib.parse import urljoin, urlparse
|
|
21
20
|
|
|
22
21
|
from dhisana.utils.clay_tools import push_to_clay_table
|
|
23
22
|
|
|
@@ -1030,9 +1029,10 @@ async def test_smtp_accounts(
|
|
|
1030
1029
|
try:
|
|
1031
1030
|
smtp_kwargs = dict(hostname=smtp_host, port=smtp_port, timeout=10)
|
|
1032
1031
|
if smtp_port == 587:
|
|
1033
|
-
smtp_kwargs["start_tls"] = True # STARTTLS upgrade
|
|
1032
|
+
smtp_kwargs["start_tls"] = True # STARTTLS upgrade on submission port
|
|
1034
1033
|
else:
|
|
1035
|
-
|
|
1034
|
+
# aiosmtplib uses `use_tls` for implicit TLS (e.g., port 465)
|
|
1035
|
+
smtp_kwargs["use_tls"] = (smtp_port == 465)
|
|
1036
1036
|
|
|
1037
1037
|
smtp = aiosmtplib.SMTP(**smtp_kwargs)
|
|
1038
1038
|
await smtp.connect()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/parse_linkedin_messages_txt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serarch_router_local_business.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_local_business_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|