dhisana 0.0.1.dev231__tar.gz → 0.0.1.dev257__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 (121) hide show
  1. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/setup.py +1 -1
  3. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/schemas/common.py +4 -0
  4. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/schemas/sales.py +34 -4
  5. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/apollo_tools.py +226 -82
  6. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/cache_output_tools.py +22 -2
  7. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/check_email_validity_tools.py +4 -4
  8. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/enrich_lead_information.py +18 -2
  9. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_content.py +3 -1
  10. dhisana-0.0.1.dev257/src/dhisana/utils/generate_custom_message.py +271 -0
  11. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_email.py +60 -17
  12. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_email_response.py +132 -117
  13. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/google_oauth_tools.py +94 -42
  14. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/google_workspace_tools.py +120 -46
  15. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/mailgun_tools.py +6 -0
  16. dhisana-0.0.1.dev257/src/dhisana/utils/mailreach_tools.py +123 -0
  17. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/microsoft365_tools.py +51 -3
  18. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/proxy_curl_tools.py +88 -21
  19. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/sendgrid_tools.py +10 -0
  20. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serpapi_search_tools.py +2 -2
  21. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/smtp_email_tools.py +109 -10
  22. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/test_connect.py +546 -3
  23. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana.egg-info/PKG-INFO +1 -1
  24. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana.egg-info/SOURCES.txt +3 -0
  25. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_apollo_company_search.py +70 -26
  26. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_apollo_lead_search.py +11 -3
  27. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_connectivity.py +16 -0
  28. dhisana-0.0.1.dev257/tests/test_mailreach.py +179 -0
  29. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/README.md +0 -0
  30. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/pyproject.toml +0 -0
  31. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/setup.cfg +0 -0
  32. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/__init__.py +0 -0
  33. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/cli/__init__.py +0 -0
  34. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/cli/cli.py +0 -0
  35. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/cli/datasets.py +0 -0
  36. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/cli/models.py +0 -0
  37. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/cli/predictions.py +0 -0
  38. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/schemas/__init__.py +0 -0
  39. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/ui/__init__.py +0 -0
  40. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/ui/components.py +0 -0
  41. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/__init__.py +0 -0
  42. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/add_mapping.py +0 -0
  43. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/agent_tools.py +0 -0
  44. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  45. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/built_with_api_tools.py +0 -0
  46. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  47. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  48. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  49. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/clay_tools.py +0 -0
  50. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/clean_properties.py +0 -0
  51. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/company_utils.py +0 -0
  52. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  53. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/compose_search_query.py +0 -0
  54. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  55. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/composite_tools.py +0 -0
  56. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/dataframe_tools.py +0 -0
  57. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/domain_parser.py +0 -0
  58. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/email_body_utils.py +0 -0
  59. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/email_parse_helpers.py +0 -0
  60. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/email_provider.py +0 -0
  61. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  62. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/fetch_openai_config.py +0 -0
  63. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/field_validators.py +0 -0
  64. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/g2_tools.py +0 -0
  65. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_flow.py +0 -0
  66. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  67. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  68. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  69. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  70. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/google_custom_search.py +0 -0
  71. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  72. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  73. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/instantly_tools.py +0 -0
  74. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/linkedin_crawler.py +0 -0
  75. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/lusha_tools.py +0 -0
  76. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  77. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/openai_helpers.py +0 -0
  78. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  79. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  80. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  81. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  82. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  83. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  84. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/profile.py +0 -0
  85. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  86. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/python_function_to_tools.py +0 -0
  87. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/research_lead.py +0 -0
  88. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  89. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  90. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/search_router.py +0 -0
  91. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/search_router_jobs.py +0 -0
  92. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  93. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  94. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  95. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serpapi_google_search.py +0 -0
  96. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  97. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  98. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serperdev_local_business.py +0 -0
  99. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/serperdev_search.py +0 -0
  100. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/trasform_json.py +0 -0
  101. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  102. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/workflow_code_model.py +0 -0
  103. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/utils/zoominfo_tools.py +0 -0
  104. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/workflow/__init__.py +0 -0
  105. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/workflow/agent.py +0 -0
  106. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/workflow/flow.py +0 -0
  107. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/workflow/task.py +0 -0
  108. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana/workflow/test.py +0 -0
  109. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana.egg-info/dependency_links.txt +0 -0
  110. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana.egg-info/entry_points.txt +0 -0
  111. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana.egg-info/requires.txt +0 -0
  112. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/src/dhisana.egg-info/top_level.txt +0 -0
  113. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_agent_tools.py +0 -0
  114. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_email_body_utils.py +0 -0
  115. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_google_document.py +0 -0
  116. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_hubspot_call_logs.py +0 -0
  117. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_linkedin_serper.py +0 -0
  118. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_mcp_connectivity.py +0 -0
  119. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_proxycurl_get_company_search_id.py +0 -0
  120. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_proxycurl_job_count.py +0 -0
  121. {dhisana-0.0.1.dev231 → dhisana-0.0.1.dev257}/tests/test_structured_output_with_mcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev231
3
+ Version: 0.0.1.dev257
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-dev231',
5
+ version='0.0.1-dev257',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -378,6 +378,8 @@ class SendEmailContext(BaseModel):
378
378
  sender_email: str
379
379
  labels: Optional[List[str]]
380
380
  body_format: BodyFormat = BodyFormat.AUTO
381
+ headers: Optional[Dict[str, str]] = None
382
+ email_open_token: Optional[str] = None
381
383
 
382
384
  class QueryEmailContext(BaseModel):
383
385
  start_time: str
@@ -392,6 +394,8 @@ class ReplyEmailContext(BaseModel):
392
394
  reply_body: str
393
395
  sender_email: str
394
396
  sender_name: str
397
+ headers: Optional[Dict[str, str]] = None
398
+ fallback_recipient: Optional[str] = None
395
399
  mark_as_read: str = "True"
396
400
  add_labels: Optional[List[str]] = None
397
401
  reply_body_format: BodyFormat = BodyFormat.AUTO
@@ -270,6 +270,7 @@ class ChannelType(str, Enum):
270
270
  LINKEDIN_CONNECT_MESSAGE = "linkedin_connect_message"
271
271
  REPLY_EMAIL = "reply_email"
272
272
  LINKEDIN_USER_MESSAGE = "linkedin_user_message"
273
+ CUSTOM_MESSAGE = "custom_message"
273
274
 
274
275
  class SenderInfo(BaseModel):
275
276
  """
@@ -291,10 +292,14 @@ class MessageGenerationInstructions(BaseModel):
291
292
  Holds the user-supplied instructions for generating the message:
292
293
  - instructions_to_generate_message: Plain text or template instructions from the user.
293
294
  - prompt_engineering_guidance: (Optional) Extra guidelines for structuring the prompt.
295
+ - allow_html: Whether HTML output is allowed.
296
+ - html_template: Optional HTML scaffolding or guidance.
294
297
  """
295
298
  instructions_to_generate_message: Optional[str] = None
296
299
  prompt_engineering_guidance: Optional[PromptEngineeringGuidance] = None
297
300
  use_cache: Optional[bool] = True
301
+ allow_html: Optional[bool] = False
302
+ html_template: Optional[str] = None
298
303
 
299
304
  class CampaignContext(BaseModel):
300
305
  """
@@ -723,7 +728,10 @@ class LeadsQueryFilters(BaseModel):
723
728
  # CHANGED: Renamed to be more descriptive
724
729
  industries: Optional[List[str]] = Field(
725
730
  default=None,
726
- description="List of organization industries. Maps to organization_industries in Apollo."
731
+ description=(
732
+ "List of organization industries (case sensitive). "
733
+ "Maps to organization_industries in Apollo."
734
+ )
727
735
  )
728
736
 
729
737
  # Potential existing fields
@@ -749,7 +757,7 @@ class LeadsQueryFilters(BaseModel):
749
757
  # CHANGED: Renamed to be more descriptive
750
758
  company_domains: Optional[List[str]] = Field(
751
759
  default=None,
752
- description="Domains of the person's employer (e.g., ['microsoft.com']). Maps to q_organization_domains."
760
+ description="Domains of the person's employer (e.g., ['apollo.io', 'microsoft.com']). Maps to q_organization_domains_list in Apollo API. Accepts up to 1,000 domains. Do not include www. or @ symbol."
753
761
  )
754
762
 
755
763
  # CHANGED: Renamed to be more descriptive
@@ -795,7 +803,20 @@ class LeadsQueryFilters(BaseModel):
795
803
  # CHANGED: Renamed for consistency
796
804
  company_industry_tag_ids: Optional[List[str]] = Field(
797
805
  default=None,
798
- description="List of industry tag IDs, e.g. ['5567cd4773696439b10b0000']. Maps to organization_industry_tag_ids."
806
+ description=(
807
+ "List of industry tag IDs, e.g. ['5567cd4773696439b10b0000']. "
808
+ "Maps to organization_industry_tag_ids."
809
+ )
810
+ )
811
+
812
+ q_organization_keyword_tags: Optional[List[str]] = Field(
813
+ default=None,
814
+ description="Organization Keyword tags to search by"
815
+ )
816
+
817
+ q_not_organization_keyword_tags: Optional[List[str]] = Field(
818
+ default=None,
819
+ description="Organization Keyword tags to search by"
799
820
  )
800
821
 
801
822
  q_organization_search_list_id: Optional[str] = Field(
@@ -861,6 +882,16 @@ class CompanyQueryFilters(BaseModel):
861
882
  default=None,
862
883
  description="List of industry tag IDs, e.g. ['5567cd4773696439b10b0000']."
863
884
  )
885
+
886
+ q_organization_keyword_tags: Optional[List[str]] = Field(
887
+ default=None,
888
+ description="Organization Keyword tags to search by"
889
+ )
890
+
891
+ q_not_organization_keyword_tags: Optional[List[str]] = Field(
892
+ default=None,
893
+ description="Organization Keyword tags to search by"
894
+ )
864
895
 
865
896
  # Revenue filters
866
897
  revenue_range_min: Optional[int] = Field(
@@ -938,4 +969,3 @@ class CompanyQueryFilters(BaseModel):
938
969
  default=None,
939
970
  description="Maximum founding year for the organization."
940
971
  )
941
-
@@ -547,8 +547,10 @@ async def search_leads_with_apollo(
547
547
  "revenueRange[max]": "revenue_range_max",
548
548
  "revenueRange[min]": "revenue_range_min",
549
549
  "currentlyUsingAnyOfTechnologyUids": "currently_using_any_of_technology_uids",
550
+ "organizationIndustries": "organization_industries",
550
551
  "organizationIndustryTagIds": "organization_industry_tag_ids",
551
552
  "notOrganizationIds": "not_organization_ids",
553
+ "qOrganizationDomainsList": "q_organization_domains_list",
552
554
  }
553
555
 
554
556
  for raw_key, raw_value_list in query_params.items():
@@ -606,13 +608,15 @@ async def search_leads_with_apollo(
606
608
  "person_titles",
607
609
  "person_seniorities",
608
610
  "organization_locations",
609
- "q_organization_domains",
611
+ "q_organization_domains_list",
610
612
  "contact_email_status",
611
613
  "organization_ids",
612
614
  "organization_num_employees_ranges",
613
615
  "person_not_titles", # <--- added so single item is forced into list
614
616
  "q_organization_job_titles",
615
617
  "organization_latest_funding_stage_cd",
618
+ "organization_industries",
619
+ "organization_industry_tag_ids",
616
620
  ):
617
621
  if isinstance(final_value, str):
618
622
  final_value = [final_value]
@@ -631,17 +635,28 @@ async def search_leads_with_apollo(
631
635
  # -----------------------------------
632
636
  else:
633
637
  dynamic_payload = {
634
- "person_titles": query.person_current_titles or [],
635
- "person_locations": query.person_locations or [],
636
- "search_signal_ids": query.filter_by_signals or [],
637
- "q_keywords": query.search_keywords or "",
638
- "organization_num_employees_ranges": (
639
- query.organization_num_employees_ranges
640
- or [f"{query.min_employees_in_organization or 1},{query.max_employees_in_organization or 1000}"]
641
- ),
642
638
  "page": 1,
643
639
  "per_page": min(max_items, 100),
644
640
  }
641
+
642
+ # Only add fields if they have values (don't pass empty defaults)
643
+ if query.person_current_titles:
644
+ dynamic_payload["person_titles"] = query.person_current_titles
645
+ if query.person_locations:
646
+ dynamic_payload["person_locations"] = query.person_locations
647
+ if query.filter_by_signals:
648
+ dynamic_payload["search_signal_ids"] = query.filter_by_signals
649
+ if query.search_keywords:
650
+ dynamic_payload["q_keywords"] = query.search_keywords
651
+
652
+ # Only add employee ranges if explicitly provided
653
+ if query.organization_num_employees_ranges:
654
+ dynamic_payload["organization_num_employees_ranges"] = query.organization_num_employees_ranges
655
+ elif query.min_employees_in_organization is not None or query.max_employees_in_organization is not None:
656
+ min_emp = query.min_employees_in_organization or 1
657
+ max_emp = query.max_employees_in_organization or 1000000
658
+ dynamic_payload["organization_num_employees_ranges"] = [f"{min_emp},{max_emp}"]
659
+
645
660
  if query.job_openings_with_titles:
646
661
  dynamic_payload["q_organization_job_titles"] = query.job_openings_with_titles
647
662
  if query.latest_funding_stages:
@@ -650,6 +665,15 @@ async def search_leads_with_apollo(
650
665
  dynamic_payload["sort_by_field"] = query.sort_by_field
651
666
  if query.sort_ascending is not None:
652
667
  dynamic_payload["sort_ascending"] = query.sort_ascending
668
+ if query.person_seniorities:
669
+ dynamic_payload["person_seniorities"] = query.person_seniorities
670
+ if query.industries:
671
+ dynamic_payload["organization_industries"] = query.industries
672
+ if query.company_industry_tag_ids:
673
+ dynamic_payload["organization_industry_tag_ids"] = query.company_industry_tag_ids
674
+ # Add company domains to include in search
675
+ if query.company_domains:
676
+ dynamic_payload["q_organization_domains_list"] = query.company_domains
653
677
 
654
678
  # -----------------------------
655
679
  # C) Fetch multiple pages
@@ -765,8 +789,10 @@ async def search_leads_with_apollo_page(
765
789
  "revenueRange[max]": "revenue_range_max",
766
790
  "revenueRange[min]": "revenue_range_min",
767
791
  "currentlyUsingAnyOfTechnologyUids": "currently_using_any_of_technology_uids",
792
+ "organizationIndustries": "organization_industries",
768
793
  "organizationIndustryTagIds": "organization_industry_tag_ids",
769
794
  "notOrganizationIds": "not_organization_ids",
795
+ "qOrganizationDomainsList": "q_organization_domains_list",
770
796
  }
771
797
 
772
798
  for raw_key, raw_value_list in query_params.items():
@@ -809,13 +835,15 @@ async def search_leads_with_apollo_page(
809
835
  "person_titles",
810
836
  "person_seniorities",
811
837
  "organization_locations",
812
- "q_organization_domains",
838
+ "q_organization_domains_list",
813
839
  "contact_email_status",
814
840
  "organization_ids",
815
841
  "organization_num_employees_ranges",
816
842
  "person_not_titles",
817
843
  "q_organization_job_titles",
818
844
  "organization_latest_funding_stage_cd",
845
+ "organization_industries",
846
+ "organization_industry_tag_ids",
819
847
  ):
820
848
  if isinstance(final_value, str):
821
849
  final_value = [final_value]
@@ -829,16 +857,26 @@ async def search_leads_with_apollo_page(
829
857
  # B) No example_url -> build from `query`
830
858
  # -----------------------------------
831
859
  else:
832
- dynamic_payload = {
833
- "person_titles": query.person_current_titles or [],
834
- "person_locations": query.person_locations or [],
835
- "search_signal_ids": query.filter_by_signals or [],
836
- "q_keywords": query.search_keywords or "",
837
- "organization_num_employees_ranges": (
838
- query.organization_num_employees_ranges
839
- or [f"{query.min_employees_in_organization or 1},{query.max_employees_in_organization or 1000}"]
840
- ),
841
- }
860
+ dynamic_payload = {}
861
+
862
+ # Only add fields if they have values (don't pass empty defaults)
863
+ if query.person_current_titles:
864
+ dynamic_payload["person_titles"] = query.person_current_titles
865
+ if query.person_locations:
866
+ dynamic_payload["person_locations"] = query.person_locations
867
+ if query.filter_by_signals:
868
+ dynamic_payload["search_signal_ids"] = query.filter_by_signals
869
+ if query.search_keywords:
870
+ dynamic_payload["q_keywords"] = query.search_keywords
871
+
872
+ # Only add employee ranges if explicitly provided
873
+ if query.organization_num_employees_ranges:
874
+ dynamic_payload["organization_num_employees_ranges"] = query.organization_num_employees_ranges
875
+ elif query.min_employees_in_organization is not None or query.max_employees_in_organization is not None:
876
+ min_emp = query.min_employees_in_organization or 1
877
+ max_emp = query.max_employees_in_organization or 1000000
878
+ dynamic_payload["organization_num_employees_ranges"] = [f"{min_emp},{max_emp}"]
879
+
842
880
  if query.job_openings_with_titles:
843
881
  dynamic_payload["q_organization_job_titles"] = query.job_openings_with_titles
844
882
  if query.latest_funding_stages:
@@ -847,6 +885,19 @@ async def search_leads_with_apollo_page(
847
885
  dynamic_payload["sort_by_field"] = query.sort_by_field
848
886
  if query.sort_ascending is not None:
849
887
  dynamic_payload["sort_ascending"] = query.sort_ascending
888
+ if query.q_organization_keyword_tags:
889
+ dynamic_payload["q_organization_keyword_tags"] = query.q_organization_keyword_tags
890
+
891
+ if query.q_not_organization_keyword_tags:
892
+ dynamic_payload["q_not_organization_keyword_tags"] = query.q_not_organization_keyword_tags
893
+ if query.industries:
894
+ dynamic_payload["organization_industries"] = query.industries
895
+ if query.company_industry_tag_ids:
896
+ dynamic_payload["organization_industry_tag_ids"] = query.company_industry_tag_ids
897
+
898
+ # Add company domains to include in search (Apollo API: q_organization_domains_list[])
899
+ if query.company_domains:
900
+ dynamic_payload["q_organization_domains_list"] = query.company_domains
850
901
 
851
902
  page_payload = dict(dynamic_payload)
852
903
  page_payload["page"] = page
@@ -1250,62 +1301,131 @@ def fill_in_company_properties(company_data: dict) -> dict:
1250
1301
  company_data: Raw company data from Apollo API
1251
1302
 
1252
1303
  Returns:
1253
- Dictionary with standardized company properties
1304
+ Dictionary matching the SmartList `Account` schema shape.
1254
1305
  """
1255
- company_properties = {}
1306
+ def _parse_keywords(value: Any) -> List[Any]:
1307
+ if value is None:
1308
+ return []
1309
+ if isinstance(value, list):
1310
+ return value
1311
+ if isinstance(value, str):
1312
+ text = value.strip()
1313
+ if not text:
1314
+ return []
1315
+ if "," in text:
1316
+ return [part.strip() for part in text.split(",") if part.strip()]
1317
+ return [text]
1318
+ return [value]
1319
+
1320
+ def _parse_compact_number(value: Any) -> Optional[float]:
1321
+ if value is None:
1322
+ return None
1323
+ if isinstance(value, (int, float)):
1324
+ return float(value)
1325
+ text = str(value).strip()
1326
+ if not text:
1327
+ return None
1328
+ text = text.replace("$", "").replace(",", "").strip()
1329
+ multiplier = 1.0
1330
+ suffix = text[-1:].upper()
1331
+ if suffix in ("K", "M", "B"):
1332
+ multiplier = {"K": 1e3, "M": 1e6, "B": 1e9}[suffix]
1333
+ text = text[:-1].strip()
1334
+ try:
1335
+ return float(text) * multiplier
1336
+ except ValueError:
1337
+ return None
1256
1338
 
1257
- # Basic company information
1258
- company_properties["organization_name"] = company_data.get("name", "")
1259
- company_properties["primary_domain"] = company_data.get("primary_domain", "")
1260
- company_properties["website_url"] = company_data.get("website_url", "")
1261
- company_properties["organization_linkedin_url"] = company_data.get("linkedin_url", "")
1262
-
1263
- # Location information
1264
- company_properties["organization_city"] = company_data.get("city", "")
1265
- company_properties["organization_state"] = company_data.get("state", "")
1266
- company_properties["organization_country"] = company_data.get("country", "")
1267
-
1268
- # Create a combined location string
1269
- location_parts = [
1270
- company_data.get("city", ""),
1271
- company_data.get("state", ""),
1272
- company_data.get("country", "")
1273
- ]
1274
- company_properties["organization_location"] = ", ".join([part for part in location_parts if part])
1275
-
1276
- # Company size and financial info
1277
- company_properties["employee_count"] = company_data.get("estimated_num_employees", 0)
1278
- company_properties["annual_revenue"] = company_data.get("annual_revenue", 0)
1279
-
1280
- # Industry and business info
1281
- company_properties["industry"] = company_data.get("industry", "")
1282
- company_properties["keywords"] = ", ".join(company_data.get("keywords", []))
1283
- company_properties["description"] = company_data.get("description", "")
1284
-
1285
- # Funding and growth
1286
- company_properties["founded_year"] = company_data.get("founded_year", "")
1287
- company_properties["funding_stage"] = company_data.get("latest_funding_stage", "")
1288
- company_properties["total_funding"] = company_data.get("total_funding", 0)
1289
-
1290
- # Technology stack
1291
- tech_stack = company_data.get("technology_names", [])
1292
- if tech_stack:
1293
- company_properties["technology_stack"] = ", ".join(tech_stack)
1294
-
1295
- # Apollo-specific IDs
1296
- company_properties["apollo_organization_id"] = company_data.get("id", "")
1297
-
1298
- # Additional metadata
1299
- company_properties["phone"] = company_data.get("phone", "")
1300
- company_properties["facebook_url"] = company_data.get("facebook_url", "")
1301
- company_properties["twitter_url"] = company_data.get("twitter_url", "")
1302
-
1303
- # Store raw data for reference
1304
- company_properties["additional_properties"] = {
1305
- "apollo_organization_data": json.dumps(cleanup_properties(company_data))
1339
+ annual_revenue = (
1340
+ company_data.get("organization_revenue")
1341
+ if company_data.get("organization_revenue") is not None
1342
+ else company_data.get("annual_revenue")
1343
+ )
1344
+ annual_revenue = _parse_compact_number(annual_revenue)
1345
+ if annual_revenue is None:
1346
+ annual_revenue = _parse_compact_number(company_data.get("organization_revenue_printed"))
1347
+
1348
+ company_size = company_data.get("estimated_num_employees")
1349
+ if company_size is not None:
1350
+ try:
1351
+ company_size = int(company_size)
1352
+ except (TypeError, ValueError):
1353
+ company_size = None
1354
+
1355
+ founded_year = company_data.get("founded_year")
1356
+ if founded_year is not None:
1357
+ try:
1358
+ founded_year = int(founded_year)
1359
+ except (TypeError, ValueError):
1360
+ founded_year = None
1361
+
1362
+ primary_phone = company_data.get("primary_phone")
1363
+ primary_phone_number = None
1364
+ if isinstance(primary_phone, dict):
1365
+ primary_phone_number = primary_phone.get("number") or primary_phone.get(
1366
+ "sanitized_number"
1367
+ )
1368
+
1369
+ phone = (
1370
+ primary_phone_number
1371
+ or company_data.get("phone")
1372
+ or company_data.get("primary_phone_number")
1373
+ or company_data.get("sanitized_phone")
1374
+ )
1375
+
1376
+ industry = company_data.get("industry")
1377
+ if not industry and isinstance(company_data.get("industries"), list):
1378
+ industries = [str(x).strip() for x in company_data["industries"] if str(x).strip()]
1379
+ industry = industries[0] if industries else None
1380
+
1381
+ billing_street = (
1382
+ company_data.get("street_address")
1383
+ or company_data.get("billing_street")
1384
+ or company_data.get("address")
1385
+ or company_data.get("raw_address")
1386
+ )
1387
+
1388
+ account: Dict[str, Any] = {
1389
+ "name": company_data.get("name"),
1390
+ "domain": company_data.get("primary_domain"),
1391
+ "website": company_data.get("website_url"),
1392
+ "phone": phone,
1393
+ "fax": company_data.get("fax") or company_data.get("fax_number"),
1394
+ "industry": industry,
1395
+ "company_size": company_size,
1396
+ "founded_year": founded_year,
1397
+ "annual_revenue": annual_revenue,
1398
+ "type": company_data.get("type") or company_data.get("organization_type"),
1399
+ "ownership": company_data.get("ownership"),
1400
+ "organization_linkedin_url": company_data.get("linkedin_url"),
1401
+ "billing_street": billing_street,
1402
+ "billing_city": company_data.get("city"),
1403
+ "billing_state": company_data.get("state"),
1404
+ "billing_zip": company_data.get("postal_code")
1405
+ or company_data.get("zip")
1406
+ or company_data.get("zipcode"),
1407
+ "billing_country": company_data.get("country"),
1408
+ "description": company_data.get("description"),
1409
+ "keywords": _parse_keywords(company_data.get("keywords")),
1410
+ "tags": [],
1411
+ "notes": [],
1412
+ "additional_properties": {
1413
+ "apollo_organization_id": company_data.get("id"),
1414
+ "facebook_url": company_data.get("facebook_url"),
1415
+ "twitter_url": company_data.get("twitter_url"),
1416
+ "funding_stage": company_data.get("latest_funding_stage"),
1417
+ "total_funding": company_data.get("total_funding"),
1418
+ "technology_names": company_data.get("technology_names"),
1419
+ "primary_phone": primary_phone if isinstance(primary_phone, dict) else None,
1420
+ "raw_address": company_data.get("raw_address"),
1421
+ "organization_revenue_printed": company_data.get("organization_revenue_printed"),
1422
+ "apollo_organization_data": json.dumps(cleanup_properties(company_data)),
1423
+ },
1424
+ "research_summary": None,
1425
+ "enchrichment_status": None,
1306
1426
  }
1307
1427
 
1308
- return company_properties
1428
+ return account
1309
1429
 
1310
1430
 
1311
1431
  @assistant_tool
@@ -1363,7 +1483,7 @@ async def search_companies_with_apollo_page(
1363
1483
  "organizationIndustries": "organization_industries",
1364
1484
  "organizationIndustryTagIds": "organization_industry_tag_ids",
1365
1485
  "qKeywords": "q_keywords",
1366
- "qOrganizationDomains": "q_organization_domains",
1486
+ "qOrganizationDomainsList": "q_organization_domains_list",
1367
1487
  "sortAscending": "sort_ascending",
1368
1488
  "sortByField": "sort_by_field",
1369
1489
  "organizationLatestFundingStageCd": "organization_latest_funding_stage_cd",
@@ -1427,7 +1547,7 @@ async def search_companies_with_apollo_page(
1427
1547
  "organization_locations",
1428
1548
  "organization_industries",
1429
1549
  "organization_industry_tag_ids",
1430
- "q_organization_domains",
1550
+ "q_organization_domains_list",
1431
1551
  "q_organization_keyword_tags",
1432
1552
  "organization_ids",
1433
1553
  "not_organization_ids",
@@ -1448,7 +1568,7 @@ async def search_companies_with_apollo_page(
1448
1568
  # -----------------------------------
1449
1569
  else:
1450
1570
  dynamic_payload = {}
1451
-
1571
+
1452
1572
  # Only add fields if they have values (Apollo doesn't like empty arrays)
1453
1573
  if query.organization_locations:
1454
1574
  dynamic_payload["organization_locations"] = query.organization_locations
@@ -1468,15 +1588,39 @@ async def search_companies_with_apollo_page(
1468
1588
  dynamic_payload["organization_num_employees_ranges"] = employee_ranges
1469
1589
 
1470
1590
  # Add optional parameters only if they have values
1591
+ def _normalize_string_list(value: Any) -> List[str]:
1592
+ if value is None:
1593
+ return []
1594
+ if isinstance(value, str):
1595
+ return [part.strip() for part in value.split(",") if part.strip()]
1596
+ if isinstance(value, list):
1597
+ normalized: List[str] = []
1598
+ for item in value:
1599
+ if item is None:
1600
+ continue
1601
+ text = str(item).strip()
1602
+ if not text:
1603
+ continue
1604
+ normalized.extend([part.strip() for part in text.split(",") if part.strip()])
1605
+ return normalized
1606
+ text = str(value).strip()
1607
+ return [text] if text else []
1608
+
1471
1609
  if query.q_keywords:
1472
- # Split comma-separated keywords into an array for company search
1473
- if isinstance(query.q_keywords, str):
1474
- keyword_tags = [tag.strip() for tag in query.q_keywords.split(",") if tag.strip()]
1475
- else:
1476
- keyword_tags = query.q_keywords
1477
- dynamic_payload["q_organization_keyword_tags"] = keyword_tags
1610
+ keywords = _normalize_string_list(query.q_keywords)
1611
+ if keywords:
1612
+ dynamic_payload["q_keywords"] = " ".join(keywords)
1613
+
1614
+ org_keyword_tags = _normalize_string_list(query.q_organization_keyword_tags)
1615
+ if org_keyword_tags:
1616
+ dynamic_payload["q_organization_keyword_tags"] = org_keyword_tags
1617
+
1618
+ not_org_keyword_tags = _normalize_string_list(query.q_not_organization_keyword_tags)
1619
+ if not_org_keyword_tags:
1620
+ dynamic_payload["q_not_organization_keyword_tags"] = not_org_keyword_tags
1621
+
1478
1622
  if query.q_organization_domains:
1479
- dynamic_payload["q_organization_domains"] = query.q_organization_domains
1623
+ dynamic_payload["q_organization_domains_list"] = query.q_organization_domains
1480
1624
  if query.revenue_range_min is not None:
1481
1625
  dynamic_payload["revenue_range_min"] = query.revenue_range_min
1482
1626
  if query.revenue_range_max is not None:
@@ -2,6 +2,7 @@ import os
2
2
  import hashlib
3
3
  import json
4
4
  import logging
5
+ from datetime import datetime, timezone
5
6
 
6
7
  from azure.storage.blob import BlobServiceClient
7
8
  from azure.core.exceptions import ResourceNotFoundError, AzureError
@@ -49,10 +50,11 @@ def cache_output(tool_name: str, key: str, value, ttl: int = None) -> bool:
49
50
  # Construct the blob name using a virtual folder for the tool name
50
51
  blob_name = f"{tool_name}/{key_hash}.json"
51
52
 
52
- # Prepare the cache data
53
+ # Prepare the cache data with timestamp for TTL expiration checking
53
54
  cache_data = {
54
55
  "value": value,
55
- "ttl": ttl
56
+ "ttl": ttl,
57
+ "cached_at": datetime.now(timezone.utc).isoformat()
56
58
  }
57
59
  data = json.dumps(cache_data)
58
60
 
@@ -88,6 +90,24 @@ def retrieve_output(tool_name: str, key: str):
88
90
  download_stream = blob_client.download_blob()
89
91
  content = download_stream.readall() # content is in bytes
90
92
  cache_data = json.loads(content.decode("utf-8"))
93
+
94
+ # Check if TTL has expired
95
+ ttl = cache_data.get("ttl")
96
+ cached_at = cache_data.get("cached_at")
97
+
98
+ if ttl is not None and cached_at is not None:
99
+ try:
100
+ cached_time = datetime.fromisoformat(cached_at)
101
+ now = datetime.now(timezone.utc)
102
+ elapsed_seconds = (now - cached_time).total_seconds()
103
+ if elapsed_seconds > ttl:
104
+ logger.info(f"Cache expired for blob '{blob_name}' (elapsed: {elapsed_seconds}s, ttl: {ttl}s)")
105
+ return None
106
+ except (ValueError, TypeError) as e:
107
+ logger.warning(f"Error parsing cached_at timestamp: {e}")
108
+ # If we can't parse the timestamp, treat as expired for safety
109
+ return None
110
+
91
111
  return cache_data.get("value")
92
112
  except ResourceNotFoundError:
93
113
  # Blob does not exist
@@ -514,8 +514,8 @@ async def guess_email_with_apollo(
514
514
  if not apollo_email:
515
515
  return {"email": "", "email_confidence": "low"}
516
516
 
517
- # quick re‑check with Hunter
518
- validation = await check_email_validity_with_hunter(apollo_email, tool_config)
517
+ # quick re‑check with ZeroBounce
518
+ validation = await check_email_validity_with_zero_bounce(apollo_email, tool_config)
519
519
  conf = validation.get("confidence", "low")
520
520
  return {"email": apollo_email, "email_confidence": conf}
521
521
 
@@ -547,7 +547,7 @@ async def check_email_validity(
547
547
  return {"email": email_id, "confidence": "low", "is_valid": False}
548
548
 
549
549
  names = [c.get("name") for c in tool_config if c.get("name")]
550
- priority = ["findymail", "hunter", "zerobounce"]
550
+ priority = ["zerobounce", "findymail", "hunter"]
551
551
 
552
552
  result: Dict[str, Any] = {"email": email_id, "confidence": "low", "is_valid": False}
553
553
 
@@ -586,7 +586,7 @@ async def guess_email(
586
586
  return {"email": "", "email_confidence": "low"}
587
587
 
588
588
  names = [c.get("name") for c in tool_config if c.get("name")]
589
- priority = ["findymail", "hunter", "zerobounce", "apollo"]
589
+ priority = ["apollo", "findymail", "hunter", "zerobounce"]
590
590
 
591
591
  result: Dict[str, Any] = {"email": "", "email_confidence": "low"}
592
592
 
@@ -765,16 +765,32 @@ async def enrich_organization_info_from_company_url(
765
765
  organization_linkedin_url: str,
766
766
  use_strict_check: bool = True,
767
767
  tool_config: Optional[List[Dict[str, Any]]] = None,
768
+ categories: Optional[bool] = None,
769
+ funding_data: Optional[bool] = None,
770
+ exit_data: Optional[bool] = None,
771
+ acquisitions: Optional[bool] = None,
772
+ extra: Optional[bool] = None,
773
+ use_cache: Optional[str] = "if-present",
774
+ fallback_to_cache: Optional[str] = "on-error",
768
775
  ) -> Dict[str, Any]:
769
776
  """
770
777
  Given an organization LinkedIn URL, attempt to enrich its data (e.g. name, website)
771
- via ProxyCurl. If data is found, set domain, then return the dict. Otherwise, return {}.
778
+ via ProxyCurl. Additional Proxycurl Company API boolean flags (categories, funding_data, etc.)
779
+ can be supplied to control the returned payload (True -> "include"). If data is found,
780
+ set domain, then return the dict. Otherwise, return {}.
772
781
  """
773
782
 
774
783
  # Call ProxyCurl to enrich
775
784
  company_data = await enrich_organization_info_from_proxycurl(
776
785
  organization_linkedin_url=organization_linkedin_url,
777
- tool_config=tool_config
786
+ tool_config=tool_config,
787
+ categories=categories,
788
+ funding_data=funding_data,
789
+ exit_data=exit_data,
790
+ acquisitions=acquisitions,
791
+ extra=extra,
792
+ use_cache=use_cache,
793
+ fallback_to_cache=fallback_to_cache,
778
794
  )
779
795
 
780
796
  # If ProxyCurl returned any data, set domain, then return