mirror of
https://github.com/run-llama/auto_rfp.git
synced 2026-07-01 21:54:05 -04:00
correctly generates RFP summary now and tracked at project
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
@@ -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[]
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user