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,303 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from dhisana.schemas.sales import SmartList
|
|
6
|
+
from dhisana.utils.generate_structured_output_internal import get_structured_output_internal
|
|
7
|
+
from dhisana.utils.workflow_code_model import WorkflowPythonCode
|
|
8
|
+
|
|
9
|
+
# Initialize logger
|
|
10
|
+
logging.basicConfig(level=logging.INFO)
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def generate_leads_code(
|
|
15
|
+
filters: str,
|
|
16
|
+
example_url: str,
|
|
17
|
+
max_pages: int = 1,
|
|
18
|
+
tool_config: Optional[List[Dict[str, Any]]] = None
|
|
19
|
+
) -> Tuple[Dict[str, Any], str]:
|
|
20
|
+
"""
|
|
21
|
+
Generate a workflow code (Python code) from an English description, specifically
|
|
22
|
+
to create a list from Sales Navigator.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A tuple of:
|
|
26
|
+
- A dict containing {'workflow_python_code': '...'}
|
|
27
|
+
- A string representing status, e.g., 'SUCCESS' or 'ERROR'.
|
|
28
|
+
"""
|
|
29
|
+
system_message = (
|
|
30
|
+
"You are a helpful AI assistant who is an expert python coder. I want you to convert "
|
|
31
|
+
"an English description of a requirement provided by user into an executable Python "
|
|
32
|
+
"function called create_list_from_sales_navigator. Your output must be valid Python code. "
|
|
33
|
+
"The provided example shows how the structure looks like and what methods you can use. "
|
|
34
|
+
"Make sure the imports are present within the function definition itself. Make sure the "
|
|
35
|
+
"logging library is imported and logger defined within the function. "
|
|
36
|
+
"Use the output function signature:\n"
|
|
37
|
+
" async def create_list_from_sales_navigator(filters, example_url, max_pages, tool_config)\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
example_of_workflow_code = (
|
|
41
|
+
'''
|
|
42
|
+
async def create_list_from_sales_navigator(filters, example_url, max_pages, tool_config):
|
|
43
|
+
"""
|
|
44
|
+
Example workflow demonstrating how to create a list from Sales Navigator.
|
|
45
|
+
Returns ("SUCCESS", unique_leads) or ("ERROR", []).
|
|
46
|
+
"""
|
|
47
|
+
# Make sure required imports are there within the function definition itself.
|
|
48
|
+
import asyncio
|
|
49
|
+
import logging
|
|
50
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
51
|
+
from dhisana.utils.agent_task import execute_task
|
|
52
|
+
from dhisana.utils.compose_salesnav_query import generate_salesnav_people_search_url
|
|
53
|
+
|
|
54
|
+
# Make sure the logger is present.
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
logging.basicConfig(level=logging.INFO)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
logger.info("Starting custom_workflow execution")
|
|
60
|
+
|
|
61
|
+
if not example_url:
|
|
62
|
+
# Generate a Sales Navigator URL
|
|
63
|
+
result = await generate_salesnav_people_search_url(
|
|
64
|
+
english_description=filters,
|
|
65
|
+
tool_config=tool_config
|
|
66
|
+
)
|
|
67
|
+
if not result:
|
|
68
|
+
logger.error("generate_salesnav_people_search_url returned no result")
|
|
69
|
+
return "ERROR", []
|
|
70
|
+
|
|
71
|
+
salesnav_url = result.get('linkedin_salenav_url_with_query_parameters', None)
|
|
72
|
+
logger.info("Sales Navigator URL obtained: %s", salesnav_url)
|
|
73
|
+
|
|
74
|
+
if not salesnav_url:
|
|
75
|
+
logger.warning("No valid URL returned; cannot proceed.")
|
|
76
|
+
return "ERROR", []
|
|
77
|
+
else:
|
|
78
|
+
salesnav_url = example_url
|
|
79
|
+
|
|
80
|
+
# Extract leads from the URL
|
|
81
|
+
command_args = {
|
|
82
|
+
"salesnav_url_list_leads": salesnav_url,
|
|
83
|
+
"max_pages": max_pages,
|
|
84
|
+
"enrich_detailed_lead_information": False,
|
|
85
|
+
"enrich_detailed_company_information": False,
|
|
86
|
+
}
|
|
87
|
+
try:
|
|
88
|
+
extraction_result = await execute_task(
|
|
89
|
+
"extract_leads_information",
|
|
90
|
+
command_args,
|
|
91
|
+
tool_config=tool_config
|
|
92
|
+
)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
logger.exception("Error while extracting leads: %s", exc)
|
|
95
|
+
return "ERROR", []
|
|
96
|
+
|
|
97
|
+
leads = extraction_result.get('data', [])
|
|
98
|
+
logger.info("Number of leads extracted: %d", len(leads))
|
|
99
|
+
|
|
100
|
+
if not leads:
|
|
101
|
+
return "SUCCESS", []
|
|
102
|
+
|
|
103
|
+
# Deduplicate leads
|
|
104
|
+
unique_leads = {}
|
|
105
|
+
for lead in leads:
|
|
106
|
+
lead_url = lead.get("user_linkedin_salesnav_url")
|
|
107
|
+
if lead_url:
|
|
108
|
+
unique_leads[lead_url] = lead
|
|
109
|
+
|
|
110
|
+
deduped_leads = list(unique_leads.values())
|
|
111
|
+
logger.info("Unique leads after deduplication: %d", len(deduped_leads))
|
|
112
|
+
|
|
113
|
+
logger.info("Completed custom_workflow with success.")
|
|
114
|
+
return "SUCCESS", deduped_leads
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.exception("Exception in custom_workflow: %s", e)
|
|
118
|
+
return "ERROR", []
|
|
119
|
+
'''
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Short note if user has or hasn't provided a Sales Navigator URL
|
|
123
|
+
user_provided_url = "Keep user_input_salesnav_url variable as empty."
|
|
124
|
+
if "linkedin.com/sales/search/" in filters:
|
|
125
|
+
user_provided_url = "Use user_input_salesnav_url as provided by user"
|
|
126
|
+
|
|
127
|
+
# Explanation of possible filters
|
|
128
|
+
supported_filters_explanation = """
|
|
129
|
+
Sales Navigator filters include (not exhaustive):
|
|
130
|
+
- PAST_COLLEAGUE
|
|
131
|
+
- CURRENT_TITLE
|
|
132
|
+
- PAST_TITLE
|
|
133
|
+
- CURRENT_COMPANY
|
|
134
|
+
- PAST_COMPANY
|
|
135
|
+
- GEOGRAPHY (REGION)
|
|
136
|
+
- INDUSTRY
|
|
137
|
+
- SCHOOL
|
|
138
|
+
- CONNECTION (RELATIONSHIP)
|
|
139
|
+
- CONNECTIONS_OF
|
|
140
|
+
- GROUP
|
|
141
|
+
- COMPANY_HEADCOUNT
|
|
142
|
+
- COMPANY_TYPE
|
|
143
|
+
- SENIORITY_LEVEL
|
|
144
|
+
- YEARS_IN_POSITION
|
|
145
|
+
- YEARS_IN_COMPANY
|
|
146
|
+
- FOLLOWING_YOUR_COMPANY (FOLLOWS_YOUR_COMPANY)
|
|
147
|
+
- VIEWED_YOUR_PROFILE
|
|
148
|
+
- CHANGED_JOBS (RECENTLY_CHANGED_JOBS)
|
|
149
|
+
- POSTED_ON_LINKEDIN
|
|
150
|
+
- MENTIONED_IN_NEWS
|
|
151
|
+
- TECHNOLOGIES_USED
|
|
152
|
+
- ANNUAL_REVENUE
|
|
153
|
+
- LEAD_INTERACTIONS (Viewed Profile, Messaged)
|
|
154
|
+
- SAVED_LEADS_AND_ACCOUNTS
|
|
155
|
+
- WITH_SHARED_EXPERIENCES
|
|
156
|
+
- FIRST_NAME
|
|
157
|
+
- LAST_NAME
|
|
158
|
+
- FUNCTION
|
|
159
|
+
- YEARS_OF_EXPERIENCE
|
|
160
|
+
- YEARS_AT_CURRENT_COMPANY
|
|
161
|
+
- YEARS_IN_CURRENT_POSITION
|
|
162
|
+
- COMPANY_HEADQUARTERS
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# Build the user prompt
|
|
166
|
+
user_prompt = f"""
|
|
167
|
+
{system_message}
|
|
168
|
+
Do the following step by step:
|
|
169
|
+
1. Think about the leads the user wants to query and filters to use for the same.
|
|
170
|
+
2. If the user has provided a sales navigator url use that.
|
|
171
|
+
3. Take a look at the examples provided to construct the URL if user has not provided one.
|
|
172
|
+
4. Think about the code example provided and see how you will fill the english_request_for_salesnav_search and user_input_salesnav_url correctly.
|
|
173
|
+
5. Generate the correct python code which takes care of above requirements.
|
|
174
|
+
6. Make sure the code is valid and returns results in the format ("SUCCESS", leads_list) or ("ERROR", []).
|
|
175
|
+
7. Return the result in valid JSON format filled in workflow_python_code.
|
|
176
|
+
|
|
177
|
+
The user wants to generate code in python that performs the following:
|
|
178
|
+
|
|
179
|
+
"{filters}"
|
|
180
|
+
|
|
181
|
+
{user_provided_url}
|
|
182
|
+
|
|
183
|
+
Example of a workflow python code:
|
|
184
|
+
{example_of_workflow_code}
|
|
185
|
+
|
|
186
|
+
If user has provided a Sales Navigator URL set it to user_input_salesnav_url in the code
|
|
187
|
+
and pass as input to generate_salesnav_people_search_url.
|
|
188
|
+
|
|
189
|
+
Each lead returned has at least:
|
|
190
|
+
full_name, first_name, last_name, email, user_linkedin_salesnav_url, organization_linkedin_salesnav_url,
|
|
191
|
+
user_linkedin_url, primary_domain_of_organization, job_title, phone, headline,
|
|
192
|
+
lead_location, organization_name, organization_website, summary_about_lead, keywords,
|
|
193
|
+
number_of_linkedin_connections
|
|
194
|
+
|
|
195
|
+
Following are some common methods available:
|
|
196
|
+
1. generate_salesnav_people_search_url - to generate Sales Navigator URL from plain English query.
|
|
197
|
+
(Supported filters: {supported_filters_explanation})
|
|
198
|
+
|
|
199
|
+
The output function signature MUST be:
|
|
200
|
+
async def create_list_from_sales_navigator(filters, example_url, max_pages, tool_config):
|
|
201
|
+
|
|
202
|
+
Double check to make sure the generated python code is valid and returns results in the format
|
|
203
|
+
("SUCCESS", leads_list) or ("ERROR", []).
|
|
204
|
+
Output HAS to be valid JSON like:
|
|
205
|
+
{{
|
|
206
|
+
"workflow_python_code": "code that has been generated"
|
|
207
|
+
}}
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
# Invoke LLM to generate code
|
|
211
|
+
response, status = await get_structured_output_internal(
|
|
212
|
+
user_prompt,
|
|
213
|
+
WorkflowPythonCode,
|
|
214
|
+
tool_config=tool_config
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Return dict + status
|
|
218
|
+
return response.model_dump(), status
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def generate_leads_salesnav(
|
|
222
|
+
filter_object: Dict[str, Any],
|
|
223
|
+
request: SmartList,
|
|
224
|
+
example_url: str,
|
|
225
|
+
tool_config: Optional[List[Dict[str, Any]]] = None
|
|
226
|
+
) -> str:
|
|
227
|
+
"""
|
|
228
|
+
1) Generates Python workflow code from user's filter_object + example_url.
|
|
229
|
+
2) Executes the code to query Sales Navigator.
|
|
230
|
+
3) Returns JSON with {"status": <STATUS>, "leads": [list_of_leads]} or an error message.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
# Calculate max_pages from request
|
|
234
|
+
# Each page is assumed to have ~20 leads
|
|
235
|
+
pages = int(request.max_items_to_search / 20) if request.max_items_to_search else 1
|
|
236
|
+
if pages < 1:
|
|
237
|
+
pages = 1
|
|
238
|
+
if pages > 90:
|
|
239
|
+
pages = 90
|
|
240
|
+
|
|
241
|
+
# Generate the code
|
|
242
|
+
response, status = await generate_leads_code(
|
|
243
|
+
filters=str(filter_object),
|
|
244
|
+
example_url=example_url,
|
|
245
|
+
max_pages=pages,
|
|
246
|
+
tool_config=tool_config
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# If successful, try to run the code
|
|
250
|
+
if status == "SUCCESS" and response and response.get("workflow_python_code"):
|
|
251
|
+
code = response["workflow_python_code"]
|
|
252
|
+
if not code:
|
|
253
|
+
return json.dumps({"error": "No workflow code generated.", "status": status})
|
|
254
|
+
|
|
255
|
+
logger.info("Generated workflow code:\n%s", code)
|
|
256
|
+
|
|
257
|
+
local_vars: Dict[str, Any] = {}
|
|
258
|
+
global_vars: Dict[str, Any] = {}
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
# Execute the generated code
|
|
262
|
+
exec(code, global_vars, local_vars)
|
|
263
|
+
create_fn = local_vars.get("create_list_from_sales_navigator")
|
|
264
|
+
if not create_fn:
|
|
265
|
+
raise RuntimeError("No 'create_list_from_sales_navigator' function found in generated code.")
|
|
266
|
+
|
|
267
|
+
async def run_create_list(flt: str, ex_url: str, mx_pages: int, t_cfg: Optional[List[Dict[str, Any]]]):
|
|
268
|
+
return await create_fn(flt, ex_url, mx_pages, t_cfg)
|
|
269
|
+
|
|
270
|
+
# Invoke the function
|
|
271
|
+
try:
|
|
272
|
+
result = await run_create_list(str(filter_object), example_url, pages, tool_config)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.exception("Error while running create_list_from_sales_navigator.")
|
|
275
|
+
return json.dumps({"status": "ERROR", "error": str(e)})
|
|
276
|
+
|
|
277
|
+
# Expect a tuple like ("SUCCESS", leads_list) or ("ERROR", [])
|
|
278
|
+
if not isinstance(result, tuple) or len(result) != 2:
|
|
279
|
+
return json.dumps({
|
|
280
|
+
"status": "ERROR",
|
|
281
|
+
"error": "Workflow code did not return an expected (status, leads_list) tuple."
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
status_returned, leads_list = result
|
|
285
|
+
if status_returned != "SUCCESS":
|
|
286
|
+
return json.dumps({
|
|
287
|
+
"status": status_returned,
|
|
288
|
+
"error": "Workflow returned an error status.",
|
|
289
|
+
"leads": leads_list
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
# Return success + leads
|
|
293
|
+
return json.dumps({
|
|
294
|
+
"status": status_returned,
|
|
295
|
+
"leads": leads_list
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.exception("Exception occurred while executing workflow code.")
|
|
300
|
+
return json.dumps({"status": "ERROR", "error": str(e)})
|
|
301
|
+
|
|
302
|
+
# If code generation failed or no code
|
|
303
|
+
return json.dumps({"error": "No workflow code generated.", "status": status})
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# LinkedIn Message Generation Code (Refactored to mirror email structure)
|
|
3
|
+
# ----------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from dhisana.schemas.sales import (
|
|
10
|
+
ContentGenerationContext,
|
|
11
|
+
ConversationContext,
|
|
12
|
+
CampaignContext,
|
|
13
|
+
Lead,
|
|
14
|
+
MessageGenerationInstructions,
|
|
15
|
+
MessageItem,
|
|
16
|
+
SenderInfo
|
|
17
|
+
)
|
|
18
|
+
from dhisana.utils.generate_structured_output_internal import (
|
|
19
|
+
get_structured_output_internal,
|
|
20
|
+
get_structured_output_with_assistant_and_vector_store
|
|
21
|
+
)
|
|
22
|
+
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
23
|
+
|
|
24
|
+
# ----------------------------------------------------------------------
|
|
25
|
+
# LinkedIn Connection Message Schema
|
|
26
|
+
# ----------------------------------------------------------------------
|
|
27
|
+
class LinkedInConnectMessage(BaseModel):
|
|
28
|
+
body: str
|
|
29
|
+
|
|
30
|
+
# ----------------------------------------------------------------------
|
|
31
|
+
# Cleanup function (similar to cleanup_email_context)
|
|
32
|
+
# ----------------------------------------------------------------------
|
|
33
|
+
def cleanup_linkedin_context(linkedin_context: ContentGenerationContext) -> ContentGenerationContext:
|
|
34
|
+
"""
|
|
35
|
+
Return a copy of ContentGenerationContext without unneeded or sensitive fields
|
|
36
|
+
for LinkedIn generation.
|
|
37
|
+
"""
|
|
38
|
+
clone_context = linkedin_context.copy(deep=True)
|
|
39
|
+
|
|
40
|
+
# Example: remove irrelevant external data for this context
|
|
41
|
+
if clone_context.external_known_data:
|
|
42
|
+
clone_context.external_known_data.external_openai_vector_store_id = None
|
|
43
|
+
|
|
44
|
+
# Remove extra fields on the lead_info if desired
|
|
45
|
+
clone_context.lead_info.task_ids = None
|
|
46
|
+
clone_context.lead_info.email_validation_status = None
|
|
47
|
+
clone_context.lead_info.linkedin_validation_status = None
|
|
48
|
+
clone_context.lead_info.research_status = None
|
|
49
|
+
clone_context.lead_info.enchrichment_status = None
|
|
50
|
+
|
|
51
|
+
return clone_context
|
|
52
|
+
|
|
53
|
+
# ----------------------------------------------------------------------
|
|
54
|
+
# Known Framework Variations (fallback if user instructions are not provided)
|
|
55
|
+
# ----------------------------------------------------------------------
|
|
56
|
+
LINKEDIN_FRAMEWORK_VARIATIONS = [
|
|
57
|
+
"Use a friendly intro mentioning their role and a quick reason to connect.",
|
|
58
|
+
"Use social proof: reference an industry success story or insight.",
|
|
59
|
+
"Use a brief mention of mutual interest or connection.",
|
|
60
|
+
"Use P-S-B style (Pain, Solution, Benefit) but under 40 words.",
|
|
61
|
+
"Use a 3-Bullet Approach: Industry/Pain, Value, Simple Ask."
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------------
|
|
65
|
+
# Core function to generate a LinkedIn copy (similar to generate_personalized_email_copy)
|
|
66
|
+
# ----------------------------------------------------------------------
|
|
67
|
+
async def generate_personalized_linkedin_copy(
|
|
68
|
+
linkedin_context: ContentGenerationContext,
|
|
69
|
+
variation_text: str,
|
|
70
|
+
tool_config: Optional[List[Dict]] = None,
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
Generate a personalized LinkedIn connection message using the provided context and instructions.
|
|
74
|
+
|
|
75
|
+
Steps:
|
|
76
|
+
1. Build a prompt referencing 6 main sections:
|
|
77
|
+
(a) Lead Info
|
|
78
|
+
(b) Sender Info
|
|
79
|
+
(c) Campaign Info
|
|
80
|
+
(d) Variation / Instructions
|
|
81
|
+
(e) External Data (if any)
|
|
82
|
+
(f) Current Conversation
|
|
83
|
+
2. Generate the LinkedIn message with or without vector store usage.
|
|
84
|
+
3. Return the final subject & body, ensuring < 40 words.
|
|
85
|
+
"""
|
|
86
|
+
cleaned_context = cleanup_linkedin_context(linkedin_context)
|
|
87
|
+
|
|
88
|
+
lead_data = cleaned_context.lead_info or Lead()
|
|
89
|
+
sender_data = cleaned_context.sender_info or SenderInfo()
|
|
90
|
+
campaign_data = cleaned_context.campaign_context or CampaignContext()
|
|
91
|
+
conversation_data = cleaned_context.current_conversation_context or ConversationContext()
|
|
92
|
+
|
|
93
|
+
# Construct the consolidated prompt
|
|
94
|
+
prompt = f"""
|
|
95
|
+
Hi AI Assistant,
|
|
96
|
+
|
|
97
|
+
Below is the context in 6 main sections. Use it to craft a concise, professional LinkedIn connection request message:
|
|
98
|
+
Linked in connect message HAS to be maximum of 40 words. DO NOT go more than 40 words.
|
|
99
|
+
|
|
100
|
+
1) Lead Information:
|
|
101
|
+
{lead_data.dict()}
|
|
102
|
+
|
|
103
|
+
2) Sender Information:
|
|
104
|
+
Full Name: {sender_data.sender_full_name or ''}
|
|
105
|
+
First Name: {sender_data.sender_first_name or ''}
|
|
106
|
+
Last Name: {sender_data.sender_last_name or ''}
|
|
107
|
+
Bio: {sender_data.sender_bio or ''}
|
|
108
|
+
|
|
109
|
+
3) Campaign Information:
|
|
110
|
+
Product Name: {campaign_data.product_name or ''}
|
|
111
|
+
Value Proposition: {campaign_data.value_prop or ''}
|
|
112
|
+
Call To Action: {campaign_data.call_to_action or ''}
|
|
113
|
+
Pain Points: {campaign_data.pain_points or []}
|
|
114
|
+
Proof Points: {campaign_data.proof_points or []}
|
|
115
|
+
|
|
116
|
+
4) Follow these Instructions to generate the likedin connection request message:
|
|
117
|
+
{variation_text}
|
|
118
|
+
|
|
119
|
+
5) External Data / Vector Store:
|
|
120
|
+
(File search or additional context if available.)
|
|
121
|
+
|
|
122
|
+
6) Current Conversation Context:
|
|
123
|
+
Email Thread: {conversation_data.current_email_thread or ''}
|
|
124
|
+
LinkedIn Thread: {conversation_data.current_linkedin_thread or ''}
|
|
125
|
+
|
|
126
|
+
IMPORTANT REQUIREMENTS:
|
|
127
|
+
- Output must be JSON "body" of the message.
|
|
128
|
+
- The entire message body must be under 40 words total.
|
|
129
|
+
- In the body DO NOT include any HTML tags like <a>, <b>, <i>, etc.
|
|
130
|
+
- The body should be in plain text.
|
|
131
|
+
- If there is a link provided in body use it as is. dont wrap it in any HTML tags.
|
|
132
|
+
- No placeholders or extra instructions in the final output.
|
|
133
|
+
- Do not include personal addresses, IDs, or irrelevant internal data.
|
|
134
|
+
- Linked in message always has saluatation Hi <First Name>, unless specified otherwise.
|
|
135
|
+
- <First Name> is the first name of the lead. Its conversational name. It does not have any special characters or spaces.
|
|
136
|
+
- Make sure the signature in body has the sender_first_name is correct and in the format user has specified.
|
|
137
|
+
- User conversational name for company name if used.
|
|
138
|
+
- Make sure the body text is well-formatted and that newline and carriage-return characters are correctly present and preserved in the message body.
|
|
139
|
+
- Do Not use em dash in the generated output.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
vector_store_id = (
|
|
143
|
+
linkedin_context.external_known_data.external_openai_vector_store_id
|
|
144
|
+
if linkedin_context.external_known_data else None
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if vector_store_id:
|
|
148
|
+
# Use the vector store if available
|
|
149
|
+
response_data, status = await get_structured_output_with_assistant_and_vector_store(
|
|
150
|
+
prompt=prompt,
|
|
151
|
+
response_format=LinkedInConnectMessage,
|
|
152
|
+
vector_store_id=vector_store_id,
|
|
153
|
+
model="gpt-5.1-chat",
|
|
154
|
+
tool_config=tool_config,
|
|
155
|
+
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
# Otherwise, generate internally
|
|
159
|
+
response_data, status = await get_structured_output_internal(
|
|
160
|
+
prompt=prompt,
|
|
161
|
+
response_format=LinkedInConnectMessage,
|
|
162
|
+
model="gpt-5.1-chat",
|
|
163
|
+
tool_config=tool_config,
|
|
164
|
+
use_cache=linkedin_context.message_instructions.use_cache if linkedin_context.message_instructions else True
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if status != "SUCCESS":
|
|
168
|
+
raise Exception("Error: Could not generate the LinkedIn message.")
|
|
169
|
+
|
|
170
|
+
# Wrap in MessageItem for consistency
|
|
171
|
+
response_item = MessageItem(
|
|
172
|
+
message_id="", # fill in if you have an ID
|
|
173
|
+
thread_id="",
|
|
174
|
+
sender_name=sender_data.sender_full_name or "",
|
|
175
|
+
sender_email=sender_data.sender_email or "",
|
|
176
|
+
receiver_name=lead_data.full_name or "",
|
|
177
|
+
receiver_email=lead_data.email or "",
|
|
178
|
+
iso_datetime=datetime.utcnow().isoformat(),
|
|
179
|
+
subject="Hi",
|
|
180
|
+
body=response_data.body
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return response_item.model_dump()
|
|
184
|
+
|
|
185
|
+
# ----------------------------------------------------------------------
|
|
186
|
+
# Primary function to generate multiple LinkedIn message variations
|
|
187
|
+
# (similar to generate_personalized_email)
|
|
188
|
+
# ----------------------------------------------------------------------
|
|
189
|
+
@assistant_tool
|
|
190
|
+
async def generate_personalized_linkedin_message(
|
|
191
|
+
linkedin_context: ContentGenerationContext,
|
|
192
|
+
number_of_variations: int = 3,
|
|
193
|
+
tool_config: Optional[List[Dict]] = None
|
|
194
|
+
) -> List[dict]:
|
|
195
|
+
"""
|
|
196
|
+
Generate multiple variations of a personalized LinkedIn connection message
|
|
197
|
+
using the provided context and instructions.
|
|
198
|
+
|
|
199
|
+
:param linkedin_context: Consolidated context for LinkedIn generation
|
|
200
|
+
:param number_of_variations: Number of variations to produce
|
|
201
|
+
:param tool_config: Optional config for tool or vector store
|
|
202
|
+
:return: A list of dictionaries, each containing 'subject' and 'body' only
|
|
203
|
+
"""
|
|
204
|
+
message_instructions = linkedin_context.message_instructions or MessageGenerationInstructions()
|
|
205
|
+
user_instructions_exist = bool((message_instructions.instructions_to_generate_message or "").strip())
|
|
206
|
+
|
|
207
|
+
linkedin_variations = []
|
|
208
|
+
|
|
209
|
+
for i in range(number_of_variations):
|
|
210
|
+
# If user provided custom instructions, use them for each variation
|
|
211
|
+
# Otherwise, pick from fallback frameworks in a round-robin fashion
|
|
212
|
+
if user_instructions_exist:
|
|
213
|
+
variation_text = message_instructions.instructions_to_generate_message
|
|
214
|
+
else:
|
|
215
|
+
variation_text = LINKEDIN_FRAMEWORK_VARIATIONS[i % len(LINKEDIN_FRAMEWORK_VARIATIONS)]
|
|
216
|
+
|
|
217
|
+
personalized_copy = await generate_personalized_linkedin_copy(
|
|
218
|
+
linkedin_context=linkedin_context,
|
|
219
|
+
variation_text=variation_text,
|
|
220
|
+
tool_config=tool_config
|
|
221
|
+
)
|
|
222
|
+
linkedin_variations.append(personalized_copy)
|
|
223
|
+
|
|
224
|
+
return linkedin_variations
|