Spaces:
Sleeping
Sleeping
| // 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 with enhanced processing | |
| */ | |
| async uploadPreviousRFP(file: File, category: string = 'general'): Promise<StoredDocument> { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| formData.append('category', category); | |
| formData.append('enhanced_processing', 'true'); // Request enhanced AI processing | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/upload-previous-rfp`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`Upload failed: ${response.statusText} - ${errorText}`); | |
| } | |
| const result = await response.json(); | |
| if (!result.success) { | |
| throw new Error(result.error || 'Upload failed'); | |
| } | |
| console.log('β Previous RFP uploaded successfully:', result.document.name); | |
| return result.document; | |
| } catch (error) { | |
| console.error('β Previous RFP upload failed:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get all previous RFP documents | |
| */ | |
| async getPreviousRFPs(): Promise<StoredDocument[]> { | |
| 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<DocumentWithContent> { | |
| 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<void> { | |
| 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<StoredDocument[]> { | |
| const allDocuments = await this.getPreviousRFPs(); | |
| return allDocuments.filter(doc => doc.category === category); | |
| } | |
| /** | |
| * Get all document contents for AI processing with enhanced relevance scoring | |
| */ | |
| async getDocumentsForAI(categoryFilter?: string): Promise<Array<{ | |
| content: string; | |
| source: string; | |
| category: string; | |
| pageNumber: number; | |
| relevanceScore: number; | |
| }>> { | |
| try { | |
| console.log(`π Fetching documents for AI processing${categoryFilter ? ` (category: ${categoryFilter})` : ''}`); | |
| const documents = categoryFilter | |
| ? await this.getPreviousRFPsByCategory(categoryFilter) | |
| : await this.getPreviousRFPs(); | |
| console.log(`π Found ${documents.length} documents to process`); | |
| const documentsWithContent = await Promise.all( | |
| documents.map(async (doc) => { | |
| try { | |
| const docWithContent = await this.getDocumentContent(doc.id); | |
| // Enhanced content processing for better AI accuracy | |
| const processedContent = this.preprocessContentForAI(docWithContent.content); | |
| const qualityScore = this.calculateDocumentQuality(processedContent); | |
| return { | |
| content: processedContent, | |
| source: doc.name, | |
| category: doc.category || 'general', | |
| pageNumber: 1, // Could be enhanced with actual page detection | |
| relevanceScore: qualityScore | |
| }; | |
| } catch (error) { | |
| console.warn(`β οΈ Failed to load content for document ${doc.name}:`, error); | |
| return null; | |
| } | |
| }) | |
| ); | |
| const validDocuments = documentsWithContent | |
| .filter(doc => doc !== null) | |
| .filter(doc => doc!.content.length > 100); // Filter out very short documents | |
| console.log(`β Successfully processed ${validDocuments.length} documents for AI`); | |
| return validDocuments as Array<{ | |
| content: string; | |
| source: string; | |
| category: string; | |
| pageNumber: number; | |
| relevanceScore: number; | |
| }>; | |
| } catch (error) { | |
| console.error('β Error fetching documents for AI:', error); | |
| return []; | |
| } | |
| } | |
| /** | |
| * Preprocess document content for better AI analysis | |
| */ | |
| private preprocessContentForAI(content: string): string { | |
| // Remove excessive whitespace and normalize line breaks | |
| let processed = content.replace(/\s+/g, ' ').trim(); | |
| // Remove common PDF artifacts | |
| processed = processed.replace(/\f/g, ' '); // Form feed characters | |
| processed = processed.replace(/[\u0000-\u001F\u007F-\u009F]/g, ' '); // Control characters | |
| // Normalize quotes and dashes | |
| processed = processed.replace(/[""]/g, '"'); | |
| processed = processed.replace(/['']/g, "'"); | |
| processed = processed.replace(/[ββ]/g, '-'); | |
| return processed; | |
| } | |
| /** | |
| * Calculate document quality score for relevance ranking | |
| */ | |
| private calculateDocumentQuality(content: string): number { | |
| let score = 0.5; // Base score | |
| // Length bonus (longer documents often have more context) | |
| const wordCount = content.split(/\s+/).length; | |
| if (wordCount > 5000) score += 0.3; | |
| else if (wordCount > 2000) score += 0.2; | |
| else if (wordCount > 500) score += 0.1; | |
| // Structure indicators | |
| if (/\d+\.|β’|βͺ|β¦/.test(content)) score += 0.1; // Has bullet points or numbering | |
| if (/section|chapter|appendix/gi.test(content)) score += 0.1; // Has sections | |
| // Business content indicators | |
| const businessTerms = [ | |
| 'experience', 'services', 'approach', 'methodology', 'team', 'qualifications', | |
| 'implementation', 'project', 'deliverable', 'timeline', 'budget', 'compliance' | |
| ]; | |
| const termMatches = businessTerms.filter(term => | |
| content.toLowerCase().includes(term) | |
| ).length; | |
| score += Math.min(0.2, termMatches * 0.02); | |
| return Math.min(1.0, score); | |
| } | |
| 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 []; | |
| } | |
| } | |
| async healthCheck(): Promise<boolean> { | |
| 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(); | |