kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add hunter.io client
rodzic
db6acfe3da
commit
e3cad9dc13
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
35
src/utils.ts
35
src/utils.ts
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Ładowanie…
Reference in New Issue