crawlforge-mcp-server 3.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/CLAUDE.md +315 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/package.json +115 -0
- package/server.js +1963 -0
- package/setup.js +112 -0
- package/src/constants/config.js +615 -0
- package/src/core/ActionExecutor.js +1104 -0
- package/src/core/AlertNotificationSystem.js +601 -0
- package/src/core/AuthManager.js +315 -0
- package/src/core/ChangeTracker.js +2306 -0
- package/src/core/JobManager.js +687 -0
- package/src/core/LLMsTxtAnalyzer.js +753 -0
- package/src/core/LocalizationManager.js +1615 -0
- package/src/core/PerformanceManager.js +828 -0
- package/src/core/ResearchOrchestrator.js +1327 -0
- package/src/core/SnapshotManager.js +1037 -0
- package/src/core/StealthBrowserManager.js +1795 -0
- package/src/core/WebhookDispatcher.js +745 -0
- package/src/core/analysis/ContentAnalyzer.js +749 -0
- package/src/core/analysis/LinkAnalyzer.js +972 -0
- package/src/core/cache/CacheManager.js +821 -0
- package/src/core/connections/ConnectionPool.js +553 -0
- package/src/core/crawlers/BFSCrawler.js +845 -0
- package/src/core/integrations/PerformanceIntegration.js +377 -0
- package/src/core/llm/AnthropicProvider.js +135 -0
- package/src/core/llm/LLMManager.js +415 -0
- package/src/core/llm/LLMProvider.js +97 -0
- package/src/core/llm/OpenAIProvider.js +127 -0
- package/src/core/processing/BrowserProcessor.js +986 -0
- package/src/core/processing/ContentProcessor.js +505 -0
- package/src/core/processing/PDFProcessor.js +448 -0
- package/src/core/processing/StreamProcessor.js +673 -0
- package/src/core/queue/QueueManager.js +98 -0
- package/src/core/workers/WorkerPool.js +585 -0
- package/src/core/workers/worker.js +743 -0
- package/src/monitoring/healthCheck.js +600 -0
- package/src/monitoring/metrics.js +761 -0
- package/src/optimization/wave3-optimizations.js +932 -0
- package/src/security/security-patches.js +120 -0
- package/src/security/security-tests.js +355 -0
- package/src/security/wave3-security.js +652 -0
- package/src/tools/advanced/BatchScrapeTool.js +1089 -0
- package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
- package/src/tools/crawl/crawlDeep.js +449 -0
- package/src/tools/crawl/mapSite.js +400 -0
- package/src/tools/extract/analyzeContent.js +624 -0
- package/src/tools/extract/extractContent.js +329 -0
- package/src/tools/extract/processDocument.js +503 -0
- package/src/tools/extract/summarizeContent.js +376 -0
- package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
- package/src/tools/research/deepResearch.js +706 -0
- package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
- package/src/tools/search/adapters/googleSearch.js +236 -0
- package/src/tools/search/adapters/searchProviderFactory.js +96 -0
- package/src/tools/search/queryExpander.js +543 -0
- package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
- package/src/tools/search/ranking/ResultRanker.js +497 -0
- package/src/tools/search/searchWeb.js +482 -0
- package/src/tools/tracking/trackChanges.js +1355 -0
- package/src/utils/CircuitBreaker.js +515 -0
- package/src/utils/ErrorHandlingConfig.js +342 -0
- package/src/utils/HumanBehaviorSimulator.js +569 -0
- package/src/utils/Logger.js +568 -0
- package/src/utils/MemoryMonitor.js +173 -0
- package/src/utils/RetryManager.js +386 -0
- package/src/utils/contentUtils.js +588 -0
- package/src/utils/domainFilter.js +612 -0
- package/src/utils/inputValidation.js +766 -0
- package/src/utils/rateLimiter.js +196 -0
- package/src/utils/robotsChecker.js +91 -0
- package/src/utils/securityMiddleware.js +416 -0
- package/src/utils/sitemapParser.js +678 -0
- package/src/utils/ssrfProtection.js +640 -0
- package/src/utils/urlNormalizer.js +168 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { OpenAIProvider } from './OpenAIProvider.js';
|
|
2
|
+
import { AnthropicProvider } from './AnthropicProvider.js';
|
|
3
|
+
import { Logger } from '../../utils/Logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LLM Manager
|
|
7
|
+
* Manages multiple LLM providers and provides unified interface
|
|
8
|
+
*/
|
|
9
|
+
export class LLMManager {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.logger = new Logger({ component: 'LLMManager' });
|
|
12
|
+
this.providers = new Map();
|
|
13
|
+
this.defaultProvider = null;
|
|
14
|
+
this.fallbackProvider = null;
|
|
15
|
+
|
|
16
|
+
this.initializeProviders(options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize available LLM providers
|
|
21
|
+
*/
|
|
22
|
+
initializeProviders(options) {
|
|
23
|
+
const {
|
|
24
|
+
openai = {},
|
|
25
|
+
anthropic = {},
|
|
26
|
+
defaultProvider = 'auto'
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
// Initialize OpenAI provider
|
|
30
|
+
if (openai.apiKey || process.env.OPENAI_API_KEY) {
|
|
31
|
+
const openaiProvider = new OpenAIProvider(openai);
|
|
32
|
+
this.providers.set('openai', openaiProvider);
|
|
33
|
+
this.logger.info('OpenAI provider initialized');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Initialize Anthropic provider
|
|
37
|
+
if (anthropic.apiKey || process.env.ANTHROPIC_API_KEY) {
|
|
38
|
+
const anthropicProvider = new AnthropicProvider(anthropic);
|
|
39
|
+
this.providers.set('anthropic', anthropicProvider);
|
|
40
|
+
this.logger.info('Anthropic provider initialized');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Set default provider
|
|
44
|
+
this.setDefaultProvider(defaultProvider);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set the default provider
|
|
49
|
+
*/
|
|
50
|
+
setDefaultProvider(providerName) {
|
|
51
|
+
if (providerName === 'auto') {
|
|
52
|
+
// Auto-select: prefer OpenAI for embeddings, fallback to Anthropic
|
|
53
|
+
if (this.providers.has('openai')) {
|
|
54
|
+
this.defaultProvider = 'openai';
|
|
55
|
+
this.fallbackProvider = this.providers.has('anthropic') ? 'anthropic' : null;
|
|
56
|
+
} else if (this.providers.has('anthropic')) {
|
|
57
|
+
this.defaultProvider = 'anthropic';
|
|
58
|
+
this.fallbackProvider = null;
|
|
59
|
+
}
|
|
60
|
+
} else if (this.providers.has(providerName)) {
|
|
61
|
+
this.defaultProvider = providerName;
|
|
62
|
+
// Set fallback to other available provider
|
|
63
|
+
for (const [name, provider] of this.providers) {
|
|
64
|
+
if (name !== providerName) {
|
|
65
|
+
this.fallbackProvider = name;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.defaultProvider) {
|
|
72
|
+
this.logger.info(`Default LLM provider set to: ${this.defaultProvider}`);
|
|
73
|
+
if (this.fallbackProvider) {
|
|
74
|
+
this.logger.info(`Fallback LLM provider: ${this.fallbackProvider}`);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
this.logger.warn('No LLM providers available');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get a provider instance
|
|
83
|
+
*/
|
|
84
|
+
getProvider(name = null) {
|
|
85
|
+
const providerName = name || this.defaultProvider;
|
|
86
|
+
if (!providerName) {
|
|
87
|
+
throw new Error('No LLM provider available');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const provider = this.providers.get(providerName);
|
|
91
|
+
if (!provider) {
|
|
92
|
+
throw new Error(`LLM provider '${providerName}' not found`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return provider;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate completion with fallback support
|
|
100
|
+
*/
|
|
101
|
+
async generateCompletion(prompt, options = {}) {
|
|
102
|
+
const { provider = null, ...llmOptions } = options;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const llmProvider = this.getProvider(provider);
|
|
106
|
+
return await llmProvider.generateCompletion(prompt, llmOptions);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.logger.warn(`Primary provider failed: ${error.message}`);
|
|
109
|
+
|
|
110
|
+
// Try fallback provider if available
|
|
111
|
+
if (this.fallbackProvider && (!provider || provider === this.defaultProvider)) {
|
|
112
|
+
try {
|
|
113
|
+
this.logger.info(`Trying fallback provider: ${this.fallbackProvider}`);
|
|
114
|
+
const fallbackLLM = this.getProvider(this.fallbackProvider);
|
|
115
|
+
return await fallbackLLM.generateCompletion(prompt, llmOptions);
|
|
116
|
+
} catch (fallbackError) {
|
|
117
|
+
this.logger.error(`Fallback provider also failed: ${fallbackError.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate embeddings with fallback support
|
|
127
|
+
*/
|
|
128
|
+
async generateEmbedding(text, options = {}) {
|
|
129
|
+
const { provider = null } = options;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const llmProvider = this.getProvider(provider);
|
|
133
|
+
return await llmProvider.generateEmbedding(text);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.logger.warn(`Primary provider embedding failed: ${error.message}`);
|
|
136
|
+
|
|
137
|
+
// Try fallback provider if available
|
|
138
|
+
if (this.fallbackProvider && (!provider || provider === this.defaultProvider)) {
|
|
139
|
+
try {
|
|
140
|
+
this.logger.info(`Trying fallback provider for embedding: ${this.fallbackProvider}`);
|
|
141
|
+
const fallbackLLM = this.getProvider(this.fallbackProvider);
|
|
142
|
+
return await fallbackLLM.generateEmbedding(text);
|
|
143
|
+
} catch (fallbackError) {
|
|
144
|
+
this.logger.error(`Fallback provider embedding also failed: ${fallbackError.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Calculate semantic similarity
|
|
154
|
+
*/
|
|
155
|
+
async calculateSimilarity(text1, text2, options = {}) {
|
|
156
|
+
const { provider = null } = options;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const llmProvider = this.getProvider(provider);
|
|
160
|
+
return await llmProvider.calculateSimilarity(text1, text2);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.logger.warn(`Primary provider similarity failed: ${error.message}`);
|
|
163
|
+
|
|
164
|
+
// Try fallback provider if available
|
|
165
|
+
if (this.fallbackProvider && (!provider || provider === this.defaultProvider)) {
|
|
166
|
+
try {
|
|
167
|
+
this.logger.info(`Trying fallback provider for similarity: ${this.fallbackProvider}`);
|
|
168
|
+
const fallbackLLM = this.getProvider(this.fallbackProvider);
|
|
169
|
+
return await fallbackLLM.calculateSimilarity(text1, text2);
|
|
170
|
+
} catch (fallbackError) {
|
|
171
|
+
this.logger.error(`Fallback provider similarity also failed: ${fallbackError.message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Generate query expansion suggestions
|
|
181
|
+
*/
|
|
182
|
+
async expandQuery(query, options = {}) {
|
|
183
|
+
const {
|
|
184
|
+
maxExpansions = 5,
|
|
185
|
+
includeContextual = true,
|
|
186
|
+
includeSynonyms = true,
|
|
187
|
+
includeRelated = true
|
|
188
|
+
} = options;
|
|
189
|
+
|
|
190
|
+
const systemPrompt = `You are a query expansion expert. Generate relevant search query variations for research purposes.
|
|
191
|
+
|
|
192
|
+
Rules:
|
|
193
|
+
1. Return only the query variations, one per line
|
|
194
|
+
2. Focus on research-oriented variations
|
|
195
|
+
3. Include different perspectives and angles
|
|
196
|
+
4. Maintain semantic relevance
|
|
197
|
+
5. Keep queries concise and searchable
|
|
198
|
+
6. Maximum ${maxExpansions} variations`;
|
|
199
|
+
|
|
200
|
+
let prompt = `Original query: "${query}"\n\nGenerate ${maxExpansions} research-focused query variations:`;
|
|
201
|
+
|
|
202
|
+
if (includeContextual) {
|
|
203
|
+
prompt += '\n- Include contextual variations';
|
|
204
|
+
}
|
|
205
|
+
if (includeSynonyms) {
|
|
206
|
+
prompt += '\n- Include synonym-based variations';
|
|
207
|
+
}
|
|
208
|
+
if (includeRelated) {
|
|
209
|
+
prompt += '\n- Include related concept variations';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const response = await this.generateCompletion(prompt, {
|
|
214
|
+
systemPrompt,
|
|
215
|
+
maxTokens: 300,
|
|
216
|
+
temperature: 0.8
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return response
|
|
220
|
+
.split('\n')
|
|
221
|
+
.map(line => line.trim())
|
|
222
|
+
.filter(line => line && !line.startsWith('-') && !line.includes(':'))
|
|
223
|
+
.slice(0, maxExpansions);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.logger.warn('LLM query expansion failed, using fallback', { error: error.message });
|
|
226
|
+
return this.fallbackQueryExpansion(query, maxExpansions);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Analyze content relevance to a topic
|
|
232
|
+
*/
|
|
233
|
+
async analyzeRelevance(content, topic, options = {}) {
|
|
234
|
+
const { maxContentLength = 2000 } = options;
|
|
235
|
+
|
|
236
|
+
const truncatedContent = content.length > maxContentLength
|
|
237
|
+
? content.substring(0, maxContentLength) + '...'
|
|
238
|
+
: content;
|
|
239
|
+
|
|
240
|
+
const systemPrompt = `You are a content relevance analyzer. Evaluate how relevant the given content is to the specified research topic.
|
|
241
|
+
|
|
242
|
+
Return a JSON object with:
|
|
243
|
+
{
|
|
244
|
+
"relevanceScore": 0.0-1.0,
|
|
245
|
+
"keyPoints": ["point1", "point2", ...],
|
|
246
|
+
"topicAlignment": "description of alignment",
|
|
247
|
+
"credibilityIndicators": ["indicator1", "indicator2", ...]
|
|
248
|
+
}`;
|
|
249
|
+
|
|
250
|
+
const prompt = `Research Topic: "${topic}"
|
|
251
|
+
|
|
252
|
+
Content to analyze:
|
|
253
|
+
${truncatedContent}
|
|
254
|
+
|
|
255
|
+
Analyze the relevance of this content to the research topic:`;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const response = await this.generateCompletion(prompt, {
|
|
259
|
+
systemPrompt,
|
|
260
|
+
maxTokens: 500,
|
|
261
|
+
temperature: 0.3
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const analysis = JSON.parse(response);
|
|
265
|
+
return {
|
|
266
|
+
relevanceScore: Math.max(0, Math.min(1, analysis.relevanceScore || 0.5)),
|
|
267
|
+
keyPoints: analysis.keyPoints || [],
|
|
268
|
+
topicAlignment: analysis.topicAlignment || '',
|
|
269
|
+
credibilityIndicators: analysis.credibilityIndicators || []
|
|
270
|
+
};
|
|
271
|
+
} catch (error) {
|
|
272
|
+
this.logger.warn('LLM relevance analysis failed, using fallback', { error: error.message });
|
|
273
|
+
return this.fallbackRelevanceAnalysis(content, topic);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Generate research synthesis
|
|
279
|
+
*/
|
|
280
|
+
async synthesizeFindings(findings, topic, options = {}) {
|
|
281
|
+
const { maxFindings = 10, includeConflicts = true } = options;
|
|
282
|
+
|
|
283
|
+
const limitedFindings = findings.slice(0, maxFindings);
|
|
284
|
+
|
|
285
|
+
const systemPrompt = `You are a research synthesis expert. Create a comprehensive synthesis of research findings on a given topic.
|
|
286
|
+
|
|
287
|
+
Generate a JSON response with:
|
|
288
|
+
{
|
|
289
|
+
"summary": "overall summary",
|
|
290
|
+
"keyInsights": ["insight1", "insight2", ...],
|
|
291
|
+
"themes": ["theme1", "theme2", ...],
|
|
292
|
+
"confidence": 0.0-1.0,
|
|
293
|
+
"gaps": ["gap1", "gap2", ...],
|
|
294
|
+
"recommendations": ["rec1", "rec2", ...]
|
|
295
|
+
}`;
|
|
296
|
+
|
|
297
|
+
const findingsText = limitedFindings
|
|
298
|
+
.map((finding, index) => `${index + 1}. ${finding.finding || finding.text || finding}`)
|
|
299
|
+
.join('\n');
|
|
300
|
+
|
|
301
|
+
const prompt = `Research Topic: "${topic}"
|
|
302
|
+
|
|
303
|
+
Research Findings:
|
|
304
|
+
${findingsText}
|
|
305
|
+
|
|
306
|
+
Synthesize these findings into a comprehensive analysis:`;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const response = await this.generateCompletion(prompt, {
|
|
310
|
+
systemPrompt,
|
|
311
|
+
maxTokens: 800,
|
|
312
|
+
temperature: 0.4
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return JSON.parse(response);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this.logger.warn('LLM synthesis failed, using fallback', { error: error.message });
|
|
318
|
+
return this.fallbackSynthesis(findings, topic);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Fallback query expansion without LLM
|
|
324
|
+
*/
|
|
325
|
+
fallbackQueryExpansion(query, maxExpansions) {
|
|
326
|
+
const variations = [];
|
|
327
|
+
const words = query.toLowerCase().split(/\s+/);
|
|
328
|
+
|
|
329
|
+
// Question variations
|
|
330
|
+
variations.push(`what is ${query}`);
|
|
331
|
+
variations.push(`how does ${query} work`);
|
|
332
|
+
variations.push(`${query} research`);
|
|
333
|
+
variations.push(`${query} analysis`);
|
|
334
|
+
variations.push(`latest ${query}`);
|
|
335
|
+
|
|
336
|
+
return variations.slice(0, maxExpansions);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Fallback relevance analysis without LLM
|
|
341
|
+
*/
|
|
342
|
+
fallbackRelevanceAnalysis(content, topic) {
|
|
343
|
+
const topicWords = topic.toLowerCase().split(/\s+/);
|
|
344
|
+
const contentWords = content.toLowerCase().split(/\s+/);
|
|
345
|
+
|
|
346
|
+
const matches = topicWords.filter(word =>
|
|
347
|
+
contentWords.some(cWord => cWord.includes(word) || word.includes(cWord))
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const relevanceScore = matches.length / topicWords.length;
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
relevanceScore: Math.min(1, relevanceScore),
|
|
354
|
+
keyPoints: [content.substring(0, 100) + '...'],
|
|
355
|
+
topicAlignment: `Found ${matches.length}/${topicWords.length} topic keywords`,
|
|
356
|
+
credibilityIndicators: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Fallback synthesis without LLM
|
|
362
|
+
*/
|
|
363
|
+
fallbackSynthesis(findings, topic) {
|
|
364
|
+
return {
|
|
365
|
+
summary: `Collected ${findings.length} findings related to ${topic}`,
|
|
366
|
+
keyInsights: findings.slice(0, 3).map(f => f.finding || f.text || f),
|
|
367
|
+
themes: ['general research'],
|
|
368
|
+
confidence: 0.5,
|
|
369
|
+
gaps: ['Limited synthesis without LLM'],
|
|
370
|
+
recommendations: ['Use LLM provider for detailed synthesis']
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if any LLM provider is available
|
|
376
|
+
*/
|
|
377
|
+
isAvailable() {
|
|
378
|
+
return this.providers.size > 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get available providers metadata
|
|
383
|
+
*/
|
|
384
|
+
getProvidersMetadata() {
|
|
385
|
+
const metadata = {};
|
|
386
|
+
for (const [name, provider] of this.providers) {
|
|
387
|
+
metadata[name] = provider.getMetadata();
|
|
388
|
+
}
|
|
389
|
+
return metadata;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Health check for all providers
|
|
394
|
+
*/
|
|
395
|
+
async healthCheck() {
|
|
396
|
+
const health = {};
|
|
397
|
+
|
|
398
|
+
for (const [name, provider] of this.providers) {
|
|
399
|
+
try {
|
|
400
|
+
const isAvailable = await provider.isAvailable();
|
|
401
|
+
health[name] = {
|
|
402
|
+
available: isAvailable,
|
|
403
|
+
metadata: provider.getMetadata()
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
health[name] = {
|
|
407
|
+
available: false,
|
|
408
|
+
error: error.message
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return health;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Logger } from '../../utils/Logger.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base LLM Provider class
|
|
5
|
+
* Defines the interface that all LLM providers must implement
|
|
6
|
+
*/
|
|
7
|
+
export class LLMProvider {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.logger = new Logger({ component: 'LLMProvider' });
|
|
10
|
+
this.config = options;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a completion from the LLM
|
|
15
|
+
* @param {string} prompt - The input prompt
|
|
16
|
+
* @param {Object} options - Generation options
|
|
17
|
+
* @returns {Promise<string>} Generated text
|
|
18
|
+
*/
|
|
19
|
+
async generateCompletion(prompt, options = {}) {
|
|
20
|
+
throw new Error('generateCompletion must be implemented by subclass');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate embeddings for semantic similarity
|
|
25
|
+
* @param {string} text - Text to embed
|
|
26
|
+
* @returns {Promise<number[]>} Embedding vector
|
|
27
|
+
*/
|
|
28
|
+
async generateEmbedding(text) {
|
|
29
|
+
throw new Error('generateEmbedding must be implemented by subclass');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Calculate semantic similarity between two texts
|
|
34
|
+
* @param {string} text1 - First text
|
|
35
|
+
* @param {string} text2 - Second text
|
|
36
|
+
* @returns {Promise<number>} Similarity score (0-1)
|
|
37
|
+
*/
|
|
38
|
+
async calculateSimilarity(text1, text2) {
|
|
39
|
+
const embedding1 = await this.generateEmbedding(text1);
|
|
40
|
+
const embedding2 = await this.generateEmbedding(text2);
|
|
41
|
+
return this.cosineSimilarity(embedding1, embedding2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculate cosine similarity between two vectors
|
|
46
|
+
* @param {number[]} vec1 - First vector
|
|
47
|
+
* @param {number[]} vec2 - Second vector
|
|
48
|
+
* @returns {number} Similarity score (0-1)
|
|
49
|
+
*/
|
|
50
|
+
cosineSimilarity(vec1, vec2) {
|
|
51
|
+
if (vec1.length !== vec2.length) {
|
|
52
|
+
throw new Error('Vectors must have the same length');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let dotProduct = 0;
|
|
56
|
+
let norm1 = 0;
|
|
57
|
+
let norm2 = 0;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < vec1.length; i++) {
|
|
60
|
+
dotProduct += vec1[i] * vec2[i];
|
|
61
|
+
norm1 += vec1[i] * vec1[i];
|
|
62
|
+
norm2 += vec2[i] * vec2[i];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const magnitude = Math.sqrt(norm1) * Math.sqrt(norm2);
|
|
66
|
+
return magnitude > 0 ? dotProduct / magnitude : 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if the provider is available and configured
|
|
71
|
+
* @returns {Promise<boolean>} True if available
|
|
72
|
+
*/
|
|
73
|
+
async isAvailable() {
|
|
74
|
+
try {
|
|
75
|
+
await this.generateCompletion('test', { maxTokens: 1 });
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get provider metadata
|
|
84
|
+
* @returns {Object} Provider information
|
|
85
|
+
*/
|
|
86
|
+
getMetadata() {
|
|
87
|
+
return {
|
|
88
|
+
name: this.constructor.name,
|
|
89
|
+
config: this.config,
|
|
90
|
+
capabilities: {
|
|
91
|
+
completion: true,
|
|
92
|
+
embedding: false,
|
|
93
|
+
similarity: false
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { LLMProvider } from './LLMProvider.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAI API Provider
|
|
5
|
+
* Implements LLM operations using OpenAI's GPT models
|
|
6
|
+
*/
|
|
7
|
+
export class OpenAIProvider extends LLMProvider {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
super(options);
|
|
10
|
+
|
|
11
|
+
this.apiKey = options.apiKey || process.env.OPENAI_API_KEY;
|
|
12
|
+
this.baseURL = options.baseURL || 'https://api.openai.com/v1';
|
|
13
|
+
this.model = options.model || 'gpt-3.5-turbo';
|
|
14
|
+
this.embeddingModel = options.embeddingModel || 'text-embedding-ada-002';
|
|
15
|
+
this.timeout = options.timeout || 30000;
|
|
16
|
+
|
|
17
|
+
if (!this.apiKey) {
|
|
18
|
+
this.logger.warn('OpenAI API key not configured');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async generateCompletion(prompt, options = {}) {
|
|
23
|
+
if (!this.apiKey) {
|
|
24
|
+
throw new Error('OpenAI API key not configured');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
maxTokens = 1000,
|
|
29
|
+
temperature = 0.7,
|
|
30
|
+
systemPrompt = null,
|
|
31
|
+
stopSequences = null
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
const messages = [];
|
|
35
|
+
|
|
36
|
+
if (systemPrompt) {
|
|
37
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
messages.push({ role: 'user', content: prompt });
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
47
|
+
'Content-Type': 'application/json'
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
model: this.model,
|
|
51
|
+
messages,
|
|
52
|
+
max_tokens: maxTokens,
|
|
53
|
+
temperature,
|
|
54
|
+
stop: stopSequences
|
|
55
|
+
}),
|
|
56
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorData = await response.json().catch(() => ({}));
|
|
61
|
+
throw new Error(`OpenAI API error: ${response.status} - ${errorData.error?.message || response.statusText}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
|
|
66
|
+
if (!data.choices || data.choices.length === 0) {
|
|
67
|
+
throw new Error('No completion generated');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return data.choices[0].message.content.trim();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.logger.error('OpenAI completion failed', { error: error.message });
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async generateEmbedding(text) {
|
|
78
|
+
if (!this.apiKey) {
|
|
79
|
+
throw new Error('OpenAI API key not configured');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`${this.baseURL}/embeddings`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
87
|
+
'Content-Type': 'application/json'
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
model: this.embeddingModel,
|
|
91
|
+
input: text
|
|
92
|
+
}),
|
|
93
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const errorData = await response.json().catch(() => ({}));
|
|
98
|
+
throw new Error(`OpenAI API error: ${response.status} - ${errorData.error?.message || response.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
|
|
103
|
+
if (!data.data || data.data.length === 0) {
|
|
104
|
+
throw new Error('No embedding generated');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return data.data[0].embedding;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.logger.error('OpenAI embedding failed', { error: error.message });
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getMetadata() {
|
|
115
|
+
return {
|
|
116
|
+
...super.getMetadata(),
|
|
117
|
+
name: 'OpenAI',
|
|
118
|
+
model: this.model,
|
|
119
|
+
embeddingModel: this.embeddingModel,
|
|
120
|
+
capabilities: {
|
|
121
|
+
completion: true,
|
|
122
|
+
embedding: true,
|
|
123
|
+
similarity: true
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|