dhisana 0.0.1.dev116__py3-none-any.whl → 0.0.1.dev236__py3-none-any.whl

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 (69) hide show
  1. dhisana/schemas/common.py +10 -1
  2. dhisana/schemas/sales.py +203 -22
  3. dhisana/utils/add_mapping.py +0 -2
  4. dhisana/utils/apollo_tools.py +739 -119
  5. dhisana/utils/built_with_api_tools.py +4 -2
  6. dhisana/utils/check_email_validity_tools.py +35 -18
  7. dhisana/utils/check_for_intent_signal.py +1 -2
  8. dhisana/utils/check_linkedin_url_validity.py +34 -8
  9. dhisana/utils/clay_tools.py +3 -2
  10. dhisana/utils/clean_properties.py +1 -4
  11. dhisana/utils/compose_salesnav_query.py +0 -1
  12. dhisana/utils/compose_search_query.py +7 -3
  13. dhisana/utils/composite_tools.py +0 -1
  14. dhisana/utils/dataframe_tools.py +2 -2
  15. dhisana/utils/email_body_utils.py +72 -0
  16. dhisana/utils/email_provider.py +174 -35
  17. dhisana/utils/enrich_lead_information.py +183 -53
  18. dhisana/utils/fetch_openai_config.py +129 -0
  19. dhisana/utils/field_validators.py +1 -1
  20. dhisana/utils/g2_tools.py +0 -1
  21. dhisana/utils/generate_content.py +0 -1
  22. dhisana/utils/generate_email.py +68 -23
  23. dhisana/utils/generate_email_response.py +294 -46
  24. dhisana/utils/generate_flow.py +0 -1
  25. dhisana/utils/generate_linkedin_connect_message.py +9 -2
  26. dhisana/utils/generate_linkedin_response_message.py +137 -66
  27. dhisana/utils/generate_structured_output_internal.py +317 -164
  28. dhisana/utils/google_custom_search.py +150 -44
  29. dhisana/utils/google_oauth_tools.py +721 -0
  30. dhisana/utils/google_workspace_tools.py +278 -54
  31. dhisana/utils/hubspot_clearbit.py +3 -1
  32. dhisana/utils/hubspot_crm_tools.py +718 -272
  33. dhisana/utils/instantly_tools.py +3 -1
  34. dhisana/utils/lusha_tools.py +10 -7
  35. dhisana/utils/mailgun_tools.py +150 -0
  36. dhisana/utils/microsoft365_tools.py +447 -0
  37. dhisana/utils/openai_assistant_and_file_utils.py +121 -177
  38. dhisana/utils/openai_helpers.py +8 -6
  39. dhisana/utils/parse_linkedin_messages_txt.py +1 -3
  40. dhisana/utils/profile.py +37 -0
  41. dhisana/utils/proxy_curl_tools.py +377 -76
  42. dhisana/utils/proxycurl_search_leads.py +426 -0
  43. dhisana/utils/research_lead.py +3 -3
  44. dhisana/utils/sales_navigator_crawler.py +1 -6
  45. dhisana/utils/salesforce_crm_tools.py +323 -50
  46. dhisana/utils/search_router.py +131 -0
  47. dhisana/utils/search_router_jobs.py +51 -0
  48. dhisana/utils/sendgrid_tools.py +126 -91
  49. dhisana/utils/serarch_router_local_business.py +75 -0
  50. dhisana/utils/serpapi_additional_tools.py +290 -0
  51. dhisana/utils/serpapi_google_jobs.py +117 -0
  52. dhisana/utils/serpapi_google_search.py +188 -0
  53. dhisana/utils/serpapi_local_business_search.py +129 -0
  54. dhisana/utils/serpapi_search_tools.py +360 -432
  55. dhisana/utils/serperdev_google_jobs.py +125 -0
  56. dhisana/utils/serperdev_local_business.py +154 -0
  57. dhisana/utils/serperdev_search.py +233 -0
  58. dhisana/utils/smtp_email_tools.py +178 -18
  59. dhisana/utils/test_connect.py +1603 -130
  60. dhisana/utils/trasform_json.py +3 -3
  61. dhisana/utils/web_download_parse_tools.py +0 -1
  62. dhisana/utils/zoominfo_tools.py +2 -3
  63. dhisana/workflow/test.py +1 -1
  64. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +1 -1
  65. dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
  66. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
  67. dhisana-0.0.1.dev116.dist-info/RECORD +0 -83
  68. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
  69. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,10 @@
1
1
  # Import necessary modules
2
- import os
3
- from typing import Any, Dict, List, Optional
4
- import aiohttp
2
+ import html as html_lib
3
+ import re
4
+ from typing import Dict, List, Optional
5
5
  from pydantic import BaseModel
6
- from enum import Enum
7
6
 
8
- from dhisana.schemas.sales import CampaignContext, ContentGenerationContext, ConversationContext, Lead, MessageGenerationInstructions, MessageItem, PromptEngineeringGuidance, SenderInfo
9
- from dhisana.utils.assistant_tool_tag import assistant_tool
7
+ from dhisana.schemas.sales import CampaignContext, ContentGenerationContext, ConversationContext, Lead, MessageGenerationInstructions, MessageItem, SenderInfo
10
8
  from dhisana.utils.generate_structured_output_internal import (
11
9
  get_structured_output_internal,
12
10
  get_structured_output_with_assistant_and_vector_store
@@ -20,9 +18,22 @@ from pydantic import BaseModel, ConfigDict
20
18
  class EmailCopy(BaseModel):
21
19
  subject: str
22
20
  body: str
21
+ body_html: Optional[str] = None
23
22
 
24
23
  model_config = ConfigDict(extra="forbid")
25
24
 
25
+
26
+ def _html_to_plain_text(html_content: str) -> str:
27
+ """Simple HTML to text conversion to backfill plain body."""
28
+ if not html_content:
29
+ return ""
30
+ # Remove tags and normalize whitespace
31
+ text = re.sub(r"<[^>]+>", " ", html_content)
32
+ text = html_lib.unescape(text)
33
+ # Collapse repeated whitespace/newlines
34
+ lines = [line.strip() for line in text.splitlines()]
35
+ return "\n".join([line for line in lines if line])
36
+
26
37
  # -----------------------------------------------------------------------------
27
38
  # Utility to Clean Up Context (if needed)
28
39
  # -----------------------------------------------------------------------------
@@ -93,6 +104,42 @@ async def generate_personalized_email_copy(
93
104
  campaign_data = cleaned_context.campaign_context or CampaignContext()
94
105
  conversation_data = cleaned_context.current_conversation_context or ConversationContext()
95
106
 
107
+ html_note = (
108
+ f"\n Provide the HTML body using this guidance/template when possible:\n {message_instructions.html_template}"
109
+ if getattr(message_instructions, "html_template", None)
110
+ else ""
111
+ )
112
+ important_requirements = """
113
+ IMPORTANT REQUIREMENTS:
114
+ - Output must be JSON with "subject", "body", and "body_html" fields.
115
+ - "body_html" should be clean HTML suitable for email (no external assets), inline styles welcome.
116
+ - "body" must be the plain-text equivalent of "body_html".
117
+ - Keep it concise and relevant. No placeholders or extra instructions.
118
+ - Do not include PII or internal references, guids or content identifiers in the email.
119
+ - Use conversational names for company/person placeholders when provided.
120
+ - Email has salutation Hi <First Name>, unless otherwise specified.
121
+ - Make sure the signature in body has the sender_first_name correct and in the format the user specified.
122
+ - Do Not Make up information. use the information provided in the context and instructions only.
123
+ - Do Not use em dash in the generated output.
124
+ """
125
+ if not getattr(message_instructions, "allow_html", False):
126
+ important_requirements = """
127
+ IMPORTANT REQUIREMENTS:
128
+ - Output must be JSON with "subject" and "body" fields only.
129
+ - In the subject or body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
130
+ - The body and subject should be in plain text.
131
+ - If there is a link provided in email use it as is. dont wrap it in any HTML tags.
132
+ - Keep it concise and relevant. No placeholders or extra instructions.
133
+ - Do not include PII or internal references, guids or content identifiers in the email.
134
+ - User conversational name for company name if used.
135
+ - Email has saluation Hi <First Name>, unless otherwise specified.
136
+ - <First Name> is the first name of the lead. Its conversational name. It does not have any special characters or spaces.
137
+ - Make sure the signature in body has the sender_first_name is correct and in the format user has specified.
138
+ - Do Not Make up information. use the information provided in the context and instructions only.
139
+ - Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
140
+ - Do Not use em dash in the generated output.
141
+ """
142
+
96
143
  # Construct the consolidated prompt
97
144
  initial_prompt = f"""
98
145
  Hi AI Assistant,
@@ -118,7 +165,7 @@ async def generate_personalized_email_copy(
118
165
  Triage Guidelines (LinkedIn): {campaign_data.linkedin_triage_guidelines or ''}
119
166
 
120
167
  4) Messaging Instructions (template/framework):
121
- {selected_instructions}
168
+ {selected_instructions}{html_note}
122
169
 
123
170
  5) External Data / Vector Store:
124
171
  (I will be provided with file_search tool if present.)
@@ -127,46 +174,43 @@ async def generate_personalized_email_copy(
127
174
  Email Thread: {conversation_data.current_email_thread or ''}
128
175
  LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
129
176
 
130
- IMPORTANT REQUIREMENTS:
131
- - Output must be JSON with "subject" and "body" fields only.
132
- - In the subject or body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
133
- - The body and subject should be in plain text.
134
- - If there is a link provided in email use it as is. dont wrap it in any HTML tags.
135
- - Keep it concise and relevant. No placeholders or extra instructions.
136
- - Do not include PII or internal references, guids or content identifiers in the email.
137
- - User conversational name for company name if used.
138
- - Email has saluation Hi <First Name>, unless otherwise specified.
139
- - <First Name> is the first name of the lead. Its conversational name. It does not have any special characters or spaces.
140
- - Do Not Make up information. use the information provided in the context and instructions only.
177
+ {important_requirements}
141
178
  """
142
179
 
143
180
  # Check if a vector store is available
144
181
  vector_store_id = (email_context.external_known_data.external_openai_vector_store_id
145
182
  if email_context.external_known_data else None)
146
183
 
147
- used_vector_store = False
148
184
  initial_response = None
149
185
  initial_status = ""
150
186
 
151
187
  # Generate initial draft
152
188
  if vector_store_id:
153
- used_vector_store = True
154
189
  initial_response, initial_status = await get_structured_output_with_assistant_and_vector_store(
155
190
  prompt=initial_prompt,
156
191
  response_format=EmailCopy,
157
192
  vector_store_id=vector_store_id,
158
- tool_config=tool_config
193
+ model="gpt-5.1-chat",
194
+ tool_config=tool_config,
195
+ use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
159
196
  )
160
197
  else:
161
198
  # Otherwise, generate the initial draft internally
162
199
  initial_response, initial_status = await get_structured_output_internal(
163
200
  prompt=initial_prompt,
164
201
  response_format=EmailCopy,
165
- tool_config=tool_config
202
+ model="gpt-5.1-chat",
203
+ tool_config=tool_config,
204
+ use_cache=email_context.message_instructions.use_cache if email_context.message_instructions else True
166
205
  )
167
206
 
168
207
  if initial_status != "SUCCESS":
169
208
  raise Exception("Error: Could not generate initial draft for the personalized email.")
209
+ plain_body = initial_response.body
210
+ html_body = getattr(initial_response, "body_html", None)
211
+ if not plain_body and html_body:
212
+ plain_body = _html_to_plain_text(html_body)
213
+
170
214
  response_item = MessageItem(
171
215
  message_id="", # or some real ID if you have it
172
216
  thread_id="",
@@ -176,7 +220,8 @@ async def generate_personalized_email_copy(
176
220
  receiver_email=email_context.lead_info.email or "",
177
221
  iso_datetime=datetime.utcnow().isoformat(),
178
222
  subject=initial_response.subject,
179
- body=initial_response.body
223
+ body=plain_body,
224
+ html_body=html_body if getattr(message_instructions, "allow_html", False) else None,
180
225
  )
181
226
  return response_item.model_dump()
182
227
 
@@ -1,14 +1,14 @@
1
- import base64
2
1
  import datetime
3
2
  from typing import Any, Dict, List, Optional
4
- import aiohttp
5
3
  from pydantic import BaseModel
6
4
 
7
5
  from dhisana.schemas.sales import (
8
6
  ContentGenerationContext,
7
+ Lead,
9
8
  MessageItem,
10
9
  MessageResponse,
11
- MessageGenerationInstructions
10
+ MessageGenerationInstructions,
11
+ SenderInfo
12
12
  )
13
13
  from dhisana.utils.assistant_tool_tag import assistant_tool
14
14
  from dhisana.utils.generate_structured_output_internal import (
@@ -22,15 +22,16 @@ from dhisana.utils.generate_structured_output_internal import (
22
22
  class InboundEmailTriageResponse(BaseModel):
23
23
  """
24
24
  Model representing the structured response for an inbound email triage.
25
- - triage_status: "AUTOMATIC" or "REQUIRES_APPROVAL"
26
- - triage_reason: Reason text if triage_status == "REQUIRES_APPROVAL"
25
+ - triage_status: "AUTOMATIC" or "END_CONVERSATION"
26
+ - triage_reason: Reason text if triage_status == "END_CONVERSATION"
27
27
  - response_action_to_take: The recommended next action (e.g. SCHEDULE_MEETING, SEND_REPLY, etc.)
28
28
  - response_message: The actual body of the email response to be sent or used for approval.
29
29
  """
30
- triage_status: str # "AUTOMATIC" or "REQUIRES_APPROVAL"
30
+ triage_status: str # "AUTOMATIC" or "END_CONVERSATION"
31
31
  triage_reason: Optional[str]
32
32
  response_action_to_take: str
33
- response_message: str
33
+ response_message: Optional[str]
34
+ meeting_offer_sent: Optional[bool]
34
35
 
35
36
 
36
37
  # ---------------------------------------------------------------------------------------
@@ -38,14 +39,180 @@ class InboundEmailTriageResponse(BaseModel):
38
39
  # ---------------------------------------------------------------------------------------
39
40
  def cleanup_reply_campaign_context(campaign_context: ContentGenerationContext) -> ContentGenerationContext:
40
41
  clone_context = campaign_context.copy(deep=True)
41
- clone_context.lead_info.task_ids = None
42
- clone_context.lead_info.email_validation_status = None
43
- clone_context.lead_info.linkedin_validation_status = None
44
- clone_context.lead_info.research_status = None
45
- clone_context.lead_info.enchrichment_status = None
42
+ if clone_context.lead_info is not None:
43
+ clone_context.lead_info.task_ids = None
44
+ clone_context.lead_info.email_validation_status = None
45
+ clone_context.lead_info.linkedin_validation_status = None
46
+ clone_context.lead_info.research_status = None
47
+ clone_context.lead_info.enchrichment_status = None
46
48
  return clone_context
47
49
 
48
50
 
51
+ # ---------------------------------------------------------------------------------------
52
+ # GET INBOUND EMAIL TRIAGE ACTION (NO EMAIL TEXT)
53
+ # ---------------------------------------------------------------------------------------
54
+ async def get_inbound_email_triage_action(
55
+ context: ContentGenerationContext,
56
+ tool_config: Optional[List[Dict]] = None
57
+ ) -> InboundEmailTriageResponse:
58
+ """
59
+ Analyzes the inbound email thread, and triage guidelines
60
+ to determine triage status, reason, and the recommended action to take.
61
+ DOES NOT generate the final email text.
62
+ """
63
+ allowed_actions = [
64
+ "UNSUBSCRIBE",
65
+ "NOT_INTERESTED",
66
+ "SCHEDULE_MEETING",
67
+ "SEND_REPLY",
68
+ "OOF_MESSAGE",
69
+ "NEED_MORE_INFO",
70
+ "FORWARD_TO_OTHER_USER",
71
+ "NO_MORE_IN_ORGANIZATION",
72
+ "OBJECTION_RAISED",
73
+ "END_CONVERSATION",
74
+ ]
75
+ current_date_iso = datetime.datetime.now().isoformat()
76
+ cleaned_context = cleanup_reply_campaign_context(context)
77
+ if not cleaned_context.current_conversation_context.current_email_thread:
78
+ cleaned_context.current_conversation_context.current_email_thread = []
79
+
80
+ if not cleaned_context.campaign_context.email_triage_guidelines:
81
+ cleaned_context.campaign_context.email_triage_guidelines = "No specific guidelines provided."
82
+
83
+ triage_prompt = f"""
84
+ You are a specialized email assistant.
85
+ Your task is to analyze the inbound email thread and the triage
86
+ guidelines below to determine the correct triage action.
87
+
88
+ allowed_actions =
89
+ {allowed_actions}
90
+
91
+ 1. Email thread or conversation:
92
+ {[thread_item.model_dump() for thread_item in cleaned_context.current_conversation_context.current_email_thread]}
93
+
94
+ 2. Triage Guidelines
95
+ -----------------------------------------------------------------
96
+ General flow
97
+ ------------
98
+ • If the request is routine, non-sensitive, and clearly actionable
99
+ → **triage_status = "AUTOMATIC"**.
100
+ • If the thread contains PII, legal, NSFW, or any sensitive content
101
+ → **triage_status = "END_CONVERSATION"** and set a short **triage_reason**.
102
+
103
+ Meeting & next-step logic
104
+ -------------------------
105
+ • Define `meeting_offer_sent` = **true** if **any** prior assistant
106
+ message in the current thread proposed a call or meeting.
107
+
108
+ • **First positive but non-committal reply**
109
+ (e.g. “Thanks”, “Sounds good”, “Will review”) **AND**
110
+ `meeting_offer_sent` is **false**
111
+ → **SEND_REPLY** asking for a 15-min call, ≤ 150 words, friendly tone.
112
+
113
+ • **Second non-committal reply** or “Will get back” **after**
114
+ `meeting_offer_sent` already true
115
+ → **END_CONVERSATION** (stop the thread unless the prospect re-engages).
116
+
117
+ • If the prospect explicitly **asks for times / suggests times /
118
+ requests your scheduling link**
119
+ → **SCHEDULE_MEETING** and include a concise reply that
120
+ (a) confirms time or provides link,
121
+ (b) thanks them, and
122
+ (c) ends with a forward-looking statement.
123
+
124
+ Handling interest & objections
125
+ ------------------------------
126
+ • If the prospect asks for **pricing, docs, case studies, or more info**
127
+ → **NEED_MORE_INFO** and craft a short response that promises to send
128
+ the material (or includes it if ≤ 150 words fits).
129
+
130
+ • If they mention **budget, timing, or competitor concerns**
131
+ → **OBJECTION_RAISED** and reply with a brief acknowledgement
132
+ + single clarifying question or value statement.
133
+
134
+ • If they request to loop in a colleague (“Please include Sarah”)
135
+ → **FORWARD_TO_OTHER_USER** and draft a one-liner tee-up.
136
+
137
+ Priority order for immediate triage
138
+ -----------------------------------
139
+ 1. “Unsubscribe”, “Remove me”, CAN-SPAM language → **UNSUBSCRIBE**
140
+ 2. Explicit lack of interest → **NOT_INTERESTED**
141
+ 3. Auto OOO / vacation responder → **OOF_MESSAGE**
142
+ 4. Explicit request to meet / suggested times → **SCHEDULE_MEETING**
143
+ 5. Prospect asks questions or raises objection → as per rules above
144
+ 6. Apply “Meeting & next-step logic”
145
+ 7. Default → **END_CONVERSATION**
146
+
147
+ Reply style (when SEND_REPLY or SCHEDULE_MEETING)
148
+ -------------------------------------------------
149
+ • Max 150 words, clear single CTA, no jargon.
150
+ • Start with a thank-you, mirror the prospect’s language briefly, then
151
+ propose next step or answer question.
152
+
153
+ If you have not proposed a meeting even once in the thread, and the user response is polite acknowledgment then you MUST request for a meeting.
154
+
155
+ • Meeting ask template (use *exact* placeholder, will be filled later):
156
+ Hi {{first_name}}, would you be open to a quick 15-min call to
157
+ understand your use-case and share notes?
158
+
159
+ • Competitor-stack mention template:
160
+ Hi {{first_name}}, thanks for sharing your current stack. Would you be
161
+ open to a 15-min call to explore where we can add value?
162
+
163
+
164
+
165
+ Custom triage guidelines provided by the user. This takes precedence over above guidelines:
166
+ {cleaned_context.campaign_context.email_triage_guidelines}
167
+
168
+ Guard-rails
169
+ -----------
170
+ • Only one unsolicited follow-up per thread. If no response, stop.
171
+ • Never disclose PII/financial data; instead **END_CONVERSATION**.
172
+ • Stay friendly, concise, and on topic.
173
+
174
+
175
+ Required JSON output
176
+ --------------------
177
+ {{
178
+ "triage_status": "...",
179
+ "triage_reason": null or "<reason>",
180
+ "response_action_to_take": "one of {allowed_actions}",
181
+ "response_message": "<only if SEND_REPLY/SCHEDULE_MEETING, else empty>"
182
+ }}
183
+
184
+ Current date is: {current_date_iso}.
185
+ -----------------------------------------------------------------
186
+ """
187
+
188
+
189
+ # If there's a vector store ID, use that approach
190
+ if (
191
+ cleaned_context.external_known_data
192
+ and cleaned_context.external_known_data.external_openai_vector_store_id
193
+ ):
194
+ triage_only, status = await get_structured_output_with_assistant_and_vector_store(
195
+ prompt=triage_prompt,
196
+ response_format=InboundEmailTriageResponse,
197
+ model="gpt-5.1-chat",
198
+ vector_store_id=cleaned_context.external_known_data.external_openai_vector_store_id,
199
+ tool_config=tool_config,
200
+ use_cache=cleaned_context.message_instructions.use_cache if cleaned_context.message_instructions else True
201
+ )
202
+ else:
203
+ triage_only, status = await get_structured_output_internal(
204
+ prompt=triage_prompt,
205
+ response_format=InboundEmailTriageResponse,
206
+ model="gpt-5.1-chat",
207
+ tool_config=tool_config,
208
+ use_cache=cleaned_context.message_instructions.use_cache if cleaned_context.message_instructions else True
209
+ )
210
+
211
+ if status != "SUCCESS":
212
+ raise Exception("Error in generating triage action.")
213
+ return triage_only
214
+
215
+
49
216
  # ---------------------------------------------------------------------------------------
50
217
  # CORE FUNCTION TO GENERATE SINGLE RESPONSE (ONE VARIATION)
51
218
  # ---------------------------------------------------------------------------------------
@@ -68,62 +235,140 @@ async def generate_inbound_email_response_copy(
68
235
  "FORWARD_TO_OTHER_USER",
69
236
  "NO_MORE_IN_ORGANIZATION",
70
237
  "OBJECTION_RAISED",
71
- "OTHER"
238
+ "END_CONVERSATION",
72
239
  ]
73
240
  current_date_iso = datetime.datetime.now().isoformat()
74
241
  cleaned_context = cleanup_reply_campaign_context(campaign_context)
75
242
  if not cleaned_context.current_conversation_context.current_email_thread:
76
243
  cleaned_context.current_conversation_context.current_email_thread = []
244
+
245
+ lead_data = cleaned_context.lead_info or Lead()
246
+ sender_data = cleaned_context.sender_info or SenderInfo()
77
247
 
78
248
  prompt = f"""
79
- You are a specialized email assistant.
80
- Your task is to analyze the user's email thread, the user/company info,
81
- and the provided triage guidelines to craft a response.
249
+ You are a specialized email assistant.
250
+ Your task is to analyze the user's email thread, the user/company info,
251
+ and the provided triage guidelines to craft an appropriate response.
82
252
 
83
- Follow these instructions to generate the reply:
84
- {variation}
253
+ Follow these instructions to generate the reply:
254
+ {variation}
85
255
 
86
- 1. Understand the email thread or conversation to respond to:
87
- {[thread_item.model_dump() for thread_item in cleaned_context.current_conversation_context.current_email_thread] if cleaned_context.current_conversation_context.current_email_thread else []}
256
+ 1. Email thread or conversation to respond to:
257
+ {[thread_item.model_dump() for thread_item in cleaned_context.current_conversation_context.current_email_thread]
258
+ if cleaned_context.current_conversation_context.current_email_thread else []}
88
259
 
89
- 2. User & Company (Lead) Info:
90
- {cleaned_context.model_dump()}
260
+ 2) Lead Information:
261
+ {lead_data.dict()}
91
262
 
92
- 3. Triage Guidelines:
93
- {cleaned_context.campaign_context.email_triage_guidelines}
263
+ Sender Information:
264
+ Full Name: {sender_data.sender_full_name or ''}
265
+ First Name: {sender_data.sender_first_name or ''}
266
+ Last Name: {sender_data.sender_last_name or ''}
267
+ Bio: {sender_data.sender_bio or ''}
268
+
94
269
 
95
- - If the request is standard, simple, or obviously handled by standard processes,
96
- set triage_status to "AUTOMATIC".
97
- - If the request is complex, sensitive, or needs special input,
98
- set triage_status to "REQUIRES_APPROVAL" and provide triage_reason.
270
+ 3. Campaign-specific triage guidelines (user overrides always win):
271
+ {cleaned_context.campaign_context.email_triage_guidelines}
99
272
 
100
- 4. Choose one action from this list: {allowed_actions}
273
+ -----------------------------------------------------------------
274
+ Core decision logic
275
+ -----------------------------------------------------------------
276
+ • If the request is routine, non-sensitive, and clearly actionable
277
+ → **triage_status = "AUTOMATIC"**.
278
+ • If the thread contains PII, finance, legal, or any sensitive/NSFW content
279
+ → **triage_status = "END_CONVERSATION"** and give a concise **triage_reason**.
101
280
 
102
- 5. Provide your recommended email body that best addresses the user's message.
103
- DO NOT reply to any PII or financial information requests; triage them as "REQUIRES_APPROVAL".
104
- DO NOT replay anything negative about my product or company {campaign_context.lead_info.organization_name}; triage them as "REQUIRES_APPROVAL".
105
- current date is : {current_date_iso}
106
- DO NOT share any link to internal or made up doucment. You can attach or send any document.
107
- If the user is asking for any document point them to organization's website found in sender information if available:
108
- {campaign_context.sender_info.model_dump()}
109
-
110
- Use conversational name for company name.
281
+ 4. Choose exactly ONE of: {allowed_actions}
282
+
283
+ -----------------------------------------------------------------
284
+ Response best practices
285
+ -----------------------------------------------------------------
286
+ MAX 150 words, friendly & concise, single clear CTA.
287
+ Begin with a thank-you, mirror the prospect’s wording briefly, then answer /
288
+ propose next step.
289
+ Never contradict, trash-talk, or disparage {campaign_context.lead_info.organization_name}.
290
+ • Plain-text only – NO HTML tags (<a>, <b>, <i>, etc.).
291
+ • If a link already exists in the inbound email, include it verbatim—do not re-wrap or shorten.
292
+
293
+ Meeting & follow-up rules
294
+ -------------------------
295
+ 1. Let `meeting_offer_sent` = **true** if any earlier assistant message offered a
296
+ meeting.
297
+ 2. If First “Thanks / Sounds good” & *no* prior meeting offer
298
+ → **SEND_REPLY** asking for a 15-min call (≤150 words).
299
+ 3. If Second non-committal reply *after* meeting_offer_sent, or explicit “not interested”
300
+ → **END_CONVERSATION**.
301
+ 4. If prospect explicitly asks for times / requests your link
302
+ → **SCHEDULE_MEETING** and confirm or propose times.
303
+ 5. If One unsolicited follow-up maximum; stop unless prospect re-engages.
304
+
305
+ If you have not proposed a meeting even once in the thread, and the user response is polite acknowledgment then you MUST request for a meeting.
306
+
307
+
308
+ Objections & info requests
309
+ --------------------------
310
+ • Pricing / docs / case-studies request → **NEED_MORE_INFO**.
311
+ • Budget, timing, or competitor concerns → **OBJECTION_RAISED**
312
+ (acknowledge + one clarifying Q or concise value point).
313
+ • “Loop in {{colleague_name}}” → **FORWARD_TO_OTHER_USER**.
314
+
315
+ Unsubscribe & priority handling
316
+ -------------------------------
317
+ 1. “Unsubscribe / Remove me” → **UNSUBSCRIBE**
318
+ 2. Clear lack of interest → **NOT_INTERESTED**
319
+ 3. Auto OOO reply → **OOF_MESSAGE**
320
+ 4. Explicit meeting request → **SCHEDULE_MEETING**
321
+ 5. Otherwise follow the Meeting & follow-up rules above
322
+ 6. Default → **END_CONVERSATION**
323
+
324
+ Style guard-rails
325
+ -----------------
326
+ • Plain language; no jargon or filler.
327
+ • Do **not** repeat previous messages verbatim.
328
+ • Signature must include sender_first_name exactly as provided.
329
+ • Check UNSUBSCRIBE / NOT_INTERESTED first before other triage.
330
+
331
+ If you have not proposed a meeting even once in the thread, and the user response is polite acknowledgment then you MUST request for a meeting.
332
+
333
+ • Meeting ask template example:
334
+ Hi {{lead_first_name}}, would you be open to a quick 15-min call to
335
+ understand your use-case and share notes?
336
+
337
+ • Competitor-stack mention template example:
338
+ Hi {{lead_first_name}}, thanks for sharing your current stack. Would you be
339
+ open to a 15-min call to explore where we can add value?
340
+
341
+ Use conversational name for company name.
111
342
  Use conversational name when using lead first name.
112
- Do not use special characters or spaces when use leads first name.
343
+ Do not use special characters or spaces when using lead’s first name.
113
344
  In the subject or body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
114
345
  The body and subject should be in plain text.
115
- If there is a link provided in email use it as is. dont wrap it in any HTML tags.
116
- DO NOT make up information. use only the information provided in the context and instructions.
346
+ If there is a link provided in the email, use it as is; do not wrap it in any HTML tags.
347
+ DO NOT make up information. Use only the information provided in the context and instructions.
348
+ Do NOT repeat the same message sent to the user in the past.
349
+ Keep the thread conversational and friendly as a good account executive would respond.
350
+ Do NOT rehash/repeat the same previous message already sent. Keep the reply to the point.
351
+ DO NOT try to spam users with multiple messages.
352
+ Current date is: {current_date_iso}.
353
+ DO NOT share any link to internal or made up document. You can attach or send any document.
354
+ If the user is asking for any additional document END_CONVERSATION and let Account executive handle it.
355
+ - Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
356
+ - Do Not use em dash in the generated output.
117
357
 
118
- Your final output must be valid JSON with the structure:
358
+ Required JSON output
359
+ --------------------
119
360
  {{
120
- "triage_status": "AUTOMATIC" or "REQUIRES_APPROVAL",
121
- "triage_reason": "<reason if requires approval; otherwise null>",
122
- "response_action_to_take": "<chosen action>",
123
- "response_message": "<the email body to respond with>"
361
+ "triage_status": "AUTOMATIC" or "END_CONVERSATION",
362
+ "triage_reason": "<reason if END_CONVERSATION; otherwise null>",
363
+ "response_action_to_take": "one of {allowed_actions}",
364
+ "response_message": "<the reply body if response_action_to_take is SEND_REPLY or SCHEDULE_MEETING; otherwise empty>"
124
365
  }}
366
+
367
+ Current date is: {current_date_iso}.
368
+ -----------------------------------------------------------------
125
369
  """
126
370
 
371
+
127
372
  # If there's a vector store ID, use that approach
128
373
  if (
129
374
  cleaned_context.external_known_data
@@ -132,6 +377,7 @@ async def generate_inbound_email_response_copy(
132
377
  initial_response, status = await get_structured_output_with_assistant_and_vector_store(
133
378
  prompt=prompt,
134
379
  response_format=InboundEmailTriageResponse,
380
+ model="gpt-5.1-chat",
135
381
  vector_store_id=cleaned_context.external_known_data.external_openai_vector_store_id,
136
382
  tool_config=tool_config
137
383
  )
@@ -139,6 +385,7 @@ async def generate_inbound_email_response_copy(
139
385
  initial_response, status = await get_structured_output_internal(
140
386
  prompt=prompt,
141
387
  response_format=InboundEmailTriageResponse,
388
+ model="gpt-5.1-chat",
142
389
  tool_config=tool_config
143
390
  )
144
391
 
@@ -164,6 +411,7 @@ async def generate_inbound_email_response_copy(
164
411
  message_item=response_item,
165
412
  response_action_to_take=initial_response.response_action_to_take
166
413
  )
414
+ print(response_message.model_dump())
167
415
  return response_message.model_dump()
168
416
 
169
417
 
@@ -1,4 +1,3 @@
1
- import logging
2
1
  from pydantic import BaseModel
3
2
  from dhisana.utils.dataframe_tools import get_structured_output
4
3
 
@@ -133,7 +133,10 @@ async def generate_personalized_linkedin_copy(
133
133
  - Do not include personal addresses, IDs, or irrelevant internal data.
134
134
  - Linked in message always has saluatation Hi <First Name>, unless specified otherwise.
135
135
  - <First Name> is the first name of the lead. Its conversational name. It does not have any special characters or spaces.
136
+ - Make sure the signature in body has the sender_first_name is correct and in the format user has specified.
136
137
  - User conversational name for company name if used.
138
+ - Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
139
+ - Do Not use em dash in the generated output.
137
140
  """
138
141
 
139
142
  vector_store_id = (
@@ -147,14 +150,18 @@ async def generate_personalized_linkedin_copy(
147
150
  prompt=prompt,
148
151
  response_format=LinkedInConnectMessage,
149
152
  vector_store_id=vector_store_id,
150
- tool_config=tool_config
153
+ model="gpt-5.1-chat",
154
+ tool_config=tool_config,
155
+ use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
151
156
  )
152
157
  else:
153
158
  # Otherwise, generate internally
154
159
  response_data, status = await get_structured_output_internal(
155
160
  prompt=prompt,
156
161
  response_format=LinkedInConnectMessage,
157
- tool_config=tool_config
162
+ model="gpt-5.1-chat",
163
+ tool_config=tool_config,
164
+ use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
158
165
  )
159
166
 
160
167
  if status != "SUCCESS":