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 | |
| */ | |
| async uploadPreviousRFP(file: File, category: string): Promise<StoredDocument> { | |
| 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<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, optionally filtered by category | |
| */ | |
| async getDocumentsForAI(categoryFilter?: string): Promise<Array<{ | |
| content: string; | |
| source: string; | |
| category: string; | |
| pageNumber: number; | |
| relevanceScore: number; | |
| }>> { | |
| 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<Array<{ | |
| content: string; | |
| source: string; | |
| category: string; | |
| relevanceScore: number; | |
| matchedKeywords: string[]; | |
| }>> { | |
| 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<void> { | |
| 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<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(); | |