diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 4092f8c10a..88836b8317 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -895,6 +895,24 @@ export function YouTubeIcon(props: React.SVGProps) { ) } +export function PeopleDataLabsIcon(props: SVGProps) { + return ( + + + + ) +} + export function PerplexityIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index b515c6ccdd..609f37382d 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -135,6 +135,7 @@ import { PackageSearchIcon, PagerDutyIcon, ParallelIcon, + PeopleDataLabsIcon, PerplexityIcon, PineconeIcon, PipedriveIcon, @@ -351,6 +352,7 @@ export const blockTypeToIconMap: Record = { outlook: OutlookIcon, pagerduty: PagerDutyIcon, parallel_ai: ParallelIcon, + peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index af967f765a..ad0f6b437a 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -131,6 +131,7 @@ "outlook", "pagerduty", "parallel_ai", + "peopledatalabs", "perplexity", "pinecone", "pipedrive", diff --git a/apps/docs/content/docs/en/tools/peopledatalabs.mdx b/apps/docs/content/docs/en/tools/peopledatalabs.mdx new file mode 100644 index 0000000000..ace546acde --- /dev/null +++ b/apps/docs/content/docs/en/tools/peopledatalabs.mdx @@ -0,0 +1,70 @@ +--- +title: People Data Labs +description: Enrich and search people and companies +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +## Description + +[People Data Labs](https://www.peopledatalabs.com/) is a person and company data provider with a global dataset of 3B+ person profiles and 25M+ company records. The Sim block exposes the core REST endpoints so agents can enrich a single contact, look up a company, or run dataset-wide search queries. + +Use this block to: +- **Person Enrich** — resolve a single person by email, phone, LinkedIn URL, or name + company/location, and pull back their work history, contact info, and skills. +- **Person Search** — run SQL or Elasticsearch DSL queries against the person dataset to build prospect lists or audience segments. +- **Company Enrich** — resolve a single company by name, website, ticker, LinkedIn URL, or PDL ID, and pull back firmographics. +- **Company Search** — query the company dataset with SQL or Elasticsearch DSL. +- **Autocomplete** — get suggested values for fields like `title`, `skill`, `industry`, or `location` to build well-formed search queries. + +Authentication uses an API key passed as the `X-Api-Key` header. Get a key from the [PDL dashboard](https://dashboard.peopledatalabs.com/). +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Enrich a single person or company with People Data Labs, or search the global person and company datasets with SQL or Elasticsearch DSL. Useful for sales enrichment, contact lookup, and CRM hygiene. + + + +## Tools + +### `pdl_person_enrich` + + +### `pdl_person_identify` + + +### `pdl_person_search` + + +### `pdl_bulk_person_enrich` + + +### `pdl_company_enrich` + + +### `pdl_company_search` + + +### `pdl_bulk_company_enrich` + + +### `pdl_clean_company` + + +### `pdl_clean_location` + + +### `pdl_clean_school` + + +### `pdl_autocomplete` + + + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index e19d3d267b..f80e850ceb 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -135,6 +135,7 @@ import { PackageSearchIcon, PagerDutyIcon, ParallelIcon, + PeopleDataLabsIcon, PerplexityIcon, PineconeIcon, PipedriveIcon, @@ -335,6 +336,7 @@ export const blockTypeToIconMap: Record = { outlook: OutlookIcon, pagerduty: PagerDutyIcon, parallel_ai: ParallelIcon, + peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 9de36c2890..b443d02090 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -9715,6 +9715,69 @@ "integrationTypes": ["search", "ai"], "tags": ["web-scraping", "llm", "agentic"] }, + { + "type": "peopledatalabs", + "slug": "people-data-labs", + "name": "People Data Labs", + "description": "Enrich and search people and companies", + "longDescription": "Enrich a single person or company with People Data Labs, or search the global person and company datasets with SQL or Elasticsearch DSL. Useful for sales enrichment, contact lookup, and CRM hygiene.", + "bgColor": "#4831C3", + "iconName": "PeopleDataLabsIcon", + "docsUrl": "https://docs.sim.ai/tools/peopledatalabs", + "operations": [ + { + "name": "Person Enrich", + "description": "Enrich a single person profile using People Data Labs. Match by email, phone, LinkedIn URL, or name + company/location. Returns work history, contact details, location, and skills." + }, + { + "name": "Person Identify", + "description": "Return up to 20 candidate person matches with confidence scores. Useful when you want to see all plausible matches rather than the single best one. Reference: https://docs.peopledatalabs.com/docs/identify-api-quickstart" + }, + { + "name": "Person Search", + "description": "Search the People Data Labs person dataset using SQL or Elasticsearch DSL. Returns up to 100 matching records per call." + }, + { + "name": "Bulk Person Enrich", + "description": "Enrich up to 100 person records in a single call. Provide a JSON array of request objects, each with a `params` object (and optional `metadata` echoed back). Reference: https://docs.peopledatalabs.com/docs/bulk-person-enrichment-api" + }, + { + "name": "Company Enrich", + "description": "Enrich a single company using People Data Labs. Match by name, website, LinkedIn URL, ticker, or PDL ID." + }, + { + "name": "Company Search", + "description": "Search the People Data Labs company dataset using SQL or Elasticsearch DSL. Returns up to 100 matching companies per call." + }, + { + "name": "Bulk Company Enrich", + "description": "Enrich up to 100 companies in a single call. Provide a JSON array of request objects, each with a `params` object. Reference: https://docs.peopledatalabs.com/docs/bulk-company-enrichment-api" + }, + { + "name": "Company Cleaner", + "description": "Normalize a company string into a canonical company record. Provide at least one of name, website, or profile (LinkedIn URL)." + }, + { + "name": "Location Cleaner", + "description": "Normalize a freeform location string into a structured locality/region/country record with coordinates." + }, + { + "name": "School Cleaner", + "description": "Normalize a school string into a canonical school record. Provide at least one of name, website, or profile (LinkedIn URL)." + }, + { + "name": "Autocomplete", + "description": "Get autocomplete suggestions for a PDL field (title, skill, company, industry, location, school, major, role, sub_role)." + } + ], + "operationCount": 11, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment"] + }, { "type": "perplexity", "slug": "perplexity", diff --git a/apps/sim/blocks/blocks/peopledatalabs.ts b/apps/sim/blocks/blocks/peopledatalabs.ts new file mode 100644 index 0000000000..b8572509bd --- /dev/null +++ b/apps/sim/blocks/blocks/peopledatalabs.ts @@ -0,0 +1,593 @@ +import { PeopleDataLabsIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { PdlPersonEnrichResponse } from '@/tools/peopledatalabs/types' + +export const PeopleDataLabsBlock: BlockConfig = { + type: 'peopledatalabs', + name: 'People Data Labs', + description: 'Enrich and search people and companies', + authMode: AuthMode.ApiKey, + longDescription: + 'Enrich a single person or company with People Data Labs, or search the global person and company datasets with SQL or Elasticsearch DSL. Useful for sales enrichment, contact lookup, and CRM hygiene.', + docsLink: 'https://docs.sim.ai/tools/peopledatalabs', + category: 'tools', + integrationType: IntegrationType.Sales, + tags: ['enrichment'], + bgColor: '#4831C3', + icon: PeopleDataLabsIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Person Enrich', id: 'pdl_person_enrich' }, + { label: 'Person Identify', id: 'pdl_person_identify' }, + { label: 'Person Search', id: 'pdl_person_search' }, + { label: 'Bulk Person Enrich', id: 'pdl_bulk_person_enrich' }, + { label: 'Company Enrich', id: 'pdl_company_enrich' }, + { label: 'Company Search', id: 'pdl_company_search' }, + { label: 'Bulk Company Enrich', id: 'pdl_bulk_company_enrich' }, + { label: 'Company Cleaner', id: 'pdl_clean_company' }, + { label: 'Location Cleaner', id: 'pdl_clean_location' }, + { label: 'School Cleaner', id: 'pdl_clean_school' }, + { label: 'Autocomplete', id: 'pdl_autocomplete' }, + ], + value: () => 'pdl_person_enrich', + }, + + // Person Enrich fields + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'jane@example.com', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + }, + { + id: 'profile', + title: 'LinkedIn URL', + type: 'short-input', + placeholder: 'https://linkedin.com/in/janedoe', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + }, + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+15551234567', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'first_name', + title: 'First Name', + type: 'short-input', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'last_name', + title: 'Last Name', + type: 'short-input', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'company', + title: 'Company', + type: 'short-input', + placeholder: 'Acme Inc or acme.com', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'location', + title: 'Location', + type: 'short-input', + placeholder: 'San Francisco, CA', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_person_identify'] }, + mode: 'advanced', + }, + { + id: 'min_likelihood', + title: 'Min Likelihood', + type: 'short-input', + placeholder: '6', + condition: { field: 'operation', value: ['pdl_person_enrich', 'pdl_company_enrich'] }, + mode: 'advanced', + }, + + // Person Search fields + { + id: 'sql', + title: 'SQL Query', + type: 'long-input', + placeholder: + "SELECT * FROM person WHERE job_title='engineer' AND location_country='united states'", + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + }, + { + id: 'query', + title: 'Elasticsearch Query (JSON)', + type: 'long-input', + placeholder: '{"bool": {"must": [{"term": {"job_title": "engineer"}}]}}', + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + mode: 'advanced', + }, + { + id: 'size', + title: 'Result Size', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + mode: 'advanced', + }, + { + id: 'scroll_token', + title: 'Scroll Token', + type: 'short-input', + placeholder: 'Token from a prior response', + condition: { field: 'operation', value: ['pdl_person_search', 'pdl_company_search'] }, + mode: 'advanced', + }, + { + id: 'dataset', + title: 'Dataset', + type: 'dropdown', + options: [ + { label: 'all', id: 'all' }, + { label: 'resume', id: 'resume' }, + { label: 'email', id: 'email' }, + { label: 'phone', id: 'phone' }, + { label: 'mobile_phone', id: 'mobile_phone' }, + { label: 'street_address', id: 'street_address' }, + { label: 'consumer_social', id: 'consumer_social' }, + { label: 'developer', id: 'developer' }, + ], + condition: { field: 'operation', value: 'pdl_person_search' }, + mode: 'advanced', + }, + + // Company Enrich fields + { + id: 'company_name', + title: 'Company Name', + type: 'short-input', + placeholder: 'Acme Inc', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + }, + { + id: 'website', + title: 'Website', + type: 'short-input', + placeholder: 'acme.com', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + }, + { + id: 'company_profile', + title: 'LinkedIn URL', + type: 'short-input', + placeholder: 'https://linkedin.com/company/acme', + condition: { field: 'operation', value: ['pdl_company_enrich', 'pdl_clean_company'] }, + mode: 'advanced', + }, + { + id: 'ticker', + title: 'Ticker', + type: 'short-input', + placeholder: 'AAPL', + condition: { field: 'operation', value: 'pdl_company_enrich' }, + mode: 'advanced', + }, + { + id: 'pdl_id', + title: 'PDL Company ID', + type: 'short-input', + condition: { field: 'operation', value: 'pdl_company_enrich' }, + mode: 'advanced', + }, + { + id: 'company_location', + title: 'Location', + type: 'short-input', + placeholder: 'San Francisco, CA', + condition: { field: 'operation', value: 'pdl_company_enrich' }, + mode: 'advanced', + }, + + // Autocomplete fields + { + id: 'field', + title: 'Field', + type: 'dropdown', + options: [ + { label: 'title', id: 'title' }, + { label: 'skill', id: 'skill' }, + { label: 'company', id: 'company' }, + { label: 'industry', id: 'industry' }, + { label: 'location_name', id: 'location_name' }, + { label: 'all_location', id: 'all_location' }, + { label: 'country', id: 'country' }, + { label: 'region', id: 'region' }, + { label: 'school', id: 'school' }, + { label: 'major', id: 'major' }, + { label: 'class', id: 'class' }, + { label: 'role', id: 'role' }, + { label: 'sub_role', id: 'sub_role' }, + { label: 'website', id: 'website' }, + ], + value: () => 'title', + condition: { field: 'operation', value: 'pdl_autocomplete' }, + required: { field: 'operation', value: 'pdl_autocomplete' }, + }, + { + id: 'text', + title: 'Search Text', + type: 'short-input', + placeholder: 'engin', + condition: { field: 'operation', value: 'pdl_autocomplete' }, + required: { field: 'operation', value: 'pdl_autocomplete' }, + }, + { + id: 'autocomplete_size', + title: 'Number of Suggestions', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'pdl_autocomplete' }, + mode: 'advanced', + }, + + // Person Identify-only fields + { + id: 'identify_locality', + title: 'Locality (City)', + type: 'short-input', + placeholder: 'San Francisco', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_region', + title: 'Region (State)', + type: 'short-input', + placeholder: 'CA', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_country', + title: 'Country', + type: 'short-input', + placeholder: 'United States', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_postal_code', + title: 'Postal Code', + type: 'short-input', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'identify_birth_date', + title: 'Birth Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'data_include', + title: 'Data Include', + type: 'short-input', + placeholder: 'work_email,personal_emails,phone_numbers', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + { + id: 'include_if_matched', + title: 'Include `matched_on`', + type: 'switch', + condition: { field: 'operation', value: 'pdl_person_identify' }, + mode: 'advanced', + }, + + // Bulk Person Enrich + { + id: 'bulk_person_requests', + title: 'Requests (JSON Array)', + type: 'long-input', + placeholder: + '[{ "params": { "profile": "https://linkedin.com/in/janedoe" } }, { "params": { "email": "john@example.com" } }]', + condition: { field: 'operation', value: 'pdl_bulk_person_enrich' }, + required: { field: 'operation', value: 'pdl_bulk_person_enrich' }, + }, + { + id: 'bulk_person_required', + title: 'Required Fields', + type: 'short-input', + placeholder: 'emails AND job_title', + condition: { field: 'operation', value: 'pdl_bulk_person_enrich' }, + mode: 'advanced', + }, + + // Bulk Company Enrich + { + id: 'bulk_company_requests', + title: 'Requests (JSON Array)', + type: 'long-input', + placeholder: '[{ "params": { "website": "acme.com" } }, { "params": { "name": "Globex" } }]', + condition: { field: 'operation', value: 'pdl_bulk_company_enrich' }, + required: { field: 'operation', value: 'pdl_bulk_company_enrich' }, + }, + { + id: 'bulk_company_required', + title: 'Required Fields', + type: 'short-input', + placeholder: 'name AND website', + condition: { field: 'operation', value: 'pdl_bulk_company_enrich' }, + mode: 'advanced', + }, + + // Location Cleaner + { + id: 'clean_location_input', + title: 'Location', + type: 'short-input', + placeholder: 'SF, CA', + condition: { field: 'operation', value: 'pdl_clean_location' }, + required: { field: 'operation', value: 'pdl_clean_location' }, + }, + + // School Cleaner + { + id: 'school_name', + title: 'School Name', + type: 'short-input', + placeholder: 'Stanford University', + condition: { field: 'operation', value: 'pdl_clean_school' }, + }, + { + id: 'school_website', + title: 'School Website', + type: 'short-input', + placeholder: 'stanford.edu', + condition: { field: 'operation', value: 'pdl_clean_school' }, + }, + { + id: 'school_profile', + title: 'School LinkedIn URL', + type: 'short-input', + placeholder: 'https://linkedin.com/school/stanford-university', + condition: { field: 'operation', value: 'pdl_clean_school' }, + mode: 'advanced', + }, + + // API Key + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your People Data Labs API key', + password: true, + required: true, + }, + ], + + tools: { + access: [ + 'pdl_person_enrich', + 'pdl_person_identify', + 'pdl_person_search', + 'pdl_bulk_person_enrich', + 'pdl_company_enrich', + 'pdl_company_search', + 'pdl_bulk_company_enrich', + 'pdl_clean_company', + 'pdl_clean_location', + 'pdl_clean_school', + 'pdl_autocomplete', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'pdl_person_enrich': + case 'pdl_person_identify': + case 'pdl_person_search': + case 'pdl_bulk_person_enrich': + case 'pdl_company_enrich': + case 'pdl_company_search': + case 'pdl_bulk_company_enrich': + case 'pdl_clean_company': + case 'pdl_clean_location': + case 'pdl_clean_school': + case 'pdl_autocomplete': + return params.operation + default: + return 'pdl_person_enrich' + } + }, + params: (params) => { + const result: Record = { ...params } + const op = params.operation + + // Strip alternate-operation aliases so stale values from prior operations + // can't leak into the current request. + result.company_profile = undefined + result.company_location = undefined + result.autocomplete_size = undefined + result.identify_locality = undefined + result.identify_region = undefined + result.identify_country = undefined + result.identify_postal_code = undefined + result.identify_birth_date = undefined + result.bulk_person_requests = undefined + result.bulk_person_required = undefined + result.bulk_company_requests = undefined + result.bulk_company_required = undefined + result.clean_location_input = undefined + result.school_name = undefined + result.school_website = undefined + result.school_profile = undefined + + // Clear shared target fields and repopulate them per-operation. The raw + // `profile`/`location`/`website` subBlocks are scoped to specific + // operations in the UI, but their values persist when the user switches + // operations — without this reset, e.g. a person LinkedIn URL would + // leak into a Company Enrich request as the company profile. + result.profile = undefined + result.location = undefined + result.name = undefined + result.website = undefined + result.company_name = undefined + + if (op === 'pdl_person_enrich' || op === 'pdl_person_identify') { + if (params.profile !== undefined) result.profile = params.profile + if (params.location !== undefined) result.location = params.location + if (params.name !== undefined) result.name = params.name + } + if (op === 'pdl_company_enrich') { + if (params.company_name !== undefined) result.name = params.company_name + else if (params.name !== undefined) result.name = params.name + if (params.website !== undefined) result.website = params.website + if (params.company_profile !== undefined) result.profile = params.company_profile + else if (params.profile !== undefined) result.profile = params.profile + if (params.company_location !== undefined) result.location = params.company_location + else if (params.location !== undefined) result.location = params.location + } + if (op === 'pdl_clean_company') { + if (params.company_name !== undefined) result.name = params.company_name + else if (params.name !== undefined) result.name = params.name + if (params.website !== undefined) result.website = params.website + if (params.company_profile !== undefined) result.profile = params.company_profile + else if (params.profile !== undefined) result.profile = params.profile + } + + // `size` is shared by search and autocomplete subBlocks; reset and + // repopulate per-operation so a stale search size can't bleed into an + // autocomplete request (or vice versa) or into operations that don't + // accept `size` at all. + result.size = undefined + if (op === 'pdl_autocomplete') { + if (params.autocomplete_size !== undefined) { + result.size = Number(params.autocomplete_size) + } + } else if (op === 'pdl_person_search' || op === 'pdl_company_search') { + if (params.size !== undefined) result.size = Number(params.size) + } + + // min_likelihood is only honored by enrich endpoints + if (op === 'pdl_person_enrich' || op === 'pdl_company_enrich') { + if (params.min_likelihood !== undefined) { + result.min_likelihood = Number(params.min_likelihood) + } + } else { + result.min_likelihood = undefined + } + + if (op === 'pdl_person_identify') { + if (params.identify_locality !== undefined) result.locality = params.identify_locality + if (params.identify_region !== undefined) result.region = params.identify_region + if (params.identify_country !== undefined) result.country = params.identify_country + if (params.identify_postal_code !== undefined) { + result.postal_code = params.identify_postal_code + } + if (params.identify_birth_date !== undefined) { + result.birth_date = params.identify_birth_date + } + } + + if (op === 'pdl_bulk_person_enrich') { + if (params.bulk_person_requests !== undefined) { + result.requests = params.bulk_person_requests + } + if (params.bulk_person_required !== undefined) { + result.required = params.bulk_person_required + } + } else if (op === 'pdl_bulk_company_enrich') { + if (params.bulk_company_requests !== undefined) { + result.requests = params.bulk_company_requests + } + if (params.bulk_company_required !== undefined) { + result.required = params.bulk_company_required + } + } + + if (op === 'pdl_clean_location') { + if (params.clean_location_input !== undefined) { + result.location = params.clean_location_input + } else if (params.location !== undefined) { + result.location = params.location + } + } + + if (op === 'pdl_clean_school') { + if (params.school_name !== undefined) result.name = params.school_name + else if (params.name !== undefined) result.name = params.name + if (params.school_website !== undefined) result.website = params.school_website + else if (params.website !== undefined) result.website = params.website + if (params.school_profile !== undefined) result.profile = params.school_profile + else if (params.profile !== undefined) result.profile = params.profile + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'People Data Labs API key' }, + // Person enrich + email: { type: 'string', description: 'Email address' }, + profile: { type: 'string', description: 'LinkedIn URL' }, + phone: { type: 'string', description: 'Phone number' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + company: { type: 'string', description: 'Company name or domain' }, + location: { type: 'string', description: 'Location' }, + min_likelihood: { type: 'number', description: 'Minimum match likelihood (1-10)' }, + // Search + sql: { type: 'string', description: 'PDL SQL query' }, + query: { type: 'string', description: 'Elasticsearch DSL query as JSON string' }, + size: { type: 'number', description: 'Result size' }, + scroll_token: { type: 'string', description: 'Pagination token from a prior response' }, + dataset: { type: 'string', description: 'Person dataset filter' }, + // Company enrich + name: { type: 'string', description: 'Company name' }, + website: { type: 'string', description: 'Company website' }, + ticker: { type: 'string', description: 'Stock ticker' }, + pdl_id: { type: 'string', description: 'PDL company ID' }, + // Autocomplete + field: { type: 'string', description: 'Autocomplete field' }, + text: { type: 'string', description: 'Search text' }, + // Identify + locality: { type: 'string', description: 'City (identify)' }, + region: { type: 'string', description: 'State/region (identify)' }, + country: { type: 'string', description: 'Country (identify)' }, + postal_code: { type: 'string', description: 'Postal code (identify)' }, + birth_date: { type: 'string', description: 'Birth date YYYY-MM-DD (identify)' }, + data_include: { type: 'string', description: 'Fields to include in identify match' }, + include_if_matched: { type: 'boolean', description: 'Include `matched_on` array per match' }, + // Bulk + requests: { type: 'string', description: 'JSON array of bulk request objects' }, + required: { type: 'string', description: 'Required-fields expression for bulk' }, + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether a record was matched (enrich/clean)' }, + likelihood: { type: 'number', description: 'Match likelihood (person enrich)' }, + person: { type: 'json', description: 'Matched person record' }, + company: { type: 'json', description: 'Matched company record' }, + location: { type: 'json', description: 'Cleaned location record' }, + school: { type: 'json', description: 'Cleaned school record' }, + matches: { type: 'json', description: 'Identify match candidates with scores' }, + total: { type: 'number', description: 'Total matches in dataset (search)' }, + scroll_token: { type: 'string', description: 'Pagination token to fetch the next page' }, + results: { type: 'json', description: 'Search or bulk result records' }, + suggestions: { type: 'json', description: 'Autocomplete suggestions' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index f5d9b40fd3..d458289879 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -148,6 +148,7 @@ import { OpenAIBlock } from '@/blocks/blocks/openai' import { OutlookBlock } from '@/blocks/blocks/outlook' import { PagerDutyBlock } from '@/blocks/blocks/pagerduty' import { ParallelBlock } from '@/blocks/blocks/parallel' +import { PeopleDataLabsBlock } from '@/blocks/blocks/peopledatalabs' import { PerplexityBlock } from '@/blocks/blocks/perplexity' import { PineconeBlock } from '@/blocks/blocks/pinecone' import { PipedriveBlock } from '@/blocks/blocks/pipedrive' @@ -399,6 +400,7 @@ export const registry: Record = { outlook: OutlookBlock, pagerduty: PagerDutyBlock, parallel_ai: ParallelBlock, + peopledatalabs: PeopleDataLabsBlock, perplexity: PerplexityBlock, pinecone: PineconeBlock, pipedrive: PipedriveBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 4092f8c10a..88836b8317 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -895,6 +895,24 @@ export function YouTubeIcon(props: React.SVGProps) { ) } +export function PeopleDataLabsIcon(props: SVGProps) { + return ( + + + + ) +} + export function PerplexityIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/peopledatalabs/autocomplete.ts b/apps/sim/tools/peopledatalabs/autocomplete.ts new file mode 100644 index 0000000000..c9306e6461 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/autocomplete.ts @@ -0,0 +1,95 @@ +import type { PdlAutocompleteParams, PdlAutocompleteResponse } from '@/tools/peopledatalabs/types' +import { buildQueryString } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const SUGGESTION_PROPERTIES = { + name: { type: 'string', description: 'Suggestion value' }, + count: { type: 'number', description: 'Number of records matching this value' }, + meta: { + type: 'object', + description: 'Field-specific metadata (e.g., for `company`: id, website, industry)', + optional: true, + }, +} as const satisfies Record + +export const autocompleteTool: ToolConfig = { + id: 'pdl_autocomplete', + name: 'PDL Autocomplete', + description: + 'Get autocomplete suggestions for a PDL field (title, skill, company, industry, location, school, major, role, sub_role).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + field: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Field to autocomplete: all_location, class, company, country, industry, location_name, major, region, role, school, sub_role, skill, title, website', + }, + text: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search text prefix', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of suggestions to return (1-100, default 10)', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + field: params.field, + text: params.text, + size: params.size, + }) + return `https://api.peopledatalabs.com/v5/autocomplete${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const items = + (data.data as { name: string; count: number; meta?: Record }[]) ?? [] + return { + success: true, + output: { + suggestions: items.map((item) => ({ + name: item.name, + count: item.count ?? 0, + meta: item.meta ?? undefined, + })), + }, + } + }, + + outputs: { + suggestions: { + type: 'array', + description: 'Autocomplete suggestions ordered by frequency', + items: { type: 'object', properties: SUGGESTION_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/bulk_company_enrich.ts b/apps/sim/tools/peopledatalabs/bulk_company_enrich.ts new file mode 100644 index 0000000000..278e8cfdb2 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/bulk_company_enrich.ts @@ -0,0 +1,112 @@ +import type { + PdlBulkCompanyEnrichParams, + PdlBulkCompanyEnrichResponse, + PdlBulkCompanyResultItem, +} from '@/tools/peopledatalabs/types' +import { projectCompany } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const BULK_COMPANY_RESULT_PROPERTIES = { + status: { type: 'number', description: 'Per-record HTTP status (200 on match)' }, + matched: { type: 'boolean', description: 'Whether this record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood (1-10), null if no match', + optional: true, + }, + metadata: { + type: 'object', + description: 'Metadata echoed back from the request', + optional: true, + }, + company: { type: 'object', description: 'Matched company record', optional: true }, +} as const satisfies Record + +export const bulkCompanyEnrichTool: ToolConfig< + PdlBulkCompanyEnrichParams, + PdlBulkCompanyEnrichResponse +> = { + id: 'pdl_bulk_company_enrich', + name: 'PDL Bulk Company Enrich', + description: + 'Enrich up to 100 companies in a single call. Provide a JSON array of request objects, each with a `params` object. Reference: https://docs.peopledatalabs.com/docs/bulk-company-enrichment-api', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + requests: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of request objects, max 100. Each item: { "params": { name | website | profile | ticker | pdl_id }, "metadata": {...optional...} }', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression applied globally to every request', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/company/enrich/bulk', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + let parsed: unknown + try { + parsed = JSON.parse(params.requests) + } catch { + throw new Error('`requests` must be valid JSON (an array of request objects)') + } + if (!Array.isArray(parsed)) { + throw new Error('`requests` must be a JSON array') + } + const body: Record = { requests: parsed } + if (params.required) body.required = params.required + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const error = (data as { error?: { message?: string } })?.error?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const items = (Array.isArray(data) ? data : []) as Array> + const results: PdlBulkCompanyResultItem[] = items.map((item) => { + const status = (item.status as number) ?? 0 + const record = (item.data as Record) ?? null + return { + status, + likelihood: (item.likelihood as number) ?? null, + matched: status === 200 && record !== null, + metadata: (item.metadata as Record) ?? null, + company: record ? projectCompany(record) : null, + } + }) + + return { success: true, output: { results } } + }, + + outputs: { + results: { + type: 'array', + description: 'Per-record results in the same order as the input requests', + items: { type: 'object', properties: BULK_COMPANY_RESULT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/bulk_person_enrich.ts b/apps/sim/tools/peopledatalabs/bulk_person_enrich.ts new file mode 100644 index 0000000000..8df13c55e2 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/bulk_person_enrich.ts @@ -0,0 +1,112 @@ +import type { + PdlBulkPersonEnrichParams, + PdlBulkPersonEnrichResponse, + PdlBulkPersonResultItem, +} from '@/tools/peopledatalabs/types' +import { projectPerson } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const BULK_PERSON_RESULT_PROPERTIES = { + status: { type: 'number', description: 'Per-record HTTP status (200 on match)' }, + matched: { type: 'boolean', description: 'Whether this record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood (1-10), null if no match', + optional: true, + }, + metadata: { + type: 'object', + description: 'Metadata echoed back from the request', + optional: true, + }, + person: { type: 'object', description: 'Matched person record', optional: true }, +} as const satisfies Record + +export const bulkPersonEnrichTool: ToolConfig< + PdlBulkPersonEnrichParams, + PdlBulkPersonEnrichResponse +> = { + id: 'pdl_bulk_person_enrich', + name: 'PDL Bulk Person Enrich', + description: + 'Enrich up to 100 person records in a single call. Provide a JSON array of request objects, each with a `params` object (and optional `metadata` echoed back). Reference: https://docs.peopledatalabs.com/docs/bulk-person-enrichment-api', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + requests: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of request objects, max 100. Each item: { "params": { email | profile | first_name+last_name+company | ... }, "metadata": {...optional...} }', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression applied globally to every request', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/person/bulk', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + let parsed: unknown + try { + parsed = JSON.parse(params.requests) + } catch { + throw new Error('`requests` must be valid JSON (an array of request objects)') + } + if (!Array.isArray(parsed)) { + throw new Error('`requests` must be a JSON array') + } + const body: Record = { requests: parsed } + if (params.required) body.required = params.required + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const error = (data as { error?: { message?: string } })?.error?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const items = (Array.isArray(data) ? data : []) as Array> + const results: PdlBulkPersonResultItem[] = items.map((item) => { + const status = (item.status as number) ?? 0 + const record = (item.data as Record) ?? null + return { + status, + matched: status === 200 && record !== null, + likelihood: (item.likelihood as number) ?? null, + metadata: (item.metadata as Record) ?? null, + person: record ? projectPerson(record) : null, + } + }) + + return { success: true, output: { results } } + }, + + outputs: { + results: { + type: 'array', + description: 'Per-record results in the same order as the input requests', + items: { type: 'object', properties: BULK_PERSON_RESULT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/company_clean.ts b/apps/sim/tools/peopledatalabs/company_clean.ts new file mode 100644 index 0000000000..65aaff4b87 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/company_clean.ts @@ -0,0 +1,89 @@ +import type { PdlCleanCompanyParams, PdlCleanCompanyResponse } from '@/tools/peopledatalabs/types' +import { PDL_COMPANY_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectCompany } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const cleanCompanyTool: ToolConfig = { + id: 'pdl_clean_company', + name: 'PDL Company Cleaner', + description: + 'Normalize a company string into a canonical company record. Provide at least one of name, website, or profile (LinkedIn URL).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Raw company name to normalize', + }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn company URL', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/company/clean', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.name) body.name = params.name + if (params.website) body.website = params.website + if (params.profile) body.profile = params.profile + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, company: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.website || data.id + return { + success: true, + output: { + matched: Boolean(hasFields), + company: hasFields ? projectCompany(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether the input was matched to a known company' }, + company: { + type: 'object', + description: 'Canonical company record', + optional: true, + properties: PDL_COMPANY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/company_enrich.ts b/apps/sim/tools/peopledatalabs/company_enrich.ts new file mode 100644 index 0000000000..66849d98cb --- /dev/null +++ b/apps/sim/tools/peopledatalabs/company_enrich.ts @@ -0,0 +1,150 @@ +import type { PdlCompanyEnrichParams, PdlCompanyEnrichResponse } from '@/tools/peopledatalabs/types' +import { PDL_COMPANY_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { buildQueryString, projectCompany } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const companyEnrichTool: ToolConfig = { + id: 'pdl_company_enrich', + name: 'PDL Company Enrich', + description: + 'Enrich a single company using People Data Labs. Match by name, website, LinkedIn URL, ticker, or PDL ID.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name', + }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website domain', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn company URL', + }, + ticker: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Stock ticker', + }, + pdl_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'PDL company ID', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company location (helps disambiguate)', + }, + locality: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'City', + }, + region: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State/region', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Country', + }, + min_likelihood: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum match likelihood (1-10)', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + name: params.name, + website: params.website, + profile: params.profile, + ticker: params.ticker, + pdl_id: params.pdl_id, + location: params.location, + locality: params.locality, + region: params.region, + country: params.country, + min_likelihood: params.min_likelihood, + required: params.required, + }) + return `https://api.peopledatalabs.com/v5/company/enrich${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, likelihood: null, company: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.website || data.id + return { + success: true, + output: { + matched: Boolean(hasFields), + likelihood: (data.likelihood as number) ?? null, + company: hasFields ? projectCompany(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether a company record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood score (1-10), null if no match', + optional: true, + }, + company: { + type: 'object', + description: 'Matched company record', + optional: true, + properties: PDL_COMPANY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/company_search.ts b/apps/sim/tools/peopledatalabs/company_search.ts new file mode 100644 index 0000000000..a98569cbe4 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/company_search.ts @@ -0,0 +1,103 @@ +import type { PdlCompanySearchParams, PdlCompanySearchResponse } from '@/tools/peopledatalabs/types' +import { PDL_COMPANY_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectCompany } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const companySearchTool: ToolConfig = { + id: 'pdl_company_search', + name: 'PDL Company Search', + description: + 'Search the People Data Labs company dataset using SQL or Elasticsearch DSL. Returns up to 100 matching companies per call.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + sql: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "PDL SQL query (e.g., \"SELECT * FROM company WHERE industry='computer software' AND size='51-200'\")", + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Elasticsearch DSL query as JSON string', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (1-100, default 1)', + }, + scroll_token: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token returned from a prior response', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/company/search', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.sql) body.sql = params.sql + if (params.query) { + try { + body.query = JSON.parse(params.query) + } catch { + body.query = params.query + } + } + if (params.size !== undefined) body.size = Number(params.size) + if (params.scroll_token) body.scroll_token = params.scroll_token + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const records = (data.data as Record[]) ?? [] + return { + success: true, + output: { + total: (data.total as number) ?? records.length, + scroll_token: (data.scroll_token as string) ?? null, + results: records.map(projectCompany), + }, + } + }, + + outputs: { + total: { type: 'number', description: 'Total matching companies in dataset' }, + scroll_token: { + type: 'string', + description: 'Pagination token to fetch the next page; null if no more results', + optional: true, + }, + results: { + type: 'array', + description: 'Company records matching the query', + items: { type: 'object', properties: PDL_COMPANY_OUTPUT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/index.ts b/apps/sim/tools/peopledatalabs/index.ts new file mode 100644 index 0000000000..8eee54c37e --- /dev/null +++ b/apps/sim/tools/peopledatalabs/index.ts @@ -0,0 +1,23 @@ +import { autocompleteTool } from '@/tools/peopledatalabs/autocomplete' +import { bulkCompanyEnrichTool } from '@/tools/peopledatalabs/bulk_company_enrich' +import { bulkPersonEnrichTool } from '@/tools/peopledatalabs/bulk_person_enrich' +import { cleanCompanyTool } from '@/tools/peopledatalabs/company_clean' +import { companyEnrichTool } from '@/tools/peopledatalabs/company_enrich' +import { companySearchTool } from '@/tools/peopledatalabs/company_search' +import { cleanLocationTool } from '@/tools/peopledatalabs/location_clean' +import { personEnrichTool } from '@/tools/peopledatalabs/person_enrich' +import { personIdentifyTool } from '@/tools/peopledatalabs/person_identify' +import { personSearchTool } from '@/tools/peopledatalabs/person_search' +import { cleanSchoolTool } from '@/tools/peopledatalabs/school_clean' + +export const pdlAutocompleteTool = autocompleteTool +export const pdlBulkCompanyEnrichTool = bulkCompanyEnrichTool +export const pdlBulkPersonEnrichTool = bulkPersonEnrichTool +export const pdlCleanCompanyTool = cleanCompanyTool +export const pdlCleanLocationTool = cleanLocationTool +export const pdlCleanSchoolTool = cleanSchoolTool +export const pdlCompanyEnrichTool = companyEnrichTool +export const pdlCompanySearchTool = companySearchTool +export const pdlPersonEnrichTool = personEnrichTool +export const pdlPersonIdentifyTool = personIdentifyTool +export const pdlPersonSearchTool = personSearchTool diff --git a/apps/sim/tools/peopledatalabs/location_clean.ts b/apps/sim/tools/peopledatalabs/location_clean.ts new file mode 100644 index 0000000000..e08e7a6f5a --- /dev/null +++ b/apps/sim/tools/peopledatalabs/location_clean.ts @@ -0,0 +1,71 @@ +import type { PdlCleanLocationParams, PdlCleanLocationResponse } from '@/tools/peopledatalabs/types' +import { PDL_LOCATION_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectLocation } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const cleanLocationTool: ToolConfig = { + id: 'pdl_clean_location', + name: 'PDL Location Cleaner', + description: + 'Normalize a freeform location string into a structured locality/region/country record with coordinates.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + location: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Raw location string (e.g., "SF, CA")', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/location/clean', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => ({ location: params.location }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, location: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.locality || data.country + return { + success: true, + output: { + matched: Boolean(hasFields), + location: hasFields ? projectLocation(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether the input was matched to a known location' }, + location: { + type: 'object', + description: 'Canonical location record', + optional: true, + properties: PDL_LOCATION_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/person_enrich.ts b/apps/sim/tools/peopledatalabs/person_enrich.ts new file mode 100644 index 0000000000..4627fccefc --- /dev/null +++ b/apps/sim/tools/peopledatalabs/person_enrich.ts @@ -0,0 +1,160 @@ +import type { PdlPersonEnrichParams, PdlPersonEnrichResponse } from '@/tools/peopledatalabs/types' +import { PDL_PERSON_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { buildQueryString, projectPerson } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const personEnrichTool: ToolConfig = { + id: 'pdl_person_enrich', + name: 'PDL Person Enrich', + description: + 'Enrich a single person profile using People Data Labs. Match by email, phone, LinkedIn URL, or name + company/location. Returns work history, contact details, location, and skills.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address to match', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number (E.164 format preferred)', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL', + }, + lid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn numeric ID', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full name (use as an alternative to first_name + last_name)', + }, + first_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name (use with last_name + company or location)', + }, + last_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name or website', + }, + school: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'School name', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Location name (city, region, or country)', + }, + min_likelihood: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum match likelihood (1-10)', + }, + required: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Required-fields expression (e.g., "emails AND job_title")', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + email: params.email, + phone: params.phone, + profile: params.profile, + lid: params.lid, + name: params.name, + first_name: params.first_name, + last_name: params.last_name, + company: params.company, + school: params.school, + location: params.location, + min_likelihood: params.min_likelihood, + required: params.required, + }) + return `https://api.peopledatalabs.com/v5/person/enrich${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { + success: true, + output: { matched: false, likelihood: null, person: null }, + } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const record = (data.data as Record) ?? null + return { + success: true, + output: { + matched: record !== null, + likelihood: (data.likelihood as number) ?? null, + person: record ? projectPerson(record) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether a person record was matched' }, + likelihood: { + type: 'number', + description: 'Match likelihood score (1-10), null if no match', + optional: true, + }, + person: { + type: 'object', + description: 'Matched person record', + optional: true, + properties: PDL_PERSON_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/person_identify.ts b/apps/sim/tools/peopledatalabs/person_identify.ts new file mode 100644 index 0000000000..c630cc26c3 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/person_identify.ts @@ -0,0 +1,216 @@ +import type { + PdlPersonIdentifyParams, + PdlPersonIdentifyResponse, +} from '@/tools/peopledatalabs/types' +import { PDL_PERSON_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { buildQueryString, projectPerson } from '@/tools/peopledatalabs/utils' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +const IDENTIFY_MATCH_PROPERTIES = { + match_score: { type: 'number', description: 'Match confidence score (1-99)' }, + matched_on: { + type: 'array', + description: 'Fields that drove the match (only when include_if_matched=true)', + optional: true, + items: { type: 'string', description: 'Field name' }, + }, + person: { + type: 'object', + description: 'Person record', + properties: PDL_PERSON_OUTPUT_PROPERTIES, + }, +} as const satisfies Record + +export const personIdentifyTool: ToolConfig = { + id: 'pdl_person_identify', + name: 'PDL Person Identify', + description: + 'Return up to 20 candidate person matches with confidence scores. Useful when you want to see all plausible matches rather than the single best one. Reference: https://docs.peopledatalabs.com/docs/identify-api-quickstart', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + email: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Email' }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL', + }, + email_hash: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'SHA-256 email hash', + }, + lid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn numeric ID', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full name', + }, + first_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name', + }, + middle_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Middle name', + }, + last_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name or website', + }, + school: { type: 'string', required: false, visibility: 'user-or-llm', description: 'School' }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Location', + }, + street_address: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Street address', + }, + locality: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'City', + }, + region: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State/region', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Country', + }, + postal_code: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Postal code', + }, + birth_date: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Birth date (YYYY-MM-DD)', + }, + data_include: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated fields to include in each match', + }, + include_if_matched: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include `matched_on` for each match', + }, + }, + + request: { + url: (params) => { + const qs = buildQueryString({ + email: params.email, + phone: params.phone, + profile: params.profile, + email_hash: params.email_hash, + lid: params.lid, + name: params.name, + first_name: params.first_name, + middle_name: params.middle_name, + last_name: params.last_name, + company: params.company, + school: params.school, + location: params.location, + street_address: params.street_address, + locality: params.locality, + region: params.region, + country: params.country, + postal_code: params.postal_code, + birth_date: params.birth_date, + data_include: params.data_include, + include_if_matched: params.include_if_matched, + }) + return `https://api.peopledatalabs.com/v5/person/identify${qs}` + }, + method: 'GET', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matches: [] } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const matches = (data.matches as Array>) ?? [] + return { + success: true, + output: { + matches: matches.map((m) => ({ + match_score: (m.match_score as number) ?? 0, + matched_on: (m.matched_on as string[]) ?? undefined, + person: projectPerson((m.data as Record) ?? {}), + })), + }, + } + }, + + outputs: { + matches: { + type: 'array', + description: 'Up to 20 candidate matches, ordered by score', + items: { type: 'object', properties: IDENTIFY_MATCH_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/person_search.ts b/apps/sim/tools/peopledatalabs/person_search.ts new file mode 100644 index 0000000000..a4983aee77 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/person_search.ts @@ -0,0 +1,111 @@ +import type { PdlPersonSearchParams, PdlPersonSearchResponse } from '@/tools/peopledatalabs/types' +import { PDL_PERSON_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectPerson } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const personSearchTool: ToolConfig = { + id: 'pdl_person_search', + name: 'PDL Person Search', + description: + 'Search the People Data Labs person dataset using SQL or Elasticsearch DSL. Returns up to 100 matching records per call.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + sql: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "PDL SQL query (e.g., \"SELECT * FROM person WHERE job_title='engineer' AND location_country='united states'\")", + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Elasticsearch DSL query as JSON string. Use either sql or query, not both.', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (1-100, default 1)', + }, + scroll_token: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token returned from a prior response', + }, + dataset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Dataset filter: all, resume, email, phone, mobile_phone, street_address, consumer_social, developer (combinable with commas, exclude with `-` prefix)', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/person/search', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.sql) body.sql = params.sql + if (params.query) { + try { + body.query = JSON.parse(params.query) + } catch { + body.query = params.query + } + } + if (params.size !== undefined) body.size = Number(params.size) + if (params.scroll_token) body.scroll_token = params.scroll_token + if (params.dataset) body.dataset = params.dataset + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const records = (data.data as Record[]) ?? [] + return { + success: true, + output: { + total: (data.total as number) ?? records.length, + scroll_token: (data.scroll_token as string) ?? null, + results: records.map(projectPerson), + }, + } + }, + + outputs: { + total: { type: 'number', description: 'Total matching records in dataset' }, + scroll_token: { + type: 'string', + description: 'Pagination token to fetch the next page; null if no more results', + optional: true, + }, + results: { + type: 'array', + description: 'Person records matching the query', + items: { type: 'object', properties: PDL_PERSON_OUTPUT_PROPERTIES }, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/school_clean.ts b/apps/sim/tools/peopledatalabs/school_clean.ts new file mode 100644 index 0000000000..5ae7cc3d50 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/school_clean.ts @@ -0,0 +1,89 @@ +import type { PdlCleanSchoolParams, PdlCleanSchoolResponse } from '@/tools/peopledatalabs/types' +import { PDL_SCHOOL_OUTPUT_PROPERTIES } from '@/tools/peopledatalabs/types' +import { projectSchool } from '@/tools/peopledatalabs/utils' +import type { ToolConfig } from '@/tools/types' + +export const cleanSchoolTool: ToolConfig = { + id: 'pdl_clean_school', + name: 'PDL School Cleaner', + description: + 'Normalize a school string into a canonical school record. Provide at least one of name, website, or profile (LinkedIn URL).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'People Data Labs API key', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Raw school name to normalize', + }, + website: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'School website', + }, + profile: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn school URL', + }, + }, + + request: { + url: () => 'https://api.peopledatalabs.com/v5/school/clean', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.name) body.name = params.name + if (params.website) body.website = params.website + if (params.profile) body.profile = params.profile + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as Record + const status = (data.status as number) ?? response.status + + if (status === 404) { + return { success: true, output: { matched: false, school: null } } + } + + if (!response.ok) { + const error = (data.error as { message?: string })?.message + throw new Error(error || `People Data Labs error: ${response.status}`) + } + + const hasFields = data.name || data.website || data.id + return { + success: true, + output: { + matched: Boolean(hasFields), + school: hasFields ? projectSchool(data) : null, + }, + } + }, + + outputs: { + matched: { type: 'boolean', description: 'Whether the input was matched to a known school' }, + school: { + type: 'object', + description: 'Canonical school record', + optional: true, + properties: PDL_SCHOOL_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/peopledatalabs/types.ts b/apps/sim/tools/peopledatalabs/types.ts new file mode 100644 index 0000000000..93a461df3b --- /dev/null +++ b/apps/sim/tools/peopledatalabs/types.ts @@ -0,0 +1,479 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +/** + * Output property definitions for People Data Labs API responses. + * Reference: https://docs.peopledatalabs.com/docs + */ + +export const PDL_PERSON_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'PDL person ID', optional: true }, + full_name: { type: 'string', description: 'Full name', optional: true }, + first_name: { type: 'string', description: 'First name', optional: true }, + last_name: { type: 'string', description: 'Last name', optional: true }, + gender: { type: 'string', description: 'Gender', optional: true }, + birth_year: { type: 'number', description: 'Birth year', optional: true }, + linkedin_url: { type: 'string', description: 'LinkedIn profile URL', optional: true }, + linkedin_username: { type: 'string', description: 'LinkedIn username', optional: true }, + twitter_url: { type: 'string', description: 'Twitter profile URL', optional: true }, + github_url: { type: 'string', description: 'GitHub profile URL', optional: true }, + facebook_url: { type: 'string', description: 'Facebook profile URL', optional: true }, + work_email: { type: 'string', description: 'Primary work email', optional: true }, + personal_emails: { + type: 'array', + description: 'Personal email addresses', + optional: true, + items: { type: 'string', description: 'Email address' }, + }, + emails: { + type: 'array', + description: 'All known email addresses', + optional: true, + items: { type: 'object', description: 'Email entry' }, + }, + phone_numbers: { + type: 'array', + description: 'Known phone numbers', + optional: true, + items: { type: 'string', description: 'Phone number' }, + }, + mobile_phone: { type: 'string', description: 'Mobile phone number', optional: true }, + job_title: { type: 'string', description: 'Current job title', optional: true }, + job_title_role: { type: 'string', description: 'Normalized job role', optional: true }, + job_title_sub_role: { + type: 'string', + description: 'Normalized job sub-role', + optional: true, + }, + job_title_levels: { + type: 'array', + description: 'Seniority levels (e.g., manager, director)', + optional: true, + items: { type: 'string', description: 'Level' }, + }, + job_company_name: { type: 'string', description: 'Current employer name', optional: true }, + job_company_website: { + type: 'string', + description: 'Current employer website', + optional: true, + }, + job_company_industry: { + type: 'string', + description: 'Current employer industry', + optional: true, + }, + job_company_size: { type: 'string', description: 'Current employer size band', optional: true }, + job_company_linkedin_url: { + type: 'string', + description: "Current employer's LinkedIn URL", + optional: true, + }, + job_start_date: { + type: 'string', + description: 'Start date at current employer (YYYY-MM)', + optional: true, + }, + location_name: { type: 'string', description: 'Full location name', optional: true }, + location_locality: { type: 'string', description: 'City', optional: true }, + location_region: { type: 'string', description: 'State/region', optional: true }, + location_country: { type: 'string', description: 'Country', optional: true }, + location_continent: { type: 'string', description: 'Continent', optional: true }, + industry: { type: 'string', description: 'Industry', optional: true }, + skills: { + type: 'array', + description: 'Skills', + optional: true, + items: { type: 'string', description: 'Skill name' }, + }, + interests: { + type: 'array', + description: 'Interests', + optional: true, + items: { type: 'string', description: 'Interest' }, + }, + experience: { + type: 'array', + description: 'Work history entries', + optional: true, + items: { type: 'object', description: 'Job experience' }, + }, + education: { + type: 'array', + description: 'Education history', + optional: true, + items: { type: 'object', description: 'Education entry' }, + }, +} as const satisfies Record + +export const PDL_COMPANY_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'PDL company ID', optional: true }, + name: { type: 'string', description: 'Company name', optional: true }, + display_name: { type: 'string', description: 'Display name', optional: true }, + website: { type: 'string', description: 'Website domain', optional: true }, + ticker: { type: 'string', description: 'Stock ticker', optional: true }, + type: { type: 'string', description: 'Company type (public, private, etc.)', optional: true }, + industry: { type: 'string', description: 'Industry', optional: true }, + size: { type: 'string', description: 'Employee size band', optional: true }, + employee_count: { type: 'number', description: 'Estimated employee count', optional: true }, + founded: { type: 'number', description: 'Year founded', optional: true }, + headline: { type: 'string', description: 'Company headline/tagline', optional: true }, + summary: { type: 'string', description: 'Company description', optional: true }, + linkedin_url: { type: 'string', description: 'LinkedIn URL', optional: true }, + linkedin_id: { type: 'string', description: 'LinkedIn ID', optional: true }, + twitter_url: { type: 'string', description: 'Twitter URL', optional: true }, + facebook_url: { type: 'string', description: 'Facebook URL', optional: true }, + location_name: { type: 'string', description: 'HQ location name', optional: true }, + location_locality: { type: 'string', description: 'HQ city', optional: true }, + location_region: { type: 'string', description: 'HQ state/region', optional: true }, + location_country: { type: 'string', description: 'HQ country', optional: true }, + tags: { + type: 'array', + description: 'Company tags', + optional: true, + items: { type: 'string', description: 'Tag' }, + }, + tickers: { + type: 'array', + description: 'All stock tickers', + optional: true, + items: { type: 'string', description: 'Ticker' }, + }, +} as const satisfies Record + +export interface PdlPersonRecord { + id?: string + full_name?: string + first_name?: string + last_name?: string + gender?: string + birth_year?: number + linkedin_url?: string + linkedin_username?: string + twitter_url?: string + github_url?: string + facebook_url?: string + work_email?: string + personal_emails?: string[] + emails?: unknown[] + phone_numbers?: string[] + mobile_phone?: string + job_title?: string + job_title_role?: string + job_title_sub_role?: string + job_title_levels?: string[] + job_company_name?: string + job_company_website?: string + job_company_industry?: string + job_company_size?: string + job_company_linkedin_url?: string + job_start_date?: string + location_name?: string + location_locality?: string + location_region?: string + location_country?: string + location_continent?: string + industry?: string + skills?: string[] + interests?: string[] + experience?: unknown[] + education?: unknown[] +} + +export interface PdlCompanyRecord { + id?: string + name?: string + display_name?: string + website?: string + ticker?: string + type?: string + industry?: string + size?: string + employee_count?: number + founded?: number + headline?: string + summary?: string + linkedin_url?: string + linkedin_id?: string + twitter_url?: string + facebook_url?: string + location_name?: string + location_locality?: string + location_region?: string + location_country?: string + tags?: string[] + tickers?: string[] +} + +export interface PdlPersonEnrichParams { + apiKey: string + email?: string + phone?: string + profile?: string + lid?: string + name?: string + first_name?: string + last_name?: string + company?: string + school?: string + location?: string + min_likelihood?: number + required?: string +} + +export interface PdlPersonEnrichResponse extends ToolResponse { + output: { + matched: boolean + likelihood: number | null + person: PdlPersonRecord | null + } +} + +export interface PdlPersonSearchParams { + apiKey: string + sql?: string + query?: string + size?: number + scroll_token?: string + dataset?: string +} + +export interface PdlPersonSearchResponse extends ToolResponse { + output: { + total: number + scroll_token: string | null + results: PdlPersonRecord[] + } +} + +export interface PdlCompanyEnrichParams { + apiKey: string + name?: string + website?: string + profile?: string + ticker?: string + pdl_id?: string + location?: string + locality?: string + region?: string + country?: string + min_likelihood?: number + required?: string +} + +export interface PdlCompanyEnrichResponse extends ToolResponse { + output: { + matched: boolean + likelihood: number | null + company: PdlCompanyRecord | null + } +} + +export interface PdlCompanySearchParams { + apiKey: string + sql?: string + query?: string + size?: number + scroll_token?: string +} + +export interface PdlCompanySearchResponse extends ToolResponse { + output: { + total: number + scroll_token: string | null + results: PdlCompanyRecord[] + } +} + +export interface PdlAutocompleteParams { + apiKey: string + field: string + text?: string + size?: number +} + +export interface PdlAutocompleteSuggestion { + name: string + count: number + meta?: Record +} + +export interface PdlAutocompleteResponse extends ToolResponse { + output: { + suggestions: PdlAutocompleteSuggestion[] + } +} + +export interface PdlBulkPersonEnrichParams { + apiKey: string + requests: string + required?: string +} + +export interface PdlBulkPersonResultItem { + status: number + likelihood: number | null + matched: boolean + metadata: Record | null + person: PdlPersonRecord | null +} + +export interface PdlBulkPersonEnrichResponse extends ToolResponse { + output: { + results: PdlBulkPersonResultItem[] + } +} + +export interface PdlBulkCompanyEnrichParams { + apiKey: string + requests: string + required?: string +} + +export interface PdlBulkCompanyResultItem { + status: number + likelihood: number | null + matched: boolean + metadata: Record | null + company: PdlCompanyRecord | null +} + +export interface PdlBulkCompanyEnrichResponse extends ToolResponse { + output: { + results: PdlBulkCompanyResultItem[] + } +} + +export interface PdlPersonIdentifyParams { + apiKey: string + email?: string + phone?: string + profile?: string + email_hash?: string + lid?: string + name?: string + first_name?: string + middle_name?: string + last_name?: string + company?: string + school?: string + location?: string + street_address?: string + locality?: string + region?: string + country?: string + postal_code?: string + birth_date?: string + data_include?: string + include_if_matched?: boolean +} + +export interface PdlPersonIdentifyMatch { + match_score: number + matched_on?: string[] + person: PdlPersonRecord +} + +export interface PdlPersonIdentifyResponse extends ToolResponse { + output: { + matches: PdlPersonIdentifyMatch[] + } +} + +export interface PdlCleanCompanyParams { + apiKey: string + name?: string + website?: string + profile?: string +} + +export interface PdlCleanCompanyResponse extends ToolResponse { + output: { + matched: boolean + company: PdlCompanyRecord | null + } +} + +export interface PdlCleanLocationParams { + apiKey: string + location: string +} + +export const PDL_LOCATION_OUTPUT_PROPERTIES = { + name: { type: 'string', description: 'Normalized location name', optional: true }, + locality: { type: 'string', description: 'City', optional: true }, + region: { type: 'string', description: 'State/region', optional: true }, + subregion: { type: 'string', description: 'Subregion (e.g., county)', optional: true }, + country: { type: 'string', description: 'Country', optional: true }, + continent: { type: 'string', description: 'Continent', optional: true }, + type: { type: 'string', description: 'Location type', optional: true }, + geo: { type: 'string', description: 'Latitude,longitude string', optional: true }, +} as const satisfies Record + +export interface PdlLocationRecord { + name?: string + locality?: string + region?: string + subregion?: string + country?: string + continent?: string + type?: string + geo?: string +} + +export interface PdlCleanLocationResponse extends ToolResponse { + output: { + matched: boolean + location: PdlLocationRecord | null + } +} + +export interface PdlCleanSchoolParams { + apiKey: string + name?: string + website?: string + profile?: string +} + +export const PDL_SCHOOL_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'PDL school ID', optional: true }, + name: { type: 'string', description: 'School name', optional: true }, + type: { + type: 'string', + description: 'School type (e.g., university, secondary)', + optional: true, + }, + website: { type: 'string', description: 'Website domain', optional: true }, + linkedin_url: { type: 'string', description: 'LinkedIn URL', optional: true }, + linkedin_id: { type: 'string', description: 'LinkedIn ID', optional: true }, + facebook_url: { type: 'string', description: 'Facebook URL', optional: true }, + twitter_url: { type: 'string', description: 'Twitter URL', optional: true }, + domain: { type: 'string', description: 'School domain', optional: true }, + location_name: { type: 'string', description: 'Location name', optional: true }, + location_locality: { type: 'string', description: 'City', optional: true }, + location_region: { type: 'string', description: 'State/region', optional: true }, + location_country: { type: 'string', description: 'Country', optional: true }, + location_continent: { type: 'string', description: 'Continent', optional: true }, +} as const satisfies Record + +export interface PdlSchoolRecord { + id?: string + name?: string + type?: string + website?: string + linkedin_url?: string + linkedin_id?: string + facebook_url?: string + twitter_url?: string + domain?: string + location_name?: string + location_locality?: string + location_region?: string + location_country?: string + location_continent?: string +} + +export interface PdlCleanSchoolResponse extends ToolResponse { + output: { + matched: boolean + school: PdlSchoolRecord | null + } +} diff --git a/apps/sim/tools/peopledatalabs/utils.ts b/apps/sim/tools/peopledatalabs/utils.ts new file mode 100644 index 0000000000..bd37a47a04 --- /dev/null +++ b/apps/sim/tools/peopledatalabs/utils.ts @@ -0,0 +1,147 @@ +import type { + PdlCompanyRecord, + PdlLocationRecord, + PdlPersonRecord, + PdlSchoolRecord, +} from '@/tools/peopledatalabs/types' + +/** + * Build a query string from non-empty params. + */ +export function buildQueryString(params: Record): string { + const search = new URLSearchParams() + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue + const str = String(value) + if (str.length === 0) continue + search.append(key, str) + } + const qs = search.toString() + return qs ? `?${qs}` : '' +} + +/** + * Project a raw PDL person record onto our flat schema. + */ +export function projectPerson(raw: Record | null | undefined): PdlPersonRecord { + const r = (raw ?? {}) as Record + const location = (r.location ?? {}) as Record + return { + id: (r.id as string) ?? undefined, + full_name: (r.full_name as string) ?? undefined, + first_name: (r.first_name as string) ?? undefined, + last_name: (r.last_name as string) ?? undefined, + gender: (r.gender as string) ?? undefined, + birth_year: (r.birth_year as number) ?? undefined, + linkedin_url: (r.linkedin_url as string) ?? undefined, + linkedin_username: (r.linkedin_username as string) ?? undefined, + twitter_url: (r.twitter_url as string) ?? undefined, + github_url: (r.github_url as string) ?? undefined, + facebook_url: (r.facebook_url as string) ?? undefined, + work_email: (r.work_email as string) ?? undefined, + personal_emails: (r.personal_emails as string[]) ?? undefined, + emails: (r.emails as unknown[]) ?? undefined, + phone_numbers: (r.phone_numbers as string[]) ?? undefined, + mobile_phone: (r.mobile_phone as string) ?? undefined, + job_title: (r.job_title as string) ?? undefined, + job_title_role: (r.job_title_role as string) ?? undefined, + job_title_sub_role: (r.job_title_sub_role as string) ?? undefined, + job_title_levels: (r.job_title_levels as string[]) ?? undefined, + job_company_name: (r.job_company_name as string) ?? undefined, + job_company_website: (r.job_company_website as string) ?? undefined, + job_company_industry: (r.job_company_industry as string) ?? undefined, + job_company_size: (r.job_company_size as string) ?? undefined, + job_company_linkedin_url: (r.job_company_linkedin_url as string) ?? undefined, + job_start_date: (r.job_start_date as string) ?? undefined, + location_name: (r.location_name as string) ?? (location.name as string) ?? undefined, + location_locality: + (r.location_locality as string) ?? (location.locality as string) ?? undefined, + location_region: (r.location_region as string) ?? (location.region as string) ?? undefined, + location_country: (r.location_country as string) ?? (location.country as string) ?? undefined, + location_continent: + (r.location_continent as string) ?? (location.continent as string) ?? undefined, + industry: (r.industry as string) ?? undefined, + skills: (r.skills as string[]) ?? undefined, + interests: (r.interests as string[]) ?? undefined, + experience: (r.experience as unknown[]) ?? undefined, + education: (r.education as unknown[]) ?? undefined, + } +} + +/** + * Project a raw PDL company record onto our flat schema. + */ +export function projectCompany(raw: Record | null | undefined): PdlCompanyRecord { + const r = (raw ?? {}) as Record + const location = (r.location ?? {}) as Record + return { + id: (r.id as string) ?? undefined, + name: (r.name as string) ?? undefined, + display_name: (r.display_name as string) ?? undefined, + website: (r.website as string) ?? undefined, + ticker: (r.ticker as string) ?? undefined, + type: (r.type as string) ?? undefined, + industry: (r.industry as string) ?? undefined, + size: (r.size as string) ?? undefined, + employee_count: (r.employee_count as number) ?? undefined, + founded: (r.founded as number) ?? undefined, + headline: (r.headline as string) ?? undefined, + summary: (r.summary as string) ?? undefined, + linkedin_url: (r.linkedin_url as string) ?? undefined, + linkedin_id: (r.linkedin_id as string) ?? undefined, + twitter_url: (r.twitter_url as string) ?? undefined, + facebook_url: (r.facebook_url as string) ?? undefined, + location_name: (r.location_name as string) ?? (location.name as string) ?? undefined, + location_locality: + (r.location_locality as string) ?? (location.locality as string) ?? undefined, + location_region: (r.location_region as string) ?? (location.region as string) ?? undefined, + location_country: (r.location_country as string) ?? (location.country as string) ?? undefined, + tags: (r.tags as string[]) ?? undefined, + tickers: (r.tickers as string[]) ?? undefined, + } +} + +/** + * Project a raw PDL location record onto our flat schema. + */ +export function projectLocation( + raw: Record | null | undefined +): PdlLocationRecord { + const r = (raw ?? {}) as Record + return { + name: (r.name as string) ?? undefined, + locality: (r.locality as string) ?? undefined, + region: (r.region as string) ?? undefined, + subregion: (r.subregion as string) ?? undefined, + country: (r.country as string) ?? undefined, + continent: (r.continent as string) ?? undefined, + type: (r.type as string) ?? undefined, + geo: (r.geo as string) ?? undefined, + } +} + +/** + * Project a raw PDL school record onto our flat schema. + */ +export function projectSchool(raw: Record | null | undefined): PdlSchoolRecord { + const r = (raw ?? {}) as Record + const location = (r.location ?? {}) as Record + return { + id: (r.id as string) ?? undefined, + name: (r.name as string) ?? undefined, + type: (r.type as string) ?? undefined, + website: (r.website as string) ?? undefined, + linkedin_url: (r.linkedin_url as string) ?? undefined, + linkedin_id: (r.linkedin_id as string) ?? undefined, + facebook_url: (r.facebook_url as string) ?? undefined, + twitter_url: (r.twitter_url as string) ?? undefined, + domain: (r.domain as string) ?? undefined, + location_name: (r.location_name as string) ?? (location.name as string) ?? undefined, + location_locality: + (r.location_locality as string) ?? (location.locality as string) ?? undefined, + location_region: (r.location_region as string) ?? (location.region as string) ?? undefined, + location_country: (r.location_country as string) ?? (location.country as string) ?? undefined, + location_continent: + (r.location_continent as string) ?? (location.continent as string) ?? undefined, + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 8da147da4e..4fbaff816b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1902,6 +1902,19 @@ import { pagerdutyUpdateIncidentTool, } from '@/tools/pagerduty' import { parallelDeepResearchTool, parallelExtractTool, parallelSearchTool } from '@/tools/parallel' +import { + pdlAutocompleteTool, + pdlBulkCompanyEnrichTool, + pdlBulkPersonEnrichTool, + pdlCleanCompanyTool, + pdlCleanLocationTool, + pdlCleanSchoolTool, + pdlCompanyEnrichTool, + pdlCompanySearchTool, + pdlPersonEnrichTool, + pdlPersonIdentifyTool, + pdlPersonSearchTool, +} from '@/tools/peopledatalabs' import { perplexityChatTool, perplexitySearchTool } from '@/tools/perplexity' import { pineconeFetchTool, @@ -4423,6 +4436,17 @@ export const tools: Record = { google_slides_create_table: googleSlidesCreateTableTool, google_slides_create_shape: googleSlidesCreateShapeTool, google_slides_insert_text: googleSlidesInsertTextTool, + pdl_person_enrich: pdlPersonEnrichTool, + pdl_person_search: pdlPersonSearchTool, + pdl_person_identify: pdlPersonIdentifyTool, + pdl_bulk_person_enrich: pdlBulkPersonEnrichTool, + pdl_company_enrich: pdlCompanyEnrichTool, + pdl_company_search: pdlCompanySearchTool, + pdl_bulk_company_enrich: pdlBulkCompanyEnrichTool, + pdl_clean_company: pdlCleanCompanyTool, + pdl_clean_location: pdlCleanLocationTool, + pdl_clean_school: pdlCleanSchoolTool, + pdl_autocomplete: pdlAutocompleteTool, perplexity_chat: perplexityChatTool, perplexity_search: perplexitySearchTool, profound_bot_logs: profoundBotLogsTool,