dhisana 0.0.1.dev23__tar.gz → 0.0.1.dev25__tar.gz
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-0.0.1.dev23 → dhisana-0.0.1.dev25}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/setup.py +1 -1
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/schemas/sales.py +1 -1
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/apollo_tools.py +1 -1
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/compose_cadence.py +34 -27
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/compose_three_step_workflow.py +1 -1
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/compose_workflow.py +11 -11
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/create_list_from_sales_navigator.py +2 -2
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/create_smart_list.py +6 -6
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/enrich_lead_information.py +4 -4
- dhisana-0.0.1.dev25/src/dhisana/utils/generate_linkedin_response_message.py +160 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/hubspot_crm_tools.py +4 -4
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/proxy_curl_tools.py +12 -12
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/sales_navigator_crawler.py +9 -9
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/serpapi_search_tools.py +6 -6
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana.egg-info/SOURCES.txt +1 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/README.md +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/setup.cfg +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/agent_task.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/tests/test_agent_tools.py +0 -0
|
@@ -243,7 +243,7 @@ class HubSpotLeadInformation(BaseModel):
|
|
|
243
243
|
lead_location: str = Field("", description="Location of the lead")
|
|
244
244
|
organization_name: str = Field("", description="Current Company where lead works")
|
|
245
245
|
organization_website: str = Field("", description="Current Company website of the lead")
|
|
246
|
-
|
|
246
|
+
organization_linkedin_url : str = Field("", description="Company LinkedIn URL")
|
|
247
247
|
# additional_properties is dict of dict-of-strings.
|
|
248
248
|
# We store all unmapped HubSpot fields as string => string.
|
|
249
249
|
additional_properties: Dict[str, Dict[str, str]] = Field(
|
|
@@ -582,7 +582,7 @@ async def enrich_user_info_with_apollo(input_user_properties, tool_config):
|
|
|
582
582
|
if person_data.get("organization") and person_data["organization"].get("name"):
|
|
583
583
|
input_user_properties["organization_name"] = person_data["organization"]["name"]
|
|
584
584
|
if person_data.get("organization") and person_data["organization"].get("linkedin_url"):
|
|
585
|
-
input_user_properties["
|
|
585
|
+
input_user_properties["organization_linkedin_url "] = person_data["organization"]["linkedin_url"]
|
|
586
586
|
if person_data.get("organization") and person_data["organization"].get("website_url"):
|
|
587
587
|
input_user_properties["organization_website"] = person_data["organization"]["website_url"]
|
|
588
588
|
if person_data.get("headline") and not input_user_properties.get("summary_about_lead"):
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Any, Dict, List, Optional, Tuple
|
|
4
4
|
|
|
5
|
+
from pydantic import BaseModel
|
|
5
6
|
|
|
6
7
|
from dhisana.utils.generate_structured_output_internal import get_structured_output_with_o1
|
|
7
8
|
from dhisana.utils.workflow_code_model import WorkflowPythonCode
|
|
@@ -10,7 +11,7 @@ from dhisana.utils.workflow_code_model import WorkflowPythonCode
|
|
|
10
11
|
logging.basicConfig(level=logging.INFO)
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
|
|
14
15
|
async def generate_code_for_campaign_cadence(
|
|
15
16
|
english_description: str,
|
|
16
17
|
tool_config: Optional[List[Dict[str, Any]]] = None
|
|
@@ -19,25 +20,25 @@ async def generate_code_for_campaign_cadence(
|
|
|
19
20
|
Generate a workflow code (Python code) from an English description.
|
|
20
21
|
|
|
21
22
|
The function produced will be named:
|
|
22
|
-
async def run_campaing_cadence(input_leads, tool_config) -> str
|
|
23
|
+
async def run_campaing_cadence(input_leads, campaign_context, tool_config) -> str
|
|
23
24
|
Returns: (response_dict, status_string)
|
|
24
25
|
"""
|
|
25
26
|
system_message = (
|
|
26
27
|
"You are a helpful AI assistant and an expert Python coder. "
|
|
27
28
|
"Convert the English description provided by the user into an executable Python function named "
|
|
28
29
|
"'run_campaing_cadence', with the signature:\n"
|
|
29
|
-
" async def run_campaing_cadence(input_leads, tool_config) -> str\n"
|
|
30
|
+
" async def run_campaing_cadence(input_leads, campaign_context, tool_config) -> str\n"
|
|
30
31
|
"The function should iterate over 'input_leads' and perform the required LinkedIn actions via execute_task. "
|
|
31
32
|
"Valid commands include: "
|
|
32
|
-
" view_linked_in_profile, send_connection_request, like_recent_post,
|
|
33
|
+
" view_linked_in_profile, send_connection_request, like_recent_post, send_linkedin_message, get_current_messages, save_lead.\n"
|
|
33
34
|
"Return 'SUCCESS' or 'ERROR' as a string."
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
example_of_workflow_code = (
|
|
37
38
|
'''
|
|
38
|
-
async def run_campaing_cadence(input_leads, tool_config):
|
|
39
|
+
async def run_campaing_cadence(input_leads, campaign_context, tool_config):
|
|
39
40
|
"""
|
|
40
|
-
Example skeleton. Run a linkedin campaign cadence with actions like view_profile,
|
|
41
|
+
Example skeleton. Run a linkedin campaign cadence with actions like view_profile, send_linkedin_message etc.
|
|
41
42
|
Returns "SUCCESS" or "ERROR".
|
|
42
43
|
"""
|
|
43
44
|
|
|
@@ -56,7 +57,7 @@ async def generate_code_for_campaign_cadence(
|
|
|
56
57
|
MAX_ACTIONS = {
|
|
57
58
|
"view_profile": 30,
|
|
58
59
|
"send_connection_request": 10,
|
|
59
|
-
"
|
|
60
|
+
"send_linkedin_message": 10,
|
|
60
61
|
"like_post": 10
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -64,7 +65,7 @@ async def generate_code_for_campaign_cadence(
|
|
|
64
65
|
for lead in input_leads[:MAX_ACTIONS["view_profile"]]:
|
|
65
66
|
await execute_task(
|
|
66
67
|
command_name="view_linked_in_profile",
|
|
67
|
-
command_args={"salesnav_url_profile": lead.get("
|
|
68
|
+
command_args={"salesnav_url_profile": lead.get("user_linkedin_salesnav_url")},
|
|
68
69
|
tool_config=tool_config
|
|
69
70
|
)
|
|
70
71
|
logger.debug("Viewed profile: %s", lead.get("full_name"))
|
|
@@ -73,23 +74,23 @@ async def generate_code_for_campaign_cadence(
|
|
|
73
74
|
for lead in input_leads[:MAX_ACTIONS["send_connection_request"]]:
|
|
74
75
|
await execute_task(
|
|
75
76
|
command_name="send_connection_request",
|
|
76
|
-
command_args={
|
|
77
|
+
command_args={
|
|
78
|
+
"salesnav_url_profile": lead.get("user_linkedin_salesnav_url"),
|
|
79
|
+
"lead_info": lead,
|
|
80
|
+
"context": campaign_context
|
|
81
|
+
},
|
|
77
82
|
tool_config=tool_config
|
|
78
83
|
)
|
|
79
84
|
logger.debug("Sent connection request: %s", lead.get("full_name"))
|
|
80
85
|
|
|
81
86
|
# Send messages
|
|
82
|
-
for lead in input_leads[:MAX_ACTIONS["
|
|
83
|
-
message_to_send = await get_personalized_linkedin_message(
|
|
84
|
-
lead_info=lead,
|
|
85
|
-
outreach_context="Hello from my LinkedIn automation!",
|
|
86
|
-
tool_config=tool_config
|
|
87
|
-
)
|
|
87
|
+
for lead in input_leads[:MAX_ACTIONS["send_linkedin_message"]]:
|
|
88
88
|
await execute_task(
|
|
89
|
-
command_name="
|
|
89
|
+
command_name="send_linkedin_message",
|
|
90
90
|
command_args={
|
|
91
|
-
"salesnav_url_profile": lead.get("
|
|
92
|
-
"
|
|
91
|
+
"salesnav_url_profile": lead.get("user_linkedin_salesnav_url"),
|
|
92
|
+
"lead_info": lead,
|
|
93
|
+
"context": campaign_context
|
|
93
94
|
},
|
|
94
95
|
tool_config=tool_config
|
|
95
96
|
)
|
|
@@ -99,7 +100,7 @@ async def generate_code_for_campaign_cadence(
|
|
|
99
100
|
for lead in input_leads[:MAX_ACTIONS["like_post"]]:
|
|
100
101
|
await execute_task(
|
|
101
102
|
command_name="like_recent_post",
|
|
102
|
-
command_args={"salesnav_url_profile": lead.get("
|
|
103
|
+
command_args={"salesnav_url_profile": lead.get("user_linkedin_salesnav_url")},
|
|
103
104
|
tool_config=tool_config
|
|
104
105
|
)
|
|
105
106
|
logger.debug("Liked recent post for lead: %s", lead.get("full_name"))
|
|
@@ -115,7 +116,7 @@ async def generate_code_for_campaign_cadence(
|
|
|
115
116
|
|
|
116
117
|
user_prompt = f"""
|
|
117
118
|
{system_message}
|
|
118
|
-
"Make sure the imports are present within the function definition itself. Make sure the logging library is imported and logger defined
|
|
119
|
+
"Make sure the imports are present within the function definition itself. Make sure the logging library is imported and logger defined within the function. "
|
|
119
120
|
The user wants to generate Python code that does this:
|
|
120
121
|
|
|
121
122
|
\"{english_description}\"
|
|
@@ -124,7 +125,7 @@ async def generate_code_for_campaign_cadence(
|
|
|
124
125
|
{example_of_workflow_code}
|
|
125
126
|
|
|
126
127
|
The final code must be valid Python, and must produce:
|
|
127
|
-
async def run_campaing_cadence(input_leads, tool_config) -> str
|
|
128
|
+
async def run_campaing_cadence(input_leads, campaign_context, tool_config) -> str
|
|
128
129
|
|
|
129
130
|
Return your final output as valid JSON with:
|
|
130
131
|
{{
|
|
@@ -136,8 +137,13 @@ async def generate_code_for_campaign_cadence(
|
|
|
136
137
|
)
|
|
137
138
|
return response.model_dump(), status
|
|
138
139
|
|
|
140
|
+
|
|
141
|
+
class CampaignContext(BaseModel):
|
|
142
|
+
instructions: str
|
|
143
|
+
|
|
144
|
+
|
|
139
145
|
async def generate_campaign_cadence_workflow_and_execute(
|
|
140
|
-
|
|
146
|
+
instructions: str,
|
|
141
147
|
input_leads: List[Dict[str, Any]],
|
|
142
148
|
tool_config: Optional[List[Dict[str, Any]]] = None
|
|
143
149
|
) -> str:
|
|
@@ -149,7 +155,7 @@ async def generate_campaign_cadence_workflow_and_execute(
|
|
|
149
155
|
JSON string describing success or error.
|
|
150
156
|
"""
|
|
151
157
|
response, status = await generate_code_for_campaign_cadence(
|
|
152
|
-
|
|
158
|
+
instructions, tool_config=tool_config
|
|
153
159
|
)
|
|
154
160
|
|
|
155
161
|
# Check if we have a successful code generation
|
|
@@ -170,12 +176,13 @@ async def generate_campaign_cadence_workflow_and_execute(
|
|
|
170
176
|
if campaign_fn is None:
|
|
171
177
|
raise RuntimeError("No 'run_campaing_cadence' function found in generated code.")
|
|
172
178
|
|
|
173
|
-
#
|
|
174
|
-
async def run_campaign(leads, cfg):
|
|
175
|
-
return await campaign_fn(leads, cfg)
|
|
179
|
+
# Helper to call the newly generated async function
|
|
180
|
+
async def run_campaign(leads, campaign_context, cfg):
|
|
181
|
+
return await campaign_fn(leads, campaign_context, cfg)
|
|
176
182
|
|
|
177
183
|
try:
|
|
178
|
-
|
|
184
|
+
campaign_context = CampaignContext(instructions=instructions)
|
|
185
|
+
result = await run_campaign(input_leads, campaign_context, tool_config)
|
|
179
186
|
except Exception as e:
|
|
180
187
|
logger.exception("Error occurred while running run_campaing_cadence.")
|
|
181
188
|
return json.dumps({"status": "ERROR", "error": str(e)})
|
{dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
@@ -196,7 +196,7 @@ async def generate_three_step_workflow_execute(
|
|
|
196
196
|
# generate_campaign_cadence_workflow_and_execute might return JSON:
|
|
197
197
|
# { "status": "SUCCESS", "result": "Campaign done" } or similar
|
|
198
198
|
result_str = await generate_campaign_cadence_workflow_and_execute(
|
|
199
|
-
|
|
199
|
+
instructions=step_3_instructions,
|
|
200
200
|
input_leads=leads_list,
|
|
201
201
|
tool_config=tool_config
|
|
202
202
|
)
|
|
@@ -42,7 +42,7 @@ async def generate_workflow_code(
|
|
|
42
42
|
"view_linked_in_profile",
|
|
43
43
|
"send_connection_request",
|
|
44
44
|
"like_recent_post",
|
|
45
|
-
"
|
|
45
|
+
"send_linkedin_message",
|
|
46
46
|
"get_current_messages",
|
|
47
47
|
]
|
|
48
48
|
|
|
@@ -79,8 +79,8 @@ async def custom_workflow(tool_config):
|
|
|
79
79
|
if not leads:
|
|
80
80
|
return "SUCCESS"
|
|
81
81
|
|
|
82
|
-
# Filter and deduplicate leads with valid
|
|
83
|
-
unique_leads = {lead['
|
|
82
|
+
# Filter and deduplicate leads with valid user_linkedin_salesnav_url
|
|
83
|
+
unique_leads = {lead['user_linkedin_salesnav_url']: lead for lead in leads if lead.get("user_linkedin_salesnav_url")}
|
|
84
84
|
unique_leads = list(unique_leads.values())
|
|
85
85
|
if not unique_leads:
|
|
86
86
|
return "SUCCESS"
|
|
@@ -128,37 +128,37 @@ async def custom_workflow(tool_config):
|
|
|
128
128
|
MAX_ACTIONS = {
|
|
129
129
|
"view_profile": 30,
|
|
130
130
|
"send_connection_request": 10,
|
|
131
|
-
"
|
|
131
|
+
"send_linkedin_message": 10,
|
|
132
132
|
"like_post": 10
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
for lead in qualified_leads[:MAX_ACTIONS["view_profile"]]:
|
|
136
136
|
await execute_task(
|
|
137
137
|
command_name="view_linked_in_profile",
|
|
138
|
-
command_args={"salesnav_url_profile": lead.get("
|
|
138
|
+
command_args={"salesnav_url_profile": lead.get("user_linkedin_salesnav_url")},
|
|
139
139
|
tool_config=tool_config
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
for lead in qualified_leads[:MAX_ACTIONS["send_connection_request"]]:
|
|
143
143
|
await execute_task(
|
|
144
144
|
command_name="send_connection_request",
|
|
145
|
-
command_args={"salesnav_url_profile": lead.get("
|
|
145
|
+
command_args={"salesnav_url_profile": lead.get("user_linkedin_salesnav_url")},
|
|
146
146
|
tool_config=tool_config
|
|
147
147
|
)
|
|
148
148
|
|
|
149
|
-
for lead in qualified_leads[:MAX_ACTIONS["
|
|
149
|
+
for lead in qualified_leads[:MAX_ACTIONS["send_linkedin_message"]]:
|
|
150
150
|
context = "<< Fill in context that can be used to generate message>>" # Fill in campaign context in which the message is being sent
|
|
151
151
|
message_to_send = await get_personalized_linkedin_message(lead_info=lead, outreach_context=context, tool_config=tool_config)
|
|
152
152
|
await execute_task(
|
|
153
|
-
command_name="
|
|
154
|
-
command_args={"salesnav_url_profile": lead.get("
|
|
153
|
+
command_name="send_linkedin_message",
|
|
154
|
+
command_args={"salesnav_url_profile": lead.get("user_linkedin_salesnav_url"), "message": message_to_send},
|
|
155
155
|
tool_config=tool_config
|
|
156
156
|
)
|
|
157
157
|
|
|
158
158
|
for lead in qualified_leads[:MAX_ACTIONS["like_post"]]:
|
|
159
159
|
await execute_task(
|
|
160
160
|
command_name="like_recent_post",
|
|
161
|
-
command_args={"salesnav_url_profile": lead.get("
|
|
161
|
+
command_args={"salesnav_url_profile": lead.get("user_linkedin_salesnav_url")},
|
|
162
162
|
tool_config=tool_config
|
|
163
163
|
)
|
|
164
164
|
|
|
@@ -181,7 +181,7 @@ async def custom_workflow(tool_config):
|
|
|
181
181
|
|
|
182
182
|
Each lead returned has the following fields in the dicionary:
|
|
183
183
|
|
|
184
|
-
full_name, first_name, last_name, email,
|
|
184
|
+
full_name, first_name, last_name, email, user_linkedin_salesnav_url, organization_linkedin_salesnav_url ,
|
|
185
185
|
user_linkedin_url, primary_domain_of_organization, job_title, phone, headline,
|
|
186
186
|
lead_location, organization_name, organization_website, summary_about_lead, keywords,
|
|
187
187
|
number_of_linkedin_connections
|
{dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/create_list_from_sales_navigator.py
RENAMED
|
@@ -99,7 +99,7 @@ async def generate_sales_nav_list_workflow_code(
|
|
|
99
99
|
# Deduplicate leads
|
|
100
100
|
unique_leads = {}
|
|
101
101
|
for lead in leads:
|
|
102
|
-
salesnav_url = lead.get("
|
|
102
|
+
salesnav_url = lead.get("user_linkedin_salesnav_url")
|
|
103
103
|
if salesnav_url:
|
|
104
104
|
unique_leads[salesnav_url] = lead
|
|
105
105
|
|
|
@@ -173,7 +173,7 @@ async def generate_sales_nav_list_workflow_code(
|
|
|
173
173
|
If user has provided a Sales Navigator URL set to to user_input_salesnav_url in the code and pass as input to generate_salesnav_people_search_url.
|
|
174
174
|
|
|
175
175
|
Each lead returned has at least:
|
|
176
|
-
full_name, first_name, last_name, email,
|
|
176
|
+
full_name, first_name, last_name, email, user_linkedin_salesnav_url, organization_linkedin_salesnav_url ,
|
|
177
177
|
user_linkedin_url, primary_domain_of_organization, job_title, phone, headline,
|
|
178
178
|
lead_location, organization_name, organization_website, summary_about_lead, keywords,
|
|
179
179
|
number_of_linkedin_connections
|
|
@@ -55,23 +55,23 @@ async def generate_smart_list_creation_code(
|
|
|
55
55
|
try:
|
|
56
56
|
logger.info("Starting custom_workflow execution")
|
|
57
57
|
|
|
58
|
-
# Step 1 : Deduplicate leads by user_linkedin_url or
|
|
58
|
+
# Step 1 : Deduplicate leads by user_linkedin_url or user_linkedin_salesnav_url
|
|
59
59
|
leads_with_linkedin = {}
|
|
60
60
|
leads_with_salesnav = {}
|
|
61
61
|
|
|
62
62
|
for lead in input_leads_list:
|
|
63
63
|
user_linkedin_url = lead.get("user_linkedin_url")
|
|
64
|
-
|
|
64
|
+
user_linkedin_salesnav_url = lead.get("user_linkedin_salesnav_url")
|
|
65
65
|
if user_linkedin_url:
|
|
66
66
|
leads_with_linkedin[user_linkedin_url] = lead
|
|
67
|
-
elif
|
|
68
|
-
leads_with_salesnav[
|
|
67
|
+
elif user_linkedin_salesnav_url:
|
|
68
|
+
leads_with_salesnav[user_linkedin_salesnav_url] = lead
|
|
69
69
|
|
|
70
70
|
unique_leads = list(leads_with_linkedin.values()) + list(leads_with_salesnav.values())
|
|
71
71
|
logger.info("Unique leads after deduplication: %d", len(unique_leads))
|
|
72
72
|
|
|
73
73
|
if not unique_leads:
|
|
74
|
-
logger.warning("No leads with valid 'user_linkedin_url' or '
|
|
74
|
+
logger.warning("No leads with valid 'user_linkedin_url' or 'user_linkedin_salesnav_url'; returning SUCCESS.")
|
|
75
75
|
return "SUCCESS", []
|
|
76
76
|
|
|
77
77
|
# Step 2 : Qulify the leads based on intent signals by calling check_for_intent_signal
|
|
@@ -123,7 +123,7 @@ async def generate_smart_list_creation_code(
|
|
|
123
123
|
{system_message}
|
|
124
124
|
Do the following step by step:
|
|
125
125
|
1. Think on how the user wants to qualify the leads.
|
|
126
|
-
2. Deduplicate the leads based on user_linkedin_url or
|
|
126
|
+
2. Deduplicate the leads based on user_linkedin_url or user_linkedin_salesnav_url as given in the example.
|
|
127
127
|
3. Fill in the right description on what is the qualification criteria and call check_for_intent_signal with signal_to_look_for_in_plan_english having qualification criteria.
|
|
128
128
|
4. Do not make up any other logic to filter in python for intent signals. Use check_for_intent_signal to qualify the leads.
|
|
129
129
|
5. Sort the qualified leads by intent_score in descending order.
|
|
@@ -15,7 +15,7 @@ from dhisana.utils.proxy_curl_tools import (
|
|
|
15
15
|
enrich_user_info_with_proxy_curl,
|
|
16
16
|
)
|
|
17
17
|
from dhisana.utils.serpapi_search_tools import (
|
|
18
|
-
|
|
18
|
+
find_organization_linkedin_url_with_google_search,
|
|
19
19
|
find_user_linkedin_url_google,
|
|
20
20
|
get_company_domain_from_google_search,
|
|
21
21
|
)
|
|
@@ -153,7 +153,7 @@ async def enrich_organization_info_from_name(
|
|
|
153
153
|
) -> None:
|
|
154
154
|
"""
|
|
155
155
|
Given a dictionary (treated like a CSV row) containing 'organization_name',
|
|
156
|
-
'
|
|
156
|
+
'organization_linkedin_url ', and 'website' keys, enrich the row only if the
|
|
157
157
|
domain and website are currently empty.
|
|
158
158
|
|
|
159
159
|
:param row: Dictionary representing a lead or company record.
|
|
@@ -162,7 +162,7 @@ async def enrich_organization_info_from_name(
|
|
|
162
162
|
"""
|
|
163
163
|
org_name_key = "organization_name"
|
|
164
164
|
org_domain_key = "primary_domain_of_organization"
|
|
165
|
-
linkedin_url_key = "
|
|
165
|
+
linkedin_url_key = "organization_linkedin_url "
|
|
166
166
|
website_key = "website"
|
|
167
167
|
|
|
168
168
|
org_name = (row.get(org_name_key) or "").strip()
|
|
@@ -182,7 +182,7 @@ async def enrich_organization_info_from_name(
|
|
|
182
182
|
linkedin_url = row.get(linkedin_url_key, "").strip()
|
|
183
183
|
if not linkedin_url:
|
|
184
184
|
# TODO: If location detail is available, pass it to the search function.
|
|
185
|
-
linkedin_url = await
|
|
185
|
+
linkedin_url = await find_organization_linkedin_url_with_google_search(
|
|
186
186
|
org_name,
|
|
187
187
|
company_location="US",
|
|
188
188
|
use_strict_check=use_strict_check,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from dhisana.utils.generate_structured_output_internal import (
|
|
4
|
+
get_structured_output_with_o1
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
class LinkedInTriageResponse(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Model representing the structured response for a LinkedIn conversation triage.
|
|
10
|
+
- triage_status: "AUTOMATIC" or "REQUIRES_APPROVAL"
|
|
11
|
+
- triage_reason: Optional reason text if triage_status == "REQUIRES_APPROVAL"
|
|
12
|
+
- response_action_to_take: The recommended next action (e.g., SEND_REPLY, WAIT_TO_SEND, STOP_SENDING, etc.)
|
|
13
|
+
- response_message: The actual message (body) to be sent or used for approval.
|
|
14
|
+
"""
|
|
15
|
+
triage_status: str # "AUTOMATIC" or "REQUIRES_APPROVAL"
|
|
16
|
+
triage_reason: Optional[str]
|
|
17
|
+
response_action_to_take: str
|
|
18
|
+
response_message: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cleanup_reply_linkedin_context(lead_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Clone or sanitize the lead info to remove unneeded or sensitive fields.
|
|
24
|
+
"""
|
|
25
|
+
cleaned_lead_info = dict(lead_info) # Shallow copy
|
|
26
|
+
|
|
27
|
+
# Example of removing or masking unwanted fields
|
|
28
|
+
cleaned_lead_info.pop("task_ids", None)
|
|
29
|
+
cleaned_lead_info.pop("research_status", None)
|
|
30
|
+
# Add more fields you want to remove as needed
|
|
31
|
+
|
|
32
|
+
return cleaned_lead_info
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def generate_linkedin_response_message(
|
|
36
|
+
lead_info: Dict[str, Any],
|
|
37
|
+
outreach_context: str,
|
|
38
|
+
connect_template: str,
|
|
39
|
+
current_conversations: str,
|
|
40
|
+
additional_instructions: str,
|
|
41
|
+
tool_config: Optional[List[Dict]] = None
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Generate a triaged LinkedIn message, deciding if it can be sent automatically
|
|
45
|
+
or requires human approval. The final JSON structure must conform to
|
|
46
|
+
LinkedInTriageResponse (triage_status, triage_reason, response_action_to_take, response_message).
|
|
47
|
+
|
|
48
|
+
The content should be genuine, authentic, and professional, avoiding clichés
|
|
49
|
+
and heavy sales pushes. It should be respectful of user preferences (e.g., not
|
|
50
|
+
spamming a user who has shown disinterest).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Possible actions for spam-avoidance, scheduling, and response
|
|
54
|
+
allowed_actions = [
|
|
55
|
+
"SEND_REPLY", # Normal: send reply
|
|
56
|
+
"WAIT_TO_SEND", # Wait because user was recently messaged
|
|
57
|
+
"STOP_SENDING", # No more messages to user
|
|
58
|
+
"SCHEDULE_MEETING", # If user requests a meeting
|
|
59
|
+
"FOLLOW_UP_LATER",
|
|
60
|
+
"NEED_MORE_INFO",
|
|
61
|
+
"OTHER"
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
cleaned_lead_info = cleanup_reply_linkedin_context(lead_info)
|
|
65
|
+
|
|
66
|
+
# Incorporate the outreach context and guidelines directly in the prompt
|
|
67
|
+
prompt = f"""
|
|
68
|
+
You are a specialized LinkedIn assistant.
|
|
69
|
+
|
|
70
|
+
Your task:
|
|
71
|
+
1. Analyze the current LinkedIn conversation.
|
|
72
|
+
2. Inspect the user (lead) info, outreach campaign context, and any additional instructions.
|
|
73
|
+
3. Decide whether to automatically send a reply or if human approval is needed (triage).
|
|
74
|
+
4. If approval is needed, provide the reason.
|
|
75
|
+
5. Choose one recommended next action from: {allowed_actions}.
|
|
76
|
+
6. Provide a short LinkedIn message body that addresses the lead's conversation.
|
|
77
|
+
|
|
78
|
+
Connect Template (for reference):
|
|
79
|
+
{connect_template}
|
|
80
|
+
|
|
81
|
+
Outreach (Campaign) Context:
|
|
82
|
+
{outreach_context}
|
|
83
|
+
|
|
84
|
+
Additional Instructions (Triage Guidelines):
|
|
85
|
+
{additional_instructions}
|
|
86
|
+
|
|
87
|
+
Current Conversations:
|
|
88
|
+
{current_conversations}
|
|
89
|
+
|
|
90
|
+
Lead Info:
|
|
91
|
+
{cleaned_lead_info}
|
|
92
|
+
|
|
93
|
+
=== IMPORTANT ANTI-SPAM AND RESPECT RULES ===
|
|
94
|
+
1. If we have sent a message to the user within the past 24 hours and the user has not responded,
|
|
95
|
+
do NOT send another message right now. Instead, triage with "WAIT_TO_SEND".
|
|
96
|
+
2. If we have sent more than 3 messages in total without any user response, do NOT send another message.
|
|
97
|
+
Instead, triage with "STOP_SENDING".
|
|
98
|
+
3. If the user explicitly says "don't reply", "not interested", or any equivalent,
|
|
99
|
+
do NOT continue the thread. Triage with "STOP_SENDING".
|
|
100
|
+
4. If the user has requested a meeting, triage as "AUTOMATIC" or "REQUIRES_APPROVAL"
|
|
101
|
+
(depending on complexity), and set response_action_to_take to "SCHEDULE_MEETING".
|
|
102
|
+
Craft a helpful response for scheduling.
|
|
103
|
+
|
|
104
|
+
=== TONE AND STYLE RULES ===
|
|
105
|
+
- Your message must be genuine, authentic, and professional.
|
|
106
|
+
- Avoid clichés and spammy or overly aggressive sales pitches.
|
|
107
|
+
- Do not push sales in an unnatural way.
|
|
108
|
+
|
|
109
|
+
=== OUTPUT FORMAT ===
|
|
110
|
+
Your final output must be valid JSON in this exact format:
|
|
111
|
+
{{
|
|
112
|
+
"triage_status": "AUTOMATIC" or "REQUIRES_APPROVAL",
|
|
113
|
+
"triage_reason": "<reason if REQUIRES_APPROVAL; else empty or null>",
|
|
114
|
+
"response_action_to_take": "<one of {allowed_actions}>",
|
|
115
|
+
"response_message": "<the new or reply message>"
|
|
116
|
+
}}
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# Decide how to call your structured output function (based on vector store or not).
|
|
120
|
+
# We'll assume there's no vector store ID for LinkedIn. If you do have one, adapt similarly.
|
|
121
|
+
response, status = await get_structured_output_with_o1(
|
|
122
|
+
prompt=prompt,
|
|
123
|
+
response_format=LinkedInTriageResponse,
|
|
124
|
+
tool_config=tool_config
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if status != 'SUCCESS':
|
|
128
|
+
raise Exception("Error in generating the triaged LinkedIn message.")
|
|
129
|
+
|
|
130
|
+
return response.model_dump()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def get_linkedin_response_message(
|
|
134
|
+
lead_info: Dict[str, Any],
|
|
135
|
+
outreach_context: str,
|
|
136
|
+
current_conversations: str,
|
|
137
|
+
additional_instructions: str,
|
|
138
|
+
tool_config: Optional[List[Dict]] = None
|
|
139
|
+
) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Returns only the 'response_message' from the triaged LinkedIn response.
|
|
142
|
+
This includes usage of the outreach (campaign) context to better tailor
|
|
143
|
+
the message to the lead.
|
|
144
|
+
"""
|
|
145
|
+
connect_template = """
|
|
146
|
+
Hi <<first_name>>,
|
|
147
|
+
<< message >>
|
|
148
|
+
Thanks,
|
|
149
|
+
<< sender_name >>
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
triaged_response = await generate_linkedin_response_message(
|
|
153
|
+
lead_info=lead_info,
|
|
154
|
+
outreach_context=outreach_context,
|
|
155
|
+
connect_template=connect_template,
|
|
156
|
+
current_conversations=current_conversations,
|
|
157
|
+
additional_instructions=additional_instructions,
|
|
158
|
+
tool_config=tool_config
|
|
159
|
+
)
|
|
160
|
+
return triaged_response.get("response_message", "")
|
|
@@ -591,7 +591,7 @@ def transform_hubspot_contact_to_lead_info(
|
|
|
591
591
|
Convert a raw HubSpot property dict into a LeadInformation object.
|
|
592
592
|
- Maps known properties (e.g., "company" -> organization_name).
|
|
593
593
|
- If any field value contains "linkedin.com/in/", sets user_linkedin_url.
|
|
594
|
-
- If any field value contains "linkedin.com/company/", sets
|
|
594
|
+
- If any field value contains "linkedin.com/company/", sets organization_linkedin_url .
|
|
595
595
|
- Builds 'full_name' if missing.
|
|
596
596
|
- Stores unmapped fields under result["additional_properties"]["hubspot_lead_information"] as strings.
|
|
597
597
|
"""
|
|
@@ -610,7 +610,7 @@ def transform_hubspot_contact_to_lead_info(
|
|
|
610
610
|
"lead_location": "",
|
|
611
611
|
"organization_name": "",
|
|
612
612
|
"organization_website": "",
|
|
613
|
-
"
|
|
613
|
+
"organization_linkedin_url ": "",
|
|
614
614
|
"additional_properties": {"hubspot_lead_information": {}},
|
|
615
615
|
}
|
|
616
616
|
|
|
@@ -629,7 +629,7 @@ def transform_hubspot_contact_to_lead_info(
|
|
|
629
629
|
result["user_linkedin_url"] = val_str
|
|
630
630
|
|
|
631
631
|
if "linkedin.com/company/" in val_str and is_valid_url(val_str):
|
|
632
|
-
result["
|
|
632
|
+
result["organization_linkedin_url "] = val_str
|
|
633
633
|
|
|
634
634
|
# 3) Build "full_name" if not explicitly given
|
|
635
635
|
if not result["full_name"]:
|
|
@@ -641,7 +641,7 @@ def transform_hubspot_contact_to_lead_info(
|
|
|
641
641
|
# (Skip the ones we already consider "standard")
|
|
642
642
|
standard_mapped_keys = set(HUBSPOT_TO_LEAD_MAPPING.keys()) | {
|
|
643
643
|
"user_linkedin_url",
|
|
644
|
-
"
|
|
644
|
+
"organization_linkedin_url ",
|
|
645
645
|
}
|
|
646
646
|
for prop_key, prop_val in hubspot_contact_properties.items():
|
|
647
647
|
if prop_key not in standard_mapped_keys:
|
|
@@ -435,7 +435,7 @@ async def find_matching_job_posting_proxy_curl(
|
|
|
435
435
|
company_name: str,
|
|
436
436
|
keywords_check: List[str],
|
|
437
437
|
optional_keywords: List[str],
|
|
438
|
-
|
|
438
|
+
organization_linkedin_url : Optional[str] = None,
|
|
439
439
|
tool_config: Optional[List[Dict]] = None
|
|
440
440
|
) -> List[str]:
|
|
441
441
|
"""
|
|
@@ -446,7 +446,7 @@ async def find_matching_job_posting_proxy_curl(
|
|
|
446
446
|
company_name (str): The name of the company.
|
|
447
447
|
keywords_check (List[str]): A list of keywords to include in the search.
|
|
448
448
|
optinal_keywords (List[str]): A list of optional keywords to include in the search.
|
|
449
|
-
|
|
449
|
+
organization_linkedin_url (Optional[str]): The LinkedIn URL of the company.
|
|
450
450
|
|
|
451
451
|
Returns:
|
|
452
452
|
List[str]: A list of job posting links.
|
|
@@ -502,10 +502,10 @@ async def find_matching_job_posting_proxy_curl(
|
|
|
502
502
|
|
|
503
503
|
text = json.dumps(json_result).lower()
|
|
504
504
|
|
|
505
|
-
if
|
|
505
|
+
if organization_linkedin_url and json_result.get('company', {}) and json_result.get('company', {}).get('url', ''):
|
|
506
506
|
result_url = json_result.get('company', {}).get('url', '').lower()
|
|
507
507
|
result_path = urlparse(result_url).path
|
|
508
|
-
company_path = urlparse(
|
|
508
|
+
company_path = urlparse(organization_linkedin_url .lower()).path
|
|
509
509
|
company_match = result_path == company_path
|
|
510
510
|
else:
|
|
511
511
|
company_match = False
|
|
@@ -632,12 +632,12 @@ async def enrich_user_info_with_proxy_curl(input_user_properties, tool_config):
|
|
|
632
632
|
input_user_properties["lead_location"] = combined.strip(", ")
|
|
633
633
|
|
|
634
634
|
if experiences and len(experiences) > 0:
|
|
635
|
-
|
|
636
|
-
if
|
|
637
|
-
input_user_properties["
|
|
635
|
+
organization_linkedin_url = experiences[0].get("company_linkedin_profile_url", "")
|
|
636
|
+
if organization_linkedin_url :
|
|
637
|
+
input_user_properties["organization_linkedin_url "] = organization_linkedin_url
|
|
638
638
|
#TODO enrich company information optionally
|
|
639
|
-
# if
|
|
640
|
-
# organization_data = await enrich_organization_info_from_proxycurl(organization_domain="", organization_linkedin_url=
|
|
639
|
+
# if organization_linkedin_url :
|
|
640
|
+
# organization_data = await enrich_organization_info_from_proxycurl(organization_domain="", organization_linkedin_url=organization_linkedin_url , tool_config=tool_config)
|
|
641
641
|
# if organization_data:
|
|
642
642
|
# additional_props["proxy_curl_organization_data"] = organization_data
|
|
643
643
|
# input_user_properties["website"] = organization_data.get("website", "")
|
|
@@ -650,9 +650,9 @@ async def enrich_user_info_with_proxy_curl(input_user_properties, tool_config):
|
|
|
650
650
|
|
|
651
651
|
if experiences and len(experiences) > 1:
|
|
652
652
|
previous_org = experiences[1]
|
|
653
|
-
|
|
654
|
-
if
|
|
655
|
-
input_user_properties["
|
|
653
|
+
previous_organization_linkedin_url = previous_org.get("company_linkedin_profile_url", "")
|
|
654
|
+
if previous_organization_linkedin_url :
|
|
655
|
+
input_user_properties["previous_organization_linkedin_url "] = previous_organization_linkedin_url
|
|
656
656
|
input_user_properties["previous_organization_name"] = previous_org.get("company", "")
|
|
657
657
|
|
|
658
658
|
first_matched = bool(
|
|
@@ -179,7 +179,7 @@ async def find_button_by_name(page: Page, button_name: str) -> Any:
|
|
|
179
179
|
return b
|
|
180
180
|
return None
|
|
181
181
|
|
|
182
|
-
async def
|
|
182
|
+
async def send_linkedin_message(page: Page, command_args: Dict[str, Any]) -> Dict[str, Any]:
|
|
183
183
|
await goto_url(page, command_args)
|
|
184
184
|
|
|
185
185
|
if "/sales/lead/" not in page.url:
|
|
@@ -368,7 +368,7 @@ async def extract_leads_from_current_page(page):
|
|
|
368
368
|
|
|
369
369
|
# Initialize these if you want placeholders (as in JS)
|
|
370
370
|
lead["user_linkedin_url"] = ""
|
|
371
|
-
lead["
|
|
371
|
+
lead["organization_linkedin_url "] = ""
|
|
372
372
|
|
|
373
373
|
# About / Experience
|
|
374
374
|
dt_elements = div.find_all("dt")
|
|
@@ -399,10 +399,10 @@ async def extract_leads_from_current_page(page):
|
|
|
399
399
|
comma_index = linkedin_id.find(",")
|
|
400
400
|
if comma_index != -1:
|
|
401
401
|
linkedin_id = linkedin_id[:comma_index]
|
|
402
|
-
lead["
|
|
402
|
+
lead["user_linkedin_salesnav_url"] = f"https://www.linkedin.com/sales/lead/{linkedin_id}"
|
|
403
403
|
else:
|
|
404
404
|
# Fallback if we didn't match or want to keep the full URL
|
|
405
|
-
lead["
|
|
405
|
+
lead["user_linkedin_salesnav_url"] = f"https://www.linkedin.com{href}"
|
|
406
406
|
|
|
407
407
|
company_link_el = div.select_one('a[href^="/sales/company/"]')
|
|
408
408
|
if company_link_el:
|
|
@@ -415,9 +415,9 @@ async def extract_leads_from_current_page(page):
|
|
|
415
415
|
comma_index = linkedin_id.find(",")
|
|
416
416
|
if comma_index != -1:
|
|
417
417
|
linkedin_id = linkedin_id[:comma_index]
|
|
418
|
-
lead["
|
|
418
|
+
lead["organization_linkedin_salesnav_url "] = f"https://www.linkedin.com/sales/company/{linkedin_id}"
|
|
419
419
|
else:
|
|
420
|
-
lead["
|
|
420
|
+
lead["organization_linkedin_salesnav_url "] = f"https://www.linkedin.com{href}"
|
|
421
421
|
|
|
422
422
|
leads.append(lead)
|
|
423
423
|
|
|
@@ -573,7 +573,7 @@ async def extract_account_from_current_page(page: Page):
|
|
|
573
573
|
|
|
574
574
|
# If we successfully copied the LinkedIn.com URL, place it into the dictionary
|
|
575
575
|
if 'copied_text' in locals() and "linkedin.com/company/" in copied_text:
|
|
576
|
-
company["
|
|
576
|
+
company["organization_linkedin_url "] = copied_text
|
|
577
577
|
|
|
578
578
|
return [company], "SUCCESS"
|
|
579
579
|
|
|
@@ -787,7 +787,7 @@ command_to_function_mapping = {
|
|
|
787
787
|
"navigate_to_url": navigate_to_url,
|
|
788
788
|
"view_linked_in_profile": view_linked_in_profile,
|
|
789
789
|
"send_connection_request": send_connection_request,
|
|
790
|
-
"
|
|
790
|
+
"send_linkedin_message": send_linkedin_message,
|
|
791
791
|
"get_current_messages": get_current_messages,
|
|
792
792
|
"extract_leads_information": extract_leads,
|
|
793
793
|
"extract_lead_information": extract_lead,
|
|
@@ -989,7 +989,7 @@ def check_and_update_throttling_limits(task: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
989
989
|
"view_linked_in_profile": 500,
|
|
990
990
|
"navigate_to_url": 500,
|
|
991
991
|
"like_recent_post": 10,
|
|
992
|
-
"
|
|
992
|
+
"send_linkedin_message": 10
|
|
993
993
|
}
|
|
994
994
|
limit = daily_limits.get(cmd_id, 25)
|
|
995
995
|
|
|
@@ -585,7 +585,7 @@ def extract_company_page(url: str) -> str:
|
|
|
585
585
|
|
|
586
586
|
|
|
587
587
|
@assistant_tool
|
|
588
|
-
async def
|
|
588
|
+
async def find_organization_linkedin_url_with_google_search(
|
|
589
589
|
company_name: str,
|
|
590
590
|
company_location: Optional[str] = None,
|
|
591
591
|
use_strict_check: bool = True,
|
|
@@ -702,7 +702,7 @@ async def get_company_website_from_linkedin_url(linkedin_url):
|
|
|
702
702
|
async def find_job_postings_google_search(
|
|
703
703
|
company_name: str,
|
|
704
704
|
keywords_check: List[str],
|
|
705
|
-
|
|
705
|
+
organization_linkedin_url : Optional[str] = None,
|
|
706
706
|
tool_config: Optional[List[Dict]] = None,
|
|
707
707
|
) -> List[str]:
|
|
708
708
|
"""
|
|
@@ -710,7 +710,7 @@ async def find_job_postings_google_search(
|
|
|
710
710
|
|
|
711
711
|
Args:
|
|
712
712
|
company_name (str): The name of the company.
|
|
713
|
-
|
|
713
|
+
organization_linkedin_url (Optional[str]): The LinkedIn URL of the company.
|
|
714
714
|
keywords_check (List[str]): A list of keywords to include in the search.
|
|
715
715
|
|
|
716
716
|
Returns:
|
|
@@ -758,11 +758,11 @@ async def find_job_postings_google_search(
|
|
|
758
758
|
# Extract all hrefs from the page
|
|
759
759
|
page_links = [a.get('href') for a in soup.find_all('a', href=True)]
|
|
760
760
|
|
|
761
|
-
# Check if
|
|
761
|
+
# Check if organization_linkedin_url and 'public_jobs_topcard-org-name' are in the page links
|
|
762
762
|
company_match = False
|
|
763
|
-
if
|
|
763
|
+
if organization_linkedin_url :
|
|
764
764
|
for page_link in page_links:
|
|
765
|
-
if (page_link and
|
|
765
|
+
if (page_link and organization_linkedin_url in page_link and
|
|
766
766
|
'public_jobs_topcard-org-name' in page_link):
|
|
767
767
|
company_match = True
|
|
768
768
|
break
|
|
@@ -47,6 +47,7 @@ src/dhisana/utils/generate_email.py
|
|
|
47
47
|
src/dhisana/utils/generate_email_response.py
|
|
48
48
|
src/dhisana/utils/generate_flow.py
|
|
49
49
|
src/dhisana/utils/generate_linkedin_connect_message.py
|
|
50
|
+
src/dhisana/utils/generate_linkedin_response_message.py
|
|
50
51
|
src/dhisana/utils/generate_structured_output_internal.py
|
|
51
52
|
src/dhisana/utils/google_custom_search.py
|
|
52
53
|
src/dhisana/utils/google_workspace_tools.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/generate_structured_output_internal.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev23 → dhisana-0.0.1.dev25}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|