Files
simstudioai--sim/apps/sim/tools/wiza/individual_reveal.ts
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

331 lines
13 KiB
TypeScript

import { sleep } from '@sim/utils/helpers'
import type { ToolConfig } from '@/tools/types'
import { wizaHosting } from '@/tools/wiza/hosting'
import type {
WizaIndividualRevealData,
WizaIndividualRevealParams,
WizaIndividualRevealResponse,
} from '@/tools/wiza/types'
const POLL_INTERVAL_MS = 2000
const MAX_POLL_TIME_MS = 120000
/** Tolerate brief Wiza outages while polling before giving up on an already-started reveal. */
const MAX_CONSECUTIVE_POLL_ERRORS = 3
/** Whether a reveal payload has reached a terminal state and no longer needs polling. */
function isTerminalReveal(d: { status?: string | null; is_complete?: boolean | null }): boolean {
return d.status === 'finished' || d.status === 'failed' || d.is_complete === true
}
/** Map a Wiza individual-reveal payload (`data` object) to the tool output shape. */
function mapRevealData(d: Record<string, unknown>): WizaIndividualRevealData {
const emails = Array.isArray(d.emails) ? (d.emails as Record<string, unknown>[]) : []
const phones = Array.isArray(d.phones) ? (d.phones as Record<string, unknown>[]) : []
return {
id: (d.id as number) ?? null,
status: (d.status as string) ?? null,
is_complete: (d.is_complete as boolean) ?? null,
name: (d.name as string) ?? null,
company: (d.company as string) ?? null,
enrichment_level: (d.enrichment_level as string) ?? null,
linkedin_profile_url: (d.linkedin_profile_url as string) ?? null,
title: (d.title as string) ?? null,
location: (d.location as string) ?? null,
email: (d.email as string) ?? null,
email_type: (d.email_type as string) ?? null,
email_status: (d.email_status as string) ?? null,
emails: emails.map((e) => ({
email: (e.email as string) ?? null,
email_type: (e.email_type as string) ?? null,
email_status: (e.email_status as string) ?? null,
})),
mobile_phone: (d.mobile_phone as string) ?? null,
phone_number: (d.phone_number as string) ?? null,
phone_status: (d.phone_status as string) ?? null,
phones: phones.map((p) => ({
number: (p.number as string) ?? null,
pretty_number: (p.pretty_number as string) ?? null,
type: (p.type as string) ?? null,
})),
company_size: (d.company_size as number) ?? null,
company_size_range: (d.company_size_range as string) ?? null,
company_type: (d.company_type as string) ?? null,
company_domain: (d.company_domain as string) ?? null,
company_locality: (d.company_locality as string) ?? null,
company_region: (d.company_region as string) ?? null,
company_country: (d.company_country as string) ?? null,
company_street: (d.company_street as string) ?? null,
company_postal_code: (d.company_postal_code as string) ?? null,
company_founded: (d.company_founded as number) ?? null,
company_funding: (d.company_funding as string) ?? null,
company_revenue: (d.company_revenue as string) ?? null,
company_industry: (d.company_industry as string) ?? null,
company_subindustry: (d.company_subindustry as string) ?? null,
company_linkedin: (d.company_linkedin as string) ?? null,
company_location: (d.company_location as string) ?? null,
company_description: (d.company_description as string) ?? null,
credits: (d.credits as Record<string, unknown>) ?? null,
}
}
export const wizaIndividualRevealTool: ToolConfig<
WizaIndividualRevealParams,
WizaIndividualRevealResponse
> = {
id: 'wiza_individual_reveal',
name: 'Wiza Individual Reveal',
description:
'Reveal a contact via LinkedIn URL, name + company/domain, or email. Starts the reveal and polls until it resolves. Uses 2 credits per valid email and 5 credits per phone, charged only on success.',
version: '1.0.0',
hosting: wizaHosting<WizaIndividualRevealParams>((_params, output) => {
let credits = 0
const emails = Array.isArray(output.emails)
? (output.emails as { email_status?: string }[])
: []
const emailValid =
output.email_status === 'valid' || emails.some((e) => e.email_status === 'valid')
// 2 credits when at least one valid email is returned.
if (emailValid) credits += 2
const phones = Array.isArray(output.phones) ? output.phones : []
const phoneFound = Boolean(output.mobile_phone || output.phone_number || phones.length > 0)
// 5 credits when at least one phone is returned.
if (phoneFound) credits += 5
return credits
}),
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Wiza API key',
},
enrichment_level: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Enrichment depth: none, partial, phone, or full',
},
profile_url: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'LinkedIn profile URL (e.g., https://linkedin.com/in/johndoe)',
},
full_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Full name (used with company or domain)',
},
company: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Company name (used with full_name)',
},
domain: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Company domain (used with full_name)',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Email address (use alone or with other identifiers)',
},
accept_work: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to accept work emails (email_options)',
},
accept_personal: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to accept personal emails (email_options)',
},
},
request: {
url: 'https://wiza.co/api/individual_reveals',
method: 'POST',
headers: (params: WizaIndividualRevealParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: WizaIndividualRevealParams) => {
const individual: Record<string, unknown> = {}
if (params.profile_url) individual.profile_url = params.profile_url
if (params.full_name) individual.full_name = params.full_name
if (params.company) individual.company = params.company
if (params.domain) individual.domain = params.domain
if (params.email) individual.email = params.email
const body: Record<string, unknown> = {
individual_reveal: individual,
enrichment_level: params.enrichment_level,
}
if (params.accept_work !== undefined || params.accept_personal !== undefined) {
const emailOptions: Record<string, unknown> = {}
if (params.accept_work !== undefined) emailOptions.accept_work = params.accept_work
if (params.accept_personal !== undefined) {
emailOptions.accept_personal = params.accept_personal
}
body.email_options = emailOptions
}
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Wiza API error: ${response.status} - ${errorText}`)
}
const json = await response.json()
return {
success: true,
output: mapRevealData(json.data ?? {}),
}
},
postProcess: async (result, params) => {
if (!result.success) return result
// Wiza can resolve synchronously (e.g. a cache hit) — the initial POST payload is
// already mapped, so skip polling when it is terminal.
if (isTerminalReveal(result.output)) {
return { success: result.output.status !== 'failed', output: result.output }
}
const revealId = result.output.id
if (revealId == null) {
// Return an explicit failure rather than throwing: a thrown error here is swallowed
// by the executor and masked as the queued (incomplete) success result.
return {
success: false,
error: 'Wiza individual reveal did not return an id',
output: result.output,
}
}
let elapsedTime = 0
let consecutiveErrors = 0
while (elapsedTime < MAX_POLL_TIME_MS) {
await sleep(POLL_INTERVAL_MS)
elapsedTime += POLL_INTERVAL_MS
const statusResponse = await fetch(
`https://wiza.co/api/individual_reveals/${encodeURIComponent(String(revealId))}`,
{
headers: {
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
},
}
)
if (!statusResponse.ok) {
// The reveal is already started (and billed by Wiza), so tolerate brief outages and
// retry rather than aborting the whole window on a single transient 5xx/429.
consecutiveErrors += 1
if (consecutiveErrors >= MAX_CONSECUTIVE_POLL_ERRORS) {
const errorText = await statusResponse.text().catch(() => '')
return {
success: false,
error: `Wiza API error: ${statusResponse.status} - ${errorText}`,
output: result.output,
}
}
continue
}
consecutiveErrors = 0
const json = await statusResponse.json()
const data = json.data ?? {}
if (isTerminalReveal(data)) {
return {
success: data.status !== 'failed',
output: mapRevealData(data),
}
}
}
return {
success: false,
error: 'Wiza individual reveal did not complete within the polling window',
output: result.output,
}
},
outputs: {
id: { type: 'number', description: 'Reveal ID' },
status: { type: 'string', description: 'queued | resolving | finished | failed' },
is_complete: { type: 'boolean', description: 'Whether the reveal has completed' },
name: { type: 'string', description: 'Full name', optional: true },
company: { type: 'string', description: 'Company name', optional: true },
enrichment_level: { type: 'string', description: 'Enrichment level used', optional: true },
linkedin_profile_url: { type: 'string', description: 'LinkedIn URL', optional: true },
title: { type: 'string', description: 'Job title', optional: true },
location: { type: 'string', description: 'Location', optional: true },
email: { type: 'string', description: 'Primary email', optional: true },
email_type: { type: 'string', description: 'Email type', optional: true },
email_status: { type: 'string', description: 'valid | risky | unfound', optional: true },
emails: {
type: 'array',
description: 'All emails found',
optional: true,
items: {
type: 'object',
properties: {
email: { type: 'string' },
email_type: { type: 'string' },
email_status: { type: 'string' },
},
},
},
mobile_phone: { type: 'string', description: 'Mobile phone', optional: true },
phone_number: { type: 'string', description: 'Direct/office phone', optional: true },
phone_status: { type: 'string', description: 'found | unfound', optional: true },
phones: {
type: 'array',
description: 'All phones found',
optional: true,
items: {
type: 'object',
properties: {
number: { type: 'string' },
pretty_number: { type: 'string' },
type: { type: 'string' },
},
},
},
company_size: { type: 'number', description: 'Employee count', optional: true },
company_size_range: { type: 'string', description: 'Headcount range', optional: true },
company_type: { type: 'string', description: 'Company type', optional: true },
company_domain: { type: 'string', description: 'Company domain', optional: true },
company_locality: { type: 'string', description: 'City', optional: true },
company_region: { type: 'string', description: 'State/region', optional: true },
company_country: { type: 'string', description: 'Country', optional: true },
company_street: { type: 'string', description: 'Street', optional: true },
company_postal_code: { type: 'string', description: 'Postal code', optional: true },
company_founded: { type: 'number', description: 'Year founded', optional: true },
company_funding: { type: 'string', description: 'Funding total', optional: true },
company_revenue: { type: 'string', description: 'Revenue', optional: true },
company_industry: { type: 'string', description: 'Industry', optional: true },
company_subindustry: { type: 'string', description: 'Subindustry', optional: true },
company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true },
company_location: { type: 'string', description: 'Full company location', optional: true },
company_description: { type: 'string', description: 'Company description', optional: true },
credits: { type: 'json', description: 'Credits consumed by the reveal', optional: true },
},
}