dhisana 0.0.1.dev243__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/__init__.py +1 -0
- dhisana/cli/__init__.py +1 -0
- dhisana/cli/cli.py +20 -0
- dhisana/cli/datasets.py +27 -0
- dhisana/cli/models.py +26 -0
- dhisana/cli/predictions.py +20 -0
- dhisana/schemas/__init__.py +1 -0
- dhisana/schemas/common.py +399 -0
- dhisana/schemas/sales.py +965 -0
- dhisana/ui/__init__.py +1 -0
- dhisana/ui/components.py +472 -0
- dhisana/utils/__init__.py +1 -0
- dhisana/utils/add_mapping.py +352 -0
- dhisana/utils/agent_tools.py +51 -0
- dhisana/utils/apollo_tools.py +1597 -0
- dhisana/utils/assistant_tool_tag.py +4 -0
- dhisana/utils/built_with_api_tools.py +282 -0
- dhisana/utils/cache_output_tools.py +98 -0
- dhisana/utils/cache_output_tools_local.py +78 -0
- dhisana/utils/check_email_validity_tools.py +717 -0
- dhisana/utils/check_for_intent_signal.py +107 -0
- dhisana/utils/check_linkedin_url_validity.py +209 -0
- dhisana/utils/clay_tools.py +43 -0
- dhisana/utils/clean_properties.py +135 -0
- dhisana/utils/company_utils.py +60 -0
- dhisana/utils/compose_salesnav_query.py +259 -0
- dhisana/utils/compose_search_query.py +759 -0
- dhisana/utils/compose_three_step_workflow.py +234 -0
- dhisana/utils/composite_tools.py +137 -0
- dhisana/utils/dataframe_tools.py +237 -0
- dhisana/utils/domain_parser.py +45 -0
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_parse_helpers.py +132 -0
- dhisana/utils/email_provider.py +375 -0
- dhisana/utils/enrich_lead_information.py +933 -0
- dhisana/utils/extract_email_content_for_llm.py +101 -0
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +426 -0
- dhisana/utils/g2_tools.py +104 -0
- dhisana/utils/generate_content.py +41 -0
- dhisana/utils/generate_custom_message.py +271 -0
- dhisana/utils/generate_email.py +278 -0
- dhisana/utils/generate_email_response.py +465 -0
- dhisana/utils/generate_flow.py +102 -0
- dhisana/utils/generate_leads_salesnav.py +303 -0
- dhisana/utils/generate_linkedin_connect_message.py +224 -0
- dhisana/utils/generate_linkedin_response_message.py +317 -0
- dhisana/utils/generate_structured_output_internal.py +462 -0
- dhisana/utils/google_custom_search.py +267 -0
- dhisana/utils/google_oauth_tools.py +727 -0
- dhisana/utils/google_workspace_tools.py +1294 -0
- dhisana/utils/hubspot_clearbit.py +96 -0
- dhisana/utils/hubspot_crm_tools.py +2440 -0
- dhisana/utils/instantly_tools.py +149 -0
- dhisana/utils/linkedin_crawler.py +168 -0
- dhisana/utils/lusha_tools.py +333 -0
- dhisana/utils/mailgun_tools.py +156 -0
- dhisana/utils/mailreach_tools.py +123 -0
- dhisana/utils/microsoft365_tools.py +455 -0
- dhisana/utils/openai_assistant_and_file_utils.py +267 -0
- dhisana/utils/openai_helpers.py +977 -0
- dhisana/utils/openapi_spec_to_tools.py +45 -0
- dhisana/utils/openapi_tool/__init__.py +1 -0
- dhisana/utils/openapi_tool/api_models.py +633 -0
- dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
- dhisana/utils/openapi_tool/openapi_tool.py +319 -0
- dhisana/utils/parse_linkedin_messages_txt.py +100 -0
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +1226 -0
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/python_function_to_tools.py +83 -0
- dhisana/utils/research_lead.py +176 -0
- dhisana/utils/sales_navigator_crawler.py +1103 -0
- dhisana/utils/salesforce_crm_tools.py +477 -0
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +162 -0
- dhisana/utils/serarch_router_local_business.py +75 -0
- dhisana/utils/serpapi_additional_tools.py +290 -0
- dhisana/utils/serpapi_google_jobs.py +117 -0
- dhisana/utils/serpapi_google_search.py +188 -0
- dhisana/utils/serpapi_local_business_search.py +129 -0
- dhisana/utils/serpapi_search_tools.py +852 -0
- dhisana/utils/serperdev_google_jobs.py +125 -0
- dhisana/utils/serperdev_local_business.py +154 -0
- dhisana/utils/serperdev_search.py +233 -0
- dhisana/utils/smtp_email_tools.py +582 -0
- dhisana/utils/test_connect.py +2087 -0
- dhisana/utils/trasform_json.py +173 -0
- dhisana/utils/web_download_parse_tools.py +189 -0
- dhisana/utils/workflow_code_model.py +5 -0
- dhisana/utils/zoominfo_tools.py +357 -0
- dhisana/workflow/__init__.py +1 -0
- dhisana/workflow/agent.py +18 -0
- dhisana/workflow/flow.py +44 -0
- dhisana/workflow/task.py +43 -0
- dhisana/workflow/test.py +90 -0
- dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
- dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
- dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
- dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
- dhisana-0.0.1.dev243.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from dhisana.schemas.sales import (
|
|
6
|
+
ContentGenerationContext,
|
|
7
|
+
Lead,
|
|
8
|
+
MessageItem,
|
|
9
|
+
MessageResponse,
|
|
10
|
+
MessageGenerationInstructions,
|
|
11
|
+
SenderInfo
|
|
12
|
+
)
|
|
13
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
14
|
+
from dhisana.utils.generate_structured_output_internal import (
|
|
15
|
+
get_structured_output_with_assistant_and_vector_store,
|
|
16
|
+
get_structured_output_internal
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------------------
|
|
20
|
+
# MODEL
|
|
21
|
+
# ---------------------------------------------------------------------------------------
|
|
22
|
+
class InboundEmailTriageResponse(BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
Model representing the structured response for an inbound email triage.
|
|
25
|
+
- triage_status: "AUTOMATIC" or "END_CONVERSATION"
|
|
26
|
+
- triage_reason: Reason text if triage_status == "END_CONVERSATION"
|
|
27
|
+
- response_action_to_take: The recommended next action (e.g. SCHEDULE_MEETING, SEND_REPLY, etc.)
|
|
28
|
+
- response_message: The actual body of the email response to be sent or used for approval.
|
|
29
|
+
"""
|
|
30
|
+
triage_status: str # "AUTOMATIC" or "END_CONVERSATION"
|
|
31
|
+
triage_reason: Optional[str]
|
|
32
|
+
response_action_to_take: str
|
|
33
|
+
response_message: Optional[str]
|
|
34
|
+
meeting_offer_sent: Optional[bool]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------------------
|
|
38
|
+
# HELPER FUNCTION TO CLEAN CONTEXT
|
|
39
|
+
# ---------------------------------------------------------------------------------------
|
|
40
|
+
def cleanup_reply_campaign_context(campaign_context: ContentGenerationContext) -> ContentGenerationContext:
|
|
41
|
+
clone_context = campaign_context.copy(deep=True)
|
|
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
|
|
48
|
+
return clone_context
|
|
49
|
+
|
|
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
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------------------
|
|
217
|
+
# CORE FUNCTION TO GENERATE SINGLE RESPONSE (ONE VARIATION)
|
|
218
|
+
# ---------------------------------------------------------------------------------------
|
|
219
|
+
async def generate_inbound_email_response_copy(
|
|
220
|
+
campaign_context: ContentGenerationContext,
|
|
221
|
+
variation: str,
|
|
222
|
+
tool_config: Optional[List[Dict]] = None
|
|
223
|
+
) -> Dict[str, Any]:
|
|
224
|
+
"""
|
|
225
|
+
Generate a single inbound email triage response based on the provided context and
|
|
226
|
+
a specific variation prompt.
|
|
227
|
+
"""
|
|
228
|
+
allowed_actions = [
|
|
229
|
+
"SCHEDULE_MEETING",
|
|
230
|
+
"SEND_REPLY",
|
|
231
|
+
"UNSUBSCRIBE",
|
|
232
|
+
"OOF_MESSAGE",
|
|
233
|
+
"NOT_INTERESTED",
|
|
234
|
+
"NEED_MORE_INFO",
|
|
235
|
+
"FORWARD_TO_OTHER_USER",
|
|
236
|
+
"NO_MORE_IN_ORGANIZATION",
|
|
237
|
+
"OBJECTION_RAISED",
|
|
238
|
+
"END_CONVERSATION",
|
|
239
|
+
]
|
|
240
|
+
current_date_iso = datetime.datetime.now().isoformat()
|
|
241
|
+
cleaned_context = cleanup_reply_campaign_context(campaign_context)
|
|
242
|
+
if not cleaned_context.current_conversation_context.current_email_thread:
|
|
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()
|
|
247
|
+
|
|
248
|
+
prompt = f"""
|
|
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.
|
|
252
|
+
|
|
253
|
+
Follow these instructions to generate the reply:
|
|
254
|
+
{variation}
|
|
255
|
+
|
|
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 []}
|
|
259
|
+
|
|
260
|
+
2) Lead Information:
|
|
261
|
+
{lead_data.dict()}
|
|
262
|
+
|
|
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
|
+
|
|
269
|
+
|
|
270
|
+
3. Campaign-specific triage guidelines (user overrides always win):
|
|
271
|
+
{cleaned_context.campaign_context.email_triage_guidelines}
|
|
272
|
+
|
|
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**.
|
|
280
|
+
|
|
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.
|
|
342
|
+
Use conversational name when using lead first name.
|
|
343
|
+
Do not use special characters or spaces when using lead’s first name.
|
|
344
|
+
In the subject or body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
|
|
345
|
+
The body and subject should be in plain text.
|
|
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.
|
|
357
|
+
|
|
358
|
+
Required JSON output
|
|
359
|
+
--------------------
|
|
360
|
+
{{
|
|
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>"
|
|
365
|
+
}}
|
|
366
|
+
|
|
367
|
+
Current date is: {current_date_iso}.
|
|
368
|
+
-----------------------------------------------------------------
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# If there's a vector store ID, use that approach
|
|
373
|
+
if (
|
|
374
|
+
cleaned_context.external_known_data
|
|
375
|
+
and cleaned_context.external_known_data.external_openai_vector_store_id
|
|
376
|
+
):
|
|
377
|
+
initial_response, status = await get_structured_output_with_assistant_and_vector_store(
|
|
378
|
+
prompt=prompt,
|
|
379
|
+
response_format=InboundEmailTriageResponse,
|
|
380
|
+
model="gpt-5.1-chat",
|
|
381
|
+
vector_store_id=cleaned_context.external_known_data.external_openai_vector_store_id,
|
|
382
|
+
tool_config=tool_config
|
|
383
|
+
)
|
|
384
|
+
else:
|
|
385
|
+
initial_response, status = await get_structured_output_internal(
|
|
386
|
+
prompt=prompt,
|
|
387
|
+
response_format=InboundEmailTriageResponse,
|
|
388
|
+
model="gpt-5.1-chat",
|
|
389
|
+
tool_config=tool_config
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if status != "SUCCESS":
|
|
393
|
+
raise Exception("Error in generating the inbound email triage response.")
|
|
394
|
+
|
|
395
|
+
response_item = MessageItem(
|
|
396
|
+
message_id="", # or generate one if appropriate
|
|
397
|
+
thread_id="",
|
|
398
|
+
sender_name=campaign_context.sender_info.sender_full_name or "",
|
|
399
|
+
sender_email=campaign_context.sender_info.sender_email or "",
|
|
400
|
+
receiver_name=campaign_context.lead_info.full_name or "",
|
|
401
|
+
receiver_email=campaign_context.lead_info.email or "",
|
|
402
|
+
iso_datetime=datetime.datetime.utcnow().isoformat(),
|
|
403
|
+
subject="", # or set some triage subject if needed
|
|
404
|
+
body=initial_response.response_message
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Build a MessageResponse that includes triage metadata plus your message item
|
|
408
|
+
response_message = MessageResponse(
|
|
409
|
+
triage_status=initial_response.triage_status,
|
|
410
|
+
triage_reason=initial_response.triage_reason,
|
|
411
|
+
message_item=response_item,
|
|
412
|
+
response_action_to_take=initial_response.response_action_to_take
|
|
413
|
+
)
|
|
414
|
+
print(response_message.model_dump())
|
|
415
|
+
return response_message.model_dump()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ---------------------------------------------------------------------------------------
|
|
419
|
+
# MAIN ENTRY POINT - GENERATE MULTIPLE VARIATIONS
|
|
420
|
+
# ---------------------------------------------------------------------------------------
|
|
421
|
+
@assistant_tool
|
|
422
|
+
async def generate_inbound_email_response_variations(
|
|
423
|
+
campaign_context: ContentGenerationContext,
|
|
424
|
+
number_of_variations: int = 3,
|
|
425
|
+
tool_config: Optional[List[Dict]] = None
|
|
426
|
+
) -> List[Dict[str, Any]]:
|
|
427
|
+
"""
|
|
428
|
+
Generate multiple inbound email triage responses, each with a different 'variation'
|
|
429
|
+
unless user instructions are provided. Returns a list of dictionaries conforming
|
|
430
|
+
to InboundEmailTriageResponse.
|
|
431
|
+
"""
|
|
432
|
+
# Default variation frameworks
|
|
433
|
+
variation_specs = [
|
|
434
|
+
"Short and friendly response focusing on quick resolution.",
|
|
435
|
+
"More formal tone referencing user’s key points in the thread.",
|
|
436
|
+
"Meeting-based approach if user needs further discussion or demo.",
|
|
437
|
+
"Lean approach focusing on clarifying user’s questions or concerns.",
|
|
438
|
+
"Solution-driven approach referencing a relevant product or case study."
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
# Check if the user provided custom instructions
|
|
442
|
+
message_instructions = campaign_context.message_instructions or MessageGenerationInstructions()
|
|
443
|
+
user_instructions = (message_instructions.instructions_to_generate_message or "").strip()
|
|
444
|
+
user_instructions_exist = bool(user_instructions)
|
|
445
|
+
|
|
446
|
+
all_variations = []
|
|
447
|
+
for i in range(number_of_variations):
|
|
448
|
+
# If user instructions exist, use them for every variation
|
|
449
|
+
if user_instructions_exist:
|
|
450
|
+
variation_text = user_instructions
|
|
451
|
+
else:
|
|
452
|
+
# Otherwise, fallback to variation_specs
|
|
453
|
+
variation_text = variation_specs[i % len(variation_specs)]
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
triaged_response = await generate_inbound_email_response_copy(
|
|
457
|
+
campaign_context=campaign_context,
|
|
458
|
+
variation=variation_text,
|
|
459
|
+
tool_config=tool_config
|
|
460
|
+
)
|
|
461
|
+
all_variations.append(triaged_response)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
raise e
|
|
464
|
+
|
|
465
|
+
return all_variations
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from dhisana.utils.dataframe_tools import get_structured_output
|
|
3
|
+
|
|
4
|
+
# Generate a workflow spec give the description in Input.
|
|
5
|
+
# TODO needs more work to generate the workflow spec with right tools and functions.
|
|
6
|
+
# Pydantic models for structured data
|
|
7
|
+
class Workflow(BaseModel):
|
|
8
|
+
workflow_json: str
|
|
9
|
+
|
|
10
|
+
async def generate_workflow_json(instructions, avilable_functions):
|
|
11
|
+
|
|
12
|
+
prompt = f"""Give user input in plain text generate a worklfow json that can use the instructions to automation workflow.
|
|
13
|
+
--------------------------------------
|
|
14
|
+
Specification of workflow:\n
|
|
15
|
+
{{
|
|
16
|
+
"id": "<workflow_id>",
|
|
17
|
+
"name": "<Name of the workflow.>",
|
|
18
|
+
"description": "<Description of the workflow>",
|
|
19
|
+
"version": "1.0",
|
|
20
|
+
"dependencies": ["<dependent task_id> Empty for the first task"],
|
|
21
|
+
"tasks": [
|
|
22
|
+
{{
|
|
23
|
+
"id": "<task_id_1>",
|
|
24
|
+
"name": "<name of the task_id_1>",
|
|
25
|
+
"description": "<description of the task_id_1>",
|
|
26
|
+
"type": "task",
|
|
27
|
+
"dependencies": [],
|
|
28
|
+
"inputs": {{
|
|
29
|
+
"<input_key_1>": {{
|
|
30
|
+
"type": "GenericList",
|
|
31
|
+
"format": "list",
|
|
32
|
+
"source": {{
|
|
33
|
+
"type": "task_output",
|
|
34
|
+
"task_id": "<task_id output that is input is for>. Keep this as <initial_input> for first task",
|
|
35
|
+
"output_key": "<output key> keep this as initial_input_list for first task"
|
|
36
|
+
}}
|
|
37
|
+
}}
|
|
38
|
+
}},
|
|
39
|
+
"operation": {{
|
|
40
|
+
"type": "python_callable",
|
|
41
|
+
"function": "The python function name to invoke",
|
|
42
|
+
"args": [
|
|
43
|
+
"<arguments to pass to python function eg input_key_1>"
|
|
44
|
+
]
|
|
45
|
+
}},
|
|
46
|
+
"outputs": {{
|
|
47
|
+
"<output_key_1>": {{
|
|
48
|
+
"type": "GenericList",
|
|
49
|
+
"format": "list",
|
|
50
|
+
"deduplication_properties": ["<de-duplication property name>"],
|
|
51
|
+
"required_properties": ["<required property name>"]
|
|
52
|
+
}}
|
|
53
|
+
}}
|
|
54
|
+
}},
|
|
55
|
+
{{
|
|
56
|
+
"id": "<task_id_2>",
|
|
57
|
+
"name": "<name of the task_id_2>",
|
|
58
|
+
"description": "<description of the task_id_2>",
|
|
59
|
+
"type": "task",
|
|
60
|
+
"dependencies": ["task_id_1"],
|
|
61
|
+
"inputs": {{
|
|
62
|
+
"<input_key_2>": {{
|
|
63
|
+
"type": "GenericList",
|
|
64
|
+
"format": "list",
|
|
65
|
+
"source": {{
|
|
66
|
+
"type": "task_output",
|
|
67
|
+
"task_id" : "task_id_1",
|
|
68
|
+
"output_key": "output_key_1"
|
|
69
|
+
}}
|
|
70
|
+
}}
|
|
71
|
+
}},
|
|
72
|
+
"operation": {{
|
|
73
|
+
"type": "python_callable",
|
|
74
|
+
"function": "The python function to invoke",
|
|
75
|
+
"args": [
|
|
76
|
+
"arguments to pass to python function eg input_key_2"
|
|
77
|
+
]
|
|
78
|
+
}},
|
|
79
|
+
"outputs": {{
|
|
80
|
+
"<output_key_2>": {{
|
|
81
|
+
"type": "GenericList",
|
|
82
|
+
"format": "list",
|
|
83
|
+
"deduplication_properties": ["<de-duplication property name>"],
|
|
84
|
+
"required_properties": ["<required property name>"]
|
|
85
|
+
}}
|
|
86
|
+
}}
|
|
87
|
+
]
|
|
88
|
+
}}
|
|
89
|
+
-------------------------------------
|
|
90
|
+
You have the fullowing python function to invoke:
|
|
91
|
+
{avilable_functions}
|
|
92
|
+
--------------
|
|
93
|
+
User instructions to convert to workflow:\n
|
|
94
|
+
{instructions}
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
extract_content, status = await get_structured_output(prompt, Workflow)
|
|
98
|
+
|
|
99
|
+
if status == "SUCCESS":
|
|
100
|
+
return extract_content.workflow_json
|
|
101
|
+
else:
|
|
102
|
+
return ""
|