client.ts•14 kB
import Exa from "exa-js";
import { z } from "zod";
import { ExaError } from './types.js';
/**
* Client for interacting with the Exa AI API
*/
export class ExaClient {
private readonly apiKey: string;
private client: any;
constructor(apiKey: string) {
if (!apiKey) {
throw new Error('API key is required');
}
this.apiKey = apiKey;
this.client = new (Exa as any)(apiKey);
}
/**
* Perform a web search using Exa AI
*/
async search(params: {
query: string;
num_results?: number;
include_domains?: string[];
exclude_domains?: string[];
start_crawl_date?: string;
end_crawl_date?: string;
start_published_date?: string;
end_published_date?: string;
use_autoprompt?: boolean;
type?: 'keyword' | 'neural' | 'magic';
category?: string;
include_text?: boolean;
include_highlights?: boolean;
include_summary?: boolean;
}) {
try {
const searchParams: any = {
query: params.query,
numResults: params.num_results || 10,
useAutoprompt: params.use_autoprompt ?? true,
type: params.type || 'neural'
};
if (params.include_domains) searchParams.includeDomains = params.include_domains;
if (params.exclude_domains) searchParams.excludeDomains = params.exclude_domains;
if (params.start_crawl_date) searchParams.startCrawlDate = params.start_crawl_date;
if (params.end_crawl_date) searchParams.endCrawlDate = params.end_crawl_date;
if (params.start_published_date) searchParams.startPublishedDate = params.start_published_date;
if (params.end_published_date) searchParams.endPublishedDate = params.end_published_date;
if (params.category) searchParams.category = params.category;
const searchResponse = await this.client.searchAndContents(
searchParams.query,
{
...searchParams,
text: params.include_text ? { maxCharacters: 2000, includeHtmlTags: false } : undefined,
highlights: params.include_highlights ? { numSentences: 3, highlightsPerUrl: 3 } : undefined,
summary: params.include_summary ? { query: params.query } : undefined
}
);
return searchResponse.results;
} catch (error) {
throw new ExaError(
error instanceof Error ? error.message : 'Failed to search',
'SEARCH_ERROR'
);
}
}
/**
* Research companies and organizations
*/
async researchCompany(params: {
company_name: string;
include_news?: boolean;
include_overview?: boolean;
include_competitors?: boolean;
include_reviews?: boolean;
max_results_per_category?: number;
}) {
try {
const results: any = {};
const maxResults = params.max_results_per_category || 5;
if (params.include_overview !== false) {
const overviewSearch = await this.search({
query: `"${params.company_name}" company overview about products services`,
num_results: maxResults,
type: 'neural',
include_text: true,
include_summary: true
});
results.overview = overviewSearch;
}
if (params.include_news) {
const newsSearch = await this.search({
query: `"${params.company_name}" latest news announcements`,
num_results: maxResults,
type: 'neural',
start_published_date: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
include_text: true,
include_highlights: true
});
results.news = newsSearch;
}
if (params.include_competitors) {
const competitorSearch = await this.search({
query: `"${params.company_name}" competitors "competing with" OR "competes with" OR "alternative to"`,
num_results: maxResults,
type: 'neural',
include_text: true,
include_highlights: true
});
results.competitors = competitorSearch;
}
if (params.include_reviews) {
const reviewSearch = await this.search({
query: `"${params.company_name}" reviews customer feedback testimonials ratings`,
num_results: maxResults,
type: 'neural',
include_text: true,
include_highlights: true
});
results.reviews = reviewSearch;
}
return results;
} catch (error) {
throw new ExaError(
error instanceof Error ? error.message : 'Failed to research company',
'RESEARCH_ERROR'
);
}
}
/**
* Crawl and extract content from URLs
*/
async crawl(params: {
urls: string[];
include_text?: boolean;
include_highlights?: boolean;
include_summary?: boolean;
summary_query?: string;
}) {
try {
const contents = await this.client.getContents(
params.urls,
{
text: params.include_text ? { maxCharacters: 5000, includeHtmlTags: false } : undefined,
highlights: params.include_highlights ? { numSentences: 5, highlightsPerUrl: 5, query: params.summary_query || "key information" } : undefined,
summary: params.include_summary ? { query: params.summary_query || "main points and key information" } : undefined
}
);
return contents.results;
} catch (error) {
throw new ExaError(
error instanceof Error ? error.message : 'Failed to crawl URLs',
'CRAWL_ERROR'
);
}
}
/**
* Search LinkedIn profiles and companies
*/
async searchLinkedIn(params: {
query: string;
search_type?: 'people' | 'companies' | 'both';
num_results?: number;
include_bios?: boolean;
include_summaries?: boolean;
}) {
try {
const searchType = params.search_type || 'both';
const numResults = params.num_results || 10;
const results: any = {};
if (searchType === 'people' || searchType === 'both') {
const peopleSearch = await this.search({
query: `site:linkedin.com/in/ ${params.query}`,
num_results: numResults,
type: 'keyword',
include_text: params.include_bios,
include_summary: params.include_summaries
});
results.people = peopleSearch;
}
if (searchType === 'companies' || searchType === 'both') {
const companySearch = await this.search({
query: `site:linkedin.com/company/ ${params.query}`,
num_results: numResults,
type: 'keyword',
include_text: params.include_bios,
include_summary: params.include_summaries
});
results.companies = companySearch;
}
return results;
} catch (error) {
throw new ExaError(
error instanceof Error ? error.message : 'Failed to search LinkedIn',
'LINKEDIN_ERROR'
);
}
}
/**
* Start a deep research task (async)
*/
async startDeepResearch(params: {
topic: string;
research_type?: 'comprehensive' | 'news' | 'academic' | 'market';
max_results?: number;
time_range?: 'week' | 'month' | 'quarter' | 'year' | 'all';
}) {
const taskId = `research_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Store task metadata (in production, this would be in a database)
global.researchTasks = global.researchTasks || {};
global.researchTasks[taskId] = {
status: 'in_progress',
started_at: new Date().toISOString(),
params: params
};
// Simulate async research (in production, this would be a queue job)
this.performDeepResearch(taskId, params).catch(error => {
global.researchTasks[taskId].status = 'failed';
global.researchTasks[taskId].error = error.message;
});
return {
task_id: taskId,
status: 'started',
message: 'Deep research task initiated. Use deep_researcher_check to monitor progress.'
};
}
/**
* Check deep research task status
*/
async checkDeepResearch(taskId: string) {
global.researchTasks = global.researchTasks || {};
const task = global.researchTasks[taskId];
if (!task) {
throw new ExaError('Task not found', 'TASK_NOT_FOUND');
}
return {
task_id: taskId,
status: task.status,
started_at: task.started_at,
completed_at: task.completed_at,
results: task.results,
error: task.error
};
}
/**
* Perform deep research (internal method)
*/
private async performDeepResearch(taskId: string, params: any) {
try {
const task = global.researchTasks[taskId];
const results: any = {
summary: '',
sections: []
};
// Determine time range
let startDate: string | undefined;
if (params.time_range && params.time_range !== 'all') {
const now = new Date();
const ranges = {
week: 7,
month: 30,
quarter: 90,
year: 365
};
const days = ranges[params.time_range as keyof typeof ranges];
startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
}
// Perform multiple searches based on research type
const searchQueries = this.getResearchQueries(params.topic, params.research_type || 'comprehensive');
for (const [section, query] of Object.entries(searchQueries)) {
const searchResults = await this.search({
query: query as string,
num_results: Math.floor((params.max_results || 50) / Object.keys(searchQueries).length),
type: 'neural',
start_published_date: startDate,
include_text: true,
include_summary: true,
include_highlights: true
});
results.sections.push({
title: section,
query: query,
results: searchResults
});
}
// Generate summary
results.summary = `Completed deep research on "${params.topic}" with ${results.sections.length} sections and ${results.sections.reduce((acc: number, s: any) => acc + s.results.length, 0)} total results.`;
task.status = 'completed';
task.completed_at = new Date().toISOString();
task.results = results;
} catch (error) {
throw error;
}
}
/**
* Get research queries based on type
*/
private getResearchQueries(topic: string, type: string) {
const queries: Record<string, Record<string, string>> = {
comprehensive: {
'Overview': `"${topic}" overview introduction "what is"`,
'Recent Developments': `"${topic}" latest news recent developments updates`,
'Key Players': `"${topic}" companies organizations leaders "key players"`,
'Analysis': `"${topic}" analysis insights trends expert opinion`,
'Future Outlook': `"${topic}" future predictions forecast "what's next"`
},
news: {
'Breaking News': `"${topic}" breaking news latest updates`,
'Industry News': `"${topic}" industry news sector updates`,
'Press Releases': `"${topic}" press release announcement official`,
'Media Coverage': `"${topic}" media coverage journalism reports`
},
academic: {
'Research Papers': `"${topic}" research paper study academic journal`,
'Studies': `"${topic}" study findings results data`,
'Reviews': `"${topic}" literature review meta-analysis systematic`,
'Theories': `"${topic}" theory framework model academic`
},
market: {
'Market Size': `"${topic}" market size revenue valuation statistics`,
'Competition': `"${topic}" competitors market share competitive landscape`,
'Trends': `"${topic}" market trends growth forecast analysis`,
'Opportunities': `"${topic}" opportunities challenges SWOT analysis`
}
};
return queries[type] || queries.comprehensive;
}
/**
* Get the Exa client instance
*/
getClient(): any {
return this.client;
}
}
// Declare global type for research tasks
declare global {
var researchTasks: Record<string, any>;
}