dhisana 0.0.1.dev272__tar.gz → 0.0.1.dev274__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 (122) hide show
  1. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/setup.py +1 -1
  3. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/email_parse_helpers.py +78 -7
  4. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_email.py +209 -99
  5. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_email_response.py +19 -14
  6. dhisana-0.0.1.dev274/src/dhisana/utils/generate_linkedin_connect_message.py +329 -0
  7. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/openai_assistant_and_file_utils.py +21 -11
  8. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana.egg-info/PKG-INFO +1 -1
  9. dhisana-0.0.1.dev272/src/dhisana/utils/generate_linkedin_connect_message.py +0 -231
  10. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/README.md +0 -0
  11. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/pyproject.toml +0 -0
  12. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/setup.cfg +0 -0
  13. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/__init__.py +0 -0
  14. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/cli/__init__.py +0 -0
  15. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/cli/cli.py +0 -0
  16. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/cli/datasets.py +0 -0
  17. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/cli/models.py +0 -0
  18. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/cli/predictions.py +0 -0
  19. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/schemas/__init__.py +0 -0
  20. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/schemas/common.py +0 -0
  21. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/schemas/sales.py +0 -0
  22. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/ui/__init__.py +0 -0
  23. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/ui/components.py +0 -0
  24. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/__init__.py +0 -0
  25. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/add_mapping.py +0 -0
  26. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/agent_tools.py +0 -0
  27. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/apollo_tools.py +0 -0
  28. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  29. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/built_with_api_tools.py +0 -0
  30. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/cache_output_tools.py +0 -0
  31. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  32. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  33. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  34. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  35. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/clay_tools.py +0 -0
  36. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/clean_properties.py +0 -0
  37. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/company_utils.py +0 -0
  38. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  39. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/compose_search_query.py +0 -0
  40. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  41. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/composite_tools.py +0 -0
  42. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/dataframe_tools.py +0 -0
  43. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/domain_parser.py +0 -0
  44. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/email_body_utils.py +0 -0
  45. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/email_provider.py +0 -0
  46. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/enrich_lead_information.py +0 -0
  47. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  48. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/fetch_openai_config.py +0 -0
  49. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/field_validators.py +0 -0
  50. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/g2_tools.py +0 -0
  51. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_content.py +0 -0
  52. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_custom_message.py +0 -0
  53. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_flow.py +0 -0
  54. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  55. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  56. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  57. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/google_custom_search.py +0 -0
  58. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/google_oauth_tools.py +0 -0
  59. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/google_workspace_tools.py +0 -0
  60. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  61. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  62. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/instantly_tools.py +0 -0
  63. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/linkedin_crawler.py +0 -0
  64. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/lusha_tools.py +0 -0
  65. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/mailgun_tools.py +0 -0
  66. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/mailreach_tools.py +0 -0
  67. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/microsoft365_tools.py +0 -0
  68. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/openai_helpers.py +0 -0
  69. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  70. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  71. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  72. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  73. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  74. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  75. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/profile.py +0 -0
  76. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  77. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  78. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/python_function_to_tools.py +0 -0
  79. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/research_lead.py +0 -0
  80. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  81. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  82. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/search_router.py +0 -0
  83. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/search_router_jobs.py +0 -0
  84. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/sendgrid_tools.py +0 -0
  85. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  86. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  87. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  88. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serpapi_google_search.py +0 -0
  89. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  90. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  91. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  92. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serperdev_local_business.py +0 -0
  93. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/serperdev_search.py +0 -0
  94. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/smtp_email_tools.py +0 -0
  95. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/test_connect.py +0 -0
  96. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/trasform_json.py +0 -0
  97. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  98. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/workflow_code_model.py +0 -0
  99. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/utils/zoominfo_tools.py +0 -0
  100. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/workflow/__init__.py +0 -0
  101. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/workflow/agent.py +0 -0
  102. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/workflow/flow.py +0 -0
  103. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/workflow/task.py +0 -0
  104. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana/workflow/test.py +0 -0
  105. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana.egg-info/SOURCES.txt +0 -0
  106. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana.egg-info/dependency_links.txt +0 -0
  107. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana.egg-info/entry_points.txt +0 -0
  108. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana.egg-info/requires.txt +0 -0
  109. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/src/dhisana.egg-info/top_level.txt +0 -0
  110. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_agent_tools.py +0 -0
  111. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_apollo_company_search.py +0 -0
  112. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_apollo_lead_search.py +0 -0
  113. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_connectivity.py +0 -0
  114. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_email_body_utils.py +0 -0
  115. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_google_document.py +0 -0
  116. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_hubspot_call_logs.py +0 -0
  117. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_linkedin_serper.py +0 -0
  118. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_mailreach.py +0 -0
  119. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_mcp_connectivity.py +0 -0
  120. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_proxycurl_get_company_search_id.py +0 -0
  121. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/tests/test_proxycurl_job_count.py +0 -0
  122. {dhisana-0.0.1.dev272 → dhisana-0.0.1.dev274}/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.dev272
3
+ Version: 0.0.1.dev274
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-dev272',
5
+ version='0.0.1-dev274',
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)
@@ -15,7 +15,25 @@ from pydantic import BaseModel, ConfigDict
15
15
  # ---------------------------------------------------------------------------------------
16
16
  # CONSTANTS
17
17
  # ---------------------------------------------------------------------------------------
18
- DEFAULT_OPENAI_MODEL = "gpt-4.1"
18
+ DEFAULT_OPENAI_MODEL = "gpt-4.1" # Larger context length - used for generating instructions
19
+ DEFAULT_EMAIL_GEN_MODEL = "gpt-5.1-chat" # Better content generation - used for generating email
20
+
21
+ # -----------------------------------------------------------------------------
22
+ # Email Generation Instructions schema (intermediate step)
23
+ # -----------------------------------------------------------------------------
24
+ class EmailGenerationInstructions(BaseModel):
25
+ """Instructions generated by the first step to guide email generation."""
26
+ recipient_context: str # Key info about the lead/recipient
27
+ sender_context: str # Key info about the sender
28
+ campaign_context: str # Product, value prop, pain points, etc.
29
+ conversation_context: str # Summary of any existing conversation
30
+ tone_and_style: str # How the email should sound
31
+ key_points_to_include: str # Main points to cover in the email
32
+ call_to_action: str # What action to request
33
+ formatting_requirements: str # HTML vs plain text, signature format, etc.
34
+
35
+ model_config = ConfigDict(extra="forbid")
36
+
19
37
 
20
38
  # -----------------------------------------------------------------------------
21
39
  # Email Copy schema
@@ -68,28 +86,18 @@ FRAMEWORK_VARIATIONS = [
68
86
  ]
69
87
 
70
88
  # -----------------------------------------------------------------------------
71
- # Core function to generate an email copy
89
+ # Step 1: Generate email instructions using gpt-4.1 (larger context)
72
90
  # -----------------------------------------------------------------------------
73
- async def generate_personalized_email_copy(
91
+ async def generate_email_instructions(
74
92
  email_context: ContentGenerationContext,
75
93
  message_instructions: MessageGenerationInstructions,
76
94
  variation_text: str,
77
95
  tool_config: Optional[List[Dict]] = None,
78
- ) -> dict:
96
+ ) -> EmailGenerationInstructions:
79
97
  """
80
- Generate a personalized email using the provided context and instructions.
81
-
82
- Steps:
83
- 1. Build a prompt referencing 6 main info:
84
- (a) Lead Info
85
- (b) Sender Info
86
- (c) Campaign Info
87
- (d) Messaging Instructions
88
- (e) Additional Data (vector store) if any
89
- (f) Current Conversation Context
90
- 2. Generate an initial draft with or without vector store usage.
91
- 3. Optionally refine if a vector store was used and user instructions were not provided.
92
- 4. Return the final subject & body.
98
+ Step 1 of 2-step email generation process.
99
+ Uses gpt-4.1 (larger context length) to process all context and generate
100
+ concise instructions for email generation.
93
101
  """
94
102
  cleaned_context = cleanup_email_context(email_context)
95
103
 
@@ -110,110 +118,212 @@ async def generate_personalized_email_copy(
110
118
  conversation_data = cleaned_context.current_conversation_context or ConversationContext()
111
119
 
112
120
  html_note = (
113
- f"\n Provide the HTML body using this guidance/template when possible:\n {message_instructions.html_template}"
121
+ f"\n HTML Template to follow: {message_instructions.html_template}"
114
122
  if getattr(message_instructions, "html_template", None)
115
123
  else ""
116
124
  )
117
- important_requirements = """
118
- IMPORTANT REQUIREMENTS:
125
+
126
+ allow_html = getattr(message_instructions, "allow_html", False)
127
+ format_requirement = "HTML email with body_html field" if allow_html else "Plain text only (no HTML tags)"
128
+
129
+ # Construct the prompt for generating instructions
130
+ instructions_prompt = f"""
131
+ Generate email writing instructions based on the following business context.
132
+
133
+ LEAD INFORMATION:
134
+ {lead_data.dict()}
135
+
136
+ SENDER INFORMATION:
137
+ Full Name: {sender_data.sender_full_name or ''}
138
+ First Name: {sender_data.sender_first_name or ''}
139
+ Last Name: {sender_data.sender_last_name or ''}
140
+ Bio: {sender_data.sender_bio or ''}
141
+ Appointment Booking URL: {sender_data.sender_appointment_booking_url or ''}
142
+
143
+ CAMPAIGN INFORMATION:
144
+ Product Name: {campaign_data.product_name or ''}
145
+ Value Proposition: {campaign_data.value_prop or ''}
146
+ Call To Action: {campaign_data.call_to_action or ''}
147
+ Pain Points: {campaign_data.pain_points or []}
148
+ Proof Points: {campaign_data.proof_points or []}
149
+ Email Triage Guidelines: {campaign_data.email_triage_guidelines or ''}
150
+ LinkedIn Triage Guidelines: {campaign_data.linkedin_triage_guidelines or ''}
151
+
152
+ MESSAGING FRAMEWORK:
153
+ {selected_instructions}{html_note}
154
+
155
+ CONVERSATION HISTORY:
156
+ Email Thread: {conversation_data.current_email_thread or ''}
157
+ LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
158
+
159
+ OUTPUT FIELDS:
160
+ - recipient_context: Key details about the lead (name, company, role, relevant background)
161
+ - sender_context: Sender name and relevant info for signature
162
+ - campaign_context: Product or service being promoted, value proposition, key pain points
163
+ - conversation_context: Summary of any prior conversation if exists
164
+ - tone_and_style: Professional, friendly, etc.
165
+ - key_points_to_include: Specific points to mention in the email
166
+ - call_to_action: What action the recipient should take
167
+ - formatting_requirements: Format is {format_requirement}. Salutation format, signature requirements.
168
+ """
169
+
170
+ # Check if a vector store is available
171
+ vector_store_id = (email_context.external_known_data.external_openai_vector_store_id
172
+ if email_context.external_known_data else None)
173
+
174
+ instructions_response = None
175
+ instructions_status = ""
176
+
177
+ # Generate instructions using gpt-4.1 (larger context)
178
+ if vector_store_id:
179
+ instructions_response, instructions_status = await get_structured_output_with_assistant_and_vector_store(
180
+ prompt=instructions_prompt,
181
+ response_format=EmailGenerationInstructions,
182
+ vector_store_id=vector_store_id,
183
+ model=DEFAULT_OPENAI_MODEL,
184
+ tool_config=tool_config,
185
+ use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
186
+ )
187
+ else:
188
+ instructions_response, instructions_status = await get_structured_output_internal(
189
+ prompt=instructions_prompt,
190
+ response_format=EmailGenerationInstructions,
191
+ model=DEFAULT_OPENAI_MODEL,
192
+ tool_config=tool_config,
193
+ use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
194
+ )
195
+
196
+ if instructions_status != "SUCCESS":
197
+ raise Exception("Error: Could not generate email instructions in step 1.")
198
+
199
+ return instructions_response
200
+
201
+
202
+ # -----------------------------------------------------------------------------
203
+ # Step 2: Generate email using gpt-5.1-chat (better content generation)
204
+ # -----------------------------------------------------------------------------
205
+ async def generate_email_from_instructions(
206
+ instructions: EmailGenerationInstructions,
207
+ message_instructions: MessageGenerationInstructions,
208
+ tool_config: Optional[List[Dict]] = None,
209
+ use_cache: bool = True,
210
+ ) -> EmailCopy:
211
+ """
212
+ Step 2 of 2-step email generation process.
213
+ Uses gpt-5.1-chat (better content generation) to generate the email
214
+ from the condensed instructions.
215
+ """
216
+ allow_html = getattr(message_instructions, "allow_html", False)
217
+
218
+ if allow_html:
219
+ output_requirements = """
220
+ OUTPUT REQUIREMENTS:
119
221
  - Output must be JSON with "subject", "body", and "body_html" fields.
120
222
  - "body_html" should be clean HTML suitable for email (no external assets), inline styles welcome.
121
223
  - "body" must be the plain-text equivalent of "body_html".
122
- - Keep it concise and relevant. No placeholders or extra instructions.
123
- - Do not include PII or internal references, guids or content identifiers in the email.
124
- - Use conversational names for company/person placeholders when provided.
125
- - Email has salutation Hi <First Name>, unless otherwise specified.
126
- - Make sure the signature in body has the sender_first_name correct and in the format the user specified.
127
- - Do Not Make up information. use the information provided in the context and instructions only.
128
- - Do Not use em dash in the generated output.
129
- """
130
- if not getattr(message_instructions, "allow_html", False):
131
- important_requirements = """
132
- IMPORTANT REQUIREMENTS:
224
+ """
225
+ else:
226
+ output_requirements = """
227
+ OUTPUT REQUIREMENTS:
133
228
  - Output must be JSON with "subject" and "body" fields only.
134
229
  - In the subject or body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
135
230
  - The body and subject should be in plain text.
136
- - If there is a link provided in email use it as is. dont wrap it in any HTML tags.
137
- - Keep it concise and relevant. No placeholders or extra instructions.
138
- - Do not include PII or internal references, guids or content identifiers in the email.
139
- - User conversational name for company name if used.
140
- - Email has saluation Hi <First Name>, unless otherwise specified.
141
- - <First Name> is the first name of the lead. Its conversational name. It does not have any special characters or spaces.
142
- - Make sure the signature in body has the sender_first_name is correct and in the format user has specified.
143
- - Do Not Make up information. use the information provided in the context and instructions only.
144
- - Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
145
- - Do Not use em dash in the generated output.
146
- """
231
+ - If there is a link provided, use it as is without wrapping in any HTML tags.
232
+ """
233
+
234
+ email_prompt = f"""
235
+ Generate a personalized business email based on these specifications:
147
236
 
148
- # Construct the consolidated prompt
149
- initial_prompt = f"""
150
- Hi AI Assistant,
237
+ RECIPIENT CONTEXT:
238
+ {instructions.recipient_context}
151
239
 
152
- Below is the context in 6 main sections. Use it to craft a concise, professional email:
240
+ SENDER CONTEXT:
241
+ {instructions.sender_context}
153
242
 
154
- 1) Lead Information:
155
- {lead_data.dict()}
243
+ CAMPAIGN CONTEXT:
244
+ {instructions.campaign_context}
156
245
 
157
- 2) Sender Information:
158
- Full Name: {sender_data.sender_full_name or ''}
159
- First Name: {sender_data.sender_first_name or ''}
160
- Last Name: {sender_data.sender_last_name or ''}
161
- Bio: {sender_data.sender_bio or ''}
162
- Appointment Booking URL: {sender_data.sender_appointment_booking_url or ''}
246
+ CONVERSATION CONTEXT:
247
+ {instructions.conversation_context}
163
248
 
164
- 3) Campaign Information:
165
- Product Name: {campaign_data.product_name or ''}
166
- Value Proposition: {campaign_data.value_prop or ''}
167
- Call To Action: {campaign_data.call_to_action or ''}
168
- Pain Points: {campaign_data.pain_points or []}
169
- Proof Points: {campaign_data.proof_points or []}
170
- Triage Guidelines (Email): {campaign_data.email_triage_guidelines or ''}
171
- Triage Guidelines (LinkedIn): {campaign_data.linkedin_triage_guidelines or ''}
249
+ TONE AND STYLE:
250
+ {instructions.tone_and_style}
172
251
 
173
- 4) Messaging Instructions (template/framework):
174
- {selected_instructions}{html_note}
252
+ KEY POINTS TO INCLUDE:
253
+ {instructions.key_points_to_include}
175
254
 
176
- 5) External Data / Vector Store:
177
- (I will be provided with file_search tool if present.)
255
+ CALL TO ACTION:
256
+ {instructions.call_to_action}
178
257
 
179
- 6) Current Conversation Context:
180
- Email Thread: {conversation_data.current_email_thread or ''}
181
- LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
258
+ FORMATTING REQUIREMENTS:
259
+ {instructions.formatting_requirements}
182
260
 
183
- {important_requirements}
261
+ {output_requirements}
262
+
263
+ ADDITIONAL REQUIREMENTS:
264
+ - Keep it concise and relevant. No placeholders.
265
+ - Do not include internal references or content identifiers in the email.
266
+ - Use only the information provided.
267
+ - Ensure the body text is well-formatted with proper newlines.
268
+ - Do not use em dash in the output.
184
269
  """
185
270
 
186
- # Check if a vector store is available
187
- vector_store_id = (email_context.external_known_data.external_openai_vector_store_id
188
- if email_context.external_known_data else None)
271
+ # Generate email using gpt-5.1-chat (better content generation)
272
+ email_response, email_status = await get_structured_output_internal(
273
+ prompt=email_prompt,
274
+ response_format=EmailCopy,
275
+ model=DEFAULT_EMAIL_GEN_MODEL,
276
+ tool_config=tool_config,
277
+ use_cache=use_cache
278
+ )
189
279
 
190
- initial_response = None
191
- initial_status = ""
280
+ if email_status != "SUCCESS":
281
+ raise Exception("Error: Could not generate email in step 2.")
282
+
283
+ return email_response
192
284
 
193
- # Generate initial draft
194
- if vector_store_id:
195
- initial_response, initial_status = await get_structured_output_with_assistant_and_vector_store(
196
- prompt=initial_prompt,
197
- response_format=EmailCopy,
198
- vector_store_id=vector_store_id,
199
- model=DEFAULT_OPENAI_MODEL,
200
- tool_config=tool_config,
201
- use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
202
- )
203
- else:
204
- # Otherwise, generate the initial draft internally
205
- initial_response, initial_status = await get_structured_output_internal(
206
- prompt=initial_prompt,
207
- response_format=EmailCopy,
208
- model=DEFAULT_OPENAI_MODEL,
209
- tool_config=tool_config,
210
- use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
211
- )
212
285
 
213
- if initial_status != "SUCCESS":
214
- raise Exception("Error: Could not generate initial draft for the personalized email.")
215
- plain_body = initial_response.body
216
- html_body = getattr(initial_response, "body_html", None)
286
+ # -----------------------------------------------------------------------------
287
+ # Core function to generate an email copy (2-step process)
288
+ # -----------------------------------------------------------------------------
289
+ async def generate_personalized_email_copy(
290
+ email_context: ContentGenerationContext,
291
+ message_instructions: MessageGenerationInstructions,
292
+ variation_text: str,
293
+ tool_config: Optional[List[Dict]] = None,
294
+ ) -> dict:
295
+ """
296
+ Generate a personalized email using a 2-step process:
297
+
298
+ Step 1: Use gpt-4.1 (larger context length) to process all context and generate
299
+ concise instructions for email generation.
300
+
301
+ Step 2: Use gpt-5.1-chat (better content generation) to generate the actual
302
+ email from the condensed instructions.
303
+
304
+ This approach leverages gpt-4.1's larger context window to process extensive
305
+ lead/campaign data, and gpt-5.1-chat's superior content generation for the
306
+ final email output.
307
+ """
308
+ # Step 1: Generate instructions using gpt-4.1
309
+ instructions = await generate_email_instructions(
310
+ email_context=email_context,
311
+ message_instructions=message_instructions,
312
+ variation_text=variation_text,
313
+ tool_config=tool_config,
314
+ )
315
+
316
+ # Step 2: Generate email using gpt-5.1-chat
317
+ use_cache = email_context.message_instructions.use_cache if email_context.message_instructions else True
318
+ email_response = await generate_email_from_instructions(
319
+ instructions=instructions,
320
+ message_instructions=message_instructions,
321
+ tool_config=tool_config,
322
+ use_cache=use_cache,
323
+ )
324
+
325
+ plain_body = email_response.body
326
+ html_body = getattr(email_response, "body_html", None)
217
327
  if not plain_body and html_body:
218
328
  plain_body = _html_to_plain_text(html_body)
219
329
 
@@ -225,7 +335,7 @@ async def generate_personalized_email_copy(
225
335
  receiver_name=email_context.lead_info.full_name or "",
226
336
  receiver_email=email_context.lead_info.email or "",
227
337
  iso_datetime=datetime.utcnow().isoformat(),
228
- subject=initial_response.subject,
338
+ subject=email_response.subject,
229
339
  body=plain_body,
230
340
  html_body=html_body if getattr(message_instructions, "allow_html", False) else None,
231
341
  )
@@ -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