mirror of
https://github.com/run-llama/auto_rfp.git
synced 2026-07-01 21:54:05 -04:00
491 lines
15 KiB
TypeScript
491 lines
15 KiB
TypeScript
import { db } from './db';
|
|
import { RfpDocument, RfpSection, RfpQuestion, AnswerSource } from '@/types/api';
|
|
|
|
export const projectService = {
|
|
// Project operations
|
|
async createProject(name: string, organizationId: string, description?: string) {
|
|
return db.project.create({
|
|
data: {
|
|
name,
|
|
description,
|
|
organizationId,
|
|
},
|
|
});
|
|
},
|
|
|
|
async getProjects(organizationId?: string) {
|
|
// If organizationId is provided, get projects for that organization only
|
|
if (organizationId) {
|
|
return db.project.findMany({
|
|
where: {
|
|
organizationId,
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
description: true,
|
|
summary: true,
|
|
eligibility: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
organizationId: true
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Otherwise get all projects (mostly for admin purposes)
|
|
return db.project.findMany({
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
description: true,
|
|
summary: true,
|
|
eligibility: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
organizationId: true,
|
|
organization: {
|
|
select: {
|
|
id: true,
|
|
name: true
|
|
}
|
|
}
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc',
|
|
},
|
|
});
|
|
},
|
|
|
|
async getProject(id: string, includeRelations = false) {
|
|
// Basic project data without expensive relations
|
|
if (!includeRelations) {
|
|
return db.project.findUnique({
|
|
where: { id },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
description: true,
|
|
summary: true,
|
|
eligibility: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
organizationId: true,
|
|
organization: {
|
|
select: {
|
|
id: true,
|
|
name: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Full project data with all relations when explicitly requested
|
|
return db.project.findUnique({
|
|
where: { id },
|
|
include: {
|
|
organization: true,
|
|
questions: {
|
|
include: {
|
|
answer: {
|
|
include: {
|
|
sources: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
},
|
|
|
|
async updateProject(id: string, data: { name?: string; description?: string }) {
|
|
return db.project.update({
|
|
where: { id },
|
|
data,
|
|
});
|
|
},
|
|
|
|
async deleteProject(id: string) {
|
|
return db.project.delete({
|
|
where: { id },
|
|
});
|
|
},
|
|
|
|
// Question operations
|
|
async saveQuestions(projectId: string, sections: RfpSection[]) {
|
|
console.log(`Starting to save questions for project ${projectId}. Total sections: ${sections.length}`);
|
|
|
|
// Process each section separately instead of in one large transaction
|
|
let questionsCreated = 0;
|
|
|
|
try {
|
|
for (const section of sections) {
|
|
console.log(`Processing section: ${section.title} with ${section.questions.length} questions`);
|
|
|
|
// Process questions in smaller batches of 10 to avoid transaction timeouts
|
|
const BATCH_SIZE = 10;
|
|
for (let i = 0; i < section.questions.length; i += BATCH_SIZE) {
|
|
const batch = section.questions.slice(i, i + BATCH_SIZE);
|
|
console.log(`Processing batch of ${batch.length} questions (${i} to ${i + batch.length - 1})`);
|
|
|
|
// Use a transaction for each batch
|
|
await db.$transaction(async (tx) => {
|
|
for (const questionItem of batch) {
|
|
try {
|
|
// Check if question already exists to avoid duplicates
|
|
const existing = await tx.question.findFirst({
|
|
where: {
|
|
referenceId: questionItem.id,
|
|
projectId,
|
|
}
|
|
});
|
|
|
|
if (!existing) {
|
|
await tx.question.create({
|
|
data: {
|
|
referenceId: questionItem.id,
|
|
text: questionItem.question,
|
|
topic: section.title,
|
|
projectId,
|
|
},
|
|
});
|
|
questionsCreated++;
|
|
} else {
|
|
console.log(`Question with referenceId ${questionItem.id} already exists, skipping`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error creating question ${questionItem.id}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`Batch completed. Total questions created so far: ${questionsCreated}`);
|
|
}
|
|
}
|
|
|
|
console.log(`All questions saved successfully. Total created: ${questionsCreated}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`Error in saveQuestions for project ${projectId}:`, error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async getQuestions(projectId: string) {
|
|
console.log("In getQuestions, projectId", projectId);
|
|
console.log(`Fetching questions for project ${projectId}`);
|
|
|
|
try {
|
|
const questions = await db.question.findMany({
|
|
where: {
|
|
projectId,
|
|
},
|
|
include: {
|
|
answer: {
|
|
include: {
|
|
sources: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
createdAt: 'asc',
|
|
},
|
|
});
|
|
|
|
console.log(`Found ${questions.length} questions for project ${projectId}`);
|
|
|
|
// If no questions found, return an empty RFP document structure
|
|
if (questions.length === 0) {
|
|
console.log(`No questions found for project ${projectId}, returning empty document`);
|
|
|
|
const project = await db.project.findUnique({
|
|
where: { id: projectId },
|
|
});
|
|
|
|
if (!project) {
|
|
console.log(`Project ${projectId} not found`);
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
documentId: projectId,
|
|
documentName: project.name,
|
|
sections: [],
|
|
extractedAt: project.createdAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
// Convert database format to application format (RfpDocument)
|
|
const groupedByTopic = questions.reduce<Record<string, RfpQuestion[]>>((acc, question) => {
|
|
const topic = question.topic;
|
|
if (!acc[topic]) {
|
|
acc[topic] = [];
|
|
}
|
|
|
|
// Map database sources to API format
|
|
const sources = question.answer?.sources?.map((source, index) => ({
|
|
id: index + 1,
|
|
fileName: source.fileName,
|
|
filePath: source.filePath || undefined,
|
|
pageNumber: source.pageNumber || undefined,
|
|
documentId: source.documentId || undefined,
|
|
relevance: source.relevance || null,
|
|
textContent: source.textContent || null,
|
|
})) || [];
|
|
|
|
acc[topic].push({
|
|
id: question.id,
|
|
question: question.text,
|
|
answer: question.answer?.text,
|
|
sources: sources.length > 0 ? sources : undefined,
|
|
referenceId: question.referenceId ?? undefined,
|
|
});
|
|
|
|
return acc;
|
|
}, {});
|
|
|
|
// Convert to sections
|
|
const sections: RfpSection[] = Object.entries(groupedByTopic).map(
|
|
([title, questions], index) => ({
|
|
id: `section_${index + 1}`,
|
|
title,
|
|
questions,
|
|
})
|
|
);
|
|
|
|
const project = await db.project.findUnique({
|
|
where: { id: projectId },
|
|
});
|
|
|
|
if (!project) {
|
|
console.log(`Project ${projectId} not found after loading questions`);
|
|
return null;
|
|
}
|
|
|
|
// Construct RFP document
|
|
const rfpDocument: RfpDocument = {
|
|
documentId: projectId,
|
|
documentName: project.name,
|
|
sections,
|
|
extractedAt: project.createdAt.toISOString(),
|
|
};
|
|
|
|
return rfpDocument;
|
|
} catch (error) {
|
|
console.error(`Error in getQuestions for project ${projectId}:`, error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// 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;
|
|
}
|
|
},
|
|
|
|
// 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<string, { text: string; sources?: AnswerSource[] }>) {
|
|
console.log(`Saving answers for project ${projectId}. Total answers: ${Object.keys(answers).length}`);
|
|
|
|
try {
|
|
let answersProcessed = 0;
|
|
const BATCH_SIZE = 5; // Smaller batch size for complex operations
|
|
const questionIds = Object.keys(answers);
|
|
|
|
// Process in smaller batches
|
|
for (let i = 0; i < questionIds.length; i += BATCH_SIZE) {
|
|
const batchIds = questionIds.slice(i, i + BATCH_SIZE);
|
|
console.log(`Processing batch of ${batchIds.length} answers (${i} to ${i + batchIds.length - 1})`);
|
|
|
|
// Increase the transaction timeout to 30 seconds
|
|
await db.$transaction(async (tx) => {
|
|
for (const questionId of batchIds) {
|
|
const answerData = answers[questionId];
|
|
const text = typeof answerData === 'string' ? answerData : answerData.text;
|
|
const sources = typeof answerData === 'string' ? [] : (answerData.sources || []);
|
|
|
|
// Check if this question belongs to the project
|
|
const question = await tx.question.findFirst({
|
|
where: {
|
|
id: questionId,
|
|
projectId,
|
|
},
|
|
});
|
|
|
|
if (!question) {
|
|
console.log(`Question ${questionId} not found for project ${projectId}, skipping`);
|
|
continue;
|
|
}
|
|
|
|
// Check if an answer already exists
|
|
const existingAnswer = await tx.answer.findUnique({
|
|
where: {
|
|
questionId,
|
|
},
|
|
include: {
|
|
sources: true,
|
|
},
|
|
});
|
|
|
|
if (existingAnswer) {
|
|
// Update existing answer
|
|
await tx.answer.update({
|
|
where: {
|
|
id: existingAnswer.id,
|
|
},
|
|
data: {
|
|
text,
|
|
},
|
|
});
|
|
|
|
// Delete existing sources
|
|
if (existingAnswer.sources.length > 0) {
|
|
await tx.source.deleteMany({
|
|
where: {
|
|
answerId: existingAnswer.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Create new sources
|
|
if (sources.length > 0) {
|
|
await Promise.all(sources.map(source =>
|
|
tx.source.create({
|
|
data: {
|
|
fileName: source.fileName,
|
|
filePath: source.filePath || null,
|
|
pageNumber: source.pageNumber?.toString() || null,
|
|
documentId: source.documentId || null,
|
|
relevance: source.relevance ? Math.round(Number(source.relevance)) : null,
|
|
textContent: source.textContent || null,
|
|
answerId: existingAnswer.id,
|
|
},
|
|
})
|
|
));
|
|
}
|
|
} else {
|
|
// Create new answer
|
|
const answer = await tx.answer.create({
|
|
data: {
|
|
text,
|
|
questionId,
|
|
},
|
|
});
|
|
|
|
// Create sources
|
|
if (sources.length > 0) {
|
|
await Promise.all(sources.map(source =>
|
|
tx.source.create({
|
|
data: {
|
|
fileName: source.fileName,
|
|
filePath: source.filePath || null,
|
|
pageNumber: source.pageNumber?.toString() || null,
|
|
documentId: source.documentId || null,
|
|
relevance: source.relevance ? Math.round(Number(source.relevance)) : null,
|
|
textContent: source.textContent || null,
|
|
answerId: answer.id,
|
|
},
|
|
})
|
|
));
|
|
}
|
|
}
|
|
|
|
answersProcessed++;
|
|
}
|
|
}, {
|
|
// Set a longer timeout (30 seconds) for complex transactions with many sources
|
|
timeout: 30000
|
|
});
|
|
|
|
console.log(`Batch completed. Total answers processed so far: ${answersProcessed}`);
|
|
}
|
|
|
|
console.log(`All answers saved successfully. Total processed: ${answersProcessed}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`Error in saveAnswers for project ${projectId}:`, error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async getAnswers(projectId: string) {
|
|
console.log(`Fetching answers for project ${projectId}`);
|
|
|
|
try {
|
|
const questions = await db.question.findMany({
|
|
where: {
|
|
projectId,
|
|
},
|
|
include: {
|
|
answer: {
|
|
include: {
|
|
sources: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
console.log(`Found ${questions.length} questions for project ${projectId}`);
|
|
|
|
// Convert to map of questionId -> answer with sources
|
|
const answers: Record<string, { text: string; sources?: AnswerSource[] }> = {};
|
|
for (const question of questions) {
|
|
if (question.answer) {
|
|
// Map database sources to API format
|
|
const sources = question.answer.sources.map((source, index) => ({
|
|
id: index + 1,
|
|
fileName: source.fileName,
|
|
filePath: source.filePath || undefined,
|
|
pageNumber: source.pageNumber || undefined,
|
|
documentId: source.documentId || undefined,
|
|
relevance: source.relevance || null,
|
|
textContent: source.textContent || null,
|
|
}));
|
|
|
|
answers[question.id] = {
|
|
text: question.answer.text,
|
|
sources: sources.length > 0 ? sources : undefined,
|
|
};
|
|
}
|
|
}
|
|
|
|
console.log(`Returning ${Object.keys(answers).length} answers for project ${projectId}`);
|
|
return answers;
|
|
} catch (error) {
|
|
console.error(`Error in getAnswers for project ${projectId}:`, error);
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
|
|
// Helper type for unique constraint
|
|
type QuestionId = {
|
|
questionId: string;
|
|
};
|