diff --git a/src/services/predict-leads-client.ts b/src/services/predict-leads-client.ts index 5cca84a..33b52b0 100644 --- a/src/services/predict-leads-client.ts +++ b/src/services/predict-leads-client.ts @@ -1,8 +1,10 @@ import defaultKy, { type KyInstance } from 'ky' import pThrottle from 'p-throttle' +import { z } from 'zod' import type { DeepNullable } from '../types.js' -import { assert, getEnv, throttleKy } from '../utils.js' +import { aiFunction, AIFunctionsProvider } from '../fns.js' +import { assert, getEnv, pruneUndefined, throttleKy } from '../utils.js' export namespace predictleads { export const throttle = pThrottle({ @@ -11,6 +13,9 @@ export namespace predictleads { strict: true }) + export const DEFAULT_PAGE_SIZE = 100 + export const MAX_PAGE_SIZE = 1000 + export type Meta = DeepNullable<{ count: number message?: string | null @@ -182,13 +187,284 @@ export namespace predictleads { }> export type JobOpeningByIdResponse = Omit + + export const EventCategorySchema = z.union([ + z + .literal('hires') + .describe( + 'Company hired new executive or senior personnel. (leadership)' + ), + z + .literal('promotes') + .describe( + 'Company promoted existing executive or senior personnel. (leadership)' + ), + z + .literal('leaves') + .describe('Executive or senior personnel left the company. (leadership)'), + z + .literal('retires') + .describe( + 'Executive or senior personnel retires from the company. (leadership)' + ), + z + .literal('acquires') + .describe('Company acquired other company. (acquisition)'), + z + .literal('merges_with') + .describe('Company merges with other company. (acquisition)'), + z + .literal('sells_assets_to') + .describe( + 'Company sells assets (like properties or warehouses) to other company. (acquisition)' + ), + z + .literal('expands_offices_to') + .describe( + 'Company opens new offices in another town, state, country or continent. (expansion)' + ), + z + .literal('expands_offices_in') + .describe('Company expands existing offices. (expansion)'), + z + .literal('expands_facilities') + .describe( + 'Company opens new or expands existing facilities like warehouses, data centers, manufacturing plants etc. (expansion)' + ), + z + .literal('opens_new_location') + .describe( + 'Company opens new service location like hotels, restaurants, bars, hospitals etc. (expansion)' + ), + z + .literal('increases_headcount_by') + .describe('Company offers new job vacancies. (expansion)'), + z + .literal('launches') + .describe('Company launches new offering. (new_offering)'), + z + .literal('integrates_with') + .describe('Company integrates with other company. (new_offering)'), + z + .literal('is_developing') + .describe('Company begins development of a new offering. (new_offering)'), + z + .literal('receives_financing') + .describe( + 'Company receives investment like venture funding, loan, grant etc. (investment)' + ), + z + .literal('invests_into') + .describe('Company invests into other company. (investment)'), + z + .literal('invests_into_assets') + .describe( + 'Company invests into assets like property, trucks, facilities etc. (investment)' + ), + z + .literal('goes_public') + .describe( + 'Company issues shares to the public for the first time. (investment)' + ), + z + .literal('closes_offices_in') + .describe('Company closes existing offices. (cost_cutting)'), + z + .literal('decreases_headcount_by') + .describe('Company lays off employees. (cost_cutting)'), + z + .literal('partners_with') + .describe('Company partners with other company. (partnership)'), + z + .literal('receives_award') + .describe( + 'Company or person at the company receives an award. (recognition)' + ), + z + .literal('recognized_as') + .describe( + 'Company or person at the company receives recognition. (recognition)' + ), + z + .literal('signs_new_client') + .describe('Company signs new client. (contract)'), + z + .literal('files_suit_against') + .describe( + 'Company files suit against other company. (corporate_challenges)' + ), + z + .literal('has_issues_with') + .describe('Company has vulnerability problems. (corporate_challenges)'), + z + .literal('identified_as_competitor_of') + .describe('New or existing competitor was identified. (relational)') + ]) + export type EventCategory = z.infer + + export const CompanyParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company') + }) + export type CompanyParams = z.infer + + export const CompanyEventsParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company'), + categories: z.array(EventCategorySchema).optional(), + found_at_from: z + .string() + .optional() + .describe('Signals found from specified date (ISO 8601).'), + found_at_until: z + .string() + .optional() + .describe('Signals found until specified date (ISO 8601).'), + page: z.number().int().positive().default(1).optional(), + limit: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .default(DEFAULT_PAGE_SIZE) + .optional(), + with_news_article_bodies: z + .boolean() + .optional() + .describe('Whether or not to include the body contents of news articles.') + }) + export type CompanyEventsParams = z.infer + + export const CompanyFinancingEventsParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company') + }) + export type CompanyFinancingEventsParams = z.infer< + typeof CompanyFinancingEventsParamsSchema + > + + export const CompanyJobOpeningsParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company'), + categories: z.array(EventCategorySchema).optional(), + found_at_from: z + .string() + .optional() + .describe('Signals found from specified date (ISO 8601).'), + found_at_until: z + .string() + .optional() + .describe('Signals found until specified date (ISO 8601).'), + limit: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .default(DEFAULT_PAGE_SIZE) + .optional(), + with_job_descriptions: z + .boolean() + .optional() + .describe('Whether or not to include the full descriptions of the jobs.'), + with_description_only: z + .boolean() + .optional() + .describe('If set, only returns job openings with descriptions.'), + with_location_only: z + .boolean() + .optional() + .describe('If set, only returns job openings with locations.'), + active_only: z + .boolean() + .optional() + .describe( + 'If set, only returns job openings that are not closed, have `last_seen_at` more recent than 5 days and were found in the last year.' + ), + not_closed: z + .boolean() + .optional() + .describe( + 'Similar to `active_only`, but without considering `last_seen_at` timestamp.' + ) + }) + export type CompanyJobOpeningsParams = z.infer< + typeof CompanyJobOpeningsParamsSchema + > + + export const CompanyTechnologiesParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company'), + categories: z.array(EventCategorySchema).optional(), + limit: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .default(DEFAULT_PAGE_SIZE) + .optional() + }) + export type CompanyTechnologiesParams = z.infer< + typeof CompanyTechnologiesParamsSchema + > + + export const CompanyConnectionsParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company'), + categories: z.array(EventCategorySchema).optional(), + limit: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .default(DEFAULT_PAGE_SIZE) + .optional() + }) + export type CompanyConnectionsParams = z.infer< + typeof CompanyConnectionsParamsSchema + > + + export const CompanyWebsiteEvolutionParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company'), + limit: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .default(DEFAULT_PAGE_SIZE) + .optional() + }) + export type CompanyWebsiteEvolutionParams = z.infer< + typeof CompanyWebsiteEvolutionParamsSchema + > + + export const CompanyGitHubReposParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company'), + limit: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .default(DEFAULT_PAGE_SIZE) + .optional() + }) + export type CompanyGitHubReposParams = z.infer< + typeof CompanyGitHubReposParamsSchema + > + + export const CompanyProductsParamsSchema = z.object({ + domain: z.string().min(3).describe('domain of the company'), + sources: z.array(z.string()).optional(), + limit: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .default(DEFAULT_PAGE_SIZE) + .optional() + }) + export type CompanyProductsParams = z.infer< + typeof CompanyProductsParamsSchema + > } -export class PredictLeadsClient { +export class PredictLeadsClient extends AIFunctionsProvider { readonly ky: KyInstance readonly apiKey: string readonly apiToken: string - readonly _maxPageSize = 100 constructor({ apiKey = getEnv('PREDICT_LEADS_API_KEY'), @@ -204,8 +480,15 @@ export class PredictLeadsClient { throttle?: boolean ky?: KyInstance } = {}) { - assert(apiKey, 'PredictLeadsClient missing required "apiKey"') - assert(apiToken, 'PredictLeadsClient missing required "apiToken"') + assert( + apiKey, + 'PredictLeadsClient missing required "apiKey" (defaults to "PREDICT_LEADS_API_KEY")' + ) + assert( + apiToken, + 'PredictLeadsClient missing required "apiToken" (defaults to "PREDICT_LEADS_API_TOKEN")' + ) + super() this.apiKey = apiKey this.apiToken = apiToken @@ -213,264 +496,303 @@ export class PredictLeadsClient { const throttledKy = throttle ? throttleKy(ky, predictleads.throttle) : ky this.ky = throttledKy.extend({ + prefixUrl: 'https://predictleads.com/api', timeout: timeoutMs, headers: { - 'X-Api-Key': apiKey, - 'X-Api-Token': apiToken + 'x-api-key': apiKey, + 'x-api-token': apiToken } }) } + @aiFunction({ + name: 'get_company', + description: + 'Returns basic information about a company given its `domain` like location, name, stock ticker, description, etc.', + inputSchema: predictleads.CompanyParamsSchema + }) + async company(domainOrOpts: string | predictleads.CompanyParams) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { domain } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky.get(`v2/companies/${domain}`).json() + } + + @aiFunction({ + name: 'get_company_events', + description: + 'Returns a list of events from news for a given company. Events are found in press releases, industry news, blogs, social media, and other online sources.', + inputSchema: predictleads.CompanyEventsParamsSchema + }) + async getCompanyEvents( + domainOrOpts: string | predictleads.CompanyEventsParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { + domain, + page = 1, + limit = predictleads.DEFAULT_PAGE_SIZE, + categories, + ...params + } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/events`, { + searchParams: pruneUndefined({ + page, + limit: String(limit), + categories: categories?.join(','), + ...params + }) + }) + .json() + } + + async getEventById(id: string) { + return this.ky.get(`v2/events/${id}`).json() + } + + @aiFunction({ + name: 'get_company_financing_events', + description: + 'Returns a list of financing events for a given company. Financing events include fundraising announcements and quarterly earning reports for public companies. They are sourced from press releases, industry news, blogs, social media, and other online sources.', + inputSchema: predictleads.CompanyFinancingEventsParamsSchema + }) + async getCompanyFinancingEvents( + domainOrOpts: string | predictleads.CompanyFinancingEventsParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { domain } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/financing_events`) + .json() + } + + @aiFunction({ + name: 'get_company_job_openings', + description: + 'Returns a list of job openings for a given company. Job openings are found on companies’ career sites and job boards.', + inputSchema: predictleads.CompanyJobOpeningsParamsSchema + }) + async getCompanyJobOpenings( + domainOrOpts: string | predictleads.CompanyJobOpeningsParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { + domain, + limit = predictleads.DEFAULT_PAGE_SIZE, + categories, + ...params + } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/job_openings`, { + searchParams: pruneUndefined({ + limit: String(limit), + categories: categories?.join(','), + ...params + }) + }) + .json() + } + + async getJobOpeningById(id: string) { + return this.ky + .get(`v2/job_openings/${id}`) + .json() + } + + @aiFunction({ + name: 'get_company_technologies', + description: 'Returns a list of technology providers for a given company.', + inputSchema: predictleads.CompanyTechnologiesParamsSchema + }) + async getCompanyTechnologies( + domainOrOpts: string | predictleads.CompanyTechnologiesParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { + domain, + limit = predictleads.DEFAULT_PAGE_SIZE, + categories, + ...params + } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/technologies`, { + searchParams: pruneUndefined({ + limit: String(limit), + categories: categories?.join(','), + ...params + }) + }) + .json() + } + + @aiFunction({ + name: 'get_company_connections', + description: + 'Returns a list of categorized business connections. Business connections can be found via backlinks or logos on /our-customers, /case-studies, /portfolio, /clients etc. pages. Business connections enable you to eg. calculate network health of a company, to build systems when new high value connections are made… Connections can be of many types: partner, vendor, investor, parent…', + inputSchema: predictleads.CompanyConnectionsParamsSchema + }) + async getCompanyConnections( + domainOrOpts: string | predictleads.CompanyConnectionsParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { + domain, + limit = predictleads.DEFAULT_PAGE_SIZE, + categories, + ...params + } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/connections`, { + searchParams: pruneUndefined({ + limit: String(limit), + categories: categories?.join(','), + ...params + }) + }) + .json() + } + + @aiFunction({ + name: 'get_company_website_evolution', + description: + 'Returns insights into how a website has changed over time. E.g., when pages like “Blog”, “Privacy policy”, “Pricing”, “Product”, “API Docs”, “Team”, “Support pages” etc were added. This can serve as a proxy to how quickly a website is growing, to determine the growth stage they are at and also to help segment websites.', + inputSchema: predictleads.CompanyWebsiteEvolutionParamsSchema + }) + async getCompanyWebsiteEvolution( + domainOrOpts: string | predictleads.CompanyWebsiteEvolutionParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { domain, limit = predictleads.DEFAULT_PAGE_SIZE, ...params } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/website_evolution`, { + searchParams: pruneUndefined({ limit: String(limit), ...params }) + }) + .json() + } + + @aiFunction({ + name: 'get_company_github_repos', + description: + 'Returns insights into how frequently a company is contributing to its public GitHub repositories.', + inputSchema: predictleads.CompanyGitHubReposParamsSchema + }) + async getCompanyGitHubRepositories( + domainOrOpts: string | predictleads.CompanyGitHubReposParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { domain, limit = predictleads.DEFAULT_PAGE_SIZE, ...params } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/github_repositories`, { + searchParams: pruneUndefined({ limit: String(limit), ...params }) + }) + .json() + } + + @aiFunction({ + name: 'get_company_products', + description: + 'Returns what kind of products / solutions / features a company is offering.', + inputSchema: predictleads.CompanyProductsParamsSchema + }) + async getCompanyProducts( + domainOrOpts: string | predictleads.CompanyProductsParams + ) { + const opts = + typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts + const { + domain, + sources, + limit = predictleads.DEFAULT_PAGE_SIZE, + ...params + } = opts + assert(domain, 'Missing required company "domain"') + + return this.ky + .get(`v2/companies/${domain}/products`, { + searchParams: pruneUndefined({ + limit: String(limit), + sources: sources?.join(','), + ...params + }) + }) + .json() + } + + async discoverStartupJobsHN(params?: { + post_datetime_from?: string + post_datetime_until?: string + min_score?: string + limit?: string + }) { + return this.ky + .get(`v2/discover/startup_platform/jobs_hn`, { + searchParams: params + }) + .json() + } + + async discoverStartupShowHN(params?: { + post_datetime_from?: string + post_datetime_until?: string + min_score?: string + limit?: string + }) { + return this.ky + .get(`v2/discover/startup_platform/show_hn`, { + searchParams: params + }) + .json() + } + + // -------------------------------------------------------------------------- + // Stateful endpoints which should generally not be used as AI functions. + // -------------------------------------------------------------------------- + async followCompany(domain: string, customCompanyIdentifier?: string) { return this.ky - .post( - `https://predictleads.com/api/v2/companies/${domain}/follow`, - customCompanyIdentifier - ? { - json: { customCompanyIdentifier } - } - : undefined - ) + .post(`v2/companies/${domain}/follow`, { + json: pruneUndefined({ customCompanyIdentifier }) + }) .json() } - async getFollowingCompanies(limit: number = this._maxPageSize) { + async getFollowingCompanies(limit: number = predictleads.DEFAULT_PAGE_SIZE) { return this.ky - .get(`https://predictleads.com/api/v2/followings`, { - searchParams: { limit } + .get(`v2/followings`, { + searchParams: { limit: String(limit) } }) .json() } async unfollowCompany(domain: string, customCompanyIdentifier?: string) { return this.ky - .post( - `https://predictleads.com/api/v2/companies/${domain}/unfollow`, - customCompanyIdentifier - ? { - json: { customCompanyIdentifier } - } - : undefined - ) + .post(`v2/companies/${domain}/unfollow`, { + json: pruneUndefined({ customCompanyIdentifier }) + }) .json() } - - async events( - domain: string, - params: { - categories?: string - found_at_from?: string - found_at_until?: string - page?: number - limit?: string - with_news_article_bodies?: boolean - } = {} - ) { - return this.ky - .get(`https://predictleads.com/api/v2/companies/${domain}/events`, { - searchParams: { page: 1, ...params } - }) - .json() - } - - async eventById(id: string) { - return this.ky - .get(`https://predictleads.com/api/v2/events/${id}`) - .json() - } - - async financingEvents(domain: string) { - return this.ky - .get( - `https://predictleads.com/api/v2/companies/${domain}/financing_events` - ) - .json() - } - - async jobOpenings( - domain: string, - params: { - categories?: string - with_job_descriptions?: boolean - active_only?: boolean - not_closed?: boolean - limit?: string - } = {} - ) { - return this.ky - .get(`https://predictleads.com/api/v2/companies/${domain}/job_openings`, { - searchParams: params - }) - .json() - } - - async jobOpeningById(id: string) { - return this.ky - .get(`https://predictleads.com/api/v2/job_openings/${id}`) - .json() - } - - async technologies( - domain: string, - params: { - categories: string - limit?: string - } - ) { - return this.ky - .get(`https://predictleads.com/api/v2/companies/${domain}/technologies`, { - searchParams: params - }) - .json() - } - - async connections( - domain: string, - params?: { - categories: string - limit?: string - } - ) { - return this.ky - .get(`https://predictleads.com/api/v2/companies/${domain}/connections`, { - searchParams: params - }) - .json() - } - - async websiteEvolution( - domain: string, - { limit = 100 }: { limit?: number } = {} - ) { - return this.ky - .get( - `https://predictleads.com/api/v2/companies/${domain}/website_evolution`, - { - searchParams: { limit } - } - ) - .json() - } - - async githubRepositories( - domain: string, - { limit = 100 }: { limit?: number } = {} - ) { - return this.ky - .get( - `https://predictleads.com/api/v2/companies/${domain}/github_repositories`, - { - searchParams: { limit } - } - ) - .json() - } - - async products( - domain: string, - params?: { - sources: string - limit?: number - } - ) { - return this.ky - .get(`https://predictleads.com/api/v2/companies/${domain}/products`, { - searchParams: params - }) - .json() - } - - async company(domain: string) { - return this.ky - .get(`https://predictleads.com/api/v2/companies/${domain}`) - .json() - } - - async discoverStartupJobs(params?: { - post_datetime_from?: string - post_datetime_until?: string - min_score?: string - limit?: string - }) { - return this.ky - .get( - `https://predictleads.com/api/v2/discover/startup_platform/jobs_hn`, - { - searchParams: params - } - ) - .json() - } - - async discoverStartupShow(params?: { - post_datetime_from?: string - post_datetime_until?: string - min_score?: string - limit?: string - }) { - return this.ky - .get( - `https://predictleads.com/api/v2/discover/startup_platform/show_hn`, - { - searchParams: params - } - ) - .json() - } - - /* - TODO this returns 500 error, even using the curl example from docs. - Also for this reason I couldn't test the other segments endpoints - curl -X POST "https://predictleads.com/api/v2/segments" - -d '{"technologies":"Salesforce", "job_categories":"sales"}' - -H "Content-Type: application/json" \ - -H 'X-Api-Key: ' \ - -H 'X-Api-Token: ' - */ - async createSegment(params: { - webhook_url?: string - locations?: string - headquarters_locations?: string - job_categories?: string - technologies?: string - found_at_from?: string - found_at_until?: string - active?: string - limit?: string - }) { - return this.ky - .post(`https://predictleads.com/api/v2/segments`, { - json: params - }) - .json() - } - - async updateSegment(params: { - id: string - webhook_url: string - active: string - }) { - return this.ky - .put( - `https://predictleads.com/api/v2/discover/startup_platform/show_hn`, - { - json: params - } - ) - .json() - } - - async showSegment(id: string) { - return this.ky - .get(`https://predictleads.com/api/v2/segments/${id}`) - .json() - } - - async showAllSegment(limit = 100) { - return this.ky - .get(`https://predictleads.com/api/v2/segments`, { - searchParams: { limit } - }) - .json() - } } diff --git a/src/types.ts b/src/types.ts index 653b16b..4a27a1c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,8 +7,11 @@ import type { AIFunctionsProvider } from './fns.js' export type { KyInstance } from 'ky' export type { ThrottledFunction } from 'p-throttle' -// TODO -export type DeepNullable = T | null +export type Nullable = T | null + +export type DeepNullable = T extends object + ? { [K in keyof T]: DeepNullable } + : Nullable export type MaybePromise = T | Promise