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,278 @@
|
|
|
1
|
+
# Import necessary modules
|
|
2
|
+
import html as html_lib
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from dhisana.schemas.sales import CampaignContext, ContentGenerationContext, ConversationContext, Lead, MessageGenerationInstructions, MessageItem, SenderInfo
|
|
8
|
+
from dhisana.utils.generate_structured_output_internal import (
|
|
9
|
+
get_structured_output_internal,
|
|
10
|
+
get_structured_output_with_assistant_and_vector_store
|
|
11
|
+
)
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
# -----------------------------------------------------------------------------
|
|
16
|
+
# Email Copy schema
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
class EmailCopy(BaseModel):
|
|
19
|
+
subject: str
|
|
20
|
+
body: str
|
|
21
|
+
body_html: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(extra="forbid")
|
|
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
|
+
|
|
37
|
+
# -----------------------------------------------------------------------------
|
|
38
|
+
# Utility to Clean Up Context (if needed)
|
|
39
|
+
# -----------------------------------------------------------------------------
|
|
40
|
+
def cleanup_email_context(email_context: ContentGenerationContext) -> ContentGenerationContext:
|
|
41
|
+
"""
|
|
42
|
+
Return a copy of ContentGenerationContext without sensitive or irrelevant fields.
|
|
43
|
+
Modify or remove fields as necessary for your project.
|
|
44
|
+
"""
|
|
45
|
+
clone_context = email_context.copy(deep=True)
|
|
46
|
+
# For demonstration, no sensitive fields in new classes by default.
|
|
47
|
+
# Adjust if you want to remove certain fields (like 'sender_bio', etc.).
|
|
48
|
+
if clone_context.external_known_data:
|
|
49
|
+
clone_context.external_known_data.external_openai_vector_store_id = None
|
|
50
|
+
return clone_context
|
|
51
|
+
|
|
52
|
+
# -----------------------------------------------------------------------------
|
|
53
|
+
# Known Framework Variations (fallback if user instructions are not provided)
|
|
54
|
+
# -----------------------------------------------------------------------------
|
|
55
|
+
FRAMEWORK_VARIATIONS = [
|
|
56
|
+
"Use PAS (Problem, Agitate, Solve) framework to write up the email.",
|
|
57
|
+
"Use VETO framework (Value, Evidence, Tie, Offer). Highlight how our product addresses the company's current goals.",
|
|
58
|
+
"Use AIDA framework (Attention, Interest, Desire, Action) to compose the email.",
|
|
59
|
+
"Use SPIN (Situation, Problem, Implication, Need-Payoff) framework to write the email.",
|
|
60
|
+
"Use BANT (Budget, Authority, Need, Timeline) framework to write the email.",
|
|
61
|
+
"Use P-S-B (Pain, Solution, Benefit) with a 3-Bullet Approach. Keep it concise.",
|
|
62
|
+
"Use Hook, Insight, Offer framework to write the email."
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# -----------------------------------------------------------------------------
|
|
66
|
+
# Core function to generate an email copy
|
|
67
|
+
# -----------------------------------------------------------------------------
|
|
68
|
+
async def generate_personalized_email_copy(
|
|
69
|
+
email_context: ContentGenerationContext,
|
|
70
|
+
message_instructions: MessageGenerationInstructions,
|
|
71
|
+
variation_text: str,
|
|
72
|
+
tool_config: Optional[List[Dict]] = None,
|
|
73
|
+
) -> dict:
|
|
74
|
+
"""
|
|
75
|
+
Generate a personalized email using the provided context and instructions.
|
|
76
|
+
|
|
77
|
+
Steps:
|
|
78
|
+
1. Build a prompt referencing 6 main info:
|
|
79
|
+
(a) Lead Info
|
|
80
|
+
(b) Sender Info
|
|
81
|
+
(c) Campaign Info
|
|
82
|
+
(d) Messaging Instructions
|
|
83
|
+
(e) Additional Data (vector store) if any
|
|
84
|
+
(f) Current Conversation Context
|
|
85
|
+
2. Generate an initial draft with or without vector store usage.
|
|
86
|
+
3. Optionally refine if a vector store was used and user instructions were not provided.
|
|
87
|
+
4. Return the final subject & body.
|
|
88
|
+
"""
|
|
89
|
+
cleaned_context = cleanup_email_context(email_context)
|
|
90
|
+
|
|
91
|
+
# Check if user provided custom instructions
|
|
92
|
+
user_custom_instructions = (message_instructions.instructions_to_generate_message or "").strip()
|
|
93
|
+
use_custom_instructions = bool(user_custom_instructions)
|
|
94
|
+
|
|
95
|
+
# Decide final instructions: user-provided or fallback variation
|
|
96
|
+
if use_custom_instructions:
|
|
97
|
+
selected_instructions = user_custom_instructions
|
|
98
|
+
else:
|
|
99
|
+
selected_instructions = variation_text
|
|
100
|
+
|
|
101
|
+
# Pull out fields or fallback to empty if None
|
|
102
|
+
lead_data = cleaned_context.lead_info or Lead()
|
|
103
|
+
sender_data = cleaned_context.sender_info or SenderInfo()
|
|
104
|
+
campaign_data = cleaned_context.campaign_context or CampaignContext()
|
|
105
|
+
conversation_data = cleaned_context.current_conversation_context or ConversationContext()
|
|
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
|
+
|
|
143
|
+
# Construct the consolidated prompt
|
|
144
|
+
initial_prompt = f"""
|
|
145
|
+
Hi AI Assistant,
|
|
146
|
+
|
|
147
|
+
Below is the context in 6 main sections. Use it to craft a concise, professional email:
|
|
148
|
+
|
|
149
|
+
1) Lead Information:
|
|
150
|
+
{lead_data.dict()}
|
|
151
|
+
|
|
152
|
+
2) Sender Information:
|
|
153
|
+
Full Name: {sender_data.sender_full_name or ''}
|
|
154
|
+
First Name: {sender_data.sender_first_name or ''}
|
|
155
|
+
Last Name: {sender_data.sender_last_name or ''}
|
|
156
|
+
Bio: {sender_data.sender_bio or ''}
|
|
157
|
+
|
|
158
|
+
3) Campaign Information:
|
|
159
|
+
Product Name: {campaign_data.product_name or ''}
|
|
160
|
+
Value Proposition: {campaign_data.value_prop or ''}
|
|
161
|
+
Call To Action: {campaign_data.call_to_action or ''}
|
|
162
|
+
Pain Points: {campaign_data.pain_points or []}
|
|
163
|
+
Proof Points: {campaign_data.proof_points or []}
|
|
164
|
+
Triage Guidelines (Email): {campaign_data.email_triage_guidelines or ''}
|
|
165
|
+
Triage Guidelines (LinkedIn): {campaign_data.linkedin_triage_guidelines or ''}
|
|
166
|
+
|
|
167
|
+
4) Messaging Instructions (template/framework):
|
|
168
|
+
{selected_instructions}{html_note}
|
|
169
|
+
|
|
170
|
+
5) External Data / Vector Store:
|
|
171
|
+
(I will be provided with file_search tool if present.)
|
|
172
|
+
|
|
173
|
+
6) Current Conversation Context:
|
|
174
|
+
Email Thread: {conversation_data.current_email_thread or ''}
|
|
175
|
+
LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
|
|
176
|
+
|
|
177
|
+
{important_requirements}
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
# Check if a vector store is available
|
|
181
|
+
vector_store_id = (email_context.external_known_data.external_openai_vector_store_id
|
|
182
|
+
if email_context.external_known_data else None)
|
|
183
|
+
|
|
184
|
+
initial_response = None
|
|
185
|
+
initial_status = ""
|
|
186
|
+
|
|
187
|
+
# Generate initial draft
|
|
188
|
+
if vector_store_id:
|
|
189
|
+
initial_response, initial_status = await get_structured_output_with_assistant_and_vector_store(
|
|
190
|
+
prompt=initial_prompt,
|
|
191
|
+
response_format=EmailCopy,
|
|
192
|
+
vector_store_id=vector_store_id,
|
|
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
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
# Otherwise, generate the initial draft internally
|
|
199
|
+
initial_response, initial_status = await get_structured_output_internal(
|
|
200
|
+
prompt=initial_prompt,
|
|
201
|
+
response_format=EmailCopy,
|
|
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
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if initial_status != "SUCCESS":
|
|
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
|
+
|
|
214
|
+
response_item = MessageItem(
|
|
215
|
+
message_id="", # or some real ID if you have it
|
|
216
|
+
thread_id="",
|
|
217
|
+
sender_name=email_context.sender_info.sender_full_name or "",
|
|
218
|
+
sender_email=email_context.sender_info.sender_email or "",
|
|
219
|
+
receiver_name=email_context.lead_info.full_name or "",
|
|
220
|
+
receiver_email=email_context.lead_info.email or "",
|
|
221
|
+
iso_datetime=datetime.utcnow().isoformat(),
|
|
222
|
+
subject=initial_response.subject,
|
|
223
|
+
body=plain_body,
|
|
224
|
+
html_body=html_body if getattr(message_instructions, "allow_html", False) else None,
|
|
225
|
+
)
|
|
226
|
+
return response_item.model_dump()
|
|
227
|
+
|
|
228
|
+
# -----------------------------------------------------------------------------
|
|
229
|
+
# Primary function to generate multiple variations
|
|
230
|
+
# -----------------------------------------------------------------------------
|
|
231
|
+
async def generate_personalized_email(
|
|
232
|
+
generation_context: ContentGenerationContext,
|
|
233
|
+
number_of_variations: int = 3,
|
|
234
|
+
tool_config: Optional[List[Dict]] = None
|
|
235
|
+
) -> List[dict]:
|
|
236
|
+
"""
|
|
237
|
+
Generates multiple personalized email variations based on the given context and instructions.
|
|
238
|
+
|
|
239
|
+
Parameters:
|
|
240
|
+
- email_context: The consolidated context for email generation.
|
|
241
|
+
- message_instructions: User-supplied instructions or templates for generating the message.
|
|
242
|
+
- number_of_variations: How many email variations to produce.
|
|
243
|
+
- tool_config: Optional tool configuration for the LLM calls.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
A list of dictionaries, each containing:
|
|
247
|
+
{
|
|
248
|
+
"subject": "string",
|
|
249
|
+
"body": "string"
|
|
250
|
+
}
|
|
251
|
+
"""
|
|
252
|
+
email_variations = []
|
|
253
|
+
message_instructions = generation_context.message_instructions
|
|
254
|
+
user_instructions_exist = bool(
|
|
255
|
+
(message_instructions.instructions_to_generate_message or "").strip()
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
for i in range(number_of_variations):
|
|
259
|
+
try:
|
|
260
|
+
# If user provided instructions, use them for each variation
|
|
261
|
+
# (skip the internal frameworks).
|
|
262
|
+
if user_instructions_exist:
|
|
263
|
+
variation_text = message_instructions.instructions_to_generate_message or ""
|
|
264
|
+
else:
|
|
265
|
+
# Otherwise, pick from known frameworks (circular indexing)
|
|
266
|
+
variation_text = FRAMEWORK_VARIATIONS[i % len(FRAMEWORK_VARIATIONS)]
|
|
267
|
+
|
|
268
|
+
email_copy = await generate_personalized_email_copy(
|
|
269
|
+
email_context=generation_context,
|
|
270
|
+
message_instructions=message_instructions,
|
|
271
|
+
variation_text=variation_text,
|
|
272
|
+
tool_config=tool_config
|
|
273
|
+
)
|
|
274
|
+
email_variations.append(email_copy)
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
raise e
|
|
278
|
+
return email_variations
|