dhisana 0.0.1.dev18__tar.gz → 0.0.1.dev20__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.dev18 → dhisana-0.0.1.dev20}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/setup.py +1 -1
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/schemas/common.py +18 -23
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/schemas/sales.py +2 -10
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_email.py +41 -16
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/hubspot_crm_tools.py +56 -49
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openai_assistant_and_file_utils.py +1 -1
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/README.md +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/setup.cfg +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/agent_task.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/compose_workflow.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/SOURCES.txt +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/tests/test_agent_tools.py +0 -0
|
@@ -9,6 +9,7 @@ from enum import Enum
|
|
|
9
9
|
|
|
10
10
|
class PermissionBase(BaseModel):
|
|
11
11
|
name: str
|
|
12
|
+
label: Optional[str] = None
|
|
12
13
|
description: Optional[str] = None
|
|
13
14
|
|
|
14
15
|
class PermissionCreate(PermissionBase):
|
|
@@ -22,6 +23,7 @@ class Permission(PermissionBase):
|
|
|
22
23
|
|
|
23
24
|
class RoleBase(BaseModel):
|
|
24
25
|
name: str
|
|
26
|
+
label: Optional[str] = None
|
|
25
27
|
description: Optional[str] = None
|
|
26
28
|
|
|
27
29
|
class RoleCreate(RoleBase):
|
|
@@ -59,23 +61,18 @@ class Organization(OrganizationBase):
|
|
|
59
61
|
# USER
|
|
60
62
|
##
|
|
61
63
|
|
|
62
|
-
class
|
|
63
|
-
id:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
class User(BaseModel):
|
|
65
|
+
id: Optional[UUID] = None
|
|
66
|
+
auth0_user_id: Optional[str] = None
|
|
67
|
+
username: Optional[str] = None
|
|
68
|
+
email: Optional[str] = None
|
|
69
|
+
organization_id: Optional[UUID] = None
|
|
67
70
|
created_at: Optional[int] = None
|
|
68
|
-
|
|
69
|
-
class UserCreate(UserBase):
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
class User(UserBase):
|
|
73
|
-
roles: List[Role] = []
|
|
71
|
+
roles: Optional[List[Role]] = []
|
|
74
72
|
|
|
75
73
|
class Config:
|
|
76
74
|
from_attributes = True
|
|
77
75
|
|
|
78
|
-
|
|
79
76
|
##
|
|
80
77
|
# AGENT + STATUSES
|
|
81
78
|
##
|
|
@@ -225,12 +222,9 @@ class Agent(BaseModel):
|
|
|
225
222
|
status: Optional[AgentStatus] = AgentStatus.PENDING
|
|
226
223
|
|
|
227
224
|
organization_id: Optional[UUID] = None
|
|
228
|
-
created_by: Optional[
|
|
229
|
-
|
|
230
|
-
# changed from datetime -> milliseconds since epoch
|
|
225
|
+
created_by: Optional[UUID] = None
|
|
231
226
|
created_at: Optional[int] = None
|
|
232
|
-
|
|
233
|
-
updated_by: Optional[str] = None
|
|
227
|
+
updated_by: Optional[UUID] = None
|
|
234
228
|
updated_at: Optional[int] = None
|
|
235
229
|
|
|
236
230
|
class Config:
|
|
@@ -259,10 +253,9 @@ class AgentInstance(BaseModel):
|
|
|
259
253
|
port: Optional[int] = None
|
|
260
254
|
|
|
261
255
|
organization_id: Optional[UUID] = None
|
|
262
|
-
created_by: Optional[
|
|
263
|
-
# changed from datetime -> milliseconds
|
|
256
|
+
created_by: Optional[UUID] = None
|
|
264
257
|
created_at: Optional[int] = None
|
|
265
|
-
updated_by: Optional[
|
|
258
|
+
updated_by: Optional[UUID] = None
|
|
266
259
|
updated_at: Optional[int] = None
|
|
267
260
|
|
|
268
261
|
class Config:
|
|
@@ -281,8 +274,9 @@ class AgentInstanceData(BaseModel):
|
|
|
281
274
|
agent_instance_id: UUID
|
|
282
275
|
organization_id: UUID
|
|
283
276
|
data: Dict[str, Any]
|
|
284
|
-
|
|
277
|
+
created_by: Optional[UUID] = None
|
|
285
278
|
created_at: Optional[int] = None
|
|
279
|
+
updated_by: Optional[UUID] = None
|
|
286
280
|
updated_at: Optional[int] = None
|
|
287
281
|
|
|
288
282
|
class Config:
|
|
@@ -318,6 +312,7 @@ class Source(SourceBase):
|
|
|
318
312
|
agent_instance_id: UUID
|
|
319
313
|
# changed from datetime -> milliseconds
|
|
320
314
|
created_at: Optional[int] = None
|
|
315
|
+
created_by: Optional[UUID] = None
|
|
321
316
|
|
|
322
317
|
class Config:
|
|
323
318
|
populate_by_name = True
|
|
@@ -358,9 +353,9 @@ class IntegrationUpdate(BaseModel):
|
|
|
358
353
|
class Integration(IntegrationBase):
|
|
359
354
|
id: UUID
|
|
360
355
|
agent_instance_id: UUID
|
|
361
|
-
organization_id: UUID
|
|
362
|
-
# changed from datetime -> milliseconds
|
|
356
|
+
organization_id: UUID
|
|
363
357
|
created_at: Optional[int] = None
|
|
358
|
+
created_by: Optional[UUID] = None
|
|
364
359
|
|
|
365
360
|
class Config:
|
|
366
361
|
from_attributes = True
|
|
@@ -25,7 +25,6 @@ class Lead(BaseModel):
|
|
|
25
25
|
organization_name: Optional[str] = None
|
|
26
26
|
organization_website: Optional[str] = None
|
|
27
27
|
summary_about_lead: Optional[str] = None
|
|
28
|
-
keywords: Optional[List[str]] = None
|
|
29
28
|
workflow_stage: Optional[str] = None
|
|
30
29
|
assigned_to: Optional[str] = None
|
|
31
30
|
engaged: Optional[bool] = None
|
|
@@ -244,6 +243,7 @@ class HubSpotLeadInformation(BaseModel):
|
|
|
244
243
|
lead_location: str = Field("", description="Location of the lead")
|
|
245
244
|
organization_name: str = Field("", description="Current Company where lead works")
|
|
246
245
|
organization_website: str = Field("", description="Current Company website of the lead")
|
|
246
|
+
company_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(
|
|
@@ -265,12 +265,4 @@ HUBSPOT_TO_LEAD_MAPPING = {
|
|
|
265
265
|
"address": "lead_location", # You can choose "city", "state", etc. if you prefer
|
|
266
266
|
"city": "lead_location",
|
|
267
267
|
"domain": "primary_domain_of_organization",
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
# Potential LinkedIn fallback properties from hubspot.
|
|
271
|
-
LINKEDIN_FALLBACK_FIELDS = [
|
|
272
|
-
"person_profile_url",
|
|
273
|
-
"user_linkedin_url",
|
|
274
|
-
"lead_linkedin_url",
|
|
275
|
-
"contact_linkedin_url",
|
|
276
|
-
]
|
|
268
|
+
}
|
|
@@ -83,31 +83,51 @@ async def generate_personalized_email_copy(
|
|
|
83
83
|
You’re an expert at crafting professional, concise, and compelling sales outreach emails.
|
|
84
84
|
Use the details below to self-contain your reasoning, ensuring personalization, a clear value proposition,
|
|
85
85
|
and adherence to the specified email template. Avoid spam triggers or irrelevant info.
|
|
86
|
+
|
|
87
|
+
**Important**:
|
|
88
|
+
1. Do your reasoning internally.
|
|
89
|
+
2. Do NOT include the chain-of-thought in the output.
|
|
90
|
+
3. The final answer must be a JSON object containing only the fields 'subject' and 'body'.
|
|
91
|
+
4. This is final copy of the email to be sent to the lead directly. DO NOT include any placeholders, comments or instructions in the final output.
|
|
92
|
+
|
|
86
93
|
Make sure to use the file_search with attached files to generate the personalized email.
|
|
87
94
|
The attached files have relevant information on case studies, product details, and customer testimonials.
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
|
|
96
|
+
Steps:
|
|
97
|
+
1. Summarize the user’s role and experience.
|
|
98
|
+
2. Summarize the company and what it does.
|
|
99
|
+
3. Highlight how our product offering/campaign aligns with the user’s needs and company goals.
|
|
100
|
+
4. Craft a personalized email with a compelling reason to reach out and a clear CTA.
|
|
101
|
+
|
|
102
|
+
Pro Tips for B2B Enterprise Emails:
|
|
103
|
+
- Personalization: Reference the prospect’s role, recent activities, relevant vertical solutions, or any mutual connections.
|
|
104
|
+
- Brevity: Keep it concise.
|
|
105
|
+
- Social Proof: Mention relevant success stories if applicable.
|
|
106
|
+
- Clear CTA: End with a single call to action.
|
|
107
|
+
|
|
108
|
+
Lead Information & Campaign Details provided by user below:
|
|
90
109
|
{email_context.model_dump()}
|
|
91
|
-
|
|
92
|
-
Following property has information about the sender of the email (Can be used in Signature):
|
|
93
|
-
sender_first_name: str
|
|
94
|
-
sender_last_name: str
|
|
95
|
-
|
|
96
|
-
Following property has information about the lead whom the email is being sent to:
|
|
97
|
-
lead_info: Lead
|
|
98
110
|
|
|
111
|
+
Sender Info (Signature Use):
|
|
112
|
+
- sender_first_name: str
|
|
113
|
+
- sender_last_name: str
|
|
114
|
+
|
|
115
|
+
Lead Info:
|
|
116
|
+
- lead_info: Lead
|
|
99
117
|
|
|
100
118
|
Output Format (JSON):
|
|
101
119
|
{{
|
|
102
120
|
"subject": "Personalized subject line.",
|
|
103
121
|
"body": "Personalized email body content."
|
|
104
122
|
}}
|
|
105
|
-
|
|
123
|
+
|
|
124
|
+
Use the following info for this variation:
|
|
106
125
|
{variation}
|
|
107
|
-
|
|
126
|
+
|
|
108
127
|
After writing, review the content for relevance, clarity, and professionalism.
|
|
109
|
-
Use personalization
|
|
110
|
-
such as city or school information.
|
|
128
|
+
Use personalization ONLY when relevant for the campaign and product; don’t add irrelevant details
|
|
129
|
+
such as city or school information. DO NOT USE any user identifiers, PII, tracking IDs, internal
|
|
130
|
+
information like deal size, or any other sensitive information in email body generated.
|
|
111
131
|
"""
|
|
112
132
|
|
|
113
133
|
response, status = await get_structured_output_with_assistant_and_vector_store(prompt=prompt,
|
|
@@ -139,9 +159,14 @@ async def generate_personalized_email(
|
|
|
139
159
|
Exception: If there is an error in processing the request.
|
|
140
160
|
"""
|
|
141
161
|
variation_specs = [
|
|
142
|
-
"Explain how the product addresses the company’s current goals and requirements.",
|
|
143
|
-
"
|
|
144
|
-
"
|
|
162
|
+
"Use VETO framework (Value, Evidence, Tie, Offer) to compose email.Explain how the product addresses the company’s current goals and requirements.",
|
|
163
|
+
"Use AIDA framework (Attention, Interest, Desire, Action) to compose email.",
|
|
164
|
+
"Use PAS (Problem, Agitate, Solve) framework to write up email.",
|
|
165
|
+
"Use SPIN (Situation, Problem, Implication, Need-Payoff) framework to write up email.",
|
|
166
|
+
"Use BANT (Budget, Authority, Need, Timeline) framework to write up email.",
|
|
167
|
+
"Use P-S-B (Pain, Solution, Benefit) framework to write up email."
|
|
168
|
+
"Use The 3-Bullet Approach (1. Industry Trend or Pain, 2. Value Statement 3. Simple Ask) framework to write up email. Keep it under 100 words."
|
|
169
|
+
"Use Hook, Insight, Offer framework to write up email."
|
|
145
170
|
]
|
|
146
171
|
email_variations = []
|
|
147
172
|
for i in range(number_of_variations):
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Hubspot CRM Tools
|
|
2
2
|
import os
|
|
3
3
|
import aiohttp
|
|
4
|
-
from dhisana.schemas.sales import HUBSPOT_TO_LEAD_MAPPING,
|
|
4
|
+
from dhisana.schemas.sales import HUBSPOT_TO_LEAD_MAPPING, HubSpotLeadInformation
|
|
5
5
|
from dhisana.utils.assistant_tool_tag import assistant_tool
|
|
6
6
|
from typing import Optional, List, Dict, Any
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
|
+
from urllib.parse import urlparse
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
# Tools to access hubspot CRM. Manage contacts, companies, deals, tickets, lists, etc.
|
|
@@ -571,27 +572,31 @@ async def fetch_last_n_activities(email: str, num_events: int, tool_config: Opti
|
|
|
571
572
|
activities_data = await response.json()
|
|
572
573
|
|
|
573
574
|
return activities_data
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
575
|
|
|
579
576
|
# --------------------------------------------------------------------
|
|
580
|
-
# 3. Helper to transform HubSpot props ->
|
|
577
|
+
# 3. Helper to transform HubSpot props -> HubSpotLeadInformation,
|
|
581
578
|
# storing unmapped fields in "additional_properties" (as strings)
|
|
582
579
|
# --------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def is_valid_url(url: str) -> bool:
|
|
583
|
+
"""Quickly check if the given string is a well-formed http/https URL."""
|
|
584
|
+
parsed = urlparse(url)
|
|
585
|
+
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
|
|
586
|
+
|
|
583
587
|
def transform_hubspot_contact_to_lead_info(
|
|
584
588
|
hubspot_contact_properties: Dict[str, Any]
|
|
585
589
|
) -> HubSpotLeadInformation:
|
|
586
590
|
"""
|
|
587
591
|
Convert a raw HubSpot property dict into a LeadInformation object.
|
|
588
592
|
- Maps known properties (e.g., "company" -> organization_name).
|
|
589
|
-
-
|
|
593
|
+
- If any field value contains "linkedin.com/in/", sets user_linkedin_url.
|
|
594
|
+
- If any field value contains "linkedin.com/company/", sets company_linkedin_url.
|
|
590
595
|
- Builds 'full_name' if missing.
|
|
591
596
|
- Stores unmapped fields under result["additional_properties"]["hubspot_lead_information"] as strings.
|
|
592
597
|
"""
|
|
593
598
|
|
|
594
|
-
# Prepare the result dict we'll use for the
|
|
599
|
+
# Prepare the result dict we'll use for the HubSpotLeadInformation constructor.
|
|
595
600
|
result = {
|
|
596
601
|
"full_name": "",
|
|
597
602
|
"first_name": "",
|
|
@@ -605,39 +610,26 @@ def transform_hubspot_contact_to_lead_info(
|
|
|
605
610
|
"lead_location": "",
|
|
606
611
|
"organization_name": "",
|
|
607
612
|
"organization_website": "",
|
|
613
|
+
"company_linkedin_url": "",
|
|
608
614
|
"additional_properties": {"hubspot_lead_information": {}},
|
|
609
615
|
}
|
|
610
616
|
|
|
611
|
-
#
|
|
612
|
-
mapped_keys = set(HUBSPOT_TO_LEAD_MAPPING.keys())
|
|
613
|
-
# Add the fallback fields to skip them in additional_properties
|
|
614
|
-
fallback_keys = set(LINKEDIN_FALLBACK_FIELDS)
|
|
615
|
-
# "user_linkedin_url" is also a standard field to check
|
|
616
|
-
mapped_keys.add("user_linkedin_url")
|
|
617
|
-
|
|
618
|
-
# 1) Map standard HubSpot properties
|
|
617
|
+
# 1) Map standard HubSpot properties to our known fields
|
|
619
618
|
for hubspot_prop, raw_value in hubspot_contact_properties.items():
|
|
620
|
-
# If property is in our known mapping, store it in the correct field
|
|
621
619
|
if hubspot_prop in HUBSPOT_TO_LEAD_MAPPING:
|
|
622
620
|
lead_field_name = HUBSPOT_TO_LEAD_MAPPING[hubspot_prop]
|
|
623
|
-
# Convert the value to a string if it's not None
|
|
624
621
|
val_str = str(raw_value) if raw_value is not None else ""
|
|
625
622
|
result[lead_field_name] = val_str
|
|
626
623
|
|
|
627
|
-
# 2)
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
break
|
|
637
|
-
result["user_linkedin_url"] = found_linkedin_url
|
|
638
|
-
else:
|
|
639
|
-
# If it's explicitly set, cast to string
|
|
640
|
-
result["user_linkedin_url"] = str(hubspot_linkedin_url) or ""
|
|
624
|
+
# 2) Look for any LinkedIn-related URLs in *any* property
|
|
625
|
+
for prop_key, prop_val in hubspot_contact_properties.items():
|
|
626
|
+
val_str = str(prop_val) if prop_val is not None else ""
|
|
627
|
+
|
|
628
|
+
if "linkedin.com/in/" in val_str and is_valid_url(val_str):
|
|
629
|
+
result["user_linkedin_url"] = val_str
|
|
630
|
+
|
|
631
|
+
if "linkedin.com/company/" in val_str and is_valid_url(val_str):
|
|
632
|
+
result["company_linkedin_url"] = val_str
|
|
641
633
|
|
|
642
634
|
# 3) Build "full_name" if not explicitly given
|
|
643
635
|
if not result["full_name"]:
|
|
@@ -646,9 +638,13 @@ def transform_hubspot_contact_to_lead_info(
|
|
|
646
638
|
result["full_name"] = (fn + " " + ln).strip()
|
|
647
639
|
|
|
648
640
|
# 4) Store any remaining/unmapped properties in additional_properties
|
|
641
|
+
# (Skip the ones we already consider "standard")
|
|
642
|
+
standard_mapped_keys = set(HUBSPOT_TO_LEAD_MAPPING.keys()) | {
|
|
643
|
+
"user_linkedin_url",
|
|
644
|
+
"company_linkedin_url",
|
|
645
|
+
}
|
|
649
646
|
for prop_key, prop_val in hubspot_contact_properties.items():
|
|
650
|
-
if prop_key not in
|
|
651
|
-
# Convert to string to avoid Pydantic errors
|
|
647
|
+
if prop_key not in standard_mapped_keys:
|
|
652
648
|
val_str = str(prop_val) if prop_val is not None else ""
|
|
653
649
|
result["additional_properties"]["hubspot_lead_information"][prop_key] = val_str
|
|
654
650
|
|
|
@@ -743,7 +739,7 @@ async def fetch_hubspot_list_records(
|
|
|
743
739
|
# --------------------------------------------------------------------
|
|
744
740
|
# Step 3: Batch read each contact to fetch all their properties
|
|
745
741
|
# --------------------------------------------------------------------
|
|
746
|
-
contact_leads: List[
|
|
742
|
+
contact_leads: List[HubSpotLeadInformation] = []
|
|
747
743
|
batch_url = "https://api.hubapi.com/crm/v3/objects/contacts/batch/read"
|
|
748
744
|
batch_size = 100 # Up to 100 IDs per request
|
|
749
745
|
|
|
@@ -1251,12 +1247,14 @@ async def fetch_hubspot_list_by_name(list_name: str, list_type: str = 'contacts'
|
|
|
1251
1247
|
raise Exception(f"Error: Received status code {response.status} with details: {error_details}")
|
|
1252
1248
|
|
|
1253
1249
|
|
|
1250
|
+
|
|
1254
1251
|
@assistant_tool
|
|
1255
1252
|
async def list_all_crm_lists(payload=None, list_type="contacts", tool_config: Optional[List[Dict]] = None):
|
|
1256
1253
|
"""
|
|
1257
1254
|
Fetches CRM lists from HubSpot with the option to filter by objectTypeId
|
|
1258
1255
|
for contacts, companies, etc. Defaults to contacts.
|
|
1259
1256
|
"""
|
|
1257
|
+
|
|
1260
1258
|
object_type_map = {
|
|
1261
1259
|
"contacts": "0-1",
|
|
1262
1260
|
"companies": "0-2",
|
|
@@ -1281,18 +1279,27 @@ async def list_all_crm_lists(payload=None, list_type="contacts", tool_config: Op
|
|
|
1281
1279
|
"Content-Type": "application/json"
|
|
1282
1280
|
}
|
|
1283
1281
|
|
|
1282
|
+
object_id = object_type_map.get(list_type, "0-1")
|
|
1283
|
+
all_results = []
|
|
1284
|
+
|
|
1284
1285
|
async with aiohttp.ClientSession() as session:
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1286
|
+
while True:
|
|
1287
|
+
async with session.post(url, headers=headers, json=payload) as response:
|
|
1288
|
+
if response.status == 200:
|
|
1289
|
+
data = await response.json()
|
|
1290
|
+
page_lists = data.get("lists", [])
|
|
1291
|
+
filtered_lists = [
|
|
1292
|
+
lst for lst in page_lists if lst.get("objectTypeId") == object_id
|
|
1293
|
+
]
|
|
1294
|
+
all_results.extend(filtered_lists)
|
|
1295
|
+
|
|
1296
|
+
if not data.get("hasMore"):
|
|
1297
|
+
break
|
|
1298
|
+
payload["offset"] = data.get("offset", 0)
|
|
1299
|
+
else:
|
|
1300
|
+
error_details = await response.text()
|
|
1301
|
+
raise Exception(
|
|
1302
|
+
f"Error: Received status code {response.status} with details: {error_details}"
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
return all_results
|
{dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
@@ -6,7 +6,7 @@ from typing import AsyncIterable, Dict, List, Optional
|
|
|
6
6
|
|
|
7
7
|
import openai
|
|
8
8
|
from fastapi import HTTPException
|
|
9
|
-
from openai import AsyncAssistantEventHandler, AsyncOpenAI
|
|
9
|
+
from openai import AsyncAssistantEventHandler, AsyncOpenAI
|
|
10
10
|
from openai.types.beta import Thread
|
|
11
11
|
from typing_extensions import override
|
|
12
12
|
from pydantic import BaseModel
|
|
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.dev18 → dhisana-0.0.1.dev20}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/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
|
|
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
|
|
File without changes
|
|
File without changes
|