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.
Files changed (76) hide show
  1. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/setup.py +1 -1
  3. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/schemas/common.py +18 -23
  4. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/schemas/sales.py +2 -10
  5. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_email.py +41 -16
  6. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/hubspot_crm_tools.py +56 -49
  7. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openai_assistant_and_file_utils.py +1 -1
  8. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/PKG-INFO +1 -1
  9. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/README.md +0 -0
  10. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/pyproject.toml +0 -0
  11. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/setup.cfg +0 -0
  12. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/__init__.py +0 -0
  13. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/__init__.py +0 -0
  14. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/cli.py +0 -0
  15. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/datasets.py +0 -0
  16. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/models.py +0 -0
  17. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/cli/predictions.py +0 -0
  18. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/schemas/__init__.py +0 -0
  19. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/ui/__init__.py +0 -0
  20. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/ui/components.py +0 -0
  21. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/__init__.py +0 -0
  22. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/agent_task.py +0 -0
  23. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/agent_tools.py +0 -0
  24. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/apollo_tools.py +0 -0
  25. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  26. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/built_with_api_tools.py +0 -0
  27. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/cache_output_tools.py +0 -0
  28. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  29. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  30. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  31. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/clay_tools.py +0 -0
  32. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/company_utils.py +0 -0
  33. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  34. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/compose_workflow.py +0 -0
  35. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/composite_tools.py +0 -0
  36. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/dataframe_tools.py +0 -0
  37. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/domain_parser.py +0 -0
  38. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/enrich_lead_information.py +0 -0
  39. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/g2_tools.py +0 -0
  40. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_email_response.py +0 -0
  41. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_flow.py +0 -0
  42. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  43. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  44. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/google_custom_search.py +0 -0
  45. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/google_workspace_tools.py +0 -0
  46. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  47. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/instantly_tools.py +0 -0
  48. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/linkedin_crawler.py +0 -0
  49. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/lusha_tools.py +0 -0
  50. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openai_helpers.py +0 -0
  51. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  52. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  53. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  54. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  55. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  56. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  57. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/python_function_to_tools.py +0 -0
  58. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/research_lead.py +0 -0
  59. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  60. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  61. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/sendgrid_tools.py +0 -0
  62. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  63. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/trasform_json.py +0 -0
  64. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  65. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/utils/zoominfo_tools.py +0 -0
  66. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/__init__.py +0 -0
  67. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/agent.py +0 -0
  68. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/flow.py +0 -0
  69. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/task.py +0 -0
  70. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana/workflow/test.py +0 -0
  71. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/SOURCES.txt +0 -0
  72. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/dependency_links.txt +0 -0
  73. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/entry_points.txt +0 -0
  74. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/requires.txt +0 -0
  75. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/src/dhisana.egg-info/top_level.txt +0 -0
  76. {dhisana-0.0.1.dev18 → dhisana-0.0.1.dev20}/tests/test_agent_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dhisana
3
- Version: 0.0.1.dev18
3
+ Version: 0.0.1.dev20
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='dhisana',
5
- version='0.0.1-dev18',
5
+ version='0.0.1-dev20',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -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 UserBase(BaseModel):
63
- id: str
64
- username: Optional[str]
65
- email: Optional[str]
66
- organization_id: Optional[UUID]
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[str] = None
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[str] = None
263
- # changed from datetime -> milliseconds
256
+ created_by: Optional[UUID] = None
264
257
  created_at: Optional[int] = None
265
- updated_by: Optional[str] = None
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
- # changed from datetime -> milliseconds
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
- Lead & Campaign Details:
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
- Use following info for this variation:
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 only when relevant for the campaign and product; don’t add irrelevant details
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
- "Provide proof points or success stories demonstrating how the solution supports similar businesses in the same industry.",
144
- "Showcase how this product outperforms competing solutions and benefits companies like the lead’s."
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, LINKEDIN_FALLBACK_FIELDS, HubSpotLeadInformation
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 -> LeadInformation,
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
- - Handles user_linkedin_url fallback from fallback fields.
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 LeadInformation constructor.
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
- # For convenience, create a set of properties we consider "handled" or "standard".
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) Handle user_linkedin_url fallback
628
- # If 'user_linkedin_url' not present or empty, check fallback fields.
629
- hubspot_linkedin_url = hubspot_contact_properties.get("user_linkedin_url")
630
- if not hubspot_linkedin_url:
631
- found_linkedin_url = ""
632
- for alt_field in LINKEDIN_FALLBACK_FIELDS:
633
- alt_val = hubspot_contact_properties.get(alt_field)
634
- if alt_val:
635
- found_linkedin_url = str(alt_val)
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 mapped_keys and prop_key not in fallback_keys:
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[LeadInformation] = []
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
- async with session.post(url, headers=headers, json=payload) as response:
1286
- if response.status == 200:
1287
- data = await response.json()
1288
- all_lists = data.get("lists", [])
1289
- object_id = object_type_map.get(list_type, "0-1")
1290
- filtered_lists = [
1291
- lst for lst in all_lists if lst.get("objectTypeId") == object_id
1292
- ]
1293
- return filtered_lists
1294
- else:
1295
- error_details = await response.text()
1296
- raise Exception(
1297
- f"Error: Received status code {response.status} with details: {error_details}"
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
@@ -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, OpenAI
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dhisana
3
- Version: 0.0.1.dev18
3
+ Version: 0.0.1.dev20
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
File without changes
File without changes