UNPKG

dbx-mcp-server

Version:

A Model Context Protocol server for Dropbox integration with AI-powered PDF analysis, multi-directory indexing, and parallel processing

215 lines 9.81 kB
import { log } from './config.js'; import { schemaManager } from './schema-manager.js'; const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"; export class AIParser { constructor() { this.apiKey = process.env.OPENROUTER_API_KEY || ''; if (!this.apiKey) { throw new Error('OPENROUTER_API_KEY environment variable is required'); } } async parseDocument(fileBuffer, fileName, mimeType, options = {}) { const { useThinking = false, maxReasoningTokens = 5000, maxRetries = 3 } = options; // Convert buffer to base64 for OpenRouter PDF API const fileBase64 = fileBuffer.toString('base64'); const dataUri = `data:${mimeType};base64,${fileBase64}`; // Use the specified Gemini model only const model = useThinking ? "google/gemini-2.5-flash-preview-05-20:thinking" : "google/gemini-2.5-flash-preview-05-20"; // Simplified prompt for structured output const prompt = this.buildStructuredPrompt(); // Prepare messages with proper OpenRouter PDF format const messages = [{ role: "user", content: [ { type: "text", text: prompt }, { type: "file", file: { filename: fileName, file_data: dataUri, }, }, ], }]; // Call OpenRouter API with retry logic and proper PDF processing const response = await this.callOpenRouterWithRetry(model, messages, fileName, dataUri, maxReasoningTokens, maxRetries); // Parse and validate response return this.parseAIResponse(response); } async parseWithRetry(fileBuffer, fileName, mimeType, options = {}) { try { return await this.parseDocument(fileBuffer, fileName, mimeType, options); } catch (error) { log.error('AI parsing failed', { fileName, error: error instanceof Error ? error.message : String(error) }); throw new AIParsingError(`Failed to parse document: ${error instanceof Error ? error.message : String(error)}`, fileName); } } buildStructuredPrompt() { return schemaManager.generateAnalysisPrompt(); } async callOpenRouterWithRetry(model, messages, fileName, dataUri, maxReasoningTokens, maxRetries) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { // Determine optimal PDF processing engine based on model capabilities const pdfEngine = this.selectOptimalPDFEngine(model); const requestBody = { model, messages, response_format: { type: "json_schema", json_schema: schemaManager.generateJSONSchema() }, plugins: [ { id: "file-parser", pdf: { engine: pdfEngine } } ] }; if (maxReasoningTokens && model.includes("thinking")) { requestBody.max_reasoning_tokens = maxReasoningTokens; } log.info('Calling OpenRouter API for PDF processing', { model, pdfEngine, fileName, fileSizeKB: Math.round(dataUri.length * 0.75 / 1024), // Estimate original size from base64 attempt, maxRetries }); const response = await fetch(`${OPENROUTER_BASE_URL}/chat/completions`, { method: "POST", headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json", "HTTP-Referer": "https://github.com/markov-github/dbx-mcp-server", "X-Title": "Dropbox MCP PDF Analyzer", }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); const error = new Error(`OpenRouter API error: ${response.status} - ${errorText}`); log.warn('OpenRouter API error', { status: response.status, error: errorText, fileName, attempt, maxRetries }); // Handle specific error types with different retry strategies if (response.status === 503) { // Service temporarily unavailable - longer delay const delay = Math.pow(2, attempt) * 2000; // Exponential backoff with longer delays log.info(`Service limit reached, retrying after ${delay}ms...`, { fileName }); await new Promise(resolve => setTimeout(resolve, delay)); lastError = error; continue; } else if (response.status >= 500 && attempt < maxRetries) { // Other server errors - standard retry const delay = Math.pow(2, attempt - 1) * 1000; log.info(`Server error, retrying after ${delay}ms...`, { fileName }); await new Promise(resolve => setTimeout(resolve, delay)); lastError = error; continue; } else if (response.status === 429 && attempt < maxRetries) { // Rate limiting - longer delay const delay = Math.pow(2, attempt) * 3000; log.info(`Rate limited, retrying after ${delay}ms...`, { fileName }); await new Promise(resolve => setTimeout(resolve, delay)); lastError = error; continue; } throw error; } const data = await response.json(); const content = data.choices[0]?.message?.content; if (!content) { throw new Error('No content returned from OpenRouter API'); } // With structured outputs, content should already be valid JSON const parsedResult = JSON.parse(content); log.info('OpenRouter API PDF processing successful', { fileName, attempt, model, pdfEngine, docType: parsedResult.doc_type, confidence: parsedResult.confidence_score }); return content; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt === maxRetries) { log.error('All OpenRouter API retry attempts failed', { fileName, error: lastError.message, maxRetries }); break; } log.warn('OpenRouter API attempt failed, retrying...', { fileName, attempt, error: lastError.message }); // Base delay with some randomization to avoid thundering herd const baseDelay = Math.pow(2, attempt - 1) * 1000; const jitter = Math.random() * 500; const delay = baseDelay + jitter; await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } selectOptimalPDFEngine(model) { // Gemini 2.5 flash preview has native file processing capabilities if (model.includes('gemini-2.5-flash-preview')) { return 'native'; } // Fallback to pdf-text for free processing (should not be needed with our model) return 'pdf-text'; } parseAIResponse(response) { try { // With structured outputs, the response should already be valid JSON const parsed = JSON.parse(response); log.info('Successfully parsed structured AI response', { doc_type: parsed.doc_type, confidence_score: parsed.confidence_score, fieldsCount: Object.keys(parsed.fields || {}).length }); return parsed; } catch (error) { log.error('Failed to parse structured AI response', { response: response.substring(0, 500), error: error instanceof Error ? error.message : String(error) }); // This should rarely happen with structured outputs throw new AIParsingError(`Failed to parse structured response: ${error instanceof Error ? error.message : String(error)}`); } } } export class AIParsingError extends Error { constructor(message, fileName) { super(`AI Parsing Error: ${message}`); this.fileName = fileName; this.name = 'AIParsingError'; } } // Export singleton instance export const aiParser = new AIParser(); //# sourceMappingURL=ai-parser.js.map