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
JavaScript
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