dhisana 0.0.1.dev272__py3-none-any.whl → 0.0.1.dev274__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.
- dhisana/utils/email_parse_helpers.py +78 -7
- dhisana/utils/generate_email.py +209 -99
- dhisana/utils/generate_email_response.py +19 -14
- dhisana/utils/generate_linkedin_connect_message.py +173 -75
- dhisana/utils/openai_assistant_and_file_utils.py +21 -11
- {dhisana-0.0.1.dev272.dist-info → dhisana-0.0.1.dev274.dist-info}/METADATA +1 -1
- {dhisana-0.0.1.dev272.dist-info → dhisana-0.0.1.dev274.dist-info}/RECORD +10 -10
- {dhisana-0.0.1.dev272.dist-info → dhisana-0.0.1.dev274.dist-info}/WHEEL +0 -0
- {dhisana-0.0.1.dev272.dist-info → dhisana-0.0.1.dev274.dist-info}/entry_points.txt +0 -0
- {dhisana-0.0.1.dev272.dist-info → dhisana-0.0.1.dev274.dist-info}/top_level.txt +0 -0
|
@@ -1,19 +1,86 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import email.utils
|
|
3
|
+
import quopri
|
|
4
|
+
from email.message import Message
|
|
3
5
|
from email.utils import parseaddr
|
|
4
6
|
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
5
8
|
from bs4 import BeautifulSoup
|
|
6
9
|
|
|
7
|
-
def decode_base64_url(data: str) ->
|
|
10
|
+
def decode_base64_url(data: str) -> bytes:
|
|
8
11
|
"""
|
|
9
|
-
|
|
12
|
+
Decode a Base64-url-encoded string (Gmail API uses URL-safe Base64).
|
|
10
13
|
"""
|
|
11
14
|
data = data.replace('-', '+').replace('_', '/')
|
|
12
15
|
# Fix padding
|
|
13
16
|
missing_padding = len(data) % 4
|
|
14
17
|
if missing_padding:
|
|
15
18
|
data += '=' * (4 - missing_padding)
|
|
16
|
-
return base64.b64decode(data)
|
|
19
|
+
return base64.b64decode(data)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_charset(headers_list: List[Dict[str, str]], mime_type: str) -> Optional[str]:
|
|
23
|
+
content_type = find_header(headers_list, "Content-Type") or mime_type or ""
|
|
24
|
+
if not content_type:
|
|
25
|
+
return None
|
|
26
|
+
msg = Message()
|
|
27
|
+
msg["Content-Type"] = content_type
|
|
28
|
+
return msg.get_content_charset()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _decode_transfer_encoding(payload: bytes, transfer_encoding: str) -> bytes:
|
|
32
|
+
encoding = (transfer_encoding or "").lower()
|
|
33
|
+
if "quoted-printable" in encoding:
|
|
34
|
+
return quopri.decodestring(payload)
|
|
35
|
+
if "base64" in encoding:
|
|
36
|
+
try:
|
|
37
|
+
return base64.b64decode(payload, validate=False)
|
|
38
|
+
except Exception:
|
|
39
|
+
return payload
|
|
40
|
+
return payload
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_MOJIBAKE_MARKERS = (
|
|
44
|
+
"\u00e2\u0080\u0099",
|
|
45
|
+
"\u00e2\u0080\u0093",
|
|
46
|
+
"\u00e2\u0080\u0094",
|
|
47
|
+
"\u00e2\u0080\u009c",
|
|
48
|
+
"\u00e2\u0080\u009d",
|
|
49
|
+
"\u00e2\u0080\u00a6",
|
|
50
|
+
"\u00c3\u00a9",
|
|
51
|
+
"\u00c3\u00a0",
|
|
52
|
+
"\u00c3\u00b6",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _repair_mojibake(text: str) -> str:
|
|
57
|
+
if not text or not any(marker in text for marker in _MOJIBAKE_MARKERS):
|
|
58
|
+
return text
|
|
59
|
+
try:
|
|
60
|
+
repaired = text.encode("latin-1").decode("utf-8")
|
|
61
|
+
except UnicodeError:
|
|
62
|
+
return text
|
|
63
|
+
if any(marker in repaired for marker in _MOJIBAKE_MARKERS):
|
|
64
|
+
return text
|
|
65
|
+
return repaired
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _decode_part_text(
|
|
69
|
+
data: str, headers_list: List[Dict[str, str]], mime_type: str
|
|
70
|
+
) -> str:
|
|
71
|
+
raw_bytes = decode_base64_url(data)
|
|
72
|
+
transfer_encoding = find_header(headers_list, "Content-Transfer-Encoding") or ""
|
|
73
|
+
decoded_bytes = _decode_transfer_encoding(raw_bytes, transfer_encoding)
|
|
74
|
+
|
|
75
|
+
charset = _get_charset(headers_list, mime_type)
|
|
76
|
+
tried = [charset, "utf-8", "windows-1252", "latin-1"]
|
|
77
|
+
for enc in [e for e in tried if e]:
|
|
78
|
+
try:
|
|
79
|
+
text = decoded_bytes.decode(enc)
|
|
80
|
+
return _repair_mojibake(text)
|
|
81
|
+
except (LookupError, UnicodeDecodeError):
|
|
82
|
+
continue
|
|
83
|
+
return _repair_mojibake(decoded_bytes.decode("utf-8", errors="replace"))
|
|
17
84
|
|
|
18
85
|
|
|
19
86
|
def parse_plain_text_from_parts(parts: List[Dict[str, Any]]) -> str:
|
|
@@ -30,7 +97,9 @@ def parse_plain_text_from_parts(parts: List[Dict[str, Any]]) -> str:
|
|
|
30
97
|
mime_type = part.get('mimeType', '')
|
|
31
98
|
data = part.get('body', {}).get('data', '')
|
|
32
99
|
if data:
|
|
33
|
-
decoded_data =
|
|
100
|
+
decoded_data = _decode_part_text(
|
|
101
|
+
data, part.get("headers", []), mime_type
|
|
102
|
+
)
|
|
34
103
|
if 'text/plain' in mime_type:
|
|
35
104
|
text_chunks.append(decoded_data)
|
|
36
105
|
elif 'text/html' in mime_type:
|
|
@@ -49,7 +118,9 @@ def extract_email_body_in_plain_text(message_data: Dict[str, Any]) -> str:
|
|
|
49
118
|
# If top-level body has data (i.e. single-part message)
|
|
50
119
|
if payload.get('body', {}).get('data'):
|
|
51
120
|
raw_data = payload['body']['data']
|
|
52
|
-
decoded_data =
|
|
121
|
+
decoded_data = _decode_part_text(
|
|
122
|
+
raw_data, payload.get("headers", []), payload.get("mimeType", "")
|
|
123
|
+
)
|
|
53
124
|
# Check if it might be HTML
|
|
54
125
|
mime_type = payload.get('mimeType', '')
|
|
55
126
|
if 'text/html' in mime_type:
|
|
@@ -85,7 +156,7 @@ def find_header(headers_list: List[Dict[str, str]], header_name: str) -> Optiona
|
|
|
85
156
|
return None
|
|
86
157
|
|
|
87
158
|
|
|
88
|
-
def parse_single_address(display_str: str) ->
|
|
159
|
+
def parse_single_address(display_str: str) -> Tuple[str, str]:
|
|
89
160
|
"""
|
|
90
161
|
Parses a single display string like "Alice <alice@example.com>"
|
|
91
162
|
returning (name, email).
|
|
@@ -112,7 +183,7 @@ def parse_address_list(display_str: str) -> List[Tuple[str, str]]:
|
|
|
112
183
|
return addresses
|
|
113
184
|
|
|
114
185
|
|
|
115
|
-
def find_all_recipients_in_headers(headers_list: List[Dict[str, str]]) ->
|
|
186
|
+
def find_all_recipients_in_headers(headers_list: List[Dict[str, str]]) -> Tuple[str, str]:
|
|
116
187
|
"""
|
|
117
188
|
Collect 'To', 'Cc', 'Bcc' headers, parse each address, and return:
|
|
118
189
|
(comma-separated receiver names, comma-separated receiver emails)
|
dhisana/utils/generate_email.py
CHANGED
|
@@ -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
|
-
#
|
|
89
|
+
# Step 1: Generate email instructions using gpt-4.1 (larger context)
|
|
72
90
|
# -----------------------------------------------------------------------------
|
|
73
|
-
async def
|
|
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
|
-
) ->
|
|
96
|
+
) -> EmailGenerationInstructions:
|
|
79
97
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
Hi AI Assistant,
|
|
237
|
+
RECIPIENT CONTEXT:
|
|
238
|
+
{instructions.recipient_context}
|
|
151
239
|
|
|
152
|
-
|
|
240
|
+
SENDER CONTEXT:
|
|
241
|
+
{instructions.sender_context}
|
|
153
242
|
|
|
154
|
-
|
|
155
|
-
|
|
243
|
+
CAMPAIGN CONTEXT:
|
|
244
|
+
{instructions.campaign_context}
|
|
156
245
|
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
174
|
-
|
|
252
|
+
KEY POINTS TO INCLUDE:
|
|
253
|
+
{instructions.key_points_to_include}
|
|
175
254
|
|
|
176
|
-
|
|
177
|
-
|
|
255
|
+
CALL TO ACTION:
|
|
256
|
+
{instructions.call_to_action}
|
|
178
257
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
|
|
258
|
+
FORMATTING REQUIREMENTS:
|
|
259
|
+
{instructions.formatting_requirements}
|
|
182
260
|
|
|
183
|
-
|
|
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
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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=
|
|
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
|
-
→ **
|
|
133
|
-
the material (or includes it if ≤ 150 words fits).
|
|
133
|
+
→ **SEND_REPLY** and ask a short clarifying question or promise a follow-up.
|
|
134
134
|
|
|
135
135
|
• If they mention **budget, timing, or competitor concerns**
|
|
136
136
|
→ **OBJECTION_RAISED** and reply with a brief acknowledgement
|
|
@@ -183,7 +183,7 @@ async def get_inbound_email_triage_action(
|
|
|
183
183
|
"triage_status": "...",
|
|
184
184
|
"triage_reason": null or "<reason>",
|
|
185
185
|
"response_action_to_take": "one of {allowed_actions}",
|
|
186
|
-
"response_message": "<only if SEND_REPLY/SCHEDULE_MEETING, else empty>"
|
|
186
|
+
"response_message": "<only if SEND_REPLY/SCHEDULE_MEETING/OBJECTION_RAISED, else empty>"
|
|
187
187
|
}}
|
|
188
188
|
|
|
189
189
|
Current date is: {current_date_iso}.
|
|
@@ -251,7 +251,6 @@ async def generate_inbound_email_response_copy(
|
|
|
251
251
|
"UNSUBSCRIBE",
|
|
252
252
|
"OOF_MESSAGE",
|
|
253
253
|
"NOT_INTERESTED",
|
|
254
|
-
"NEED_MORE_INFO",
|
|
255
254
|
"FORWARD_TO_OTHER_USER",
|
|
256
255
|
"NO_MORE_IN_ORGANIZATION",
|
|
257
256
|
"OBJECTION_RAISED",
|
|
@@ -282,7 +281,7 @@ async def generate_inbound_email_response_copy(
|
|
|
282
281
|
if cleaned_context.current_conversation_context.current_email_thread else []}
|
|
283
282
|
|
|
284
283
|
2) Lead information:
|
|
285
|
-
{lead_data.
|
|
284
|
+
{lead_data.model_dump()}
|
|
286
285
|
|
|
287
286
|
Sender information:
|
|
288
287
|
- Full name: {sender_data.sender_full_name or ''}
|
|
@@ -316,16 +315,17 @@ async def generate_inbound_email_response_copy(
|
|
|
316
315
|
Choose exactly ONE action from:
|
|
317
316
|
{allowed_actions}
|
|
318
317
|
|
|
318
|
+
If you need more info, use SEND_REPLY and ask a short clarifying question.
|
|
319
|
+
|
|
319
320
|
Priority order:
|
|
320
321
|
1. UNSUBSCRIBE
|
|
321
322
|
2. NOT_INTERESTED
|
|
322
323
|
3. OOF_MESSAGE
|
|
323
324
|
4. SCHEDULE_MEETING
|
|
324
325
|
5. FORWARD_TO_OTHER_USER
|
|
325
|
-
6.
|
|
326
|
-
7.
|
|
327
|
-
8.
|
|
328
|
-
9. END_CONVERSATION
|
|
326
|
+
6. OBJECTION_RAISED
|
|
327
|
+
7. SEND_REPLY
|
|
328
|
+
8. END_CONVERSATION
|
|
329
329
|
|
|
330
330
|
=====================================================
|
|
331
331
|
HOW THE RESPONSE SHOULD SOUND
|
|
@@ -391,7 +391,7 @@ async def generate_inbound_email_response_copy(
|
|
|
391
391
|
"triage_status": "AUTOMATIC" or "END_CONVERSATION",
|
|
392
392
|
"triage_reason": "<string if END_CONVERSATION, otherwise null>",
|
|
393
393
|
"response_action_to_take": "one of {allowed_actions}",
|
|
394
|
-
"response_message": "<reply body only if SEND_REPLY
|
|
394
|
+
"response_message": "<reply body only if SEND_REPLY/SCHEDULE_MEETING/OBJECTION_RAISED, otherwise empty>"
|
|
395
395
|
}}
|
|
396
396
|
|
|
397
397
|
=====================================================
|
|
@@ -446,6 +446,12 @@ async def generate_inbound_email_response_copy(
|
|
|
446
446
|
f"Campaign ID: {campaign_id}"
|
|
447
447
|
)
|
|
448
448
|
|
|
449
|
+
response_action = initial_response.response_action_to_take
|
|
450
|
+
if response_action == "NEED_MORE_INFO":
|
|
451
|
+
response_action = "SEND_REPLY"
|
|
452
|
+
|
|
453
|
+
response_message = initial_response.response_message or ""
|
|
454
|
+
|
|
449
455
|
response_item = MessageItem(
|
|
450
456
|
message_id="", # or generate one if appropriate
|
|
451
457
|
thread_id="",
|
|
@@ -455,7 +461,7 @@ async def generate_inbound_email_response_copy(
|
|
|
455
461
|
receiver_email=campaign_context.lead_info.email or "",
|
|
456
462
|
iso_datetime=datetime.datetime.utcnow().isoformat(),
|
|
457
463
|
subject="", # or set some triage subject if needed
|
|
458
|
-
body=
|
|
464
|
+
body=response_message
|
|
459
465
|
)
|
|
460
466
|
|
|
461
467
|
# Build a MessageResponse that includes triage metadata plus your message item
|
|
@@ -463,9 +469,8 @@ async def generate_inbound_email_response_copy(
|
|
|
463
469
|
triage_status=initial_response.triage_status,
|
|
464
470
|
triage_reason=initial_response.triage_reason,
|
|
465
471
|
message_item=response_item,
|
|
466
|
-
response_action_to_take=
|
|
472
|
+
response_action_to_take=response_action
|
|
467
473
|
)
|
|
468
|
-
print(response_message.model_dump())
|
|
469
474
|
return response_message.model_dump()
|
|
470
475
|
|
|
471
476
|
|
|
@@ -24,7 +24,22 @@ from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
|
24
24
|
# ---------------------------------------------------------------------------------------
|
|
25
25
|
# CONSTANTS
|
|
26
26
|
# ---------------------------------------------------------------------------------------
|
|
27
|
-
DEFAULT_OPENAI_MODEL = "gpt-4.1"
|
|
27
|
+
DEFAULT_OPENAI_MODEL = "gpt-4.1" # Larger context length - used for generating instructions
|
|
28
|
+
DEFAULT_EMAIL_GEN_MODEL = "gpt-5.1-chat" # Better content generation - used for generating message
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ----------------------------------------------------------------------
|
|
32
|
+
# LinkedIn Generation Instructions Schema (intermediate step)
|
|
33
|
+
# ----------------------------------------------------------------------
|
|
34
|
+
class LinkedInGenerationInstructions(BaseModel):
|
|
35
|
+
"""Instructions generated by the first step to guide LinkedIn message generation."""
|
|
36
|
+
recipient_context: str # Key info about the lead/recipient
|
|
37
|
+
sender_context: str # Key info about the sender
|
|
38
|
+
campaign_context: str # Product, value prop, pain points, etc.
|
|
39
|
+
conversation_context: str # Summary of any existing conversation
|
|
40
|
+
tone_and_style: str # How the message should sound
|
|
41
|
+
key_points_to_include: str # Main points to cover
|
|
42
|
+
call_to_action: str # What action to request
|
|
28
43
|
|
|
29
44
|
|
|
30
45
|
# ----------------------------------------------------------------------
|
|
@@ -68,26 +83,17 @@ LINKEDIN_FRAMEWORK_VARIATIONS = [
|
|
|
68
83
|
]
|
|
69
84
|
|
|
70
85
|
# ----------------------------------------------------------------------
|
|
71
|
-
#
|
|
86
|
+
# Step 1: Generate LinkedIn instructions using gpt-4.1 (larger context)
|
|
72
87
|
# ----------------------------------------------------------------------
|
|
73
|
-
async def
|
|
88
|
+
async def generate_linkedin_instructions(
|
|
74
89
|
linkedin_context: ContentGenerationContext,
|
|
75
90
|
variation_text: str,
|
|
76
91
|
tool_config: Optional[List[Dict]] = None,
|
|
77
|
-
) ->
|
|
92
|
+
) -> LinkedInGenerationInstructions:
|
|
78
93
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
1. Build a prompt referencing 6 main sections:
|
|
83
|
-
(a) Lead Info
|
|
84
|
-
(b) Sender Info
|
|
85
|
-
(c) Campaign Info
|
|
86
|
-
(d) Variation / Instructions
|
|
87
|
-
(e) External Data (if any)
|
|
88
|
-
(f) Current Conversation
|
|
89
|
-
2. Generate the LinkedIn message with or without vector store usage.
|
|
90
|
-
3. Return the final subject & body, ensuring < 40 words.
|
|
94
|
+
Step 1 of 2-step LinkedIn message generation process.
|
|
95
|
+
Uses gpt-4.1 (larger context length) to process all context and generate
|
|
96
|
+
concise instructions for message generation.
|
|
91
97
|
"""
|
|
92
98
|
cleaned_context = cleanup_linkedin_context(linkedin_context)
|
|
93
99
|
|
|
@@ -96,54 +102,43 @@ async def generate_personalized_linkedin_copy(
|
|
|
96
102
|
campaign_data = cleaned_context.campaign_context or CampaignContext()
|
|
97
103
|
conversation_data = cleaned_context.current_conversation_context or ConversationContext()
|
|
98
104
|
|
|
99
|
-
# Construct the
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- In the body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
|
|
137
|
-
- The body should be in plain text.
|
|
138
|
-
- If there is a link provided in body use it as is. dont wrap it in any HTML tags.
|
|
139
|
-
- No placeholders or extra instructions in the final output.
|
|
140
|
-
- Do not include personal addresses, IDs, or irrelevant internal data.
|
|
141
|
-
- Linked in message always has saluatation Hi <First Name>, unless specified otherwise.
|
|
142
|
-
- <First Name> is the first name of the lead. Its conversational name. It does not have any special characters or spaces.
|
|
143
|
-
- Make sure the signature in body has the sender_first_name is correct and in the format user has specified.
|
|
144
|
-
- User conversational name for company name if used.
|
|
145
|
-
- Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
|
|
146
|
-
- Do Not use em dash in the generated output.
|
|
105
|
+
# Construct the prompt for generating instructions
|
|
106
|
+
instructions_prompt = f"""
|
|
107
|
+
Generate LinkedIn connection message instructions based on the following business context.
|
|
108
|
+
The final message must be under 40 words.
|
|
109
|
+
|
|
110
|
+
LEAD INFORMATION:
|
|
111
|
+
{lead_data.dict()}
|
|
112
|
+
|
|
113
|
+
SENDER INFORMATION:
|
|
114
|
+
Full Name: {sender_data.sender_full_name or ''}
|
|
115
|
+
First Name: {sender_data.sender_first_name or ''}
|
|
116
|
+
Last Name: {sender_data.sender_last_name or ''}
|
|
117
|
+
Bio: {sender_data.sender_bio or ''}
|
|
118
|
+
Appointment Booking URL: {sender_data.sender_appointment_booking_url or ''}
|
|
119
|
+
|
|
120
|
+
CAMPAIGN INFORMATION:
|
|
121
|
+
Product Name: {campaign_data.product_name or ''}
|
|
122
|
+
Value Proposition: {campaign_data.value_prop or ''}
|
|
123
|
+
Call To Action: {campaign_data.call_to_action or ''}
|
|
124
|
+
Pain Points: {campaign_data.pain_points or []}
|
|
125
|
+
Proof Points: {campaign_data.proof_points or []}
|
|
126
|
+
|
|
127
|
+
MESSAGING FRAMEWORK:
|
|
128
|
+
{variation_text}
|
|
129
|
+
|
|
130
|
+
CONVERSATION HISTORY:
|
|
131
|
+
Email Thread: {conversation_data.current_email_thread or ''}
|
|
132
|
+
LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
|
|
133
|
+
|
|
134
|
+
OUTPUT FIELDS:
|
|
135
|
+
- recipient_context: Key details about the lead (name, company, role)
|
|
136
|
+
- sender_context: Sender name and relevant info for signature
|
|
137
|
+
- campaign_context: Product or service, value proposition, key pain points
|
|
138
|
+
- conversation_context: Summary of any prior conversation if exists
|
|
139
|
+
- tone_and_style: Professional, friendly, etc.
|
|
140
|
+
- key_points_to_include: Main points for the message (keep brief for 40 word limit)
|
|
141
|
+
- call_to_action: What action the recipient should take
|
|
147
142
|
"""
|
|
148
143
|
|
|
149
144
|
vector_store_id = (
|
|
@@ -151,28 +146,131 @@ async def generate_personalized_linkedin_copy(
|
|
|
151
146
|
if linkedin_context.external_known_data else None
|
|
152
147
|
)
|
|
153
148
|
|
|
149
|
+
instructions_response = None
|
|
150
|
+
instructions_status = ""
|
|
151
|
+
|
|
152
|
+
# Generate instructions using gpt-4.1 (larger context)
|
|
154
153
|
if vector_store_id:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
response_format=LinkedInConnectMessage,
|
|
154
|
+
instructions_response, instructions_status = await get_structured_output_with_assistant_and_vector_store(
|
|
155
|
+
prompt=instructions_prompt,
|
|
156
|
+
response_format=LinkedInGenerationInstructions,
|
|
159
157
|
vector_store_id=vector_store_id,
|
|
160
158
|
model=DEFAULT_OPENAI_MODEL,
|
|
161
159
|
tool_config=tool_config,
|
|
162
160
|
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
163
161
|
)
|
|
164
162
|
else:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
response_format=LinkedInConnectMessage,
|
|
163
|
+
instructions_response, instructions_status = await get_structured_output_internal(
|
|
164
|
+
prompt=instructions_prompt,
|
|
165
|
+
response_format=LinkedInGenerationInstructions,
|
|
169
166
|
model=DEFAULT_OPENAI_MODEL,
|
|
170
167
|
tool_config=tool_config,
|
|
171
168
|
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
172
169
|
)
|
|
173
170
|
|
|
174
|
-
if
|
|
175
|
-
raise Exception("Error: Could not generate
|
|
171
|
+
if instructions_status != "SUCCESS":
|
|
172
|
+
raise Exception("Error: Could not generate LinkedIn instructions in step 1.")
|
|
173
|
+
|
|
174
|
+
return instructions_response
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ----------------------------------------------------------------------
|
|
178
|
+
# Step 2: Generate LinkedIn message using gpt-5.1-chat (better content)
|
|
179
|
+
# ----------------------------------------------------------------------
|
|
180
|
+
async def generate_linkedin_from_instructions(
|
|
181
|
+
instructions: LinkedInGenerationInstructions,
|
|
182
|
+
tool_config: Optional[List[Dict]] = None,
|
|
183
|
+
use_cache: bool = True,
|
|
184
|
+
) -> LinkedInConnectMessage:
|
|
185
|
+
"""
|
|
186
|
+
Step 2 of 2-step LinkedIn message generation process.
|
|
187
|
+
Uses gpt-5.1-chat (better content generation) to generate the message
|
|
188
|
+
from the condensed instructions.
|
|
189
|
+
"""
|
|
190
|
+
message_prompt = f"""
|
|
191
|
+
Generate a personalized LinkedIn connection request message based on these specifications.
|
|
192
|
+
The message must be under 40 words total.
|
|
193
|
+
|
|
194
|
+
RECIPIENT CONTEXT:
|
|
195
|
+
{instructions.recipient_context}
|
|
196
|
+
|
|
197
|
+
SENDER CONTEXT:
|
|
198
|
+
{instructions.sender_context}
|
|
199
|
+
|
|
200
|
+
CAMPAIGN CONTEXT:
|
|
201
|
+
{instructions.campaign_context}
|
|
202
|
+
|
|
203
|
+
CONVERSATION CONTEXT:
|
|
204
|
+
{instructions.conversation_context}
|
|
205
|
+
|
|
206
|
+
TONE AND STYLE:
|
|
207
|
+
{instructions.tone_and_style}
|
|
208
|
+
|
|
209
|
+
KEY POINTS TO INCLUDE:
|
|
210
|
+
{instructions.key_points_to_include}
|
|
211
|
+
|
|
212
|
+
CALL TO ACTION:
|
|
213
|
+
{instructions.call_to_action}
|
|
214
|
+
|
|
215
|
+
OUTPUT REQUIREMENTS:
|
|
216
|
+
- Output must be JSON with "body" field only.
|
|
217
|
+
- The entire message body must be under 40 words total.
|
|
218
|
+
- Plain text only, no HTML tags.
|
|
219
|
+
- Use salutation Hi followed by first name, unless specified otherwise.
|
|
220
|
+
- Use only the information provided.
|
|
221
|
+
- Do not use em dash in the output.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
# Generate message using gpt-5.1-chat (better content generation)
|
|
225
|
+
message_response, message_status = await get_structured_output_internal(
|
|
226
|
+
prompt=message_prompt,
|
|
227
|
+
response_format=LinkedInConnectMessage,
|
|
228
|
+
model=DEFAULT_EMAIL_GEN_MODEL,
|
|
229
|
+
tool_config=tool_config,
|
|
230
|
+
use_cache=use_cache
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if message_status != "SUCCESS":
|
|
234
|
+
raise Exception("Error: Could not generate LinkedIn message in step 2.")
|
|
235
|
+
|
|
236
|
+
return message_response
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ----------------------------------------------------------------------
|
|
240
|
+
# Core function to generate a LinkedIn copy (2-step process)
|
|
241
|
+
# ----------------------------------------------------------------------
|
|
242
|
+
async def generate_personalized_linkedin_copy(
|
|
243
|
+
linkedin_context: ContentGenerationContext,
|
|
244
|
+
variation_text: str,
|
|
245
|
+
tool_config: Optional[List[Dict]] = None,
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""
|
|
248
|
+
Generate a personalized LinkedIn connection message using a 2-step process:
|
|
249
|
+
|
|
250
|
+
Step 1: Use gpt-4.1 (larger context length) to process all context and generate
|
|
251
|
+
concise instructions for message generation.
|
|
252
|
+
|
|
253
|
+
Step 2: Use gpt-5.1-chat (better content generation) to generate the actual
|
|
254
|
+
LinkedIn message from the condensed instructions.
|
|
255
|
+
"""
|
|
256
|
+
cleaned_context = cleanup_linkedin_context(linkedin_context)
|
|
257
|
+
lead_data = cleaned_context.lead_info or Lead()
|
|
258
|
+
sender_data = cleaned_context.sender_info or SenderInfo()
|
|
259
|
+
|
|
260
|
+
# Step 1: Generate instructions using gpt-4.1
|
|
261
|
+
instructions = await generate_linkedin_instructions(
|
|
262
|
+
linkedin_context=linkedin_context,
|
|
263
|
+
variation_text=variation_text,
|
|
264
|
+
tool_config=tool_config,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Step 2: Generate message using gpt-5.1-chat
|
|
268
|
+
use_cache = linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
269
|
+
message_response = await generate_linkedin_from_instructions(
|
|
270
|
+
instructions=instructions,
|
|
271
|
+
tool_config=tool_config,
|
|
272
|
+
use_cache=use_cache,
|
|
273
|
+
)
|
|
176
274
|
|
|
177
275
|
# Wrap in MessageItem for consistency
|
|
178
276
|
response_item = MessageItem(
|
|
@@ -184,7 +282,7 @@ async def generate_personalized_linkedin_copy(
|
|
|
184
282
|
receiver_email=lead_data.email or "",
|
|
185
283
|
iso_datetime=datetime.utcnow().isoformat(),
|
|
186
284
|
subject="Hi",
|
|
187
|
-
body=
|
|
285
|
+
body=message_response.body
|
|
188
286
|
)
|
|
189
287
|
|
|
190
288
|
return response_item.model_dump()
|
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
11
|
import traceback
|
|
12
|
-
from typing import Any, Dict, List, Optional
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from fastapi import HTTPException
|
|
15
15
|
|
|
@@ -52,6 +52,14 @@ async def delete_vector_store(
|
|
|
52
52
|
client = create_openai_client(tool_config)
|
|
53
53
|
try:
|
|
54
54
|
client.vector_stores.delete(vector_store_id=vector_store_id)
|
|
55
|
+
except openai.NotFoundError:
|
|
56
|
+
logging.warning(f"Vector store not found during delete: {vector_store_id}")
|
|
57
|
+
except openai.APIStatusError as e:
|
|
58
|
+
if getattr(e, "status_code", None) == 404:
|
|
59
|
+
logging.warning(f"Vector store not found during delete: {vector_store_id}")
|
|
60
|
+
return
|
|
61
|
+
logging.error(f"Error deleting vector store {vector_store_id}: {e}")
|
|
62
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
55
63
|
except Exception as e:
|
|
56
64
|
logging.error(f"Error deleting vector store {vector_store_id}: {e}")
|
|
57
65
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -74,10 +82,11 @@ async def upload_file_openai_and_vector_store(
|
|
|
74
82
|
|
|
75
83
|
try:
|
|
76
84
|
if isinstance(file_path_or_bytes, str):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
with open(file_path_or_bytes, "rb") as f:
|
|
86
|
+
file_upload = client.files.create(
|
|
87
|
+
file=f,
|
|
88
|
+
purpose=purpose,
|
|
89
|
+
)
|
|
81
90
|
elif isinstance(file_path_or_bytes, bytes):
|
|
82
91
|
file_upload = client.files.create(
|
|
83
92
|
file=(file_name, file_path_or_bytes, mime_type),
|
|
@@ -108,10 +117,11 @@ async def upload_file_openai(
|
|
|
108
117
|
|
|
109
118
|
try:
|
|
110
119
|
if isinstance(file_path_or_bytes, str):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
with open(file_path_or_bytes, "rb") as f:
|
|
121
|
+
file_upload = client.files.create(
|
|
122
|
+
file=f,
|
|
123
|
+
purpose=purpose,
|
|
124
|
+
)
|
|
115
125
|
else:
|
|
116
126
|
file_upload = client.files.create(
|
|
117
127
|
file=(file_name, file_path_or_bytes, mime_type),
|
|
@@ -217,7 +227,7 @@ async def run_response_text(
|
|
|
217
227
|
max_tokens: int = 2048,
|
|
218
228
|
store: bool = True,
|
|
219
229
|
tool_config: Optional[List[Dict]] = None,
|
|
220
|
-
) ->
|
|
230
|
+
) -> Tuple[str, str]:
|
|
221
231
|
"""Plain text completion via the Responses API."""
|
|
222
232
|
client = create_openai_client(tool_config)
|
|
223
233
|
|
|
@@ -241,7 +251,7 @@ async def run_response_structured(
|
|
|
241
251
|
max_tokens: int = 1024,
|
|
242
252
|
store: bool = True,
|
|
243
253
|
tool_config: Optional[List[Dict]] = None,
|
|
244
|
-
) ->
|
|
254
|
+
) -> Tuple[Any, str]:
|
|
245
255
|
"""Structured JSON output via Responses API."""
|
|
246
256
|
client = create_openai_client(tool_config)
|
|
247
257
|
|
|
@@ -30,7 +30,7 @@ dhisana/utils/composite_tools.py,sha256=ZlwHCp7PXjYFUWUEeR_fTF0Z4Wg-4F6eBi1reE3F
|
|
|
30
30
|
dhisana/utils/dataframe_tools.py,sha256=R6eUXjwR5SG6_K87rWjj4T5PT2w6xvVF2EKBajIv-RE,9242
|
|
31
31
|
dhisana/utils/domain_parser.py,sha256=Kw5MPP06wK2azWQzuSiOE-DffOezLqDyF-L9JEBsMSU,1206
|
|
32
32
|
dhisana/utils/email_body_utils.py,sha256=rlCVjdBlqNnEiUberJGXGcrYY1GQOkW0-aB6AEpS3L4,2302
|
|
33
|
-
dhisana/utils/email_parse_helpers.py,sha256=
|
|
33
|
+
dhisana/utils/email_parse_helpers.py,sha256=rl72ggS-yoB-w3ZHW2sevKJulQ-_8iLdpVTH6QnKPcs,6789
|
|
34
34
|
dhisana/utils/email_provider.py,sha256=ukW_0nHcjTQmpnE9pdJci78LrZcsK1_0v6kcgc2ChPY,14573
|
|
35
35
|
dhisana/utils/enrich_lead_information.py,sha256=O0fV-8MlXFT_z5aXvmvXVT76AISN94GpvAOlq3q_Phw,39411
|
|
36
36
|
dhisana/utils/extract_email_content_for_llm.py,sha256=SQmMZ3YJtm3ZI44XiWEVAItcAwrsSSy1QzDne7LTu_Q,3713
|
|
@@ -39,11 +39,11 @@ dhisana/utils/field_validators.py,sha256=BZgNCpBG264aRqNUu_J67c6zfr15zlAaIw2XRy8
|
|
|
39
39
|
dhisana/utils/g2_tools.py,sha256=a4vmBYCBvLae5CdpOhMN1oNlvO8v9J1B5Sd8T5PzuU8,3346
|
|
40
40
|
dhisana/utils/generate_content.py,sha256=kkf-aPuA32BNgwk_j5N6unYHOZpO7zIfO6zP95XM9fA,2298
|
|
41
41
|
dhisana/utils/generate_custom_message.py,sha256=tQsryytoYKP5uF3bRENeZks1LvOMFCP6L1487P_r_hk,12072
|
|
42
|
-
dhisana/utils/generate_email.py,sha256=
|
|
43
|
-
dhisana/utils/generate_email_response.py,sha256=
|
|
42
|
+
dhisana/utils/generate_email.py,sha256=cEIr9HbGduk-l_0dNY2lCBABFBmmcOHaMyI7oy8ChWQ,16470
|
|
43
|
+
dhisana/utils/generate_email_response.py,sha256=LPsi2Orhb0W9SG8HDN2aA0w2pgW_c_HTDFAlw9R1cns,21391
|
|
44
44
|
dhisana/utils/generate_flow.py,sha256=QMn6bWo0nH0fBvy2Ebub1XfH5udnVAqsPsbIqCtQPXU,4728
|
|
45
45
|
dhisana/utils/generate_leads_salesnav.py,sha256=FG7q6GSm9IywZ9TgQnn5_N3QNfiI-Qk2gaO_3GS99nY,12236
|
|
46
|
-
dhisana/utils/generate_linkedin_connect_message.py,sha256=
|
|
46
|
+
dhisana/utils/generate_linkedin_connect_message.py,sha256=niKnL1YwtROVsPctaIQFHeSIvr4ewqEougUBAvMmoC0,13385
|
|
47
47
|
dhisana/utils/generate_linkedin_response_message.py,sha256=mWoSs5p2JSTIoFZFGm86x1kgs67J7dHPvGKZPzcdGdU,14569
|
|
48
48
|
dhisana/utils/generate_structured_output_internal.py,sha256=gJ6Bdwz2VexL505crMOgYYHOhmZo0MCghRoDkLP62WM,33654
|
|
49
49
|
dhisana/utils/google_custom_search.py,sha256=5rQ4uAF-hjFpd9ooJkd6CjRvSmhZHhqM0jfHItsbpzk,10071
|
|
@@ -57,7 +57,7 @@ dhisana/utils/lusha_tools.py,sha256=MdiWlxBBjSNpSKz8rhNOyLPtbeh-YWHgGiUq54vN_gM,
|
|
|
57
57
|
dhisana/utils/mailgun_tools.py,sha256=brOgfEx-ciqEdDkXEfzMBfXkG0kRWVscg76tQDXb_lk,5826
|
|
58
58
|
dhisana/utils/mailreach_tools.py,sha256=uJ_gIcg8qrj5-k3jnYYhpwLVnQncoA1swzr5Jfkc1JU,3864
|
|
59
59
|
dhisana/utils/microsoft365_tools.py,sha256=ClqBzTrJ2SZM5K9nsOFyyHRfV-d-6jlxXNpNONtgLlY,18596
|
|
60
|
-
dhisana/utils/openai_assistant_and_file_utils.py,sha256
|
|
60
|
+
dhisana/utils/openai_assistant_and_file_utils.py,sha256=snaawY7Y94q-XMzjK7UUylLy9fWlcmpWSOKcu1u04LE,9507
|
|
61
61
|
dhisana/utils/openai_helpers.py,sha256=ZK9S5-jcLCpiiD6XBLkCqYcNz-AGYmO9xh4e2H-FDLo,40155
|
|
62
62
|
dhisana/utils/openapi_spec_to_tools.py,sha256=oBLVq3WeDWvW9O02NCvY8bxQURQdKwHJHGcX8bC_b2I,1926
|
|
63
63
|
dhisana/utils/parse_linkedin_messages_txt.py,sha256=g3N_ac70mAEuDDQ7Ott6mkOaBwI3ZvcsJD3R9RlYwPQ,3320
|
|
@@ -95,8 +95,8 @@ dhisana/workflow/agent.py,sha256=esv7_i_XuMkV2j1nz_UlsHov_m6X5WZZiZm_tG4OBHU,565
|
|
|
95
95
|
dhisana/workflow/flow.py,sha256=xWE3qQbM7j2B3FH8XnY3zOL_QXX4LbTW4ArndnEYJE0,1638
|
|
96
96
|
dhisana/workflow/task.py,sha256=HlWz9mtrwLYByoSnePOemBUBrMEcj7KbgNjEE1oF5wo,1830
|
|
97
97
|
dhisana/workflow/test.py,sha256=E7lRnXK0PguTNzyasHytLzTJdkqIPxG5_4qk4hMEeKc,3399
|
|
98
|
-
dhisana-0.0.1.
|
|
99
|
-
dhisana-0.0.1.
|
|
100
|
-
dhisana-0.0.1.
|
|
101
|
-
dhisana-0.0.1.
|
|
102
|
-
dhisana-0.0.1.
|
|
98
|
+
dhisana-0.0.1.dev274.dist-info/METADATA,sha256=sPREfHEIvxxGk24-YL0zauwjU1n_pAUvaVmyLL_XKj0,1190
|
|
99
|
+
dhisana-0.0.1.dev274.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
100
|
+
dhisana-0.0.1.dev274.dist-info/entry_points.txt,sha256=jujxteZmNI9EkEaK-pOCoWuBujU8TCevdkfl9ZcKHek,49
|
|
101
|
+
dhisana-0.0.1.dev274.dist-info/top_level.txt,sha256=NETTHt6YifG_P7XtRHbQiXZlgSFk9Qh9aR-ng1XTf4s,8
|
|
102
|
+
dhisana-0.0.1.dev274.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|