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.
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/setup.py +1 -1
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/schemas/sales.py +169 -1
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/agent_task.py +1 -1
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/apollo_tools.py +175 -73
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/check_for_intent_signal.py +18 -10
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_three_step_workflow.py +4 -4
- dhisana-0.0.1.dev34/src/dhisana/utils/generate_leads.py +58 -0
- 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
- dhisana-0.0.1.dev34/src/dhisana/utils/generate_smartlist.py +171 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_structured_output_internal.py +2 -1
- 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
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/PKG-INFO +1 -1
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/SOURCES.txt +4 -2
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/README.md +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/setup.cfg +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_cadence.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_workflow.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/tests/test_agent_tools.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
309
|
+
raise ValueError("Minimum employees must be less than maximum employees.")
|
|
316
310
|
|
|
317
|
-
|
|
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":
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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(
|
|
374
|
-
contacts = data.get(
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
404
|
+
"status": e.status,
|
|
405
|
+
"message": str(e),
|
|
406
|
+
"url": str(e.request_info.url),
|
|
407
|
+
"headers": dict(e.headers),
|
|
405
408
|
}
|
|
406
|
-
return [
|
|
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
|
-
|
|
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
|
{dhisana-0.0.1.dev33 → dhisana-0.0.1.dev34}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
@@ -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.
|
|
12
|
-
from dhisana.utils.
|
|
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
|
|
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
|
|
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
|