correctly generates RFP summary now and tracked at project

This commit is contained in:
Zhaoqi Li
2025-08-13 11:44:04 -07:00
parent d11c16e148
commit 91df641eee
10 changed files with 139 additions and 10 deletions
@@ -227,6 +227,23 @@ export function ProjectOverview({ onViewQuestions, projectId, orgId }: ProjectOv
</div>
</div>
{/* RFP Summary */}
{project.summary && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Info className="h-5 w-5" />
RFP Summary
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground leading-relaxed">
{project.summary}
</p>
</CardContent>
</Card>
)}
{/* Consolidated Project Summary */}
<Card>
<CardContent>
+11 -6
View File
@@ -84,12 +84,17 @@ export function ProjectCard({ project, onProjectDeleted }: ProjectCardProps) {
<Card className="hover:shadow-lg hover:bg-accent/50 transition-all duration-200 cursor-pointer flex flex-col h-full min-h-[180px]">
<CardHeader className="pb-2 flex-shrink-0">
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg line-clamp-2">{project.name}</CardTitle>
<CardDescription className="mt-1 line-clamp-3 min-h-[60px]">
{project.description || 'No description available'}
</CardDescription>
</div>
<div className="flex-1">
<CardTitle className="text-lg line-clamp-2">{project.name}</CardTitle>
<CardDescription className="mt-1 line-clamp-3 min-h-[60px]">
{project.summary
? (project.summary.length > 100
? `${project.summary.substring(0, 100)}...`
: project.summary)
: (project.description || 'No description available')
}
</CardDescription>
</div>
<div className="flex items-center gap-2 ml-2">
<Badge variant={status === "Completed" ? "default" : "secondary"} className="flex-shrink-0">
{status}
+5
View File
@@ -8,6 +8,11 @@ export interface IAIQuestionExtractor {
* Extract structured questions from document content
*/
extractQuestions(content: string, documentName: string): Promise<ExtractedQuestions>;
/**
* Generate a summary of the RFP document
*/
generateSummary(content: string, documentName: string): Promise<string>;
}
/**
+17
View File
@@ -24,6 +24,7 @@ export const projectService = {
id: true,
name: true,
description: true,
summary: true,
createdAt: true,
updatedAt: true,
organizationId: true
@@ -40,6 +41,7 @@ export const projectService = {
id: true,
name: true,
description: true,
summary: true,
createdAt: true,
updatedAt: true,
organizationId: true,
@@ -65,6 +67,7 @@ export const projectService = {
id: true,
name: true,
description: true,
summary: true,
createdAt: true,
updatedAt: true,
organizationId: true,
@@ -276,6 +279,20 @@ export const projectService = {
}
},
// Summary operations
async saveSummary(projectId: string, summary: string) {
try {
await db.project.update({
where: { id: projectId },
data: { summary },
});
console.log(`Successfully saved summary for project ${projectId}`);
} catch (error) {
console.error(`Error saving summary for project ${projectId}:`, error);
throw error;
}
},
// Answer operations
async saveAnswers(projectId: string, answers: Record<string, { text: string; sources?: AnswerSource[] }>) {
console.log(`Saving answers for project ${projectId}. Total answers: ${Object.keys(answers).length}`);
+51
View File
@@ -29,6 +29,37 @@ export class OpenAIQuestionExtractor implements IAIQuestionExtractor {
}
}
/**
* Generate a summary of the RFP document
*/
async generateSummary(content: string, documentName: string): Promise<string> {
try {
const systemPrompt = this.getSummarySystemPrompt();
const response = await this.client.chat.completions.create({
model: this.config.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: this.formatUserPrompt(content, documentName) }
],
temperature: 0.3, // Slightly higher for more creative summaries
max_tokens: 500, // Limit summary length
});
const assistantMessage = response.choices[0]?.message?.content;
if (!assistantMessage) {
throw new AIServiceError('Empty response from OpenAI for summary generation');
}
return assistantMessage.trim();
} catch (error) {
if (error instanceof AIServiceError) {
throw error;
}
throw new AIServiceError(`Summary generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Extract structured questions from document content
*/
@@ -71,6 +102,26 @@ export class OpenAIQuestionExtractor implements IAIQuestionExtractor {
}
}
/**
* Get the system prompt for RFP summary generation
*/
private getSummarySystemPrompt(): string {
return `
You are an expert at analyzing RFP (Request for Proposal) documents and creating concise, informative summaries.
Your task is to read through the RFP document and create a comprehensive paragraph summary that captures:
1. The purpose and scope of the project/procurement
2. Key requirements and deliverables
3. Important dates, deadlines, or timelines mentioned
4. Any special qualifications or criteria for vendors
5. The overall scale or nature of the work
Write a clear, professional summary in paragraph form (3-5 sentences) that would help someone quickly understand what this RFP is about and what the organization is seeking. Focus on the most important aspects that potential bidders would need to know.
Do not include section numbers, question lists, or administrative details like submission instructions. Focus on the substance of what is being procured.
`.trim();
}
/**
* Get the system prompt for question extraction
*/
+33 -4
View File
@@ -13,11 +13,17 @@ export class QuestionExtractionService implements IQuestionExtractionService {
*/
async processDocument(request: ExtractQuestionsRequest): Promise<ExtractQuestionsResponse> {
try {
// Stage 1: Extract questions using AI
const extractedQuestions = await this.extractQuestions(request.content, request.documentName);
// Stage 1: Extract questions and generate summary using AI (parallel execution)
const [extractedQuestions, summary] = await Promise.all([
this.extractQuestions(request.content, request.documentName),
this.generateSummary(request.content, request.documentName)
]);
// Stage 2: Save to database
await this.saveQuestions(request.projectId, extractedQuestions.sections);
// Stage 2: Save to database (questions and summary)
await Promise.all([
this.saveQuestions(request.projectId, extractedQuestions.sections),
this.saveSummary(request.projectId, summary)
]);
// Stage 3: Return structured response
const response: ExtractQuestionsResponse = {
@@ -25,6 +31,7 @@ export class QuestionExtractionService implements IQuestionExtractionService {
documentName: request.documentName,
sections: extractedQuestions.sections,
extractedAt: new Date().toISOString(),
summary, // Include summary in response
};
return response;
@@ -47,6 +54,17 @@ export class QuestionExtractionService implements IQuestionExtractionService {
}
}
/**
* Generate summary from content using AI
*/
private async generateSummary(content: string, documentName: string): Promise<string> {
try {
return await openAIQuestionExtractor.generateSummary(content, documentName);
} catch (error) {
throw new AIServiceError(`AI summary generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Save extracted questions to storage
*/
@@ -58,6 +76,17 @@ export class QuestionExtractionService implements IQuestionExtractionService {
}
}
/**
* Save generated summary to storage
*/
async saveSummary(projectId: string, summary: string): Promise<void> {
try {
await projectService.saveSummary(projectId, summary);
} catch (error) {
throw new DatabaseError(`Failed to save summary: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get extraction statistics
*/
+1
View File
@@ -30,6 +30,7 @@ export const ExtractQuestionsResponseSchema = z.object({
documentName: z.string(),
sections: z.array(SectionSchema),
extractedAt: z.string(),
summary: z.string().optional(), // RFP summary generated by AI
});
// Type exports
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "summary" TEXT;
+1
View File
@@ -71,6 +71,7 @@ model Project {
id String @id @default(cuid())
name String
description String?
summary String? // RFP summary generated from uploaded documents
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
questions Question[]
+1
View File
@@ -2,6 +2,7 @@ export interface Project {
id: string;
name: string;
description?: string;
summary?: string; // RFP summary generated from uploaded documents
createdAt: string;
progress?: number;
status?: string;