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.
Files changed (121) hide show
  1. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/setup.py +1 -1
  3. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/email_parse_helpers.py +78 -7
  4. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_email_response.py +19 -14
  5. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openai_assistant_and_file_utils.py +21 -11
  6. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/smtp_email_tools.py +7 -13
  7. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/test_connect.py +3 -3
  8. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/PKG-INFO +1 -1
  9. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/README.md +0 -0
  10. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/pyproject.toml +0 -0
  11. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/setup.cfg +0 -0
  12. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/__init__.py +0 -0
  13. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/__init__.py +0 -0
  14. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/cli.py +0 -0
  15. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/datasets.py +0 -0
  16. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/models.py +0 -0
  17. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/cli/predictions.py +0 -0
  18. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/schemas/__init__.py +0 -0
  19. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/schemas/common.py +0 -0
  20. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/schemas/sales.py +0 -0
  21. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/ui/__init__.py +0 -0
  22. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/ui/components.py +0 -0
  23. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/__init__.py +0 -0
  24. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/add_mapping.py +0 -0
  25. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/agent_tools.py +0 -0
  26. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/apollo_tools.py +0 -0
  27. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  28. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/built_with_api_tools.py +0 -0
  29. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/cache_output_tools.py +0 -0
  30. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  31. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  32. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  33. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  34. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/clay_tools.py +0 -0
  35. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/clean_properties.py +0 -0
  36. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/company_utils.py +0 -0
  37. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  38. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/compose_search_query.py +0 -0
  39. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  40. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/composite_tools.py +0 -0
  41. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/dataframe_tools.py +0 -0
  42. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/domain_parser.py +0 -0
  43. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/email_body_utils.py +0 -0
  44. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/email_provider.py +0 -0
  45. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/enrich_lead_information.py +0 -0
  46. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  47. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/fetch_openai_config.py +0 -0
  48. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/field_validators.py +0 -0
  49. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/g2_tools.py +0 -0
  50. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_content.py +0 -0
  51. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_custom_message.py +0 -0
  52. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_email.py +0 -0
  53. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_flow.py +0 -0
  54. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  55. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  56. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  57. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  58. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/google_custom_search.py +0 -0
  59. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/google_oauth_tools.py +0 -0
  60. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/google_workspace_tools.py +0 -0
  61. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  62. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  63. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/instantly_tools.py +0 -0
  64. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/linkedin_crawler.py +0 -0
  65. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/lusha_tools.py +0 -0
  66. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/mailgun_tools.py +0 -0
  67. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/mailreach_tools.py +0 -0
  68. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/microsoft365_tools.py +0 -0
  69. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openai_helpers.py +0 -0
  70. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  71. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  72. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  73. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  74. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  75. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  76. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/profile.py +0 -0
  77. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  78. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  79. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/python_function_to_tools.py +0 -0
  80. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/research_lead.py +0 -0
  81. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  82. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  83. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/search_router.py +0 -0
  84. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/search_router_jobs.py +0 -0
  85. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/sendgrid_tools.py +0 -0
  86. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  87. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  88. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  89. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_google_search.py +0 -0
  90. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  91. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  92. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  93. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serperdev_local_business.py +0 -0
  94. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/serperdev_search.py +0 -0
  95. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/trasform_json.py +0 -0
  96. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  97. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/workflow_code_model.py +0 -0
  98. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/utils/zoominfo_tools.py +0 -0
  99. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/__init__.py +0 -0
  100. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/agent.py +0 -0
  101. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/flow.py +0 -0
  102. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/task.py +0 -0
  103. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana/workflow/test.py +0 -0
  104. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/SOURCES.txt +0 -0
  105. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/dependency_links.txt +0 -0
  106. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/entry_points.txt +0 -0
  107. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/requires.txt +0 -0
  108. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/src/dhisana.egg-info/top_level.txt +0 -0
  109. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_agent_tools.py +0 -0
  110. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_apollo_company_search.py +0 -0
  111. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_apollo_lead_search.py +0 -0
  112. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_connectivity.py +0 -0
  113. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_email_body_utils.py +0 -0
  114. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_google_document.py +0 -0
  115. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_hubspot_call_logs.py +0 -0
  116. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_linkedin_serper.py +0 -0
  117. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_mailreach.py +0 -0
  118. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_mcp_connectivity.py +0 -0
  119. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_proxycurl_get_company_search_id.py +0 -0
  120. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_proxycurl_job_count.py +0 -0
  121. {dhisana-0.0.1.dev273 → dhisana-0.0.1.dev275}/tests/test_structured_output_with_mcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev273
3
+ Version: 0.0.1.dev275
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-dev273',
5
+ version='0.0.1-dev275',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -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) -> str:
10
+ def decode_base64_url(data: str) -> bytes:
8
11
  """
9
- Decodes a Base64-url-encoded string (Gmail API uses URL-safe Base64).
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).decode('utf-8', errors='ignore')
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 = decode_base64_url(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 = decode_base64_url(raw_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) -> (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]]) -> (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
- → **NEED_MORE_INFO** and craft a short response that promises to send
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.dict()}
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. NEED_MORE_INFO
326
- 7. OBJECTION_RAISED
327
- 8. SEND_REPLY
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 or SCHEDULE_MEETING, otherwise empty>"
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=initial_response.response_message
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=initial_response.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
 
@@ -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
- file_upload = client.files.create(
78
- file=open(file_path_or_bytes, "rb"),
79
- purpose=purpose,
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
- file_upload = client.files.create(
112
- file=open(file_path_or_bytes, "rb"),
113
- purpose=purpose,
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
- ) -> (str, str):
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
- ) -> (Any, str):
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/email_io.py
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
- else:
193
- smtp_kwargs["tls"] = True
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
- else:
561
- smtp_kwargs["tls"] = True
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
- smtp_kwargs["tls"] = (smtp_port == 465) # implicit SSL on 465
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev273
3
+ Version: 0.0.1.dev275
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
File without changes
File without changes