Skip to content

Commit

Permalink
feat: add hunter.io client
Browse files Browse the repository at this point in the history
  • Loading branch information
transitive-bullshit committed Jun 30, 2024
1 parent db6acfe commit e3cad9d
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 16 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
18 changes: 15 additions & 3 deletions bin/scratch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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: '[email protected]'
// })
const res = await hunter.emailFinder({
domain: 'aomni.com',
first_name: 'David',
last_name: 'Zhang'
})
console.log(JSON.stringify(res, null, 2))
}

Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down Expand Up @@ -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)
Expand Down
322 changes: 322 additions & 0 deletions src/services/hunter-client.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading

0 comments on commit e3cad9d

Please sign in to comment.