d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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
369 lines
13 KiB
TypeScript
369 lines
13 KiB
TypeScript
import { sleep } from '@sim/utils/helpers'
|
|
import { dropcontactHosting } from '@/tools/dropcontact/hosting'
|
|
import type {
|
|
DropcontactEmailEntry,
|
|
DropcontactEnrichContactParams,
|
|
DropcontactEnrichContactResponse,
|
|
DropcontactEnrichedContact,
|
|
} from '@/tools/dropcontact/types'
|
|
import type { ToolConfig } from '@/tools/types'
|
|
|
|
const POLL_INTERVAL_MS = 5000
|
|
const MAX_POLL_TIME_MS = 120000
|
|
|
|
/**
|
|
* Map the first contact from the Dropcontact poll result data array to the
|
|
* flat tool output shape.
|
|
*
|
|
* @param contact - Raw contact object from the Dropcontact API poll response
|
|
* @returns Structured output matching `DropcontactEnrichContactResponse.output`
|
|
*/
|
|
function mapContactData(
|
|
requestId: string | null,
|
|
contact: DropcontactEnrichedContact
|
|
): DropcontactEnrichContactResponse['output'] {
|
|
const emailEntries = Array.isArray(contact.email)
|
|
? (contact.email as DropcontactEmailEntry[])
|
|
: null
|
|
const firstEmail = emailEntries?.[0] ?? null
|
|
|
|
return {
|
|
request_id: requestId,
|
|
email_found: Boolean(firstEmail?.email),
|
|
email: firstEmail?.email ?? null,
|
|
emails: emailEntries,
|
|
qualification: firstEmail?.qualification ?? null,
|
|
first_name: contact.first_name ?? null,
|
|
last_name: contact.last_name ?? null,
|
|
full_name: contact.full_name ?? null,
|
|
civility: contact.civility ?? null,
|
|
phone: contact.phone ?? null,
|
|
mobile_phone: contact.mobile_phone ?? null,
|
|
company: contact.company ?? null,
|
|
website: contact.website ?? null,
|
|
company_linkedin: contact.company_linkedin ?? null,
|
|
linkedin: contact.linkedin ?? null,
|
|
country: contact.country ?? null,
|
|
siren: contact.siren ?? null,
|
|
siret: contact.siret ?? null,
|
|
siret_address: contact.siret_address ?? null,
|
|
siret_zip: contact.siret_zip ?? null,
|
|
siret_city: contact.siret_city ?? null,
|
|
vat: contact.vat ?? null,
|
|
nb_employees: contact.nb_employees ?? null,
|
|
employee_count: contact.employee_count ?? null,
|
|
naf5_code: contact.naf5_code ?? null,
|
|
naf5_des: contact.naf5_des ?? null,
|
|
industry: contact.industry ?? null,
|
|
job: contact.job ?? null,
|
|
job_level: contact.job_level ?? null,
|
|
job_function: contact.job_function ?? null,
|
|
company_turnover: contact.company_turnover ?? null,
|
|
company_results: contact.company_results ?? null,
|
|
}
|
|
}
|
|
|
|
export const dropcontactEnrichContactTool: ToolConfig<
|
|
DropcontactEnrichContactParams,
|
|
DropcontactEnrichContactResponse
|
|
> = {
|
|
id: 'dropcontact_enrich_contact',
|
|
name: 'Dropcontact Enrich Contact',
|
|
description:
|
|
'Enrich a contact with verified B2B email, phone, company data, and LinkedIn info via Dropcontact. Submits an async enrichment request, then polls until the result is ready (up to 2 minutes). Charges 1 credit only when a verified email is returned. Provide at least one of: email, first_name+last_name+company, full_name+company, or linkedin URL.',
|
|
version: '1.0.0',
|
|
|
|
hosting: dropcontactHosting<DropcontactEnrichContactParams>((_params, output) => {
|
|
// 1 credit per contact when a verified email is found.
|
|
// Source: https://developer.dropcontact.com (retrieved 2026-05)
|
|
return output.email_found === true ? 1 : 0
|
|
}),
|
|
|
|
params: {
|
|
apiKey: {
|
|
type: 'string',
|
|
required: true,
|
|
visibility: 'user-only',
|
|
description: 'Dropcontact API key (X-Access-Token)',
|
|
},
|
|
email: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Email address of the contact to enrich',
|
|
},
|
|
first_name: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'First name of the contact',
|
|
},
|
|
last_name: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Last name of the contact',
|
|
},
|
|
full_name: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Full name (alternative to first_name + last_name)',
|
|
},
|
|
company: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Company name',
|
|
},
|
|
website: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Company website (e.g. acme.com)',
|
|
},
|
|
num_siren: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'French company SIREN number',
|
|
},
|
|
phone: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Phone number',
|
|
},
|
|
linkedin: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'LinkedIn profile URL',
|
|
},
|
|
country: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Country code (ISO 3166-1 alpha-2, e.g. "US", "FR")',
|
|
},
|
|
siren: {
|
|
type: 'boolean',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Include SIREN/SIRET enrichment (France only)',
|
|
},
|
|
language: {
|
|
type: 'string',
|
|
required: false,
|
|
visibility: 'user-or-llm',
|
|
description: 'Language for returned data (e.g. "en", "fr")',
|
|
},
|
|
},
|
|
|
|
request: {
|
|
// Submit endpoint: POST https://api.dropcontact.com/v1/enrich/all
|
|
// Source: https://developer.dropcontact.com (retrieved 2026-05)
|
|
url: 'https://api.dropcontact.com/v1/enrich/all',
|
|
method: 'POST',
|
|
headers: (params: DropcontactEnrichContactParams) => ({
|
|
'X-Access-Token': params.apiKey,
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
body: (params: DropcontactEnrichContactParams) => {
|
|
const contact: Record<string, unknown> = {}
|
|
if (params.email) contact.email = params.email
|
|
if (params.first_name) contact.first_name = params.first_name
|
|
if (params.last_name) contact.last_name = params.last_name
|
|
if (params.full_name) contact.full_name = params.full_name
|
|
if (params.company) contact.company = params.company
|
|
if (params.website) contact.website = params.website
|
|
if (params.num_siren) contact.num_siren = params.num_siren
|
|
if (params.phone) contact.phone = params.phone
|
|
if (params.linkedin) contact.linkedin = params.linkedin
|
|
if (params.country) contact.country = params.country
|
|
|
|
const body: Record<string, unknown> = { data: [contact] }
|
|
if (params.siren !== undefined) body.siren = params.siren
|
|
if (params.language) body.language = params.language
|
|
|
|
return body
|
|
},
|
|
},
|
|
|
|
transformResponse: async (response: Response) => {
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
throw new Error(`Dropcontact API error: ${response.status} - ${errorText}`)
|
|
}
|
|
const json = await response.json()
|
|
if (json.error) {
|
|
throw new Error(`Dropcontact API error: ${String(json.reason ?? json.error)}`)
|
|
}
|
|
// Submit response includes request_id; enrichment is async
|
|
return {
|
|
success: true,
|
|
output: {
|
|
request_id: (json.request_id as string) ?? null,
|
|
email_found: false,
|
|
email: null,
|
|
emails: null,
|
|
qualification: null,
|
|
first_name: null,
|
|
last_name: null,
|
|
full_name: null,
|
|
civility: null,
|
|
phone: null,
|
|
mobile_phone: null,
|
|
company: null,
|
|
website: null,
|
|
company_linkedin: null,
|
|
linkedin: null,
|
|
country: null,
|
|
siren: null,
|
|
siret: null,
|
|
siret_address: null,
|
|
siret_zip: null,
|
|
siret_city: null,
|
|
vat: null,
|
|
nb_employees: null,
|
|
employee_count: null,
|
|
naf5_code: null,
|
|
naf5_des: null,
|
|
industry: null,
|
|
job: null,
|
|
job_level: null,
|
|
job_function: null,
|
|
company_turnover: null,
|
|
company_results: null,
|
|
},
|
|
}
|
|
},
|
|
|
|
postProcess: async (result, params) => {
|
|
if (!result.success) return result
|
|
|
|
const requestId = result.output.request_id
|
|
if (!requestId) {
|
|
throw new Error('Dropcontact enrichment did not return a request_id')
|
|
}
|
|
|
|
let elapsedTime = 0
|
|
while (elapsedTime < MAX_POLL_TIME_MS) {
|
|
await sleep(POLL_INTERVAL_MS)
|
|
elapsedTime += POLL_INTERVAL_MS
|
|
|
|
// Poll endpoint: GET https://api.dropcontact.com/v1/enrich/all/{request_id}
|
|
// Source: https://developer.dropcontact.com (retrieved 2026-05)
|
|
const pollResponse = await fetch(
|
|
`https://api.dropcontact.com/v1/enrich/all/${encodeURIComponent(requestId)}`,
|
|
{
|
|
headers: {
|
|
'X-Access-Token': params.apiKey,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
)
|
|
|
|
if (!pollResponse.ok) {
|
|
const errorText = await pollResponse.text()
|
|
throw new Error(`Dropcontact poll error: ${pollResponse.status} - ${errorText}`)
|
|
}
|
|
|
|
const json = await pollResponse.json()
|
|
|
|
// Error state: { error: true|string, reason?: string }
|
|
if (json.error) {
|
|
throw new Error(`Dropcontact enrichment failed: ${String(json.reason ?? json.error)}`)
|
|
}
|
|
|
|
// Pending: { success: false, error: false, reason: "Request not ready yet..." }
|
|
if (!json.success) continue
|
|
|
|
// Ready: { success: true, data: [...], error: false }
|
|
|
|
const contacts = Array.isArray(json.data) ? json.data : []
|
|
const contact = (contacts[0] ?? {}) as DropcontactEnrichedContact
|
|
|
|
return {
|
|
success: true,
|
|
output: mapContactData(requestId, contact),
|
|
}
|
|
}
|
|
|
|
throw new Error('Dropcontact enrichment did not complete within the polling window')
|
|
},
|
|
|
|
outputs: {
|
|
request_id: { type: 'string', description: 'Dropcontact async request ID', optional: true },
|
|
email_found: { type: 'boolean', description: 'Whether a verified email was found' },
|
|
email: { type: 'string', description: 'Primary verified email address', optional: true },
|
|
emails: {
|
|
type: 'array',
|
|
description: 'All email addresses returned (each with email and qualification)',
|
|
optional: true,
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
email: { type: 'string', description: 'Email address' },
|
|
qualification: {
|
|
type: 'string',
|
|
description: 'Email qualification (e.g. nominative@pro)',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
qualification: {
|
|
type: 'string',
|
|
description: 'Primary email qualification (e.g. nominative@pro, catch_all@pro)',
|
|
optional: true,
|
|
},
|
|
first_name: { type: 'string', description: 'First name', optional: true },
|
|
last_name: { type: 'string', description: 'Last name', optional: true },
|
|
full_name: { type: 'string', description: 'Full name', optional: true },
|
|
civility: { type: 'string', description: 'Civility (Mr, Mrs, etc.)', optional: true },
|
|
phone: { type: 'string', description: 'Phone number', optional: true },
|
|
mobile_phone: { type: 'string', description: 'Mobile phone number', optional: true },
|
|
company: { type: 'string', description: 'Company name', optional: true },
|
|
website: { type: 'string', description: 'Company website', optional: true },
|
|
company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true },
|
|
linkedin: { type: 'string', description: 'Personal LinkedIn URL', optional: true },
|
|
country: { type: 'string', description: 'Country code (ISO 3166-1 alpha-2)', optional: true },
|
|
siren: { type: 'string', description: 'French SIREN number', optional: true },
|
|
siret: { type: 'string', description: 'French SIRET number', optional: true },
|
|
siret_address: { type: 'string', description: 'SIRET registered address', optional: true },
|
|
siret_zip: { type: 'string', description: 'SIRET registered postal code', optional: true },
|
|
siret_city: { type: 'string', description: 'SIRET registered city', optional: true },
|
|
vat: { type: 'string', description: 'VAT number', optional: true },
|
|
nb_employees: { type: 'string', description: 'Employee count range', optional: true },
|
|
employee_count: {
|
|
type: 'number',
|
|
description: 'Exact employee count (Growth plan and above)',
|
|
optional: true,
|
|
},
|
|
naf5_code: { type: 'string', description: 'NAF/APE code (France)', optional: true },
|
|
naf5_des: {
|
|
type: 'string',
|
|
description: 'NAF/APE code description (France)',
|
|
optional: true,
|
|
},
|
|
industry: { type: 'string', description: 'Industry classification', optional: true },
|
|
job: { type: 'string', description: 'Job title', optional: true },
|
|
job_level: {
|
|
type: 'string',
|
|
description: 'Job seniority level (e.g. C-level, Director)',
|
|
optional: true,
|
|
},
|
|
job_function: {
|
|
type: 'string',
|
|
description: 'Job function (e.g. Sales, Engineering)',
|
|
optional: true,
|
|
},
|
|
company_turnover: {
|
|
type: 'string',
|
|
description: 'Company revenue/turnover range',
|
|
optional: true,
|
|
},
|
|
company_results: { type: 'string', description: 'Company net results', optional: true },
|
|
},
|
|
}
|