// Backend service for RFP Management System const API_BASE_URL = '/api'; export interface StoredDocument { id: string; name: string; category?: string; fileType: string; uploadDate: string; size?: number; contentLength?: number; processed?: boolean; } export interface CurrentRFPDocument { id: string; name: string; fileType: string; uploadDate: string; contentLength?: number; processed?: boolean; } export interface DocumentWithContent { id: string; name: string; category?: string; fileType: string; uploadDate: string; content: string; } class BackendService { /** * Upload a previous RFP document to backend storage */ async uploadPreviousRFP(file: File, category: string): Promise { const formData = new FormData(); formData.append('file', file); formData.append('category', category); const response = await fetch(`${API_BASE_URL}/upload-previous-rfp`, { method: 'POST', body: formData }); if (!response.ok) { throw new Error(`Upload failed: ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Upload failed'); } return result.document; } /** * Get all previous RFP documents */ async getPreviousRFPs(): Promise { const response = await fetch(`${API_BASE_URL}/previous-rfps`); if (!response.ok) { throw new Error(`Failed to fetch documents: ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to fetch documents'); } return result.documents; } /** * Get document content by ID */ async getDocumentContent(id: string): Promise { const response = await fetch(`${API_BASE_URL}/document/${id}/content`); if (!response.ok) { throw new Error(`Failed to fetch document content: ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to fetch document content'); } return { ...result.document, content: result.content }; } /** * Delete a document */ async deleteDocument(id: string): Promise { const response = await fetch(`${API_BASE_URL}/document/${id}`, { method: 'DELETE' }); if (!response.ok) { throw new Error(`Failed to delete document: ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to delete document'); } } /** * Get documents filtered by category */ async getPreviousRFPsByCategory(category: string): Promise { const allDocuments = await this.getPreviousRFPs(); return allDocuments.filter(doc => doc.category === category); } /** * Get all document contents for AI processing, optionally filtered by category */ async getDocumentsForAI(categoryFilter?: string): Promise> { try { const documents = categoryFilter ? await this.getPreviousRFPsByCategory(categoryFilter) : await this.getPreviousRFPs(); const documentsWithContent = await Promise.all( documents.map(async (doc) => { try { const docWithContent = await this.getDocumentContent(doc.id); return { content: docWithContent.content, source: doc.name, category: doc.category || 'general', pageNumber: 1, // Default page number, can be enhanced relevanceScore: 1.0 // Default relevance score }; } catch (error) { console.warn(`Failed to get content for document ${doc.name}:`, error); return null; } }) ); return documentsWithContent.filter(doc => doc !== null) as Array<{ content: string; source: string; category: string; pageNumber: number; relevanceScore: number; }>; } catch (error) { console.error('Failed to get documents for AI:', error); return []; } } /** * Search documents with semantic similarity */ async searchDocuments(query: string, options?: { topK?: number; categoryFilter?: string; useSemanticSearch?: boolean; }): Promise> { try { // Import vector storage const { vectorStorage } = await import('./vectorStorage'); // Check if we have documents in vector storage const stats = vectorStorage.getStats(); if (stats.totalChunks === 0) { console.log('Vector storage empty, populating with documents...'); await this.populateVectorStorage(); } // Perform search const searchMethod = options?.useSemanticSearch !== false ? vectorStorage.semanticSearch.bind(vectorStorage) : vectorStorage.search.bind(vectorStorage); const results = searchMethod(query, { topK: options?.topK || 10, categoryFilter: options?.categoryFilter }); return results.map(result => ({ content: result.chunk.content, source: result.chunk.metadata.source, category: result.chunk.metadata.category || 'general', relevanceScore: result.score, matchedKeywords: result.matchedKeywords })); } catch (error) { console.error('Search documents failed:', error); // Fallback to basic retrieval const basicResults = await this.getDocumentsForAI(options?.categoryFilter); return basicResults.map(doc => ({ content: doc.content, source: doc.source, category: doc.category, relevanceScore: doc.relevanceScore, matchedKeywords: [] })); } } /** * Populate vector storage with all documents */ private async populateVectorStorage(): Promise { try { const { vectorStorage } = await import('./vectorStorage'); const documents = await this.getPreviousRFPs(); console.log(`Populating vector storage with ${documents.length} documents...`); for (const doc of documents) { try { const docWithContent = await this.getDocumentContent(doc.id); await vectorStorage.addDocument(docWithContent.content, { source: doc.name, category: doc.category, pageNumber: 1 }); } catch (error) { console.warn(`Failed to add document ${doc.name} to vector storage:`, error); } } const stats = vectorStorage.getStats(); console.log(`Vector storage populated: ${stats.totalChunks} chunks from ${stats.totalDocuments} documents`); } catch (error) { console.error('Failed to populate vector storage:', error); } } async healthCheck(): Promise { try { const response = await fetch(`${API_BASE_URL}/health`); return response.ok; } catch { return false; } } /** * Format answer using Ollama AI */ async formatAnswer(question: string, rawAnswer: string, context?: string): Promise<{ success: boolean; formattedAnswer: string; originalAnswer: string; }> { try { console.log('🔧 BackendService.formatAnswer called with:', { questionLength: question?.length || 0, answerLength: rawAnswer?.length || 0, context: context || 'none' }); const requestBody = { question, rawAnswer, context }; console.log('📤 Making request to:', `${API_BASE_URL}/format-answer`); console.log('📦 Request body:', requestBody); const response = await fetch(`${API_BASE_URL}/format-answer`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); console.log('📥 Response status:', response.status, response.statusText); if (!response.ok) { const errorText = await response.text(); console.error('❌ Response not ok:', errorText); throw new Error(`Format answer failed: ${response.statusText} - ${errorText}`); } const result = await response.json(); console.log('✅ Format result:', result); return { success: result.success, formattedAnswer: result.formattedAnswer || rawAnswer, originalAnswer: result.originalAnswer || rawAnswer }; } catch (error) { console.error('❌ Failed to format answer:', error); if (error instanceof Error) { console.error('❌ Error details:', { name: error.name, message: error.message, stack: error.stack }); } // Return original answer as fallback return { success: false, formattedAnswer: rawAnswer, originalAnswer: rawAnswer }; } } /** * Generate a comprehensive 1500+ word answer across all reference documents */ async comprehensiveAnswer(question: string): Promise<{ success: boolean; question: string; generatedAnswer: string; wordCount: number; citations: Array<{ citation: string; raw: string }>; sourceDocuments?: Array<{ id: number; file: string }>; error?: string; }> { try { const response = await fetch(`${API_BASE_URL}/generate-comprehensive-rfp`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }) }); if (!response.ok) { const text = await response.text(); throw new Error(`Comprehensive generation failed: ${response.status} ${text}`); } const result = await response.json(); if (!result.success) { return { success: false, question, generatedAnswer: result.generatedAnswer || 'NO ANSWER GENERATED', wordCount: result.wordCount || 0, citations: result.citations || [], sourceDocuments: result.sourceDocuments, error: result.error || 'Comprehensive generation failed' }; } return { success: true, question: result.question, generatedAnswer: result.generatedAnswer, wordCount: result.wordCount, citations: result.citations || [], sourceDocuments: result.sourceDocuments }; } catch (error) { console.error('❌ Comprehensive answer error:', error); return { success: false, question, generatedAnswer: 'Failed to generate comprehensive answer.', wordCount: 0, citations: [], error: error instanceof Error ? error.message : 'Unknown error' }; } } } export const backendService = new BackendService();