jblyons15-research-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +120 -0
- package/dist/index.js +377 -0
- package/package.json +29 -0
- package/src/index.ts +496 -0
- package/tsconfig.json +17 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { ResearchRequest, ResearchResult, ResearchJob, ResearchJobStatus, ResearchError, CacheEntry, RetryPolicy } from '@crossover/research-types';
|
|
3
|
+
export declare class ResearchErrorHandler {
|
|
4
|
+
static create(type: ResearchError['type'], code: string, message: string, details?: Record<string, any>, retryable?: boolean): ResearchError;
|
|
5
|
+
static fromPerplexityError(error: any): ResearchError;
|
|
6
|
+
static fromFirecrawlError(error: any): ResearchError;
|
|
7
|
+
static isRetryable(error: ResearchError): boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class RetryEngine {
|
|
10
|
+
private policy;
|
|
11
|
+
constructor(policy?: RetryPolicy);
|
|
12
|
+
execute<T>(fn: () => Promise<T>, context?: string): Promise<T>;
|
|
13
|
+
private calculateDelay;
|
|
14
|
+
private sleep;
|
|
15
|
+
}
|
|
16
|
+
export declare class CacheClient {
|
|
17
|
+
private cache;
|
|
18
|
+
get(queryHash: string): Promise<CacheEntry | null>;
|
|
19
|
+
set(queryHash: string, result: ResearchResult, ttlMs?: any): Promise<void>;
|
|
20
|
+
clear(): Promise<void>;
|
|
21
|
+
getStats(): {
|
|
22
|
+
size: number;
|
|
23
|
+
totalHits: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare class PerplexityClient {
|
|
27
|
+
private apiKey;
|
|
28
|
+
private retryEngine;
|
|
29
|
+
constructor(apiKey: string);
|
|
30
|
+
search(query: string, options?: {
|
|
31
|
+
domainFilter?: {
|
|
32
|
+
allow?: string[];
|
|
33
|
+
deny?: string[];
|
|
34
|
+
};
|
|
35
|
+
recency?: 'day' | 'week' | 'month';
|
|
36
|
+
}): Promise<{
|
|
37
|
+
sources: Array<{
|
|
38
|
+
url: string;
|
|
39
|
+
title: string;
|
|
40
|
+
snippet: string;
|
|
41
|
+
}>;
|
|
42
|
+
totalResults: number;
|
|
43
|
+
executionMs: number;
|
|
44
|
+
}>;
|
|
45
|
+
agent(request: {
|
|
46
|
+
messages: Array<{
|
|
47
|
+
role: string;
|
|
48
|
+
content: string;
|
|
49
|
+
}>;
|
|
50
|
+
model?: string;
|
|
51
|
+
tools?: any[];
|
|
52
|
+
systemPrompt?: string;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
synthesis: string;
|
|
55
|
+
citations: string[];
|
|
56
|
+
executionMs: number;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
export declare class FirecrawlClient {
|
|
60
|
+
private apiKey;
|
|
61
|
+
private retryEngine;
|
|
62
|
+
constructor(apiKey: string);
|
|
63
|
+
scrape(url: string, schema?: Record<string, any>): Promise<{
|
|
64
|
+
data: Record<string, any>;
|
|
65
|
+
markdown: string;
|
|
66
|
+
executionMs: number;
|
|
67
|
+
}>;
|
|
68
|
+
batchScrape(urls: string[]): Promise<Array<{
|
|
69
|
+
url: string;
|
|
70
|
+
data: Record<string, any>;
|
|
71
|
+
error?: string;
|
|
72
|
+
}>>;
|
|
73
|
+
}
|
|
74
|
+
export declare class CortexClient {
|
|
75
|
+
private supabase;
|
|
76
|
+
constructor(supabase: SupabaseClient);
|
|
77
|
+
createResearchRequest(request: Omit<ResearchRequest, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Promise<string>;
|
|
78
|
+
getResearchJob(jobId: string): Promise<ResearchJob | null>;
|
|
79
|
+
updateResearchJob(jobId: string, updates: Partial<ResearchJob>): Promise<void>;
|
|
80
|
+
createReport(report: {
|
|
81
|
+
entityType: string;
|
|
82
|
+
reportType: string;
|
|
83
|
+
content: string;
|
|
84
|
+
model: string;
|
|
85
|
+
cost: number;
|
|
86
|
+
latencyMs: number;
|
|
87
|
+
}): Promise<string>;
|
|
88
|
+
subscribe(jobId: string, callback: (status: ResearchJobStatus) => void): void;
|
|
89
|
+
}
|
|
90
|
+
export declare class ResearchOrchestrator {
|
|
91
|
+
private perplexity;
|
|
92
|
+
private firecrawl;
|
|
93
|
+
private cortex;
|
|
94
|
+
private cache;
|
|
95
|
+
private retryEngine;
|
|
96
|
+
constructor(config: {
|
|
97
|
+
perplexityApiKey: string;
|
|
98
|
+
firecrawlApiKey: string;
|
|
99
|
+
supabaseUrl: string;
|
|
100
|
+
supabaseAnonKey: string;
|
|
101
|
+
});
|
|
102
|
+
submitRequest(request: Omit<ResearchRequest, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Promise<string>;
|
|
103
|
+
getStatus(jobId: string): Promise<ResearchJobStatus | null>;
|
|
104
|
+
getResults(jobId: string): Promise<ResearchResult | null>;
|
|
105
|
+
onStatusChange(jobId: string, callback: (status: ResearchJobStatus) => void): void;
|
|
106
|
+
getCacheStats(): {
|
|
107
|
+
size: number;
|
|
108
|
+
totalHits: number;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
declare const _default: {
|
|
112
|
+
ResearchOrchestrator: typeof ResearchOrchestrator;
|
|
113
|
+
ResearchErrorHandler: typeof ResearchErrorHandler;
|
|
114
|
+
RetryEngine: typeof RetryEngine;
|
|
115
|
+
CacheClient: typeof CacheClient;
|
|
116
|
+
PerplexityClient: typeof PerplexityClient;
|
|
117
|
+
FirecrawlClient: typeof FirecrawlClient;
|
|
118
|
+
CortexClient: typeof CortexClient;
|
|
119
|
+
};
|
|
120
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// @crossover/research-sdk - Complete Package Definition
|
|
2
|
+
// Client libraries for Supabase, Perplexity, Firecrawl, error handling, caching
|
|
3
|
+
import { createClient } from '@supabase/supabase-js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// ERROR HANDLER - Canonical error handling (SSOT)
|
|
6
|
+
// ============================================================================
|
|
7
|
+
export class ResearchErrorHandler {
|
|
8
|
+
static create(type, code, message, details = {}, retryable = false) {
|
|
9
|
+
return {
|
|
10
|
+
type,
|
|
11
|
+
code,
|
|
12
|
+
message,
|
|
13
|
+
retryable,
|
|
14
|
+
details,
|
|
15
|
+
timestamp: new Date(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
static fromPerplexityError(error) {
|
|
19
|
+
if (error?.status === 429) {
|
|
20
|
+
return this.create('RATE_LIMIT', 'PERPLEXITY_RATE_LIMITED', error.message, { error }, true);
|
|
21
|
+
}
|
|
22
|
+
if (error?.status === 401) {
|
|
23
|
+
return this.create('AUTH', 'PERPLEXITY_AUTH_FAILED', error.message, { error }, false);
|
|
24
|
+
}
|
|
25
|
+
if (error?.status >= 500) {
|
|
26
|
+
return this.create('API', 'PERPLEXITY_SERVER_ERROR', error.message, { error }, true);
|
|
27
|
+
}
|
|
28
|
+
return this.create('API', 'PERPLEXITY_ERROR', error.message, { error }, false);
|
|
29
|
+
}
|
|
30
|
+
static fromFirecrawlError(error) {
|
|
31
|
+
if (error?.status === 429) {
|
|
32
|
+
return this.create('RATE_LIMIT', 'FIRECRAWL_RATE_LIMITED', error.message, { error }, true);
|
|
33
|
+
}
|
|
34
|
+
if (error?.status === 401) {
|
|
35
|
+
return this.create('AUTH', 'FIRECRAWL_AUTH_FAILED', error.message, { error }, false);
|
|
36
|
+
}
|
|
37
|
+
if (error?.status >= 500) {
|
|
38
|
+
return this.create('API', 'FIRECRAWL_SERVER_ERROR', error.message, { error }, true);
|
|
39
|
+
}
|
|
40
|
+
return this.create('API', 'FIRECRAWL_ERROR', error.message, { error }, false);
|
|
41
|
+
}
|
|
42
|
+
static isRetryable(error) {
|
|
43
|
+
return error.retryable && error.type !== 'VALIDATION';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// RETRY ENGINE - Canonical retry logic (SSOT)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
export class RetryEngine {
|
|
50
|
+
constructor(policy = CANONICAL_RETRY_POLICY) {
|
|
51
|
+
this.policy = policy;
|
|
52
|
+
}
|
|
53
|
+
async execute(fn, context = 'operation') {
|
|
54
|
+
let lastError = null;
|
|
55
|
+
for (let attempt = 0; attempt <= this.policy.maxRetries; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
return await fn();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
61
|
+
if (attempt === this.policy.maxRetries) {
|
|
62
|
+
throw lastError;
|
|
63
|
+
}
|
|
64
|
+
const delayMs = this.calculateDelay(attempt);
|
|
65
|
+
console.log(`${context} failed, retrying in ${delayMs}ms (attempt ${attempt + 1}/${this.policy.maxRetries})`);
|
|
66
|
+
await this.sleep(delayMs);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw lastError;
|
|
70
|
+
}
|
|
71
|
+
calculateDelay(attempt) {
|
|
72
|
+
const exponential = this.policy.initialDelayMs * Math.pow(this.policy.backoffMultiplier, attempt);
|
|
73
|
+
return Math.min(exponential, this.policy.maxDelayMs);
|
|
74
|
+
}
|
|
75
|
+
sleep(ms) {
|
|
76
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// CACHE CLIENT - Canonical caching (SSOT)
|
|
81
|
+
// ============================================================================
|
|
82
|
+
export class CacheClient {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.cache = new Map();
|
|
85
|
+
}
|
|
86
|
+
async get(queryHash) {
|
|
87
|
+
const entry = this.cache.get(queryHash);
|
|
88
|
+
if (!entry)
|
|
89
|
+
return null;
|
|
90
|
+
if (entry.expiresAt < new Date()) {
|
|
91
|
+
this.cache.delete(queryHash);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
// Record hit
|
|
95
|
+
entry.hitCount += 1;
|
|
96
|
+
return entry;
|
|
97
|
+
}
|
|
98
|
+
async set(queryHash, result, ttlMs = CANONICAL_CACHE_TTL_MS) {
|
|
99
|
+
const entry = {
|
|
100
|
+
queryHash,
|
|
101
|
+
query: '', // Would be populated in real implementation
|
|
102
|
+
result,
|
|
103
|
+
hitCount: 0,
|
|
104
|
+
createdAt: new Date(),
|
|
105
|
+
expiresAt: new Date(Date.now() + ttlMs),
|
|
106
|
+
};
|
|
107
|
+
this.cache.set(queryHash, entry);
|
|
108
|
+
}
|
|
109
|
+
async clear() {
|
|
110
|
+
this.cache.clear();
|
|
111
|
+
}
|
|
112
|
+
getStats() {
|
|
113
|
+
const totalHits = Array.from(this.cache.values()).reduce((sum, e) => sum + e.hitCount, 0);
|
|
114
|
+
return { size: this.cache.size, totalHits };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// PERPLEXITY CLIENT - Canonical Perplexity integration
|
|
119
|
+
// ============================================================================
|
|
120
|
+
export class PerplexityClient {
|
|
121
|
+
constructor(apiKey) {
|
|
122
|
+
this.apiKey = apiKey;
|
|
123
|
+
this.retryEngine = new RetryEngine();
|
|
124
|
+
}
|
|
125
|
+
async search(query, options) {
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
return this.retryEngine.execute(async () => {
|
|
128
|
+
const response = await fetch('https://api.perplexity.ai/search', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
query,
|
|
136
|
+
search_type: 'web',
|
|
137
|
+
search_domain_filter: options?.domainFilter,
|
|
138
|
+
recency: options?.recency || 'month',
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw ResearchErrorHandler.fromPerplexityError({
|
|
143
|
+
status: response.status,
|
|
144
|
+
message: `HTTP ${response.status}`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const data = await response.json();
|
|
148
|
+
const executionMs = Date.now() - startTime;
|
|
149
|
+
return {
|
|
150
|
+
sources: data.results?.map((r) => ({
|
|
151
|
+
url: r.url,
|
|
152
|
+
title: r.title,
|
|
153
|
+
snippet: r.snippet,
|
|
154
|
+
})) || [],
|
|
155
|
+
totalResults: data.total_results || 0,
|
|
156
|
+
executionMs,
|
|
157
|
+
};
|
|
158
|
+
}, 'Perplexity search');
|
|
159
|
+
}
|
|
160
|
+
async agent(request) {
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
return this.retryEngine.execute(async () => {
|
|
163
|
+
const response = await fetch('https://api.perplexity.ai/chat/completions', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
model: request.model || 'sonar-pro',
|
|
171
|
+
messages: request.messages,
|
|
172
|
+
tools: request.tools,
|
|
173
|
+
system: request.systemPrompt,
|
|
174
|
+
temperature: 0.7,
|
|
175
|
+
top_p: 1,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw ResearchErrorHandler.fromPerplexityError({
|
|
180
|
+
status: response.status,
|
|
181
|
+
message: `HTTP ${response.status}`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const data = await response.json();
|
|
185
|
+
const executionMs = Date.now() - startTime;
|
|
186
|
+
return {
|
|
187
|
+
synthesis: data.choices?.[0]?.message?.content || '',
|
|
188
|
+
citations: data.citations || [],
|
|
189
|
+
executionMs,
|
|
190
|
+
};
|
|
191
|
+
}, 'Perplexity agent');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// FIRECRAWL CLIENT - Canonical web scraping
|
|
196
|
+
// ============================================================================
|
|
197
|
+
export class FirecrawlClient {
|
|
198
|
+
constructor(apiKey) {
|
|
199
|
+
this.apiKey = apiKey;
|
|
200
|
+
this.retryEngine = new RetryEngine();
|
|
201
|
+
}
|
|
202
|
+
async scrape(url, schema) {
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
return this.retryEngine.execute(async () => {
|
|
205
|
+
const response = await fetch('https://api.firecrawl.dev/v1/scrape', {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: {
|
|
208
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
209
|
+
'Content-Type': 'application/json',
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
url,
|
|
213
|
+
extractorOptions: schema ? {
|
|
214
|
+
mode: 'llm-extraction',
|
|
215
|
+
extractionSchema: schema,
|
|
216
|
+
} : undefined,
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
throw ResearchErrorHandler.fromFirecrawlError({
|
|
221
|
+
status: response.status,
|
|
222
|
+
message: `HTTP ${response.status}`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const data = await response.json();
|
|
226
|
+
const executionMs = Date.now() - startTime;
|
|
227
|
+
return {
|
|
228
|
+
data: data.data || {},
|
|
229
|
+
markdown: data.markdown || '',
|
|
230
|
+
executionMs,
|
|
231
|
+
};
|
|
232
|
+
}, `Firecrawl scrape ${url}`);
|
|
233
|
+
}
|
|
234
|
+
async batchScrape(urls) {
|
|
235
|
+
return Promise.all(urls.map(async (url) => {
|
|
236
|
+
try {
|
|
237
|
+
const result = await this.scrape(url);
|
|
238
|
+
return { url, data: result.data };
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
url,
|
|
243
|
+
data: {},
|
|
244
|
+
error: error instanceof Error ? error.message : String(error),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// CORTEX CLIENT - Supabase integration
|
|
252
|
+
// ============================================================================
|
|
253
|
+
export class CortexClient {
|
|
254
|
+
constructor(supabase) {
|
|
255
|
+
this.supabase = supabase;
|
|
256
|
+
}
|
|
257
|
+
async createResearchRequest(request) {
|
|
258
|
+
const { data, error } = await this.supabase
|
|
259
|
+
.from('cortex.client_research_requests')
|
|
260
|
+
.insert({
|
|
261
|
+
...request,
|
|
262
|
+
status: 'pending',
|
|
263
|
+
created_at: new Date(),
|
|
264
|
+
updated_at: new Date(),
|
|
265
|
+
})
|
|
266
|
+
.select('id')
|
|
267
|
+
.single();
|
|
268
|
+
if (error)
|
|
269
|
+
throw error;
|
|
270
|
+
return data.id;
|
|
271
|
+
}
|
|
272
|
+
async getResearchJob(jobId) {
|
|
273
|
+
const { data, error } = await this.supabase
|
|
274
|
+
.from('cortex.research_jobs')
|
|
275
|
+
.select('*')
|
|
276
|
+
.eq('id', jobId)
|
|
277
|
+
.single();
|
|
278
|
+
if (error && error.code !== 'PGRST116')
|
|
279
|
+
throw error;
|
|
280
|
+
return data || null;
|
|
281
|
+
}
|
|
282
|
+
async updateResearchJob(jobId, updates) {
|
|
283
|
+
const { error } = await this.supabase
|
|
284
|
+
.from('cortex.research_jobs')
|
|
285
|
+
.update({
|
|
286
|
+
...updates,
|
|
287
|
+
updated_at: new Date(),
|
|
288
|
+
})
|
|
289
|
+
.eq('id', jobId);
|
|
290
|
+
if (error)
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
async createReport(report) {
|
|
294
|
+
const { data, error } = await this.supabase
|
|
295
|
+
.from('ai_reports')
|
|
296
|
+
.insert({
|
|
297
|
+
entity_type: report.entityType,
|
|
298
|
+
report_type: report.reportType,
|
|
299
|
+
content: report.content,
|
|
300
|
+
model: report.model,
|
|
301
|
+
cost: report.cost,
|
|
302
|
+
latency_ms: report.latencyMs,
|
|
303
|
+
generated_at: new Date(),
|
|
304
|
+
})
|
|
305
|
+
.select('id')
|
|
306
|
+
.single();
|
|
307
|
+
if (error)
|
|
308
|
+
throw error;
|
|
309
|
+
return data.id;
|
|
310
|
+
}
|
|
311
|
+
subscribe(jobId, callback) {
|
|
312
|
+
this.supabase
|
|
313
|
+
.from(`cortex.research_jobs:id=eq.${jobId}`)
|
|
314
|
+
.on('*', (payload) => {
|
|
315
|
+
callback({
|
|
316
|
+
jobId,
|
|
317
|
+
status: payload.new.status,
|
|
318
|
+
progress: payload.new.progress || 0,
|
|
319
|
+
error: payload.new.error,
|
|
320
|
+
lastUpdate: new Date(),
|
|
321
|
+
});
|
|
322
|
+
})
|
|
323
|
+
.subscribe();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// RESEARCH ORCHESTRATOR - Main entry point
|
|
328
|
+
// ============================================================================
|
|
329
|
+
export class ResearchOrchestrator {
|
|
330
|
+
constructor(config) {
|
|
331
|
+
this.perplexity = new PerplexityClient(config.perplexityApiKey);
|
|
332
|
+
this.firecrawl = new FirecrawlClient(config.firecrawlApiKey);
|
|
333
|
+
this.cortex = new CortexClient(createClient(config.supabaseUrl, config.supabaseAnonKey));
|
|
334
|
+
this.cache = new CacheClient();
|
|
335
|
+
this.retryEngine = new RetryEngine();
|
|
336
|
+
}
|
|
337
|
+
async submitRequest(request) {
|
|
338
|
+
// This just creates the request record
|
|
339
|
+
// The actual execution happens in the Edge Function
|
|
340
|
+
const jobId = await this.cortex.createResearchRequest(request);
|
|
341
|
+
return jobId;
|
|
342
|
+
}
|
|
343
|
+
async getStatus(jobId) {
|
|
344
|
+
const job = await this.cortex.getResearchJob(jobId);
|
|
345
|
+
if (!job)
|
|
346
|
+
return null;
|
|
347
|
+
return {
|
|
348
|
+
jobId,
|
|
349
|
+
status: job.status,
|
|
350
|
+
progress: job.progress || 0,
|
|
351
|
+
error: job.error,
|
|
352
|
+
lastUpdate: job.updatedAt,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async getResults(jobId) {
|
|
356
|
+
const job = await this.cortex.getResearchJob(jobId);
|
|
357
|
+
return job?.result || null;
|
|
358
|
+
}
|
|
359
|
+
onStatusChange(jobId, callback) {
|
|
360
|
+
this.cortex.subscribe(jobId, callback);
|
|
361
|
+
}
|
|
362
|
+
getCacheStats() {
|
|
363
|
+
return this.cache.getStats();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// EXPORTS
|
|
368
|
+
// ============================================================================
|
|
369
|
+
export default {
|
|
370
|
+
ResearchOrchestrator,
|
|
371
|
+
ResearchErrorHandler,
|
|
372
|
+
RetryEngine,
|
|
373
|
+
CacheClient,
|
|
374
|
+
PerplexityClient,
|
|
375
|
+
FirecrawlClient,
|
|
376
|
+
CortexClient,
|
|
377
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jblyons15-research-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Research orchestration SDK for CommandCenter",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"research",
|
|
9
|
+
"orchestration",
|
|
10
|
+
"perplexity",
|
|
11
|
+
"firecrawl",
|
|
12
|
+
"supabase"
|
|
13
|
+
],
|
|
14
|
+
"author": "Crossover Research",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@supabase/supabase-js": "^2.38.0",
|
|
18
|
+
"jblyons15-research-types": "^1.0.0",
|
|
19
|
+
"zod": "^3.22.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"type-check": "tsc --noEmit"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// @crossover/research-sdk - Complete Package Definition
|
|
2
|
+
// Client libraries for Supabase, Perplexity, Firecrawl, error handling, caching
|
|
3
|
+
|
|
4
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js'
|
|
5
|
+
import type {
|
|
6
|
+
ResearchRequest,
|
|
7
|
+
ResearchResult,
|
|
8
|
+
ResearchJob,
|
|
9
|
+
ResearchJobStatus,
|
|
10
|
+
ResearchError,
|
|
11
|
+
CacheEntry,
|
|
12
|
+
RetryPolicy,
|
|
13
|
+
CANONICAL_RETRY_POLICY,
|
|
14
|
+
CANONICAL_CACHE_TTL_MS,
|
|
15
|
+
} from '@crossover/research-types'
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// ERROR HANDLER - Canonical error handling (SSOT)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export class ResearchErrorHandler {
|
|
22
|
+
static create(
|
|
23
|
+
type: ResearchError['type'],
|
|
24
|
+
code: string,
|
|
25
|
+
message: string,
|
|
26
|
+
details: Record<string, any> = {},
|
|
27
|
+
retryable = false
|
|
28
|
+
): ResearchError {
|
|
29
|
+
return {
|
|
30
|
+
type,
|
|
31
|
+
code,
|
|
32
|
+
message,
|
|
33
|
+
retryable,
|
|
34
|
+
details,
|
|
35
|
+
timestamp: new Date(),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static fromPerplexityError(error: any): ResearchError {
|
|
40
|
+
if (error?.status === 429) {
|
|
41
|
+
return this.create('RATE_LIMIT', 'PERPLEXITY_RATE_LIMITED', error.message, { error }, true)
|
|
42
|
+
}
|
|
43
|
+
if (error?.status === 401) {
|
|
44
|
+
return this.create('AUTH', 'PERPLEXITY_AUTH_FAILED', error.message, { error }, false)
|
|
45
|
+
}
|
|
46
|
+
if (error?.status >= 500) {
|
|
47
|
+
return this.create('API', 'PERPLEXITY_SERVER_ERROR', error.message, { error }, true)
|
|
48
|
+
}
|
|
49
|
+
return this.create('API', 'PERPLEXITY_ERROR', error.message, { error }, false)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static fromFirecrawlError(error: any): ResearchError {
|
|
53
|
+
if (error?.status === 429) {
|
|
54
|
+
return this.create('RATE_LIMIT', 'FIRECRAWL_RATE_LIMITED', error.message, { error }, true)
|
|
55
|
+
}
|
|
56
|
+
if (error?.status === 401) {
|
|
57
|
+
return this.create('AUTH', 'FIRECRAWL_AUTH_FAILED', error.message, { error }, false)
|
|
58
|
+
}
|
|
59
|
+
if (error?.status >= 500) {
|
|
60
|
+
return this.create('API', 'FIRECRAWL_SERVER_ERROR', error.message, { error }, true)
|
|
61
|
+
}
|
|
62
|
+
return this.create('API', 'FIRECRAWL_ERROR', error.message, { error }, false)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static isRetryable(error: ResearchError): boolean {
|
|
66
|
+
return error.retryable && error.type !== 'VALIDATION'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// RETRY ENGINE - Canonical retry logic (SSOT)
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
export class RetryEngine {
|
|
75
|
+
constructor(private policy: RetryPolicy = CANONICAL_RETRY_POLICY) {}
|
|
76
|
+
|
|
77
|
+
async execute<T>(
|
|
78
|
+
fn: () => Promise<T>,
|
|
79
|
+
context: string = 'operation'
|
|
80
|
+
): Promise<T> {
|
|
81
|
+
let lastError: Error | null = null
|
|
82
|
+
|
|
83
|
+
for (let attempt = 0; attempt <= this.policy.maxRetries; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
return await fn()
|
|
86
|
+
} catch (error) {
|
|
87
|
+
lastError = error instanceof Error ? error : new Error(String(error))
|
|
88
|
+
|
|
89
|
+
if (attempt === this.policy.maxRetries) {
|
|
90
|
+
throw lastError
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const delayMs = this.calculateDelay(attempt)
|
|
94
|
+
console.log(`${context} failed, retrying in ${delayMs}ms (attempt ${attempt + 1}/${this.policy.maxRetries})`)
|
|
95
|
+
await this.sleep(delayMs)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw lastError
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private calculateDelay(attempt: number): number {
|
|
103
|
+
const exponential = this.policy.initialDelayMs * Math.pow(this.policy.backoffMultiplier, attempt)
|
|
104
|
+
return Math.min(exponential, this.policy.maxDelayMs)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private sleep(ms: number): Promise<void> {
|
|
108
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// CACHE CLIENT - Canonical caching (SSOT)
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
export class CacheClient {
|
|
117
|
+
private cache = new Map<string, CacheEntry>()
|
|
118
|
+
|
|
119
|
+
async get(queryHash: string): Promise<CacheEntry | null> {
|
|
120
|
+
const entry = this.cache.get(queryHash)
|
|
121
|
+
|
|
122
|
+
if (!entry) return null
|
|
123
|
+
if (entry.expiresAt < new Date()) {
|
|
124
|
+
this.cache.delete(queryHash)
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Record hit
|
|
129
|
+
entry.hitCount += 1
|
|
130
|
+
return entry
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async set(
|
|
134
|
+
queryHash: string,
|
|
135
|
+
result: ResearchResult,
|
|
136
|
+
ttlMs = CANONICAL_CACHE_TTL_MS
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
const entry: CacheEntry = {
|
|
139
|
+
queryHash,
|
|
140
|
+
query: '', // Would be populated in real implementation
|
|
141
|
+
result,
|
|
142
|
+
hitCount: 0,
|
|
143
|
+
createdAt: new Date(),
|
|
144
|
+
expiresAt: new Date(Date.now() + ttlMs),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.cache.set(queryHash, entry)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async clear(): Promise<void> {
|
|
151
|
+
this.cache.clear()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getStats(): { size: number; totalHits: number } {
|
|
155
|
+
const totalHits = Array.from(this.cache.values()).reduce((sum, e) => sum + e.hitCount, 0)
|
|
156
|
+
return { size: this.cache.size, totalHits }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// PERPLEXITY CLIENT - Canonical Perplexity integration
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
export class PerplexityClient {
|
|
165
|
+
private retryEngine = new RetryEngine()
|
|
166
|
+
|
|
167
|
+
constructor(private apiKey: string) {}
|
|
168
|
+
|
|
169
|
+
async search(query: string, options?: {
|
|
170
|
+
domainFilter?: { allow?: string[]; deny?: string[] }
|
|
171
|
+
recency?: 'day' | 'week' | 'month'
|
|
172
|
+
}): Promise<{
|
|
173
|
+
sources: Array<{ url: string; title: string; snippet: string }>
|
|
174
|
+
totalResults: number
|
|
175
|
+
executionMs: number
|
|
176
|
+
}> {
|
|
177
|
+
const startTime = Date.now()
|
|
178
|
+
|
|
179
|
+
return this.retryEngine.execute(async () => {
|
|
180
|
+
const response = await fetch('https://api.perplexity.ai/search', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: {
|
|
183
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
},
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
query,
|
|
188
|
+
search_type: 'web',
|
|
189
|
+
search_domain_filter: options?.domainFilter,
|
|
190
|
+
recency: options?.recency || 'month',
|
|
191
|
+
}),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
throw ResearchErrorHandler.fromPerplexityError({
|
|
196
|
+
status: response.status,
|
|
197
|
+
message: `HTTP ${response.status}`,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const data = await response.json()
|
|
202
|
+
const executionMs = Date.now() - startTime
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
sources: data.results?.map((r: any) => ({
|
|
206
|
+
url: r.url,
|
|
207
|
+
title: r.title,
|
|
208
|
+
snippet: r.snippet,
|
|
209
|
+
})) || [],
|
|
210
|
+
totalResults: data.total_results || 0,
|
|
211
|
+
executionMs,
|
|
212
|
+
}
|
|
213
|
+
}, 'Perplexity search')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async agent(request: {
|
|
217
|
+
messages: Array<{ role: string; content: string }>
|
|
218
|
+
model?: string
|
|
219
|
+
tools?: any[]
|
|
220
|
+
systemPrompt?: string
|
|
221
|
+
}): Promise<{
|
|
222
|
+
synthesis: string
|
|
223
|
+
citations: string[]
|
|
224
|
+
executionMs: number
|
|
225
|
+
}> {
|
|
226
|
+
const startTime = Date.now()
|
|
227
|
+
|
|
228
|
+
return this.retryEngine.execute(async () => {
|
|
229
|
+
const response = await fetch('https://api.perplexity.ai/chat/completions', {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: {
|
|
232
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
model: request.model || 'sonar-pro',
|
|
237
|
+
messages: request.messages,
|
|
238
|
+
tools: request.tools,
|
|
239
|
+
system: request.systemPrompt,
|
|
240
|
+
temperature: 0.7,
|
|
241
|
+
top_p: 1,
|
|
242
|
+
}),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
throw ResearchErrorHandler.fromPerplexityError({
|
|
247
|
+
status: response.status,
|
|
248
|
+
message: `HTTP ${response.status}`,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const data = await response.json()
|
|
253
|
+
const executionMs = Date.now() - startTime
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
synthesis: data.choices?.[0]?.message?.content || '',
|
|
257
|
+
citations: data.citations || [],
|
|
258
|
+
executionMs,
|
|
259
|
+
}
|
|
260
|
+
}, 'Perplexity agent')
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// FIRECRAWL CLIENT - Canonical web scraping
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
export class FirecrawlClient {
|
|
269
|
+
private retryEngine = new RetryEngine()
|
|
270
|
+
|
|
271
|
+
constructor(private apiKey: string) {}
|
|
272
|
+
|
|
273
|
+
async scrape(url: string, schema?: Record<string, any>): Promise<{
|
|
274
|
+
data: Record<string, any>
|
|
275
|
+
markdown: string
|
|
276
|
+
executionMs: number
|
|
277
|
+
}> {
|
|
278
|
+
const startTime = Date.now()
|
|
279
|
+
|
|
280
|
+
return this.retryEngine.execute(async () => {
|
|
281
|
+
const response = await fetch('https://api.firecrawl.dev/v1/scrape', {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: {
|
|
284
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
285
|
+
'Content-Type': 'application/json',
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify({
|
|
288
|
+
url,
|
|
289
|
+
extractorOptions: schema ? {
|
|
290
|
+
mode: 'llm-extraction',
|
|
291
|
+
extractionSchema: schema,
|
|
292
|
+
} : undefined,
|
|
293
|
+
}),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
throw ResearchErrorHandler.fromFirecrawlError({
|
|
298
|
+
status: response.status,
|
|
299
|
+
message: `HTTP ${response.status}`,
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const data = await response.json()
|
|
304
|
+
const executionMs = Date.now() - startTime
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
data: data.data || {},
|
|
308
|
+
markdown: data.markdown || '',
|
|
309
|
+
executionMs,
|
|
310
|
+
}
|
|
311
|
+
}, `Firecrawl scrape ${url}`)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async batchScrape(urls: string[]): Promise<Array<{
|
|
315
|
+
url: string
|
|
316
|
+
data: Record<string, any>
|
|
317
|
+
error?: string
|
|
318
|
+
}>> {
|
|
319
|
+
return Promise.all(
|
|
320
|
+
urls.map(async url => {
|
|
321
|
+
try {
|
|
322
|
+
const result = await this.scrape(url)
|
|
323
|
+
return { url, data: result.data }
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return {
|
|
326
|
+
url,
|
|
327
|
+
data: {},
|
|
328
|
+
error: error instanceof Error ? error.message : String(error),
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// CORTEX CLIENT - Supabase integration
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
export class CortexClient {
|
|
341
|
+
constructor(private supabase: SupabaseClient) {}
|
|
342
|
+
|
|
343
|
+
async createResearchRequest(request: Omit<ResearchRequest, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Promise<string> {
|
|
344
|
+
const { data, error } = await this.supabase
|
|
345
|
+
.from('cortex.client_research_requests')
|
|
346
|
+
.insert({
|
|
347
|
+
...request,
|
|
348
|
+
status: 'pending',
|
|
349
|
+
created_at: new Date(),
|
|
350
|
+
updated_at: new Date(),
|
|
351
|
+
})
|
|
352
|
+
.select('id')
|
|
353
|
+
.single()
|
|
354
|
+
|
|
355
|
+
if (error) throw error
|
|
356
|
+
return data.id
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async getResearchJob(jobId: string): Promise<ResearchJob | null> {
|
|
360
|
+
const { data, error } = await this.supabase
|
|
361
|
+
.from('cortex.research_jobs')
|
|
362
|
+
.select('*')
|
|
363
|
+
.eq('id', jobId)
|
|
364
|
+
.single()
|
|
365
|
+
|
|
366
|
+
if (error && error.code !== 'PGRST116') throw error
|
|
367
|
+
return data || null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async updateResearchJob(jobId: string, updates: Partial<ResearchJob>): Promise<void> {
|
|
371
|
+
const { error } = await this.supabase
|
|
372
|
+
.from('cortex.research_jobs')
|
|
373
|
+
.update({
|
|
374
|
+
...updates,
|
|
375
|
+
updated_at: new Date(),
|
|
376
|
+
})
|
|
377
|
+
.eq('id', jobId)
|
|
378
|
+
|
|
379
|
+
if (error) throw error
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async createReport(report: {
|
|
383
|
+
entityType: string
|
|
384
|
+
reportType: string
|
|
385
|
+
content: string
|
|
386
|
+
model: string
|
|
387
|
+
cost: number
|
|
388
|
+
latencyMs: number
|
|
389
|
+
}): Promise<string> {
|
|
390
|
+
const { data, error } = await this.supabase
|
|
391
|
+
.from('ai_reports')
|
|
392
|
+
.insert({
|
|
393
|
+
entity_type: report.entityType,
|
|
394
|
+
report_type: report.reportType,
|
|
395
|
+
content: report.content,
|
|
396
|
+
model: report.model,
|
|
397
|
+
cost: report.cost,
|
|
398
|
+
latency_ms: report.latencyMs,
|
|
399
|
+
generated_at: new Date(),
|
|
400
|
+
})
|
|
401
|
+
.select('id')
|
|
402
|
+
.single()
|
|
403
|
+
|
|
404
|
+
if (error) throw error
|
|
405
|
+
return data.id
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
subscribe(jobId: string, callback: (status: ResearchJobStatus) => void): void {
|
|
409
|
+
this.supabase
|
|
410
|
+
.from(`cortex.research_jobs:id=eq.${jobId}`)
|
|
411
|
+
.on('*', (payload) => {
|
|
412
|
+
callback({
|
|
413
|
+
jobId,
|
|
414
|
+
status: payload.new.status,
|
|
415
|
+
progress: payload.new.progress || 0,
|
|
416
|
+
error: payload.new.error,
|
|
417
|
+
lastUpdate: new Date(),
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
.subscribe()
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// RESEARCH ORCHESTRATOR - Main entry point
|
|
426
|
+
// ============================================================================
|
|
427
|
+
|
|
428
|
+
export class ResearchOrchestrator {
|
|
429
|
+
private perplexity: PerplexityClient
|
|
430
|
+
private firecrawl: FirecrawlClient
|
|
431
|
+
private cortex: CortexClient
|
|
432
|
+
private cache: CacheClient
|
|
433
|
+
private retryEngine: RetryEngine
|
|
434
|
+
|
|
435
|
+
constructor(config: {
|
|
436
|
+
perplexityApiKey: string
|
|
437
|
+
firecrawlApiKey: string
|
|
438
|
+
supabaseUrl: string
|
|
439
|
+
supabaseAnonKey: string
|
|
440
|
+
}) {
|
|
441
|
+
this.perplexity = new PerplexityClient(config.perplexityApiKey)
|
|
442
|
+
this.firecrawl = new FirecrawlClient(config.firecrawlApiKey)
|
|
443
|
+
this.cortex = new CortexClient(
|
|
444
|
+
createClient(config.supabaseUrl, config.supabaseAnonKey)
|
|
445
|
+
)
|
|
446
|
+
this.cache = new CacheClient()
|
|
447
|
+
this.retryEngine = new RetryEngine()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async submitRequest(request: Omit<ResearchRequest, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Promise<string> {
|
|
451
|
+
// This just creates the request record
|
|
452
|
+
// The actual execution happens in the Edge Function
|
|
453
|
+
const jobId = await this.cortex.createResearchRequest(request)
|
|
454
|
+
return jobId
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async getStatus(jobId: string): Promise<ResearchJobStatus | null> {
|
|
458
|
+
const job = await this.cortex.getResearchJob(jobId)
|
|
459
|
+
if (!job) return null
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
jobId,
|
|
463
|
+
status: job.status,
|
|
464
|
+
progress: job.progress || 0,
|
|
465
|
+
error: job.error,
|
|
466
|
+
lastUpdate: job.updatedAt,
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async getResults(jobId: string): Promise<ResearchResult | null> {
|
|
471
|
+
const job = await this.cortex.getResearchJob(jobId)
|
|
472
|
+
return job?.result || null
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
onStatusChange(jobId: string, callback: (status: ResearchJobStatus) => void): void {
|
|
476
|
+
this.cortex.subscribe(jobId, callback)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
getCacheStats(): { size: number; totalHits: number } {
|
|
480
|
+
return this.cache.getStats()
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// EXPORTS
|
|
486
|
+
// ============================================================================
|
|
487
|
+
|
|
488
|
+
export default {
|
|
489
|
+
ResearchOrchestrator,
|
|
490
|
+
ResearchErrorHandler,
|
|
491
|
+
RetryEngine,
|
|
492
|
+
CacheClient,
|
|
493
|
+
PerplexityClient,
|
|
494
|
+
FirecrawlClient,
|
|
495
|
+
CortexClient,
|
|
496
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "bundler"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|