chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import type { PdlAutocompleteParams, PdlAutocompleteResponse } from '@/tools/peopledatalabs/types'
|
||||
import { PEOPLEDATALABS_API_KEY_PREFIX } 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<string, OutputProperty>
|
||||
|
||||
export const autocompleteTool: ToolConfig<PdlAutocompleteParams, PdlAutocompleteResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
// Autocomplete is free on PDL (rate-limited only) — no credits consumed.
|
||||
pricing: { type: 'per_request', cost: 0 },
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
titlecase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return name fields in title case',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const qs = buildQueryString({
|
||||
field: params.field,
|
||||
text: params.text,
|
||||
size: params.size,
|
||||
titlecase: params.titlecase,
|
||||
})
|
||||
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<string, unknown>
|
||||
|
||||
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<string, unknown> }[]) ?? []
|
||||
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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import type {
|
||||
PdlBulkCompanyEnrichParams,
|
||||
PdlBulkCompanyEnrichResponse,
|
||||
PdlBulkCompanyResultItem,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { PDL_CREDIT_USD, PEOPLEDATALABS_API_KEY_PREFIX } from '@/tools/peopledatalabs/types'
|
||||
import { countBulkMatched, 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<string, OutputProperty>
|
||||
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// PDL charges 1 credit per matched record; unmatched records in the batch are free.
|
||||
getCost: (_params, output) => {
|
||||
const matched = countBulkMatched(output)
|
||||
return { cost: matched * PDL_CREDIT_USD, metadata: { credits: matched } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'custom',
|
||||
requestsPerMinute: 10,
|
||||
dimensions: [
|
||||
{
|
||||
name: 'credits',
|
||||
limitPerMinute: 600,
|
||||
extractUsage: (_params, response) => countBulkMatched(response),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
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<string, unknown> = { 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<Record<string, unknown>>
|
||||
const results: PdlBulkCompanyResultItem[] = items.map((item) => {
|
||||
const status = (item.status as number) ?? 0
|
||||
const record = (item.data as Record<string, unknown>) ?? null
|
||||
return {
|
||||
status,
|
||||
likelihood: (item.likelihood as number) ?? null,
|
||||
matched: status === 200 && record !== null,
|
||||
metadata: (item.metadata as Record<string, unknown>) ?? 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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import type {
|
||||
PdlBulkPersonEnrichParams,
|
||||
PdlBulkPersonEnrichResponse,
|
||||
PdlBulkPersonResultItem,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { PDL_CREDIT_USD, PEOPLEDATALABS_API_KEY_PREFIX } from '@/tools/peopledatalabs/types'
|
||||
import { countBulkMatched, 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<string, OutputProperty>
|
||||
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// PDL charges 1 credit per matched record; unmatched records in the batch are free.
|
||||
getCost: (_params, output) => {
|
||||
const matched = countBulkMatched(output)
|
||||
return { cost: matched * PDL_CREDIT_USD, metadata: { credits: matched } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'custom',
|
||||
requestsPerMinute: 10,
|
||||
dimensions: [
|
||||
{
|
||||
name: 'credits',
|
||||
limitPerMinute: 600,
|
||||
extractUsage: (_params, response) => countBulkMatched(response),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
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<string, unknown> = { 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<Record<string, unknown>>
|
||||
const results: PdlBulkPersonResultItem[] = items.map((item) => {
|
||||
const status = (item.status as number) ?? 0
|
||||
const record = (item.data as Record<string, unknown>) ?? null
|
||||
return {
|
||||
status,
|
||||
matched: status === 200 && record !== null,
|
||||
likelihood: (item.likelihood as number) ?? null,
|
||||
metadata: (item.metadata as Record<string, unknown>) ?? 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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { PdlCleanCompanyParams, PdlCleanCompanyResponse } from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_COMPANY_OUTPUT_PROPERTIES,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { projectCompany } from '@/tools/peopledatalabs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const cleanCompanyTool: ToolConfig<PdlCleanCompanyParams, PdlCleanCompanyResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
// Cleaner APIs are free on PDL (rate-limited only) — no credits consumed.
|
||||
pricing: { type: 'per_request', cost: 0 },
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
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<string, unknown> = {}
|
||||
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<string, unknown>
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import type { PdlCompanyEnrichParams, PdlCompanyEnrichResponse } from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_COMPANY_OUTPUT_PROPERTIES,
|
||||
PDL_CREDIT_USD,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { buildQueryString, projectCompany } from '@/tools/peopledatalabs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const companyEnrichTool: ToolConfig<PdlCompanyEnrichParams, PdlCompanyEnrichResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// PDL charges 1 credit per matched request; misses (404) are free.
|
||||
getCost: (_params, output) => {
|
||||
const cost = output.matched === true ? PDL_CREDIT_USD : 0
|
||||
return { cost, metadata: { credits: output.matched === true ? 1 : 0 } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
titlecase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return name fields in title case',
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
titlecase: params.titlecase,
|
||||
})
|
||||
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<string, unknown>
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { PdlCompanySearchParams, PdlCompanySearchResponse } from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_COMPANY_OUTPUT_PROPERTIES,
|
||||
PDL_CREDIT_USD,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { projectCompany } from '@/tools/peopledatalabs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const companySearchTool: ToolConfig<PdlCompanySearchParams, PdlCompanySearchResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// PDL Search charges 1 credit per company returned (up to the requested size).
|
||||
getCost: (_params, output) => {
|
||||
const results = output.results
|
||||
if (!Array.isArray(results)) {
|
||||
throw new Error('PDL company search response missing results, cannot determine cost')
|
||||
}
|
||||
return { cost: results.length * PDL_CREDIT_USD, metadata: { credits: results.length } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'custom',
|
||||
requestsPerMinute: 30,
|
||||
dimensions: [
|
||||
{
|
||||
name: 'credits',
|
||||
limitPerMinute: 600,
|
||||
extractUsage: (_params, response) =>
|
||||
Array.isArray(response.results) ? response.results.length : 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',
|
||||
},
|
||||
titlecase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return name fields in title case',
|
||||
},
|
||||
},
|
||||
|
||||
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<string, unknown> = {}
|
||||
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.titlecase !== undefined) body.titlecase = params.titlecase
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = (await response.json()) as Record<string, unknown>
|
||||
const status = (data.status as number) ?? response.status
|
||||
|
||||
if (status === 404) {
|
||||
return { success: true, output: { total: 0, scroll_token: null, results: [] } }
|
||||
}
|
||||
|
||||
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<string, unknown>[]) ?? []
|
||||
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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { PdlCleanLocationParams, PdlCleanLocationResponse } from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_LOCATION_OUTPUT_PROPERTIES,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { projectLocation } from '@/tools/peopledatalabs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const cleanLocationTool: ToolConfig<PdlCleanLocationParams, PdlCleanLocationResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
// Cleaner APIs are free on PDL (rate-limited only) — no credits consumed.
|
||||
pricing: { type: 'per_request', cost: 0 },
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
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<string, unknown>
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import type { PdlPersonEnrichParams, PdlPersonEnrichResponse } from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_CREDIT_USD,
|
||||
PDL_PERSON_OUTPUT_PROPERTIES,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { buildQueryString, projectPerson } from '@/tools/peopledatalabs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const personEnrichTool: ToolConfig<PdlPersonEnrichParams, PdlPersonEnrichResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// PDL charges 1 credit per matched request; misses (404) are free.
|
||||
getCost: (_params, output) => {
|
||||
const cost = output.matched === true ? PDL_CREDIT_USD : 0
|
||||
return { cost, metadata: { credits: output.matched === true ? 1 : 0 } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
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")',
|
||||
},
|
||||
titlecase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return name fields in title case',
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
titlecase: params.titlecase,
|
||||
})
|
||||
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<string, unknown>
|
||||
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<string, unknown>) ?? 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import type {
|
||||
PdlPersonIdentifyParams,
|
||||
PdlPersonIdentifyResponse,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_CREDIT_USD,
|
||||
PDL_PERSON_OUTPUT_PROPERTIES,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} 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<string, OutputProperty>
|
||||
|
||||
export const personIdentifyTool: ToolConfig<PdlPersonIdentifyParams, PdlPersonIdentifyResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// PDL Identify charges 1 credit per call that returns matches, regardless of count.
|
||||
getCost: (_params, output) => {
|
||||
const matches = output.matches
|
||||
if (!Array.isArray(matches)) {
|
||||
throw new Error('PDL identify response missing matches, cannot determine cost')
|
||||
}
|
||||
const cost = matches.length > 0 ? PDL_CREDIT_USD : 0
|
||||
return { cost, metadata: { credits: matches.length > 0 ? 1 : 0 } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
titlecase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return name fields in title case',
|
||||
},
|
||||
},
|
||||
|
||||
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,
|
||||
titlecase: params.titlecase,
|
||||
})
|
||||
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<string, unknown>
|
||||
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<Record<string, unknown>>) ?? []
|
||||
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<string, unknown>) ?? {}),
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
matches: {
|
||||
type: 'array',
|
||||
description: 'Up to 20 candidate matches, ordered by score',
|
||||
items: { type: 'object', properties: IDENTIFY_MATCH_PROPERTIES },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { PdlPersonSearchParams, PdlPersonSearchResponse } from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_CREDIT_USD,
|
||||
PDL_PERSON_OUTPUT_PROPERTIES,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { projectPerson } from '@/tools/peopledatalabs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const personSearchTool: ToolConfig<PdlPersonSearchParams, PdlPersonSearchResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// PDL Search charges 1 credit per profile returned (up to the requested size).
|
||||
getCost: (_params, output) => {
|
||||
const results = output.results
|
||||
if (!Array.isArray(results)) {
|
||||
throw new Error('PDL person search response missing results, cannot determine cost')
|
||||
}
|
||||
return { cost: results.length * PDL_CREDIT_USD, metadata: { credits: results.length } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'custom',
|
||||
requestsPerMinute: 30,
|
||||
dimensions: [
|
||||
{
|
||||
name: 'credits',
|
||||
limitPerMinute: 600,
|
||||
extractUsage: (_params, response) =>
|
||||
Array.isArray(response.results) ? response.results.length : 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)',
|
||||
},
|
||||
titlecase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return name fields in title case',
|
||||
},
|
||||
},
|
||||
|
||||
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<string, unknown> = {}
|
||||
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
|
||||
if (params.titlecase !== undefined) body.titlecase = params.titlecase
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = (await response.json()) as Record<string, unknown>
|
||||
const status = (data.status as number) ?? response.status
|
||||
|
||||
if (status === 404) {
|
||||
return { success: true, output: { total: 0, scroll_token: null, results: [] } }
|
||||
}
|
||||
|
||||
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<string, unknown>[]) ?? []
|
||||
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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { PdlCleanSchoolParams, PdlCleanSchoolResponse } from '@/tools/peopledatalabs/types'
|
||||
import {
|
||||
PDL_SCHOOL_OUTPUT_PROPERTIES,
|
||||
PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
import { projectSchool } from '@/tools/peopledatalabs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const cleanSchoolTool: ToolConfig<PdlCleanSchoolParams, PdlCleanSchoolResponse> = {
|
||||
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',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: PEOPLEDATALABS_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'peopledatalabs',
|
||||
// Cleaner APIs are free on PDL (rate-limited only) — no credits consumed.
|
||||
pricing: { type: 'per_request', cost: 0 },
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
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<string, unknown> = {}
|
||||
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<string, unknown>
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Env var prefix for Sim-hosted People Data Labs keys.
|
||||
* Resolved at runtime as `PEOPLEDATALABS_API_KEY_COUNT` + `PEOPLEDATALABS_API_KEY_1..N`.
|
||||
*/
|
||||
export const PEOPLEDATALABS_API_KEY_PREFIX = 'PEOPLEDATALABS_API_KEY'
|
||||
|
||||
/**
|
||||
* Dollar cost per People Data Labs credit when billing hosted-key usage.
|
||||
*
|
||||
* PDL does not report credits in its responses, so cost is derived from match counts:
|
||||
* enrich/identify consume 1 credit per matched request, search consumes 1 credit per
|
||||
* profile returned, and bulk consumes 1 credit per matched record. Misses (HTTP 404)
|
||||
* are not charged. Cleaner and autocomplete endpoints are free (rate-limited only).
|
||||
*
|
||||
* Estimate based on the self-serve Pro plan (~$0.28/credit) — see
|
||||
* https://www.peopledatalabs.com/pricing. Tune if Sim's plan rate changes.
|
||||
*/
|
||||
export const PDL_CREDIT_USD = 0.28
|
||||
|
||||
/**
|
||||
* 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<string, OutputProperty>
|
||||
|
||||
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<string, OutputProperty>
|
||||
|
||||
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
|
||||
titlecase?: boolean
|
||||
}
|
||||
|
||||
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
|
||||
titlecase?: boolean
|
||||
}
|
||||
|
||||
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
|
||||
titlecase?: boolean
|
||||
}
|
||||
|
||||
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
|
||||
titlecase?: boolean
|
||||
}
|
||||
|
||||
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
|
||||
titlecase?: boolean
|
||||
}
|
||||
|
||||
interface PdlAutocompleteSuggestion {
|
||||
name: string
|
||||
count: number
|
||||
meta?: Record<string, unknown>
|
||||
}
|
||||
|
||||
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<string, unknown> | 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<string, unknown> | 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
|
||||
titlecase?: boolean
|
||||
}
|
||||
|
||||
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<string, OutputProperty>
|
||||
|
||||
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<string, OutputProperty>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import type {
|
||||
PdlCompanyRecord,
|
||||
PdlLocationRecord,
|
||||
PdlPersonRecord,
|
||||
PdlSchoolRecord,
|
||||
} from '@/tools/peopledatalabs/types'
|
||||
|
||||
/**
|
||||
* Count matched records in a bulk enrich output `results` array.
|
||||
* PDL charges one credit per matched record, so this drives both hosted-key
|
||||
* cost and the post-execution credit rate-limit dimension.
|
||||
*/
|
||||
export function countBulkMatched(output: Record<string, unknown>): number {
|
||||
const results = output.results
|
||||
if (!Array.isArray(results)) return 0
|
||||
return results.reduce((count, item) => {
|
||||
const matched = (item as { matched?: unknown }).matched
|
||||
return matched === true ? count + 1 : count
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a query string from non-empty params.
|
||||
*/
|
||||
export function buildQueryString(params: Record<string, unknown>): 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<string, unknown> | null | undefined): PdlPersonRecord {
|
||||
const r = (raw ?? {}) as Record<string, unknown>
|
||||
const location = (r.location ?? {}) as Record<string, unknown>
|
||||
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<string, unknown> | null | undefined): PdlCompanyRecord {
|
||||
const r = (raw ?? {}) as Record<string, unknown>
|
||||
const location = (r.location ?? {}) as Record<string, unknown>
|
||||
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<string, unknown> | null | undefined
|
||||
): PdlLocationRecord {
|
||||
const r = (raw ?? {}) as Record<string, unknown>
|
||||
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<string, unknown> | null | undefined): PdlSchoolRecord {
|
||||
const r = (raw ?? {}) as Record<string, unknown>
|
||||
const location = (r.location ?? {}) as Record<string, unknown>
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user