dhisana 0.0.1.dev33__tar.gz → 0.0.1.dev34__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 (88) hide show
  1. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/setup.py +1 -1
  3. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/schemas/sales.py +169 -1
  4. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/agent_task.py +1 -1
  5. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/apollo_tools.py +175 -73
  6. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/check_for_intent_signal.py +18 -10
  7. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_three_step_workflow.py +4 -4
  8. dhisana-0.0.1.dev34/src/dhisana/utils/generate_leads.py +58 -0
  9. dhisana-0.0.1.dev33/src/dhisana/utils/create_list_from_sales_navigator.py → dhisana-0.0.1.dev34/src/dhisana/utils/generate_leads_salesnav.py +10 -4
  10. dhisana-0.0.1.dev34/src/dhisana/utils/generate_smartlist.py +171 -0
  11. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_structured_output_internal.py +2 -1
  12. dhisana-0.0.1.dev33/src/dhisana/utils/create_smart_list.py → dhisana-0.0.1.dev34/src/dhisana/utils/qualify_leads.py +19 -19
  13. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/PKG-INFO +1 -1
  14. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/SOURCES.txt +4 -2
  15. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/README.md +0 -0
  16. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/pyproject.toml +0 -0
  17. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/setup.cfg +0 -0
  18. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/__init__.py +0 -0
  19. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/__init__.py +0 -0
  20. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/cli.py +0 -0
  21. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/datasets.py +0 -0
  22. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/models.py +0 -0
  23. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/predictions.py +0 -0
  24. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/schemas/__init__.py +0 -0
  25. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/schemas/common.py +0 -0
  26. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/ui/__init__.py +0 -0
  27. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/ui/components.py +0 -0
  28. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/__init__.py +0 -0
  29. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/agent_tools.py +0 -0
  30. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  31. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/built_with_api_tools.py +0 -0
  32. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/cache_output_tools.py +0 -0
  33. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  34. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  35. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  36. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/clay_tools.py +0 -0
  37. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/company_utils.py +0 -0
  38. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_cadence.py +0 -0
  39. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  40. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_search_query.py +0 -0
  41. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_workflow.py +0 -0
  42. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/composite_tools.py +0 -0
  43. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/dataframe_tools.py +0 -0
  44. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/domain_parser.py +0 -0
  45. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/enrich_lead_information.py +0 -0
  46. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  47. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/g2_tools.py +0 -0
  48. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_content.py +0 -0
  49. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_email.py +0 -0
  50. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_email_response.py +0 -0
  51. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_flow.py +0 -0
  52. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  53. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  54. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/google_custom_search.py +0 -0
  55. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/google_workspace_tools.py +0 -0
  56. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  57. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  58. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/instantly_tools.py +0 -0
  59. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/linkedin_crawler.py +0 -0
  60. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/lusha_tools.py +0 -0
  61. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  62. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openai_helpers.py +0 -0
  63. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  64. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  65. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  66. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  67. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  68. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  69. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/python_function_to_tools.py +0 -0
  70. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/research_lead.py +0 -0
  71. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  72. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  73. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/sendgrid_tools.py +0 -0
  74. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  75. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/trasform_json.py +0 -0
  76. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  77. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/workflow_code_model.py +0 -0
  78. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/zoominfo_tools.py +0 -0
  79. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/__init__.py +0 -0
  80. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/agent.py +0 -0
  81. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/flow.py +0 -0
  82. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/task.py +0 -0
  83. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/test.py +0 -0
  84. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/dependency_links.txt +0 -0
  85. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/entry_points.txt +0 -0
  86. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/requires.txt +0 -0
  87. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/top_level.txt +0 -0
  88. {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/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.dev33
3
+ Version: 0.0.1.dev34
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-dev33',
5
+ version='0.0.1-dev34',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -279,4 +279,172 @@ HUBSPOT_TO_LEAD_MAPPING = {
279
279
  "address": "lead_location", # You can choose "city", "state", etc. if you prefer
280
280
  "city": "lead_location",
281
281
  "domain": "primary_domain_of_organization",
282
- }
282
+ }
283
+
284
+
285
+
286
+ class SmartListStatus(str, Enum):
287
+ DRAFT = "DRAFT"
288
+ ACTIVE = "ACTIVE"
289
+ COMPLETED = "COMPLETED"
290
+
291
+
292
+ class SmartList(BaseModel):
293
+ id: Optional[UUID] = None
294
+ name: Optional[str] = None
295
+ description: Optional[str] = None
296
+ instructions: Optional[str] = None
297
+ max_leads: Optional[int] = None
298
+ use_online_search: Optional[bool] = False
299
+ category: Optional[str] = None
300
+
301
+ status: SmartListStatus = SmartListStatus.DRAFT
302
+ start_date: Optional[int] = None
303
+ end_date: Optional[int] = None
304
+
305
+ agent_instance_id: Optional[UUID] = None
306
+ organization_id: Optional[UUID] = None
307
+ created_by: Optional[UUID] = None
308
+ created_at: Optional[int] = None
309
+ updated_by: Optional[UUID] = None
310
+ updated_at: Optional[int] = None
311
+
312
+ class Config:
313
+ from_attributes = True
314
+
315
+
316
+ class SmartListLead(BaseModel):
317
+ id: Optional[UUID] = None
318
+
319
+ smart_list_id: Optional[UUID] = None
320
+
321
+ full_name: Optional[str] = None
322
+ first_name: Optional[str] = None
323
+ last_name: Optional[str] = None
324
+ email: Optional[str] = None
325
+ user_linkedin_url: Optional[str] = None
326
+ user_linkedin_salesnav_url: Optional[str] = None
327
+ organization_linkedin_url: Optional[str] = None
328
+ organization_linkedin_salesnav_url: Optional[str] = None
329
+ primary_domain_of_organization: Optional[str] = None
330
+ job_title: Optional[str] = None
331
+ phone: Optional[str] = None
332
+ headline: Optional[str] = None
333
+ lead_location: Optional[str] = None
334
+ organization_name: Optional[str] = None
335
+ organization_website: Optional[str] = None
336
+ summary_about_lead: Optional[str] = None
337
+ keywords: Optional[List[str]] = None
338
+ additional_properties: Optional[Dict[str, str]] = None
339
+ research_summary: Optional[str] = None
340
+
341
+ qualification_score: Optional[float] = None
342
+ qualification_reason: Optional[str] = None
343
+ source: Optional[str] = None
344
+
345
+ agent_instance_id: Optional[UUID] = None
346
+ organization_id: Optional[UUID] = None
347
+ created_by: Optional[UUID] = None
348
+ created_at: Optional[int] = None
349
+ updated_by: Optional[UUID] = None
350
+ updated_at: Optional[int] = None
351
+
352
+ class Config:
353
+ from_attributes = True
354
+
355
+
356
+ class SmartListLog(BaseModel):
357
+ id: Optional[UUID] = None
358
+ message: str
359
+
360
+ smart_list_id: UUID
361
+
362
+ agent_instance_id: Optional[UUID] = None
363
+ organization_id: Optional[UUID] = None
364
+ created_by: Optional[UUID] = None
365
+ created_at: Optional[int] = None
366
+ updated_by: Optional[UUID] = None
367
+ updated_at: Optional[int] = None
368
+
369
+ class Config:
370
+ from_attributes = True
371
+
372
+ class LeadsQueryFilters(BaseModel):
373
+ """
374
+ Defines the filter parameters used to query leads in the Apollo database.
375
+ All fields are optional and will default to None if not specified by user.
376
+ """
377
+ # Existing fields
378
+ job_titles_to_search: Optional[List[str]] = Field(
379
+ default=None,
380
+ description="List of job titles to include in the search."
381
+ )
382
+ locations_to_search: Optional[List[str]] = Field(
383
+ default=None,
384
+ description="List of personal locations (cities, states, countries)."
385
+ )
386
+ min_number_of_employees_in_organization: Optional[int] = Field(
387
+ default=None,
388
+ description="Minimum number of employees (>=1). Default=1 if omitted."
389
+ )
390
+ max_number_of_employees_in_organization: Optional[int] = Field(
391
+ default=None,
392
+ description="Maximum number of employees (<=100000). Default=1000 if omitted."
393
+ )
394
+ filter_by_signals: Optional[List[str]] = Field(
395
+ default=None,
396
+ description="List of signals to filter by, e.g. ['RECENT_JOB_CHANGE']."
397
+ )
398
+ max_number_of_items_to_return: Optional[int] = Field(
399
+ default=None,
400
+ description="Max # of items (<=5000). Default=100."
401
+ )
402
+ industries: Optional[List[str]] = Field(
403
+ default=None,
404
+ description="List of industries. Default=[] if omitted."
405
+ )
406
+
407
+ # Fields that were not previously used by the search, but can exist
408
+ min_revenue_of_the_company: Optional[int] = Field(
409
+ default=None,
410
+ description="Minimum company revenue (not currently used in People Search)."
411
+ )
412
+ max_revenue_of_the_company: Optional[int] = Field(
413
+ default=None,
414
+ description="Maximum company revenue (not currently used in People Search)."
415
+ )
416
+ job_functions: Optional[List[str]] = Field(
417
+ default=None,
418
+ description="List of job functions (not directly used by the new People Search)."
419
+ )
420
+
421
+ # --------------------------------------------------
422
+ # New fields from the updated Apollo doc
423
+ # --------------------------------------------------
424
+ q_keywords: Optional[str] = Field(
425
+ default=None,
426
+ description="A string of keywords to filter results."
427
+ )
428
+ organization_domains: Optional[List[str]] = Field(
429
+ default=None,
430
+ description="Domains of the person's employer (e.g., ['microsoft.com'])."
431
+ )
432
+ organization_locations: Optional[List[str]] = Field(
433
+ default=None,
434
+ description="List of HQ locations for the employer (city, state, country)."
435
+ )
436
+ contact_email_status: Optional[List[str]] = Field(
437
+ default=None,
438
+ description="Email statuses to filter by, e.g. ['verified', 'unavailable']."
439
+ )
440
+ organization_ids: Optional[List[str]] = Field(
441
+ default=None,
442
+ description="Apollo IDs for the companies/employers (string IDs)."
443
+ )
444
+ person_seniorities: Optional[List[str]] = Field(
445
+ default=None,
446
+ description="List of job seniorities, e.g. ['manager', 'director', 'vp']. "
447
+ "Apollo supports: owner, founder, c_suite, partner, vp, head, "
448
+ "director, manager, senior, entry, intern."
449
+ )
450
+
@@ -55,7 +55,7 @@ async def execute_task(
55
55
 
56
56
  # Unique ID for this request
57
57
  request_id = str(uuid.uuid4())
58
- api_base_url = os.environ.get("AGENT_SERVICE_URL", "https://api.dhisana.ai/v1")
58
+ api_base_url = os.environ.get("AGENT_SERVICE_URL", "https://api-agent.dhisana.ai/v1")
59
59
 
60
60
  # The payload to send when adding a task
61
61
  payload = {
@@ -5,8 +5,11 @@ import logging
5
5
  import os
6
6
  import aiohttp
7
7
  import backoff
8
- from typing import Dict, List, Optional
8
+ from typing import Any, Dict, List, Optional
9
9
  from datetime import datetime, timedelta
10
+
11
+ from pydantic import BaseModel
12
+ from dhisana.schemas.sales import LeadsQueryFilters, SmartList, SmartListLead
10
13
  from dhisana.utils.cache_output_tools import cache_output,retrieve_output
11
14
  from dhisana.utils.assistant_tool_tag import assistant_tool
12
15
 
@@ -272,87 +275,81 @@ async def fetch_apollo_data(session, url, headers, payload):
272
275
  response.raise_for_status()
273
276
 
274
277
 
278
+
279
+ # --------------------------------------------------
280
+ # The People Search Function
281
+ # --------------------------------------------------
275
282
 
276
- @assistant_tool
277
283
  async def search_people_with_apollo(
278
- job_titles: List[str],
279
- locations: List[str],
280
- min_number_of_employees: int,
281
- max_number_of_employees: int,
282
- filter_by_signals: List[str],
283
- max_number_of_items_to_return: int,
284
- industries: List[str],
285
- tool_config: Optional[List[Dict]] = None,
284
+ job_titles: List[str] = [],
285
+ locations: List[str] = [],
286
+ min_number_of_employees: int = 1,
287
+ max_number_of_employees: int = 1000,
288
+ filter_by_signals: List[str] = [],
289
+ max_number_of_items_to_return: int = 100,
290
+ industries: List[str] = [],
291
+ q_keywords: Optional[str] = None,
292
+ organization_domains: Optional[List[str]] = None,
293
+ organization_locations: Optional[List[str]] = None,
294
+ contact_email_status: Optional[List[str]] = None,
295
+ organization_ids: Optional[List[str]] = None,
296
+ person_seniorities: Optional[List[str]] = None,
297
+ tool_config: Optional[List[Dict[str, Any]]] = None
286
298
  ) -> List[Dict]:
287
299
  """
288
- Search for individuals on Apollo based on specified criteria.
289
-
290
- Parameters:
291
- - **job_titles** (*List[str]*): Job titles to include in the search.
292
- - **locations** (*List[str]*): Locations to filter the search.
293
- - **min_number_of_employees** (*int*): Minimum number of employees in the organization.
294
- Must be >= 1. Default is 1
295
- - **max_number_of_employees** (*int*): Maximum number of employees in the organization.
296
- Must be <= 100000 and greater than min_number_of_employees. Default 1000
297
- - **filter_by_signals** (*List[str]*): Signals to filter by. Valid options:
298
- - "RECENT_JOB_CHANGE"
299
- - "RAPID_EXPANSION"
300
- - **max_number_of_items_to_return** (*int*): Maximum number of results to return. Defaults to 100,
301
- with a maximum allowed value of 5000.
302
- - **industries** (*List[str]*): A list of industries to filter by. Defaults to [] for no filter. Default empty []
303
-
304
- Returns:
305
- - **List[Dict]**: A list of individual records (dictionaries) matching the specified criteria,
306
- or a single-item list containing a JSON-encoded error dictionary if an error occurs.
300
+ Searches Apollo using the People Search endpoint and returns raw results (people + contacts).
307
301
  """
308
302
 
309
- # Validate min and max employees
303
+ # Validate min/max employees
310
304
  if min_number_of_employees < 1:
311
305
  raise ValueError("Minimum number of employees must be at least 1.")
312
306
  if max_number_of_employees > 100000:
313
307
  raise ValueError("Maximum number of employees must not exceed 100,000.")
314
308
  if min_number_of_employees >= max_number_of_employees:
315
- raise ValueError("Minimum number of employees must be less than the maximum number.")
309
+ raise ValueError("Minimum employees must be less than maximum employees.")
316
310
 
317
- APOLLO_API_KEY = get_apollo_access_token(tool_config)
318
-
319
- # Log the search parameters for debugging
320
- logging.info(
321
- f"Initiating search with parameters: "
322
- f"Job titles: {job_titles}, Locations: {locations}, "
323
- f"Employee range: {min_number_of_employees}-{max_number_of_employees}, "
324
- f"Signals: {filter_by_signals}, "
325
- f"Industries: {industries}, "
326
- f"Max items: {max_number_of_items_to_return}"
327
- )
328
-
329
- # Ensure a positive number of items to return
311
+ # Enforce search result limits
330
312
  if max_number_of_items_to_return <= 0:
331
313
  max_number_of_items_to_return = 10
314
+ elif max_number_of_items_to_return > 5000:
315
+ max_number_of_items_to_return = 5000
332
316
 
317
+ api_key = get_apollo_access_token(tool_config)
333
318
  headers = {
334
319
  "Cache-Control": "no-cache",
335
320
  "Content-Type": "application/json",
336
- "X-Api-Key": APOLLO_API_KEY,
321
+ "X-Api-Key": api_key,
337
322
  }
338
323
 
339
- # Apollo endpoint for searching
340
324
  url = "https://api.apollo.io/v1/mixed_people/search"
341
325
 
342
- # Mapping of filter signals to their Apollo-specific IDs
326
+ # Convert signals to Apollo-specific IDs
343
327
  signal_mapping = {
344
328
  "RECENT_JOB_CHANGE": "643daa349293c1cdaa4d00f8",
345
- "RAPID_EXPANSION": "643daa3f9293c1cdaa4d00fa"
329
+ "RAPID_EXPANSION": "643daa3f9293c1cdaa4d00fa",
346
330
  }
347
-
348
- # Translate requested signals into Apollo signal IDs
349
331
  search_signal_ids = [signal_mapping[s] for s in filter_by_signals if s in signal_mapping]
350
332
 
333
+ logging.info(
334
+ f"Apollo Search:\n"
335
+ f" job_titles={job_titles}\n"
336
+ f" person_locations={locations}\n"
337
+ f" organization_num_employees_ranges=[{min_number_of_employees},{max_number_of_employees}]\n"
338
+ f" filter_by_signals={filter_by_signals}\n"
339
+ f" industries={industries}\n"
340
+ f" q_keywords={q_keywords}\n"
341
+ f" organization_domains={organization_domains}\n"
342
+ f" organization_locations={organization_locations}\n"
343
+ f" contact_email_status={contact_email_status}\n"
344
+ f" organization_ids={organization_ids}\n"
345
+ f" person_seniorities={person_seniorities}\n"
346
+ f" max_items={max_number_of_items_to_return}"
347
+ )
348
+
351
349
  async with aiohttp.ClientSession() as session:
352
350
  results = []
353
351
  page = 1
354
- # Apollo API allows up to 100 items per page
355
- per_page = min(max_number_of_items_to_return, 100)
352
+ per_page = min(max_number_of_items_to_return, 100) # 100 max per page
356
353
 
357
354
  while len(results) < max_number_of_items_to_return:
358
355
  payload = {
@@ -361,53 +358,158 @@ async def search_people_with_apollo(
361
358
  "search_signal_ids": search_signal_ids,
362
359
  "organization_num_employees_ranges": [f"{min_number_of_employees},{max_number_of_employees}"],
363
360
  "page": page,
364
- "per_page": per_page
361
+ "per_page": per_page,
365
362
  }
366
363
 
367
- # Include industries filter if provided
364
+ # Conditionally add new parameters
365
+ if q_keywords:
366
+ payload["q_keywords"] = q_keywords
367
+ if organization_domains:
368
+ payload["q_organization_domains"] = organization_domains
369
+ if organization_locations:
370
+ payload["organization_locations"] = organization_locations
371
+ if contact_email_status:
372
+ payload["contact_email_status"] = contact_email_status
373
+ if organization_ids:
374
+ payload["organization_ids"] = organization_ids
375
+ if person_seniorities:
376
+ payload["person_seniorities"] = person_seniorities
368
377
  if industries:
369
378
  payload["organization_industries"] = industries
370
379
 
371
380
  try:
372
381
  data = await fetch_apollo_data(session, url, headers, payload)
373
- people = data.get('people', [])
374
- contacts = data.get('contacts', [])
382
+ people = data.get("people", [])
383
+ contacts = data.get("contacts", [])
375
384
 
376
- # If no results found, stop the loop
377
385
  if not people and not contacts:
378
- break
386
+ break # No more results
379
387
 
380
- # Add the retrieved results
381
388
  results.extend(people + contacts)
382
389
 
383
- # Handle pagination
384
- pagination = data.get('pagination', {})
385
- current_page = pagination.get('page', 1)
386
- total_pages = pagination.get('total_pages', 1)
390
+ pagination = data.get("pagination", {})
391
+ current_page = pagination.get("page", 1)
392
+ total_pages = pagination.get("total_pages", 1)
387
393
 
388
394
  if current_page >= total_pages:
389
- # No more pages to fetch
390
395
  break
391
-
392
396
  page += 1
393
397
 
394
398
  except aiohttp.ClientResponseError as e:
395
- # If rate-limited, wait and retry
399
+ # Handle rate limiting
396
400
  if e.status == 429:
397
401
  await asyncio.sleep(30)
398
402
  else:
399
- # Return error details as a JSON string in a list
400
403
  error_details = {
401
- 'status': e.status,
402
- 'message': str(e),
403
- 'url': str(e.request_info.url),
404
- 'headers': dict(e.headers),
404
+ "status": e.status,
405
+ "message": str(e),
406
+ "url": str(e.request_info.url),
407
+ "headers": dict(e.headers),
405
408
  }
406
- return [json.dumps(error_details)]
409
+ return [error_details] # Return partial error info
407
410
 
408
- # Return only up to the requested number of items
409
411
  return results[:max_number_of_items_to_return]
410
412
 
413
+ # --------------------------------------------------
414
+ # Convert Apollo Search Results -> SmartListLead
415
+ # --------------------------------------------------
416
+
417
+ async def search_leads_with_apollo(
418
+ query: LeadsQueryFilters,
419
+ request: SmartList,
420
+ tool_config: Optional[List[Dict[str, Any]]] = None
421
+ ) -> List[SmartListLead]:
422
+ """
423
+ Given a LeadsQueryFilters object, run the Apollo People Search.
424
+ Format each result record as a SmartListLead.
425
+ """
426
+
427
+ # 1) Apply defaults if any field is None
428
+ job_titles = query.job_titles_to_search or []
429
+ locations = query.locations_to_search or []
430
+ min_employees = query.min_number_of_employees_in_organization or 1
431
+ max_employees = query.max_number_of_employees_in_organization or 1000
432
+ signals = query.filter_by_signals or []
433
+ max_items = request.max_leads or 10
434
+ if max_items > 2000:
435
+ max_items = 2000
436
+
437
+ industries = query.industries or []
438
+
439
+ # Additional filters
440
+ q_keywords = query.q_keywords
441
+ organization_domains = query.organization_domains or []
442
+ organization_locations = query.organization_locations or []
443
+ contact_email_status = query.contact_email_status or []
444
+ organization_ids = query.organization_ids or []
445
+ person_seniorities = query.person_seniorities or []
446
+
447
+ # 2) Fetch raw results (list of dict) from Apollo
448
+ results = await search_people_with_apollo(
449
+ job_titles=job_titles,
450
+ locations=locations,
451
+ min_number_of_employees=min_employees,
452
+ max_number_of_employees=max_employees,
453
+ filter_by_signals=signals,
454
+ max_number_of_items_to_return=max_items,
455
+ industries=industries,
456
+ q_keywords=q_keywords,
457
+ organization_domains=organization_domains,
458
+ organization_locations=organization_locations,
459
+ contact_email_status=contact_email_status,
460
+ organization_ids=organization_ids,
461
+ person_seniorities=person_seniorities,
462
+ tool_config=tool_config
463
+ )
464
+
465
+ # 3) Convert raw results into SmartListLead objects
466
+ leads_list: List[SmartListLead] = []
467
+ for apollo_person in results:
468
+ # Apollo typically returns fields like "name", "first_name", "last_name", etc.
469
+ # "organization" can be nested.
470
+ org_data = apollo_person.get("organization", {})
471
+
472
+ lead = SmartListLead(
473
+ # Map standard fields
474
+ full_name=apollo_person.get("name"),
475
+ first_name=apollo_person.get("first_name"),
476
+ last_name=apollo_person.get("last_name"),
477
+ email=apollo_person.get("email"),
478
+ user_linkedin_url=apollo_person.get("linkedin_url"),
479
+ phone=apollo_person.get("contact", {}).get("sanitized_phone"),
480
+ job_title=apollo_person.get("title"),
481
+ headline=apollo_person.get("headline"),
482
+
483
+ # Map organization fields
484
+ organization_name=org_data.get("name"),
485
+ organization_linkedin_url=org_data.get("linkedin_url"),
486
+ organization_website=org_data.get("website_url"),
487
+ primary_domain_of_organization=org_data.get("primary_domain"),
488
+
489
+ # We can combine city/state or anything else for "lead_location"
490
+ lead_location=", ".join(
491
+ filter(None, [apollo_person.get("city", ""), apollo_person.get("state", "")])
492
+ ) or None,
493
+
494
+ # For demonstration, fill from the request if needed
495
+ agent_instance_id=request.agent_instance_id,
496
+ organization_id=request.organization_id,
497
+ created_by=None, # You could set this to request.created_by, etc.
498
+ )
499
+
500
+ if "domain.com" in lead.email:
501
+ lead.email = ""
502
+
503
+ # If Apollo returns extra fields you'd like to preserve, store them in additional_properties
504
+ # as JSON or key/value pairs:
505
+ additional_props = {}
506
+ # e.g., store the entire record or partial:
507
+ additional_props["raw_apollo_data"] = json.dumps(apollo_person)
508
+ lead.additional_properties = additional_props
509
+
510
+ leads_list.append(lead)
511
+
512
+ return leads_list
411
513
 
412
514
  @assistant_tool
413
515
  async def get_organization_domain_from_apollo(
@@ -14,6 +14,7 @@ logging.basicConfig(level=logging.INFO)
14
14
 
15
15
  class IntentSignalScoring(BaseModel):
16
16
  score_based_on_intent_signal: int
17
+ reasoning_for_score_bing_high: str
17
18
 
18
19
 
19
20
  async def check_for_intent_signal(
@@ -27,21 +28,24 @@ async def check_for_intent_signal(
27
28
  Evaluate a 'lead' for a specific intent signal and return an integer score from 0–5.
28
29
  """
29
30
 
31
+ logger.info("check_for_intent_signal called with lead=%s, intent_signal_type=%s", lead.get("full_name"), intent_signal_type)
32
+
30
33
  search_results_text = ""
31
34
  if add_search_results:
32
- # Correctly call get_search_results_for_insights with intent_signal_type
35
+ logger.info("Fetching search results for lead='%s' with signal='%s'", lead.get("full_name"), intent_signal_type)
33
36
  search_results = await get_search_results_for_insights(
34
37
  lead=lead,
35
38
  english_description=signal_to_look_for_in_plan_english,
36
39
  intent_signal_type=intent_signal_type,
37
40
  tool_config=tool_config
38
41
  )
42
+ logger.info("Received search results count: %d", len(search_results))
39
43
 
40
- # Build a readable string from returned queries and results
41
- # Each item in 'search_results' is a dict: {"query": str, "results": <json-encoded list of SERP results>}
42
44
  for item in search_results:
43
45
  query_str = item.get("query", "")
44
46
  results_str = item.get("results", "")
47
+ logger.info("Search query: %s", query_str)
48
+ logger.info("Search results snippet: %s", results_str[:100]) # Show partial snippet
45
49
  search_results_text += f"Query: {query_str}\nResults: {results_str}\n\n"
46
50
 
47
51
  user_prompt = f"""
@@ -67,28 +71,32 @@ async def check_for_intent_signal(
67
71
 
68
72
  Return your answer in valid JSON with the key 'score_based_on_intent_signal'.
69
73
  Make sure it is an integer between 0 and 5.
74
+ Add small reasoning_for_score_bing_high describing why you gave the score score_based_on_intent_signal as high if you are giving high score.
70
75
  """
76
+ logger.info("Constructed user prompt for LLM.")
71
77
 
72
- logger.info("Scoring intent signal '%s' for lead: %s", intent_signal_type, lead.get("full_name", "Unknown"))
73
-
74
- # The helper returns (model_instance or None, status_str)
75
78
  response_any, status = await get_structured_output_internal(
76
79
  user_prompt,
77
80
  IntentSignalScoring,
78
81
  tool_config=tool_config
79
82
  )
83
+ logger.info("Intent signal scoring call completed with status=%s", status)
80
84
 
81
85
  if status != "SUCCESS" or response_any is None:
86
+ logger.error("Failed to generate an intent signal score from the LLM.")
82
87
  raise Exception("Failed to generate an intent signal score from the LLM.")
83
88
 
84
- # Cast to your specific model so the type checker is satisfied
85
89
  response = cast(IntentSignalScoring, response_any)
86
90
  score = response.score_based_on_intent_signal
91
+ reasoning = response.reasoning_for_score_bing_high[:200] # Show partial if very long
92
+ lead["qualification_score"] = score
93
+ lead["qualification_reason"] = response.reasoning_for_score_bing_high
87
94
 
88
95
  logger.info(
89
- "Lead '%s' scored %d for intent signal '%s'.",
96
+ "Lead '%s' scored %d for intent signal '%s'. Reason partial: %s",
90
97
  lead.get("full_name", "Unknown"),
91
98
  score,
92
- intent_signal_type
99
+ intent_signal_type,
100
+ reasoning
93
101
  )
94
- return score
102
+ return score
@@ -8,8 +8,8 @@ from dhisana.utils.generate_structured_output_internal import get_structured_out
8
8
  from dhisana.utils.workflow_code_model import WorkflowPythonCode
9
9
 
10
10
  # Example imports: adapt paths to your actual modules
11
- from dhisana.utils.create_list_from_sales_navigator import generate_sales_nav_list_workflow_and_execute
12
- from dhisana.utils.create_smart_list import generate_smart_list_creation_code_and_execute
11
+ from dhisana.utils.generate_leads import generate_leads
12
+ from dhisana.utils.qualify_leads import qualify_leads
13
13
  from dhisana.utils.compose_cadence import generate_campaign_cadence_workflow_and_execute
14
14
 
15
15
  # Initialize logger
@@ -127,7 +127,7 @@ async def generate_three_step_workflow_execute(
127
127
  else:
128
128
  # We interpret step_1_instructions as a user query for sales nav
129
129
  logger.info("Generating leads from sales nav instructions: %s", step_1_instructions)
130
- result_str = await generate_sales_nav_list_workflow_and_execute(
130
+ result_str = await generate_leads(
131
131
  user_query=step_1_instructions,
132
132
  tool_config=tool_config
133
133
  )
@@ -162,7 +162,7 @@ async def generate_three_step_workflow_execute(
162
162
  try:
163
163
  # generate_smart_list_creation_code_and_execute typically returns JSON:
164
164
  # { "status": "SUCCESS", "qualified_leads": [...] }
165
- result_str = await generate_smart_list_creation_code_and_execute(
165
+ result_str = await qualify_leads(
166
166
  user_query=step_2_instructions,
167
167
  input_leads_list=leads_list,
168
168
  tool_config=tool_config