feat: add hunter.io client

pull/659/head
Travis Fischer 2024-06-30 00:31:07 -07:00
rodzic db6acfe3da
commit e3cad9dc13
6 zmienionych plików z 365 dodań i 14 usunięć

Wyświetl plik

@ -3,6 +3,7 @@
"extends": ["@fisch0920/eslint-config/node"],
"rules": {
"unicorn/no-static-only-class": "off",
"unicorn/no-array-reduce": "off",
"@typescript-eslint/naming-convention": "off"
}
}

Wyświetl plik

@ -19,7 +19,8 @@ import restoreCursor from 'restore-cursor'
// import { MidjourneyClient } from '../src/index.js'
// import { BingClient } from '../src/index.js'
// import { TavilyClient } from '../src/index.js'
import { SocialDataClient } from '../src/index.js'
// import { SocialDataClient } from '../src/index.js'
import { HunterClient } from '../src/index.js'
/**
* Scratch pad for testing.
@ -120,8 +121,19 @@ async function main() {
// })
// console.log(JSON.stringify(res, null, 2))
const socialData = new SocialDataClient()
const res = await socialData.getUserByUsername('transitive_bs')
// const socialData = new SocialDataClient()
// const res = await socialData.getUserByUsername('transitive_bs')
// console.log(JSON.stringify(res, null, 2))
const hunter = new HunterClient()
// const res = await hunter.emailVerifier({
// email: 'travis@transitivebullsh.it'
// })
const res = await hunter.emailFinder({
domain: 'aomni.com',
first_name: 'David',
last_name: 'Zhang'
})
console.log(JSON.stringify(res, null, 2))
}

Wyświetl plik

@ -138,6 +138,7 @@ Depending on the AI SDK and tool you want to use, you'll also need to install th
| [E2B](https://e2b.dev) | `e2b` | Hosted Python code intrepreter sandbox which is really useful for data analysis, flexible code execution, and advanced reasoning on-the-fly. |
| [Exa](https://docs.exa.ai) | `ExaClient` | Web search tailored for LLMs. |
| [Firecrawl](https://www.firecrawl.dev) | `FirecrawlClient` | Website scraping and sanitization. |
| [Hunter](https://hunter.io) | `HunterClient` | Email finder, verifier, and enrichment. |
| [Midjourney](https://www.imagineapi.dev) | `MidjourneyClient` | Unofficial Midjourney client for generative images. |
| [Novu](https://novu.co) | `NovuClient` | Sending notifications (email, SMS, in-app, push, etc). |
| [People Data Labs](https://www.peopledatalabs.com) | `PeopleDataLabsClient` | People & company data (WIP). |
@ -204,6 +205,7 @@ See the [examples](./examples) directory for examples of how to use each of thes
- replicate
- huggingface
- [skyvern](https://github.com/Skyvern-AI/skyvern)
- pull from [clay](https://www.clay.com/integrations)
- pull from [langchain](https://github.com/langchain-ai/langchainjs/tree/main/langchain)
- provide a converter for langchain `DynamicStructuredTool`
- pull from [nango](https://docs.nango.dev/integrations/overview)

Wyświetl plik

@ -0,0 +1,322 @@
import defaultKy, { type KyInstance } from 'ky'
import { z } from 'zod'
import { aiFunction, AIFunctionsProvider } from '../fns.js'
import {
assert,
getEnv,
pruneNullOrUndefinedDeep,
sanitizeSearchParams
} from '../utils.js'
export namespace hunter {
export const API_BASE_URL = 'https://api.hunter.io'
export const DepartmentSchema = z.enum([
'executive',
'it',
'finance',
'management',
'sales',
'legal',
'support',
'hr',
'marketing',
'communication',
'education',
'design',
'health',
'operations'
])
export type Department = z.infer<typeof DepartmentSchema>
export const SenioritySchema = z.enum(['junior', 'senior', 'executive'])
export type Seniority = z.infer<typeof SenioritySchema>
export const PersonFieldSchema = z.enum([
'full_name',
'position',
'phone_number'
])
export type PersonField = z.infer<typeof PersonFieldSchema>
export const DomainSearchOptionsSchema = z.object({
domain: z.string().optional().describe('domain to search for'),
company: z.string().optional().describe('company name to search for'),
limit: z.number().int().positive().optional(),
offset: z.number().int().nonnegative().optional(),
type: z.enum(['personal', 'generic']).optional(),
seniority: z.union([SenioritySchema, z.array(SenioritySchema)]).optional(),
department: z
.union([DepartmentSchema, z.array(DepartmentSchema)])
.optional(),
required_field: z
.union([PersonFieldSchema, z.array(PersonFieldSchema)])
.optional()
})
export type DomainSearchOptions = z.infer<typeof DomainSearchOptionsSchema>
export const EmailFinderOptionsSchema = z.object({
domain: z.string().optional().describe('domain to search for'),
company: z.string().optional().describe('company name to search for'),
first_name: z.string().describe("person's first name"),
last_name: z.string().describe("person's last name"),
max_duration: z.number().int().positive().min(3).max(20).optional()
})
export type EmailFinderOptions = z.infer<typeof EmailFinderOptionsSchema>
export const EmailVerifierOptionsSchema = z.object({
email: z.string().describe('email address to verify')
})
export type EmailVerifierOptions = z.infer<typeof EmailVerifierOptionsSchema>
export interface DomainSearchResponse {
data: DomainSearchData
meta: {
results: number
limit: number
offset: number
params: {
domain?: string
company?: string
type?: string
seniority?: string
department?: string
}
}
}
export interface DomainSearchData {
domain: string
disposable: boolean
webmail?: boolean
accept_all?: boolean
pattern?: string
organization?: string
description?: string
industry?: string
twitter?: string
facebook?: string
linkedin?: string
instagram?: string
youtube?: string
technologies?: string[]
country?: string
state?: string
city?: string
postal_code?: string
street?: string
headcount?: string
company_type?: string
emails?: Email[]
linked_domains?: string[]
}
export interface Email {
value: string
type: string
confidence: number
first_name?: string
last_name?: string
position?: string
seniority?: string
department?: string
linkedin?: string
twitter?: string
phone_number?: string
verification?: Verification
sources?: Source[]
}
export interface Source {
domain: string
uri: string
extracted_on: string
last_seen_on: string
still_on_page?: boolean
}
export interface Verification {
date: string
status: string
}
export interface EmailFinderResponse {
data: EmailFinderData
meta: {
params: {
first_name?: string
last_name?: string
full_name?: string
domain?: string
company?: string
max_duration?: string
}
}
}
export interface EmailFinderData {
first_name: string
last_name: string
email: string
score: number
domain: string
accept_all: boolean
position?: string
twitter?: any
linkedin_url?: any
phone_number?: any
company?: string
sources?: Source[]
verification?: Verification
}
export interface EmailVerifierResponse {
data: EmailVerifierData
meta: {
params: {
email: string
}
}
}
export interface EmailVerifierData {
status:
| 'valid'
| 'invalid'
| 'accept_all'
| 'webmail'
| 'disposable'
| 'unknown'
result: 'deliverable' | 'undeliverable' | 'risky'
score: number
email: string
regexp: boolean
gibberish: boolean
disposable: boolean
webmail: boolean
mx_records: boolean
smtp_server: boolean
smtp_check: boolean
accept_all: boolean
block: boolean
sources?: Source[]
_deprecation_notice?: string
}
}
/**
* Lightweight wrapper around Hunter.io email finder, verifier, and enrichment
* APIs.
*
* @see https://hunter.io/api-documentation
*/
export class HunterClient extends AIFunctionsProvider {
protected readonly ky: KyInstance
protected readonly apiKey: string
protected readonly apiBaseUrl: string
constructor({
apiKey = getEnv('HUNTER_API_KEY'),
apiBaseUrl = hunter.API_BASE_URL,
ky = defaultKy
}: {
apiKey?: string
apiBaseUrl?: string
ky?: KyInstance
} = {}) {
assert(
apiKey,
'HunterClient missing required "apiKey" (defaults to "HUNTER_API_KEY")'
)
super()
this.apiKey = apiKey
this.apiBaseUrl = apiBaseUrl
this.ky = ky.extend({
prefixUrl: this.apiBaseUrl
})
}
@aiFunction({
name: 'hunter_domain_search',
description:
'Gets all the email addresses associated with a given company or domain.',
inputSchema: hunter.DomainSearchOptionsSchema.pick({
domain: true,
company: true
})
})
async domainSearch(domainOrOpts: string | hunter.DomainSearchOptions) {
const opts =
typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts
if (!opts.domain && !opts.company) {
throw new Error('Either "domain" or "company" is required')
}
const res = await this.ky
.get('v2/domain-search', {
searchParams: sanitizeSearchParams(
{
...opts,
api_key: this.apiKey
},
{ csv: true }
)
})
.json<hunter.DomainSearchResponse>()
return pruneNullOrUndefinedDeep(res)
}
@aiFunction({
name: 'hunter_email_finder',
description:
'Finds the most likely email address from a domain name, a first name and a last name.',
inputSchema: hunter.EmailFinderOptionsSchema.pick({
domain: true,
company: true,
first_name: true,
last_name: true
})
})
async emailFinder(opts: hunter.EmailFinderOptions) {
if (!opts.domain && !opts.company) {
throw new Error('Either "domain" or "company" is required')
}
const res = await this.ky
.get('v2/email-finder', {
searchParams: sanitizeSearchParams({
...opts,
api_key: this.apiKey
})
})
.json<hunter.EmailFinderResponse>()
return pruneNullOrUndefinedDeep(res)
}
@aiFunction({
name: 'hunter_email_verifier',
description: 'Verifies the deliverability of an email address.',
inputSchema: hunter.EmailVerifierOptionsSchema
})
async emailVerifier(emailOrOpts: string | hunter.EmailVerifierOptions) {
const opts =
typeof emailOrOpts === 'string' ? { email: emailOrOpts } : emailOrOpts
const res = await this.ky
.get('v2/email-verifier', {
searchParams: sanitizeSearchParams({
...opts,
api_key: this.apiKey
})
})
.json<hunter.EmailVerifierResponse>()
return pruneNullOrUndefinedDeep(res)
}
}

Wyświetl plik

@ -4,6 +4,7 @@ export * from './dexa-client.js'
export * from './diffbot-client.js'
export * from './exa-client.js'
export * from './firecrawl-client.js'
export * from './hunter-client.js'
export * from './midjourney-client.js'
export * from './novu-client.js'
export * from './people-data-labs-client.js'

Wyświetl plik

@ -134,21 +134,34 @@ export function sanitizeSearchParams(
string,
string | number | boolean | string[] | number[] | boolean[] | undefined
>
| object
| object,
{ csv = false }: { csv?: boolean } = {}
): URLSearchParams {
return new URLSearchParams(
Object.entries(searchParams).flatMap(([key, value]) => {
if (key === undefined || value === undefined) {
return []
}
const entries = Object.entries(searchParams).flatMap(([key, value]) => {
if (key === undefined || value === undefined) {
return []
}
if (Array.isArray(value)) {
return value.map((v) => [key, String(v)])
}
if (Array.isArray(value)) {
return value.map((v) => [key, String(v)])
}
return [[key, String(value)]]
}) as [string, string][]
return [[key, String(value)]]
}) as [string, string][]
if (!csv) {
return new URLSearchParams(entries)
}
const csvEntries = entries.reduce(
(acc, [key, value]) => ({
...acc,
[key]: acc[key] ? `${acc[key]},${value}` : value
}),
{} as any
)
return new URLSearchParams(csvEntries)
}
/**