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.
Files changed (102) hide show
  1. dhisana/__init__.py +1 -0
  2. dhisana/cli/__init__.py +1 -0
  3. dhisana/cli/cli.py +20 -0
  4. dhisana/cli/datasets.py +27 -0
  5. dhisana/cli/models.py +26 -0
  6. dhisana/cli/predictions.py +20 -0
  7. dhisana/schemas/__init__.py +1 -0
  8. dhisana/schemas/common.py +399 -0
  9. dhisana/schemas/sales.py +965 -0
  10. dhisana/ui/__init__.py +1 -0
  11. dhisana/ui/components.py +472 -0
  12. dhisana/utils/__init__.py +1 -0
  13. dhisana/utils/add_mapping.py +352 -0
  14. dhisana/utils/agent_tools.py +51 -0
  15. dhisana/utils/apollo_tools.py +1597 -0
  16. dhisana/utils/assistant_tool_tag.py +4 -0
  17. dhisana/utils/built_with_api_tools.py +282 -0
  18. dhisana/utils/cache_output_tools.py +98 -0
  19. dhisana/utils/cache_output_tools_local.py +78 -0
  20. dhisana/utils/check_email_validity_tools.py +717 -0
  21. dhisana/utils/check_for_intent_signal.py +107 -0
  22. dhisana/utils/check_linkedin_url_validity.py +209 -0
  23. dhisana/utils/clay_tools.py +43 -0
  24. dhisana/utils/clean_properties.py +135 -0
  25. dhisana/utils/company_utils.py +60 -0
  26. dhisana/utils/compose_salesnav_query.py +259 -0
  27. dhisana/utils/compose_search_query.py +759 -0
  28. dhisana/utils/compose_three_step_workflow.py +234 -0
  29. dhisana/utils/composite_tools.py +137 -0
  30. dhisana/utils/dataframe_tools.py +237 -0
  31. dhisana/utils/domain_parser.py +45 -0
  32. dhisana/utils/email_body_utils.py +72 -0
  33. dhisana/utils/email_parse_helpers.py +132 -0
  34. dhisana/utils/email_provider.py +375 -0
  35. dhisana/utils/enrich_lead_information.py +933 -0
  36. dhisana/utils/extract_email_content_for_llm.py +101 -0
  37. dhisana/utils/fetch_openai_config.py +129 -0
  38. dhisana/utils/field_validators.py +426 -0
  39. dhisana/utils/g2_tools.py +104 -0
  40. dhisana/utils/generate_content.py +41 -0
  41. dhisana/utils/generate_custom_message.py +271 -0
  42. dhisana/utils/generate_email.py +278 -0
  43. dhisana/utils/generate_email_response.py +465 -0
  44. dhisana/utils/generate_flow.py +102 -0
  45. dhisana/utils/generate_leads_salesnav.py +303 -0
  46. dhisana/utils/generate_linkedin_connect_message.py +224 -0
  47. dhisana/utils/generate_linkedin_response_message.py +317 -0
  48. dhisana/utils/generate_structured_output_internal.py +462 -0
  49. dhisana/utils/google_custom_search.py +267 -0
  50. dhisana/utils/google_oauth_tools.py +727 -0
  51. dhisana/utils/google_workspace_tools.py +1294 -0
  52. dhisana/utils/hubspot_clearbit.py +96 -0
  53. dhisana/utils/hubspot_crm_tools.py +2440 -0
  54. dhisana/utils/instantly_tools.py +149 -0
  55. dhisana/utils/linkedin_crawler.py +168 -0
  56. dhisana/utils/lusha_tools.py +333 -0
  57. dhisana/utils/mailgun_tools.py +156 -0
  58. dhisana/utils/mailreach_tools.py +123 -0
  59. dhisana/utils/microsoft365_tools.py +455 -0
  60. dhisana/utils/openai_assistant_and_file_utils.py +267 -0
  61. dhisana/utils/openai_helpers.py +977 -0
  62. dhisana/utils/openapi_spec_to_tools.py +45 -0
  63. dhisana/utils/openapi_tool/__init__.py +1 -0
  64. dhisana/utils/openapi_tool/api_models.py +633 -0
  65. dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +271 -0
  66. dhisana/utils/openapi_tool/openapi_tool.py +319 -0
  67. dhisana/utils/parse_linkedin_messages_txt.py +100 -0
  68. dhisana/utils/profile.py +37 -0
  69. dhisana/utils/proxy_curl_tools.py +1226 -0
  70. dhisana/utils/proxycurl_search_leads.py +426 -0
  71. dhisana/utils/python_function_to_tools.py +83 -0
  72. dhisana/utils/research_lead.py +176 -0
  73. dhisana/utils/sales_navigator_crawler.py +1103 -0
  74. dhisana/utils/salesforce_crm_tools.py +477 -0
  75. dhisana/utils/search_router.py +131 -0
  76. dhisana/utils/search_router_jobs.py +51 -0
  77. dhisana/utils/sendgrid_tools.py +162 -0
  78. dhisana/utils/serarch_router_local_business.py +75 -0
  79. dhisana/utils/serpapi_additional_tools.py +290 -0
  80. dhisana/utils/serpapi_google_jobs.py +117 -0
  81. dhisana/utils/serpapi_google_search.py +188 -0
  82. dhisana/utils/serpapi_local_business_search.py +129 -0
  83. dhisana/utils/serpapi_search_tools.py +852 -0
  84. dhisana/utils/serperdev_google_jobs.py +125 -0
  85. dhisana/utils/serperdev_local_business.py +154 -0
  86. dhisana/utils/serperdev_search.py +233 -0
  87. dhisana/utils/smtp_email_tools.py +582 -0
  88. dhisana/utils/test_connect.py +2087 -0
  89. dhisana/utils/trasform_json.py +173 -0
  90. dhisana/utils/web_download_parse_tools.py +189 -0
  91. dhisana/utils/workflow_code_model.py +5 -0
  92. dhisana/utils/zoominfo_tools.py +357 -0
  93. dhisana/workflow/__init__.py +1 -0
  94. dhisana/workflow/agent.py +18 -0
  95. dhisana/workflow/flow.py +44 -0
  96. dhisana/workflow/task.py +43 -0
  97. dhisana/workflow/test.py +90 -0
  98. dhisana-0.0.1.dev243.dist-info/METADATA +43 -0
  99. dhisana-0.0.1.dev243.dist-info/RECORD +102 -0
  100. dhisana-0.0.1.dev243.dist-info/WHEEL +5 -0
  101. dhisana-0.0.1.dev243.dist-info/entry_points.txt +2 -0
  102. 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