dhisana 0.0.1.dev85__py3-none-any.whl → 0.0.1.dev236__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dhisana/schemas/common.py +33 -0
- dhisana/schemas/sales.py +224 -23
- dhisana/utils/add_mapping.py +72 -63
- dhisana/utils/apollo_tools.py +739 -109
- dhisana/utils/built_with_api_tools.py +4 -2
- dhisana/utils/cache_output_tools.py +23 -23
- dhisana/utils/check_email_validity_tools.py +456 -458
- dhisana/utils/check_for_intent_signal.py +1 -2
- dhisana/utils/check_linkedin_url_validity.py +34 -8
- dhisana/utils/clay_tools.py +3 -2
- dhisana/utils/clean_properties.py +3 -1
- dhisana/utils/compose_salesnav_query.py +0 -1
- dhisana/utils/compose_search_query.py +7 -3
- dhisana/utils/composite_tools.py +0 -1
- dhisana/utils/dataframe_tools.py +2 -2
- dhisana/utils/email_body_utils.py +72 -0
- dhisana/utils/email_provider.py +375 -0
- dhisana/utils/enrich_lead_information.py +585 -85
- dhisana/utils/fetch_openai_config.py +129 -0
- dhisana/utils/field_validators.py +1 -1
- dhisana/utils/g2_tools.py +0 -1
- dhisana/utils/generate_content.py +0 -1
- dhisana/utils/generate_email.py +69 -16
- dhisana/utils/generate_email_response.py +298 -41
- dhisana/utils/generate_flow.py +0 -1
- dhisana/utils/generate_linkedin_connect_message.py +19 -6
- dhisana/utils/generate_linkedin_response_message.py +156 -65
- dhisana/utils/generate_structured_output_internal.py +351 -131
- dhisana/utils/google_custom_search.py +150 -44
- dhisana/utils/google_oauth_tools.py +721 -0
- dhisana/utils/google_workspace_tools.py +391 -25
- dhisana/utils/hubspot_clearbit.py +3 -1
- dhisana/utils/hubspot_crm_tools.py +771 -167
- dhisana/utils/instantly_tools.py +3 -1
- dhisana/utils/lusha_tools.py +10 -7
- dhisana/utils/mailgun_tools.py +150 -0
- dhisana/utils/microsoft365_tools.py +447 -0
- dhisana/utils/openai_assistant_and_file_utils.py +121 -177
- dhisana/utils/openai_helpers.py +19 -16
- dhisana/utils/parse_linkedin_messages_txt.py +2 -3
- dhisana/utils/profile.py +37 -0
- dhisana/utils/proxy_curl_tools.py +507 -206
- dhisana/utils/proxycurl_search_leads.py +426 -0
- dhisana/utils/research_lead.py +121 -68
- dhisana/utils/sales_navigator_crawler.py +1 -6
- dhisana/utils/salesforce_crm_tools.py +323 -50
- dhisana/utils/search_router.py +131 -0
- dhisana/utils/search_router_jobs.py +51 -0
- dhisana/utils/sendgrid_tools.py +126 -91
- dhisana/utils/serarch_router_local_business.py +75 -0
- dhisana/utils/serpapi_additional_tools.py +290 -0
- dhisana/utils/serpapi_google_jobs.py +117 -0
- dhisana/utils/serpapi_google_search.py +188 -0
- dhisana/utils/serpapi_local_business_search.py +129 -0
- dhisana/utils/serpapi_search_tools.py +363 -432
- dhisana/utils/serperdev_google_jobs.py +125 -0
- dhisana/utils/serperdev_local_business.py +154 -0
- dhisana/utils/serperdev_search.py +233 -0
- dhisana/utils/smtp_email_tools.py +576 -0
- dhisana/utils/test_connect.py +1765 -92
- dhisana/utils/trasform_json.py +95 -16
- dhisana/utils/web_download_parse_tools.py +0 -1
- dhisana/utils/zoominfo_tools.py +2 -3
- dhisana/workflow/test.py +1 -1
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +5 -2
- dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
- dhisana-0.0.1.dev85.dist-info/RECORD +0 -81
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
- {dhisana-0.0.1.dev85.dist-info → dhisana-0.0.1.dev236.dist-info}/top_level.txt +0 -0
dhisana/schemas/common.py
CHANGED
|
@@ -363,3 +363,36 @@ class Integration(IntegrationBase):
|
|
|
363
363
|
|
|
364
364
|
Integration.model_rebuild()
|
|
365
365
|
IntegrationUpdate.model_rebuild()
|
|
366
|
+
|
|
367
|
+
class BodyFormat(str, Enum):
|
|
368
|
+
AUTO = "auto"
|
|
369
|
+
HTML = "html"
|
|
370
|
+
TEXT = "text"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class SendEmailContext(BaseModel):
|
|
374
|
+
recipient: str
|
|
375
|
+
subject: str
|
|
376
|
+
body: str
|
|
377
|
+
sender_name: str
|
|
378
|
+
sender_email: str
|
|
379
|
+
labels: Optional[List[str]]
|
|
380
|
+
body_format: BodyFormat = BodyFormat.AUTO
|
|
381
|
+
|
|
382
|
+
class QueryEmailContext(BaseModel):
|
|
383
|
+
start_time: str
|
|
384
|
+
end_time: str
|
|
385
|
+
sender_email: str
|
|
386
|
+
unread_only: bool = True
|
|
387
|
+
labels: Optional[List[str]] = None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class ReplyEmailContext(BaseModel):
|
|
391
|
+
message_id: str
|
|
392
|
+
reply_body: str
|
|
393
|
+
sender_email: str
|
|
394
|
+
sender_name: str
|
|
395
|
+
fallback_recipient: Optional[str] = None
|
|
396
|
+
mark_as_read: str = "True"
|
|
397
|
+
add_labels: Optional[List[str]] = None
|
|
398
|
+
reply_body_format: BodyFormat = BodyFormat.AUTO
|
dhisana/schemas/sales.py
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
1
3
|
from uuid import UUID
|
|
2
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator
|
|
3
5
|
from typing import List, Optional, Dict, Any
|
|
4
6
|
from enum import Enum
|
|
5
7
|
from typing import Optional, List, Dict, Literal
|
|
6
8
|
|
|
7
|
-
from dhisana.schemas.common import User
|
|
8
9
|
|
|
9
10
|
# -----------------------------
|
|
10
11
|
# Lead-List-Specific Schemas
|
|
11
12
|
# -----------------------------
|
|
12
13
|
|
|
13
14
|
class Lead(BaseModel):
|
|
14
|
-
id: Optional[
|
|
15
|
+
id: Optional[UUID] = None
|
|
15
16
|
full_name: Optional[str] = None
|
|
16
17
|
first_name: Optional[str] = None
|
|
17
18
|
last_name: Optional[str] = None
|
|
18
19
|
email: Optional[str] = None
|
|
19
20
|
user_linkedin_url: Optional[str] = None
|
|
20
21
|
user_linkedin_salesnav_url: Optional[str] = None
|
|
22
|
+
organization_linkedin_url: Optional[str] = None
|
|
23
|
+
organization_linkedin_salesnav_url: Optional[str] = None
|
|
24
|
+
linkedin_follower_count: Optional[int] = None
|
|
21
25
|
primary_domain_of_organization: Optional[str] = None
|
|
26
|
+
twitter_handle: Optional[str] = None
|
|
27
|
+
twitch_handle: Optional[str] = None
|
|
28
|
+
github_handle: Optional[str] = None
|
|
22
29
|
job_title: Optional[str] = None
|
|
23
30
|
phone: Optional[str] = None
|
|
24
31
|
headline: Optional[str] = None
|
|
@@ -26,25 +33,69 @@ class Lead(BaseModel):
|
|
|
26
33
|
organization_name: Optional[str] = None
|
|
27
34
|
organization_website: Optional[str] = None
|
|
28
35
|
summary_about_lead: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
qualification_score: Optional[float] = None
|
|
38
|
+
qualification_reason: Optional[str] = None
|
|
39
|
+
revenue: Optional[str] = None
|
|
40
|
+
company_size: Optional[str] = None
|
|
41
|
+
industry: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
keywords: Optional[Any] = None
|
|
44
|
+
tags: List[str] = []
|
|
45
|
+
notes: List[str] = []
|
|
46
|
+
additional_properties: Optional[Dict[str, Any]] = {}
|
|
29
47
|
workflow_stage: Optional[str] = None
|
|
30
|
-
|
|
31
|
-
engaged:
|
|
48
|
+
|
|
49
|
+
engaged: bool = False
|
|
32
50
|
last_contact: Optional[int] = None
|
|
33
|
-
additional_properties: Optional[Dict[str, str]] = None
|
|
34
51
|
research_summary: Optional[str] = None
|
|
35
52
|
task_ids: Optional[List[str]] = None
|
|
36
|
-
email_validation_status: Optional[
|
|
37
|
-
|
|
38
|
-
] = None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
email_validation_status: Optional[str] = None
|
|
54
|
+
linkedin_validation_status: Optional[str] = None
|
|
55
|
+
research_status: Optional[str] = None
|
|
56
|
+
enchrichment_status: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@field_validator("linkedin_follower_count", mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def parse_linkedin_follower_count(cls, v):
|
|
62
|
+
if v is None or v == "":
|
|
63
|
+
return None
|
|
64
|
+
if isinstance(v, str):
|
|
65
|
+
v = v.strip()
|
|
66
|
+
if v == "":
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
return int(v)
|
|
70
|
+
except ValueError:
|
|
71
|
+
raise ValueError("linkedin_follower_count must be an integer")
|
|
72
|
+
return v
|
|
73
|
+
|
|
74
|
+
@field_validator("notes", mode="before")
|
|
75
|
+
@classmethod
|
|
76
|
+
def ensure_notes_list(cls, v):
|
|
77
|
+
"""Coerce notes to a list of strings.
|
|
78
|
+
Handles legacy cases where the DB may contain a scalar or JSON string.
|
|
79
|
+
"""
|
|
80
|
+
if v is None:
|
|
81
|
+
return []
|
|
82
|
+
if isinstance(v, list):
|
|
83
|
+
# Ensure all elements are strings
|
|
84
|
+
return [str(item) if not isinstance(item, str) else item for item in v]
|
|
85
|
+
if isinstance(v, str):
|
|
86
|
+
# Try to parse JSON array; if not, wrap as single-note list
|
|
87
|
+
try:
|
|
88
|
+
parsed = json.loads(v)
|
|
89
|
+
if isinstance(parsed, list):
|
|
90
|
+
return [str(item) if not isinstance(item, str) else item for item in parsed]
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
return [v]
|
|
94
|
+
# Fallback: wrap any other scalar/object as a single string entry
|
|
95
|
+
try:
|
|
96
|
+
return [json.dumps(v)]
|
|
97
|
+
except Exception:
|
|
98
|
+
return [str(v)]
|
|
48
99
|
|
|
49
100
|
|
|
50
101
|
class LeadList(BaseModel):
|
|
@@ -240,9 +291,14 @@ class MessageGenerationInstructions(BaseModel):
|
|
|
240
291
|
Holds the user-supplied instructions for generating the message:
|
|
241
292
|
- instructions_to_generate_message: Plain text or template instructions from the user.
|
|
242
293
|
- prompt_engineering_guidance: (Optional) Extra guidelines for structuring the prompt.
|
|
294
|
+
- allow_html: Whether HTML output is allowed.
|
|
295
|
+
- html_template: Optional HTML scaffolding or guidance.
|
|
243
296
|
"""
|
|
244
297
|
instructions_to_generate_message: Optional[str] = None
|
|
245
298
|
prompt_engineering_guidance: Optional[PromptEngineeringGuidance] = None
|
|
299
|
+
use_cache: Optional[bool] = True
|
|
300
|
+
allow_html: Optional[bool] = False
|
|
301
|
+
html_template: Optional[str] = None
|
|
246
302
|
|
|
247
303
|
class CampaignContext(BaseModel):
|
|
248
304
|
"""
|
|
@@ -272,6 +328,10 @@ class MessageItem(BaseModel):
|
|
|
272
328
|
...,
|
|
273
329
|
description="Unique identifier for the message"
|
|
274
330
|
)
|
|
331
|
+
thread_id: str = Field(
|
|
332
|
+
...,
|
|
333
|
+
description="Unique identifier for the conversation thread"
|
|
334
|
+
)
|
|
275
335
|
sender_name: str = Field(
|
|
276
336
|
...,
|
|
277
337
|
description="Sender's display name (if available)"
|
|
@@ -300,6 +360,7 @@ class MessageItem(BaseModel):
|
|
|
300
360
|
...,
|
|
301
361
|
description="Body of the message in plain text"
|
|
302
362
|
)
|
|
363
|
+
html_body: Optional[str] = None
|
|
303
364
|
|
|
304
365
|
class MessageResponse(BaseModel):
|
|
305
366
|
"""
|
|
@@ -435,14 +496,14 @@ class HubSpotLeadInformation(BaseModel):
|
|
|
435
496
|
organization_name: str = Field("", description="Current Company where lead works")
|
|
436
497
|
organization_website: str = Field("", description="Current Company website of the lead")
|
|
437
498
|
organization_linkedin_url : str = Field("", description="Company LinkedIn URL")
|
|
438
|
-
additional_properties: Optional[Dict[str,
|
|
499
|
+
additional_properties: Optional[Dict[str, Any]] = None
|
|
439
500
|
|
|
440
501
|
class HubSpotCompanyinformation(BaseModel):
|
|
441
502
|
primary_domain_of_organization: str = Field("", description="Primary domain of the organization")
|
|
442
503
|
organization_name: str = Field("", description="Current Company where lead works")
|
|
443
504
|
organization_website: str = Field("", description="Current Company website of the lead")
|
|
444
505
|
organization_linkedin_url : str = Field("", description="Company LinkedIn URL")
|
|
445
|
-
additional_properties: Optional[Dict[str,
|
|
506
|
+
additional_properties: Optional[Dict[str, Any]] = None
|
|
446
507
|
|
|
447
508
|
|
|
448
509
|
# --------------------------------------------------------------------
|
|
@@ -459,6 +520,7 @@ HUBSPOT_TO_LEAD_MAPPING = {
|
|
|
459
520
|
"address": "lead_location", # You can choose "city", "state", etc. if you prefer
|
|
460
521
|
"city": "lead_location",
|
|
461
522
|
"domain": "primary_domain_of_organization",
|
|
523
|
+
"hs_linkedin_url": "user_linkedin_url",
|
|
462
524
|
}
|
|
463
525
|
|
|
464
526
|
class SmartListStatus(str, Enum):
|
|
@@ -478,6 +540,12 @@ class SmartListSourceType(str, Enum):
|
|
|
478
540
|
CSV = "CSV"
|
|
479
541
|
GOOGLE_SHEETS = "GOOGLE_SHEETS"
|
|
480
542
|
CUSTOM_WEBSITE = "CUSTOM_WEBSITE"
|
|
543
|
+
GITHUB = "GITHUB"
|
|
544
|
+
ICP_SEARCH = "ICP_SEARCH"
|
|
545
|
+
LOCAL_BUSINESS = "LOCAL_BUSINESS"
|
|
546
|
+
GOOGLE_JOBS = "GOOGLE_JOBS"
|
|
547
|
+
WEBHOOK = "WEBHOOK"
|
|
548
|
+
GOOGLE_CUSTOM_SITE_SEARCH = "GOOGLE_CUSTOM_SITE_SEARCH"
|
|
481
549
|
|
|
482
550
|
class SourceConfiguration(BaseModel):
|
|
483
551
|
"""
|
|
@@ -494,6 +562,15 @@ class SourceConfiguration(BaseModel):
|
|
|
494
562
|
file_path: Optional[str] = None
|
|
495
563
|
# For Google Sheets
|
|
496
564
|
source_url: Optional[str] = None
|
|
565
|
+
# For Github
|
|
566
|
+
github_search_query: Optional[str] = None
|
|
567
|
+
github_max_repos: Optional[int] = None
|
|
568
|
+
github_max_contributors: Optional[int] = None
|
|
569
|
+
|
|
570
|
+
# Custom website inputs
|
|
571
|
+
custom_instructions_for_doing_pagination: Optional[str] = None
|
|
572
|
+
custom_instructions_for_data_extraction_from_page: Optional[str] = None
|
|
573
|
+
custom_instruction_to_fetch_details_page: Optional[str] = None
|
|
497
574
|
|
|
498
575
|
class SmartListSource(BaseModel):
|
|
499
576
|
"""
|
|
@@ -555,6 +632,8 @@ class SmartListLead(BaseModel):
|
|
|
555
632
|
organization_linkedin_url: Optional[str] = None
|
|
556
633
|
organization_linkedin_salesnav_url: Optional[str] = None
|
|
557
634
|
primary_domain_of_organization: Optional[str] = None
|
|
635
|
+
twitter_handle: Optional[str] = None
|
|
636
|
+
github_handle: Optional[str] = None
|
|
558
637
|
job_title: Optional[str] = None
|
|
559
638
|
phone: Optional[str] = None
|
|
560
639
|
headline: Optional[str] = None
|
|
@@ -562,13 +641,18 @@ class SmartListLead(BaseModel):
|
|
|
562
641
|
organization_name: Optional[str] = None
|
|
563
642
|
organization_website: Optional[str] = None
|
|
564
643
|
summary_about_lead: Optional[str] = None
|
|
565
|
-
keywords: Optional[
|
|
566
|
-
additional_properties: Optional[Dict[str,
|
|
644
|
+
keywords: Optional[Any] = None
|
|
645
|
+
additional_properties: Optional[Dict[str, Any]] = None
|
|
567
646
|
research_summary: Optional[str] = None
|
|
568
647
|
|
|
569
648
|
qualification_score: Optional[float] = None
|
|
570
649
|
qualification_reason: Optional[str] = None
|
|
571
650
|
source: Optional[str] = None
|
|
651
|
+
|
|
652
|
+
email_validation_status: Optional[str] = None
|
|
653
|
+
linkedin_validation_status: Optional[str] = None
|
|
654
|
+
research_status: Optional[str] = None
|
|
655
|
+
enchrichment_status: Optional[str] = None
|
|
572
656
|
|
|
573
657
|
agent_instance_id: Optional[UUID] = None
|
|
574
658
|
organization_id: Optional[UUID] = None
|
|
@@ -576,7 +660,10 @@ class SmartListLead(BaseModel):
|
|
|
576
660
|
created_at: Optional[int] = None
|
|
577
661
|
updated_by: Optional[UUID] = None
|
|
578
662
|
updated_at: Optional[int] = None
|
|
579
|
-
|
|
663
|
+
|
|
664
|
+
revenue: Optional[str] = None
|
|
665
|
+
company_size: Optional[str] = None
|
|
666
|
+
industry: Optional[str] = None
|
|
580
667
|
class Config:
|
|
581
668
|
from_attributes = True
|
|
582
669
|
|
|
@@ -741,3 +828,117 @@ class LeadsQueryFilters(BaseModel):
|
|
|
741
828
|
description="Ranges for organization number of employees."
|
|
742
829
|
)
|
|
743
830
|
|
|
831
|
+
|
|
832
|
+
class CompanyQueryFilters(BaseModel):
|
|
833
|
+
"""
|
|
834
|
+
Defines the filter parameters for querying companies/organizations in the Apollo database.
|
|
835
|
+
All fields are optional and default to None if not specified by user.
|
|
836
|
+
"""
|
|
837
|
+
|
|
838
|
+
# Core company search parameters
|
|
839
|
+
organization_locations: Optional[List[str]] = Field(
|
|
840
|
+
default=None,
|
|
841
|
+
description="List of organization headquarters locations (city, state, country)."
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
organization_num_employees_ranges: Optional[List[str]] = Field(
|
|
845
|
+
default=None,
|
|
846
|
+
description="Employee count ranges, e.g. ['1,10', '11,50', '51,200']. Use specific ranges."
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
min_employees: Optional[int] = Field(
|
|
850
|
+
default=None,
|
|
851
|
+
description="Minimum number of employees (>=1). Internally converted to a numeric range."
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
max_employees: Optional[int] = Field(
|
|
855
|
+
default=None,
|
|
856
|
+
description="Maximum number of employees (<=100000). Internally converted to a numeric range."
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
organization_industries: Optional[List[str]] = Field(
|
|
860
|
+
default=None,
|
|
861
|
+
description="List of organization industries."
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
organization_industry_tag_ids: Optional[List[str]] = Field(
|
|
865
|
+
default=None,
|
|
866
|
+
description="List of industry tag IDs, e.g. ['5567cd4773696439b10b0000']."
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
# Revenue filters
|
|
870
|
+
revenue_range_min: Optional[int] = Field(
|
|
871
|
+
default=None,
|
|
872
|
+
description="Minimum company revenue in USD."
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
revenue_range_max: Optional[int] = Field(
|
|
876
|
+
default=None,
|
|
877
|
+
description="Maximum company revenue in USD."
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# Funding and growth
|
|
881
|
+
organization_latest_funding_stage_cd: Optional[List[str]] = Field(
|
|
882
|
+
default=None,
|
|
883
|
+
description="List of funding stage codes, e.g. ['2', '3', '10']."
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Technology and keywords
|
|
887
|
+
currently_using_any_of_technology_uids: Optional[List[str]] = Field(
|
|
888
|
+
default=None,
|
|
889
|
+
description="Technology UIDs used by the organization, e.g. ['google_font_api']."
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
q_keywords: Optional[str] = Field(
|
|
893
|
+
default=None,
|
|
894
|
+
description="Keywords to search for in company descriptions, names, etc."
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
q_organization_domains: Optional[List[str]] = Field(
|
|
898
|
+
default=None,
|
|
899
|
+
description="Specific company domains to search for, e.g. ['microsoft.com', 'google.com']."
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
# Company-specific filters
|
|
903
|
+
organization_ids: Optional[List[str]] = Field(
|
|
904
|
+
default=None,
|
|
905
|
+
description="Specific Apollo organization IDs to include."
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
not_organization_ids: Optional[List[str]] = Field(
|
|
909
|
+
default=None,
|
|
910
|
+
description="Apollo organization IDs to exclude from results."
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
# Search lists
|
|
914
|
+
q_organization_search_list_id: Optional[str] = Field(
|
|
915
|
+
default=None,
|
|
916
|
+
description="Include only organizations in a specific search list."
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
q_not_organization_search_list_id: Optional[str] = Field(
|
|
920
|
+
default=None,
|
|
921
|
+
description="Exclude organizations in a specific search list."
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# Sorting
|
|
925
|
+
sort_by_field: Optional[str] = Field(
|
|
926
|
+
default=None,
|
|
927
|
+
description="Sort field, e.g. 'name', 'employee_count', 'last_updated', etc."
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
sort_ascending: Optional[bool] = Field(
|
|
931
|
+
default=None,
|
|
932
|
+
description="Sort ascending (True) or descending (False)."
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# Additional filters that might be useful
|
|
936
|
+
organization_founded_year_min: Optional[int] = Field(
|
|
937
|
+
default=None,
|
|
938
|
+
description="Minimum founding year for the organization."
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
organization_founded_year_max: Optional[int] = Field(
|
|
942
|
+
default=None,
|
|
943
|
+
description="Maximum founding year for the organization."
|
|
944
|
+
)
|
dhisana/utils/add_mapping.py
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
-
from urllib.parse import urlparse
|
|
3
2
|
from typing import List, Optional
|
|
4
|
-
import hashlib
|
|
5
3
|
import logging
|
|
6
4
|
from typing import Optional, Dict, Any
|
|
7
|
-
from pydantic import ValidationError
|
|
8
5
|
from dhisana.schemas.sales import MessageItem
|
|
9
6
|
from dhisana.utils.cache_output_tools import (
|
|
10
7
|
retrieve_output,
|
|
@@ -12,29 +9,38 @@ from dhisana.utils.cache_output_tools import (
|
|
|
12
9
|
)
|
|
13
10
|
from dhisana.utils.field_validators import normalize_linkedin_url, normalize_salesnav_url
|
|
14
11
|
from dhisana.utils.parse_linkedin_messages_txt import parse_conversation
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
17
14
|
|
|
15
|
+
def get_short_id_from_salesnav_url(sn_url: str) -> str:
|
|
16
|
+
import re
|
|
17
|
+
if not sn_url:
|
|
18
|
+
return ""
|
|
19
|
+
if "/sales/lead/" in sn_url:
|
|
20
|
+
pattern = r"linkedin\.com/sales/lead/([^/?#,]+)"
|
|
21
|
+
match = re.search(pattern, sn_url, re.IGNORECASE)
|
|
22
|
+
if not match:
|
|
23
|
+
return ""
|
|
24
|
+
sales_nav_id = re.sub(r"[^\w-]", "", match.group(1))
|
|
25
|
+
# Arbitrary example logic: if it starts with ACw or ACo, strip those and return 8 chars
|
|
26
|
+
if sales_nav_id.startswith("ACw") or sales_nav_id.startswith("ACo"):
|
|
27
|
+
return sales_nav_id[3:11]
|
|
28
|
+
return ""
|
|
18
29
|
|
|
19
30
|
async def add_mapping_tool(mapping: dict) -> dict:
|
|
20
31
|
"""
|
|
21
32
|
Create a two-way (forward & reverse) cache mapping between:
|
|
22
33
|
- user_linkedin_url (normalized)
|
|
23
|
-
- user_linkedin_salesnav_url (normalized)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- SN
|
|
34
|
+
- user_linkedin_salesnav_url (normalized, with short ID extracted if possible)
|
|
35
|
+
|
|
36
|
+
Also store single-direction entries for easy lookup:
|
|
37
|
+
- LN→SN (key: "mapping_ln:<sha_of_normalized_ln>")
|
|
38
|
+
- SN→LN (key: "mapping_sn:<sha_of_short_id_or_full_sn>")
|
|
27
39
|
|
|
28
40
|
The cache content has the actual (raw) user-provided URLs,
|
|
29
41
|
only the keys are lowercased/hashed.
|
|
30
42
|
|
|
31
43
|
Returns a dict with status, message, and data.
|
|
32
|
-
|
|
33
|
-
Example 'mapping':
|
|
34
|
-
{
|
|
35
|
-
"user_linkedin_url": "linkedin.com/in/some-user/",
|
|
36
|
-
"user_linkedin_salesnav_url": "https://www.linkedin.com/sales/lead/123456,NAME_SEARCH"
|
|
37
|
-
}
|
|
38
44
|
"""
|
|
39
45
|
user_linkedin_url = mapping.get("user_linkedin_url", "").strip()
|
|
40
46
|
user_linkedin_salesnav_url = mapping.get("user_linkedin_salesnav_url", "").strip()
|
|
@@ -48,16 +54,19 @@ async def add_mapping_tool(mapping: dict) -> dict:
|
|
|
48
54
|
# Normalize
|
|
49
55
|
ln_url_norm = normalize_linkedin_url(user_linkedin_url)
|
|
50
56
|
sn_url_norm = normalize_salesnav_url(user_linkedin_salesnav_url)
|
|
57
|
+
salesnav_short_id = get_short_id_from_salesnav_url(sn_url_norm)
|
|
58
|
+
|
|
59
|
+
# We'll use short_id in the forward/reverse *mapping strings*
|
|
60
|
+
forward_str = f"{ln_url_norm}→{salesnav_short_id}"
|
|
61
|
+
reverse_str = f"{salesnav_short_id}→{ln_url_norm}"
|
|
51
62
|
|
|
52
|
-
# Forward & reverse combined keys (to check if identical mapping was previously stored)
|
|
53
|
-
forward_str = f"{ln_url_norm}→{sn_url_norm}"
|
|
54
|
-
reverse_str = f"{sn_url_norm}→{ln_url_norm}"
|
|
55
63
|
forward_key = "mapping:" + hashlib.sha256(forward_str.encode("utf-8")).hexdigest()
|
|
56
64
|
reverse_key = "mapping:" + hashlib.sha256(reverse_str.encode("utf-8")).hexdigest()
|
|
57
65
|
|
|
58
|
-
#
|
|
59
|
-
forward_cached = retrieve_output("
|
|
66
|
+
# Use the same namespace used later by the single-direction logic
|
|
67
|
+
forward_cached = retrieve_output("tool_mappings_linkedin_id", forward_key)
|
|
60
68
|
if forward_cached:
|
|
69
|
+
# If we found an identical forward mapping, return success
|
|
61
70
|
if (
|
|
62
71
|
forward_cached.get("user_linkedin_url") == ln_url_norm
|
|
63
72
|
and forward_cached.get("user_linkedin_salesnav_url") == sn_url_norm
|
|
@@ -68,12 +77,12 @@ async def add_mapping_tool(mapping: dict) -> dict:
|
|
|
68
77
|
"data": forward_cached
|
|
69
78
|
}
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
reverse_cached = retrieve_output("tool_mappings", reverse_key)
|
|
80
|
+
reverse_cached = retrieve_output("tool_mappings_linkedin_id", reverse_key)
|
|
73
81
|
if reverse_cached:
|
|
82
|
+
# If we found an identical reverse mapping, return success
|
|
74
83
|
if (
|
|
75
84
|
reverse_cached.get("user_linkedin_url") == ln_url_norm
|
|
76
|
-
and reverse_cached.get("
|
|
85
|
+
and reverse_cached.get("salesnav_short_id") == salesnav_short_id
|
|
77
86
|
):
|
|
78
87
|
return {
|
|
79
88
|
"status": "SUCCESS",
|
|
@@ -84,34 +93,36 @@ async def add_mapping_tool(mapping: dict) -> dict:
|
|
|
84
93
|
# Create object for storing in forward & reverse
|
|
85
94
|
serialized_results = {
|
|
86
95
|
"user_linkedin_url": ln_url_norm,
|
|
87
|
-
"user_linkedin_salesnav_url": sn_url_norm
|
|
96
|
+
"user_linkedin_salesnav_url": sn_url_norm,
|
|
97
|
+
"salesnav_short_id": salesnav_short_id
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
# Store them
|
|
91
|
-
cache_output("
|
|
92
|
-
cache_output("
|
|
100
|
+
# Store them in the same place we retrieve them from
|
|
101
|
+
cache_output("tool_mappings_linkedin_id", forward_key, serialized_results)
|
|
102
|
+
cache_output("tool_mappings_linkedin_id", reverse_key, serialized_results)
|
|
93
103
|
|
|
94
104
|
# ------------------------------------------------------------------------
|
|
95
|
-
# Additional single-direction keys to enable easy lookups
|
|
96
|
-
# LN key: "mapping_ln:<sha_of_normalized_ln
|
|
97
|
-
# SN key: "mapping_sn:<
|
|
98
|
-
# And store the *raw* original user URLs as requested
|
|
105
|
+
# Additional single-direction keys to enable easy lookups:
|
|
106
|
+
# LN key: "mapping_ln:<sha_of_normalized_ln>"
|
|
107
|
+
# SN key: "mapping_sn:<sha_of_short_id_or_full_sn>"
|
|
99
108
|
# ------------------------------------------------------------------------
|
|
100
109
|
ln_lower = ln_url_norm.lower()
|
|
101
|
-
sn_lower = sn_url_norm.lower()
|
|
102
110
|
|
|
111
|
+
# For SN, we prefer the short ID if it's not empty; otherwise fall back to the full URL.
|
|
112
|
+
sn_lookup_value = salesnav_short_id if salesnav_short_id else sn_url_norm
|
|
103
113
|
ln_key = "mapping_ln:" + hashlib.sha256(ln_lower.encode("utf-8")).hexdigest()
|
|
104
|
-
sn_key = "mapping_sn:" + hashlib.sha256(
|
|
114
|
+
sn_key = "mapping_sn:" + hashlib.sha256(sn_lookup_value.lower().encode("utf-8")).hexdigest()
|
|
105
115
|
|
|
106
|
-
# Cache the raw user-provided URLs
|
|
116
|
+
# Cache the raw user-provided (normalized) URLs
|
|
107
117
|
single_direction_data = {
|
|
108
|
-
"raw_linkedin_url": ln_url_norm,
|
|
109
|
-
"raw_salesnav_url": sn_url_norm,
|
|
118
|
+
"raw_linkedin_url": ln_url_norm, # no forced lowercasing in the stored data
|
|
119
|
+
"raw_salesnav_url": sn_url_norm,
|
|
120
|
+
"salesnav_short_id": salesnav_short_id
|
|
110
121
|
}
|
|
111
122
|
logger.info("mapping data cached: %s", single_direction_data)
|
|
112
123
|
|
|
113
|
-
cache_output("
|
|
114
|
-
cache_output("
|
|
124
|
+
cache_output("tool_mappings_linkedin_id", ln_key, single_direction_data)
|
|
125
|
+
cache_output("tool_mappings_linkedin_id", sn_key, single_direction_data)
|
|
115
126
|
|
|
116
127
|
return {
|
|
117
128
|
"status": "SUCCESS",
|
|
@@ -119,43 +130,42 @@ async def add_mapping_tool(mapping: dict) -> dict:
|
|
|
119
130
|
"data": serialized_results
|
|
120
131
|
}
|
|
121
132
|
|
|
122
|
-
|
|
123
133
|
async def get_salesnav_url_for_linkedin_url(raw_ln_url: str) -> Optional[str]:
|
|
124
134
|
"""
|
|
125
135
|
Given a LinkedIn URL, normalize it and look up the corresponding
|
|
126
136
|
*raw* sales nav URL from the single-direction cache.
|
|
127
|
-
|
|
137
|
+
Returns None if not found.
|
|
128
138
|
"""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
ln_key = "mapping_ln:" + hashlib.sha256(ln_norm.lower().encode("utf-8")).hexdigest()
|
|
139
|
+
ln_norm = normalize_linkedin_url(raw_ln_url).lower()
|
|
140
|
+
ln_key = "mapping_ln:" + hashlib.sha256(ln_norm.encode("utf-8")).hexdigest()
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
# Must retrieve from the same namespace we used when caching
|
|
143
|
+
record = retrieve_output("tool_mappings_linkedin_id", ln_key)
|
|
134
144
|
if not record:
|
|
135
145
|
return None
|
|
136
146
|
|
|
137
|
-
# record["raw_salesnav_url"] is the original user_linkedin_salesnav_url
|
|
138
147
|
return record.get("raw_salesnav_url")
|
|
139
148
|
|
|
140
|
-
|
|
141
149
|
async def get_linkedin_url_for_salesnav_url(raw_sn_url: str) -> Optional[str]:
|
|
142
150
|
"""
|
|
143
151
|
Given a Sales Navigator URL, normalize it and look up the corresponding
|
|
144
152
|
*raw* LinkedIn URL from the single-direction cache.
|
|
145
|
-
|
|
153
|
+
Uses the short ID if available; otherwise falls back to the full SN URL.
|
|
154
|
+
Returns None if not found in cache.
|
|
146
155
|
"""
|
|
147
|
-
# Normalize & lower-case for key
|
|
148
156
|
sn_norm = normalize_salesnav_url(raw_sn_url)
|
|
149
|
-
|
|
157
|
+
salesnav_short_id = get_short_id_from_salesnav_url(sn_norm)
|
|
150
158
|
|
|
151
|
-
|
|
159
|
+
# Use short ID if present, else the normalized URL
|
|
160
|
+
sn_lookup_value = salesnav_short_id if salesnav_short_id else sn_norm
|
|
161
|
+
sn_key = "mapping_sn:" + hashlib.sha256(sn_lookup_value.lower().encode("utf-8")).hexdigest()
|
|
162
|
+
|
|
163
|
+
record = retrieve_output("tool_mappings_linkedin_id", sn_key)
|
|
152
164
|
if not record:
|
|
153
165
|
return None
|
|
154
166
|
|
|
155
|
-
# record["raw_linkedin_url"] is the original user_linkedin_url
|
|
156
167
|
return record.get("raw_linkedin_url")
|
|
157
168
|
|
|
158
|
-
|
|
159
169
|
async def cache_enriched_lead_info_from_salesnav(lead_info: dict, agent_id: str) -> Dict[str, Any]:
|
|
160
170
|
"""
|
|
161
171
|
Cache lead information using the old SHA-256 approach if and only if:
|
|
@@ -263,7 +273,6 @@ async def retrieve_lead_html_from_salesnav(user_linkedin_url: str, agent_id: str
|
|
|
263
273
|
|
|
264
274
|
return retrieve_output("tool_mappings", sn_key)
|
|
265
275
|
|
|
266
|
-
|
|
267
276
|
async def cache_touchpoint_status(
|
|
268
277
|
lead_info: Dict[str, Any],
|
|
269
278
|
agent_id: str,
|
|
@@ -272,9 +281,8 @@ async def cache_touchpoint_status(
|
|
|
272
281
|
"""
|
|
273
282
|
Cache TouchPointStatus data for a given lead.
|
|
274
283
|
The cache key is generated from the SHA-256 hash of the normalized LinkedIn URL
|
|
275
|
-
|
|
284
|
+
plus the agent_id.
|
|
276
285
|
"""
|
|
277
|
-
# ...existing code...
|
|
278
286
|
if not lead_info or not lead_info.get("user_linkedin_url"):
|
|
279
287
|
return {
|
|
280
288
|
"status": "ERROR",
|
|
@@ -283,7 +291,6 @@ async def cache_touchpoint_status(
|
|
|
283
291
|
|
|
284
292
|
user_linkedin_url = lead_info["user_linkedin_url"].strip()
|
|
285
293
|
sn_norm = normalize_linkedin_url(user_linkedin_url)
|
|
286
|
-
|
|
287
294
|
sn_key = "touchpoint_status:" + hashlib.sha256((sn_norm.lower() + agent_id).encode("utf-8")).hexdigest()
|
|
288
295
|
|
|
289
296
|
cache_output("touchpoint_status", sn_key, touchpoint_data)
|
|
@@ -300,9 +307,8 @@ async def retrieve_touchpoint_status(
|
|
|
300
307
|
):
|
|
301
308
|
"""
|
|
302
309
|
Retrieve TouchPointStatus data from the cache for a given user LinkedIn URL
|
|
303
|
-
and
|
|
310
|
+
and agent_id.
|
|
304
311
|
"""
|
|
305
|
-
# ...existing code...
|
|
306
312
|
sn_norm = normalize_linkedin_url(user_linkedin_url.strip())
|
|
307
313
|
sn_key = "touchpoint_status:" + hashlib.sha256((sn_norm.lower() + agent_id).encode("utf-8")).hexdigest()
|
|
308
314
|
|
|
@@ -313,14 +319,16 @@ async def retrieve_connection_status(
|
|
|
313
319
|
user_linkedin_url: str,
|
|
314
320
|
agent_id: str
|
|
315
321
|
):
|
|
316
|
-
|
|
322
|
+
"""
|
|
323
|
+
Retrieve minimal "connection status" from the TouchPointStatus cache.
|
|
324
|
+
"""
|
|
317
325
|
sn_norm = normalize_linkedin_url(user_linkedin_url.strip())
|
|
318
326
|
sn_key = "touchpoint_status:" + hashlib.sha256((sn_norm.lower() + agent_id).encode("utf-8")).hexdigest()
|
|
319
327
|
|
|
320
328
|
cached_data = retrieve_output("touchpoint_status", sn_key) or {}
|
|
321
329
|
connection_degree = cached_data.get("connection_degree", "")
|
|
322
330
|
connection_status = {
|
|
323
|
-
"connection_degree":
|
|
331
|
+
"connection_degree": connection_degree,
|
|
324
332
|
"connection_request_status": cached_data.get("connection_request_status", ""),
|
|
325
333
|
"is_connected_on_linkedin": connection_degree == "1st"
|
|
326
334
|
}
|
|
@@ -332,12 +340,13 @@ async def get_lead_linkedin_messages(user_linkedin_url: str, agent_id: str) -> O
|
|
|
332
340
|
The cache key includes agent_id in the SHA-256 hash.
|
|
333
341
|
"""
|
|
334
342
|
sn_norm = normalize_linkedin_url(user_linkedin_url)
|
|
335
|
-
sn_key = "salesnav_lead_messages_raw:" + hashlib.sha256(
|
|
336
|
-
|
|
337
|
-
|
|
343
|
+
sn_key = "salesnav_lead_messages_raw:" + hashlib.sha256(
|
|
344
|
+
(sn_norm + agent_id).encode("utf-8")
|
|
345
|
+
).hexdigest()
|
|
338
346
|
|
|
347
|
+
list_of_dicts = retrieve_output("lead_linkedin_messages_", sn_key) or []
|
|
348
|
+
messages: List[MessageItem] = []
|
|
339
349
|
for message in list_of_dicts:
|
|
340
350
|
message_item = MessageItem(**message)
|
|
341
351
|
messages.append(message_item)
|
|
342
|
-
|
|
343
352
|
return messages
|