diff --git a/app/projects/components/project-overview.tsx b/app/projects/components/project-overview.tsx index df05917e..47e49c9e 100644 --- a/app/projects/components/project-overview.tsx +++ b/app/projects/components/project-overview.tsx @@ -244,6 +244,30 @@ export function ProjectOverview({ onViewQuestions, projectId, orgId }: ProjectOv )} + {/* Vendor Eligibility */} + {project.eligibility && project.eligibility.length > 0 && ( + + + + + Vendor Eligibility Requirements + + + + + + + )} + {/* Consolidated Project Summary */} diff --git a/lib/interfaces/ai-service.ts b/lib/interfaces/ai-service.ts index 88af81f6..9f2034df 100644 --- a/lib/interfaces/ai-service.ts +++ b/lib/interfaces/ai-service.ts @@ -13,6 +13,11 @@ export interface IAIQuestionExtractor { * Generate a summary of the RFP document */ generateSummary(content: string, documentName: string): Promise; + + /** + * Extract vendor eligibility requirements as bullet points + */ + extractEligibility(content: string, documentName: string): Promise; } /** diff --git a/lib/project-service.ts b/lib/project-service.ts index efdb8bfa..5fc1d634 100644 --- a/lib/project-service.ts +++ b/lib/project-service.ts @@ -25,6 +25,7 @@ export const projectService = { name: true, description: true, summary: true, + eligibility: true, createdAt: true, updatedAt: true, organizationId: true @@ -42,6 +43,7 @@ export const projectService = { name: true, description: true, summary: true, + eligibility: true, createdAt: true, updatedAt: true, organizationId: true, @@ -68,6 +70,7 @@ export const projectService = { name: true, description: true, summary: true, + eligibility: true, createdAt: true, updatedAt: true, organizationId: true, @@ -293,6 +296,20 @@ export const projectService = { } }, + // Eligibility operations + async saveEligibility(projectId: string, eligibility: string[]) { + try { + await db.project.update({ + where: { id: projectId }, + data: { eligibility }, + }); + console.log(`Successfully saved eligibility for project ${projectId}`); + } catch (error) { + console.error(`Error saving eligibility for project ${projectId}:`, error); + throw error; + } + }, + // Answer operations async saveAnswers(projectId: string, answers: Record) { console.log(`Saving answers for project ${projectId}. Total answers: ${Object.keys(answers).length}`); diff --git a/lib/services/openai-question-extractor.ts b/lib/services/openai-question-extractor.ts index e2826461..7854efae 100644 --- a/lib/services/openai-question-extractor.ts +++ b/lib/services/openai-question-extractor.ts @@ -60,6 +60,49 @@ export class OpenAIQuestionExtractor implements IAIQuestionExtractor { } } + /** + * Extract vendor eligibility requirements from RFP document + */ + async extractEligibility(content: string, documentName: string): Promise { + try { + const systemPrompt = this.getEligibilitySystemPrompt(); + + const response = await this.client.chat.completions.create({ + model: this.config.model, + response_format: { type: "json_object" }, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: this.formatUserPrompt(content, documentName) } + ], + temperature: 0.1, // Low temperature for precise extraction + max_tokens: 1000, // Allow for comprehensive eligibility lists + }); + + const assistantMessage = response.choices[0]?.message?.content; + if (!assistantMessage) { + throw new AIServiceError('Empty response from OpenAI for eligibility extraction'); + } + + // Parse and validate the JSON response + const rawData = JSON.parse(assistantMessage); + + // Expect format: { "eligibility": ["requirement 1", "requirement 2", ...] } + if (!rawData.eligibility || !Array.isArray(rawData.eligibility)) { + throw new AIServiceError('Invalid eligibility format from AI service'); + } + + return rawData.eligibility.filter((item: any) => typeof item === 'string' && item.trim().length > 0); + } catch (error) { + if (error instanceof SyntaxError) { + throw new AIServiceError('Invalid JSON response from AI service for eligibility extraction'); + } + if (error instanceof AIServiceError) { + throw error; + } + throw new AIServiceError(`Eligibility extraction failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + /** * Extract structured questions from document content */ @@ -122,6 +165,42 @@ Do not include section numbers, question lists, or administrative details like s `.trim(); } + /** + * Get the system prompt for vendor eligibility extraction + */ + private getEligibilitySystemPrompt(): string { + return ` +You are an expert at analyzing RFP (Request for Proposal) documents and extracting vendor eligibility requirements. + +Your task is to read through the RFP document and identify all key eligibility criteria that vendors must meet to qualify for this proposal. Focus on extracting: + +1. Minimum experience requirements (years in business, project experience) +2. Technical qualifications and certifications +3. Financial requirements (bonding, insurance, revenue thresholds) +4. Geographic restrictions or preferences +5. Industry-specific licenses or accreditations +6. Staff qualifications and expertise requirements +7. Past performance criteria +8. Legal and compliance requirements +9. Size classifications (small business, minority-owned, etc.) +10. Any other mandatory qualifications mentioned + +Format your response as a JSON object with an "eligibility" array containing clear, concise bullet points. Each requirement should be a standalone statement that a vendor can easily evaluate against their own qualifications. + +Example format: +{ + "eligibility": [ + "Minimum 5 years of experience in software development", + "Must hold current ISO 27001 certification", + "Annual revenue of at least $10 million", + "Licensed to operate in the State of California" + ] +} + +Focus only on mandatory requirements, not preferences. If no clear eligibility criteria are found, return an empty array. + `.trim(); + } + /** * Get the system prompt for question extraction */ diff --git a/lib/services/question-extraction-service.ts b/lib/services/question-extraction-service.ts index 6c7b30e0..920a867a 100644 --- a/lib/services/question-extraction-service.ts +++ b/lib/services/question-extraction-service.ts @@ -13,16 +13,18 @@ export class QuestionExtractionService implements IQuestionExtractionService { */ async processDocument(request: ExtractQuestionsRequest): Promise { try { - // Stage 1: Extract questions and generate summary using AI (parallel execution) - const [extractedQuestions, summary] = await Promise.all([ + // Stage 1: Extract questions, generate summary, and extract eligibility using AI (parallel execution) + const [extractedQuestions, summary, eligibility] = await Promise.all([ this.extractQuestions(request.content, request.documentName), - this.generateSummary(request.content, request.documentName) + this.generateSummary(request.content, request.documentName), + this.extractEligibility(request.content, request.documentName) ]); - // Stage 2: Save to database (questions and summary) + // Stage 2: Save to database (questions, summary, and eligibility) await Promise.all([ this.saveQuestions(request.projectId, extractedQuestions.sections), - this.saveSummary(request.projectId, summary) + this.saveSummary(request.projectId, summary), + this.saveEligibility(request.projectId, eligibility) ]); // Stage 3: Return structured response @@ -32,6 +34,7 @@ export class QuestionExtractionService implements IQuestionExtractionService { sections: extractedQuestions.sections, extractedAt: new Date().toISOString(), summary, // Include summary in response + eligibility, // Include eligibility in response }; return response; @@ -65,6 +68,17 @@ export class QuestionExtractionService implements IQuestionExtractionService { } } + /** + * Extract eligibility requirements from content using AI + */ + private async extractEligibility(content: string, documentName: string): Promise { + try { + return await openAIQuestionExtractor.extractEligibility(content, documentName); + } catch (error) { + throw new AIServiceError(`AI eligibility extraction failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + /** * Save extracted questions to storage */ @@ -87,6 +101,17 @@ export class QuestionExtractionService implements IQuestionExtractionService { } } + /** + * Save extracted eligibility requirements to storage + */ + async saveEligibility(projectId: string, eligibility: string[]): Promise { + try { + await projectService.saveEligibility(projectId, eligibility); + } catch (error) { + throw new DatabaseError(`Failed to save eligibility: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + /** * Get extraction statistics */ diff --git a/lib/validators/extract-questions.ts b/lib/validators/extract-questions.ts index 22869fcb..c886fe42 100644 --- a/lib/validators/extract-questions.ts +++ b/lib/validators/extract-questions.ts @@ -31,6 +31,7 @@ export const ExtractQuestionsResponseSchema = z.object({ sections: z.array(SectionSchema), extractedAt: z.string(), summary: z.string().optional(), // RFP summary generated by AI + eligibility: z.array(z.string()).optional(), // Vendor eligibility requirements as bullet points }); // Type exports diff --git a/prisma/migrations/20250813184727_add_project_eligibility/migration.sql b/prisma/migrations/20250813184727_add_project_eligibility/migration.sql new file mode 100644 index 00000000..d3aec581 --- /dev/null +++ b/prisma/migrations/20250813184727_add_project_eligibility/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "projects" ADD COLUMN "eligibility" TEXT[]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 341586d0..4650a741 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,6 +72,7 @@ model Project { name String description String? summary String? // RFP summary generated from uploaded documents + eligibility String[] // Vendor eligibility requirements as bullet points createdAt DateTime @default(now()) updatedAt DateTime @updatedAt questions Question[] diff --git a/types/project.ts b/types/project.ts index e3bdcb48..b4c2b72e 100644 --- a/types/project.ts +++ b/types/project.ts @@ -3,6 +3,7 @@ export interface Project { name: string; description?: string; summary?: string; // RFP summary generated from uploaded documents + eligibility?: string[]; // Vendor eligibility requirements as bullet points createdAt: string; progress?: number; status?: string;