Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@ interface ScoringPatterns {
|
||||
export interface EvaluationSummaryResult {
|
||||
id: string
|
||||
projectId: string
|
||||
stageId: string
|
||||
roundId: string
|
||||
summaryJson: AIResponsePayload & { scoringPatterns: ScoringPatterns }
|
||||
generatedAt: Date
|
||||
model: string
|
||||
@@ -123,6 +123,15 @@ Analyze these evaluations and return a JSON object with this exact structure:
|
||||
"recommendation": "A brief recommendation based on the evaluation consensus"
|
||||
}
|
||||
|
||||
Example output:
|
||||
{
|
||||
"overallAssessment": "The project received strong scores (avg 7.8/10) with high consensus among evaluators. Key strengths in innovation were balanced by concerns about scalability.",
|
||||
"strengths": ["Innovative approach to coral reef monitoring", "Strong team expertise in marine biology"],
|
||||
"weaknesses": ["Limited scalability plan", "Budget projections need more detail"],
|
||||
"themes": [{"theme": "Innovation", "sentiment": "positive", "frequency": 3}, {"theme": "Scalability", "sentiment": "negative", "frequency": 2}],
|
||||
"recommendation": "Recommended for advancement with condition to address scalability concerns in next round."
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- Base your analysis only on the provided evaluation data
|
||||
- Identify common themes across evaluator feedback
|
||||
@@ -194,12 +203,12 @@ export function computeScoringPatterns(
|
||||
*/
|
||||
export async function generateSummary({
|
||||
projectId,
|
||||
stageId,
|
||||
roundId,
|
||||
userId,
|
||||
prisma,
|
||||
}: {
|
||||
projectId: string
|
||||
stageId: string
|
||||
roundId: string
|
||||
userId: string
|
||||
prisma: PrismaClient
|
||||
}): Promise<EvaluationSummaryResult> {
|
||||
@@ -216,13 +225,13 @@ export async function generateSummary({
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' })
|
||||
}
|
||||
|
||||
// Fetch submitted evaluations for this project in this stage
|
||||
// Fetch submitted evaluations for this project in this round
|
||||
const evaluations = await prisma.evaluation.findMany({
|
||||
where: {
|
||||
status: 'SUBMITTED',
|
||||
assignment: {
|
||||
projectId,
|
||||
stageId,
|
||||
roundId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
@@ -244,13 +253,13 @@ export async function generateSummary({
|
||||
if (evaluations.length === 0) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'No submitted evaluations found for this project in this stage',
|
||||
message: 'No submitted evaluations found for this project in this round',
|
||||
})
|
||||
}
|
||||
|
||||
// Get evaluation form criteria for this stage
|
||||
// Get evaluation form criteria for this round
|
||||
const form = await prisma.evaluationForm.findFirst({
|
||||
where: { stageId, isActive: true },
|
||||
where: { roundId, isActive: true },
|
||||
select: { criteriaJson: true },
|
||||
})
|
||||
|
||||
@@ -278,49 +287,83 @@ export async function generateSummary({
|
||||
let aiResponse: AIResponsePayload
|
||||
let tokensUsed = 0
|
||||
|
||||
const MAX_PARSE_RETRIES = 2
|
||||
let parseAttempts = 0
|
||||
let response: Awaited<ReturnType<typeof openai.chat.completions.create>>
|
||||
|
||||
try {
|
||||
const params = buildCompletionParams(model, {
|
||||
messages: [
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
jsonMode: true,
|
||||
temperature: 0.3,
|
||||
temperature: 0.1,
|
||||
maxTokens: 2000,
|
||||
})
|
||||
|
||||
const response = await openai.chat.completions.create(params)
|
||||
const usage = extractTokenUsage(response)
|
||||
response = await openai.chat.completions.create(params)
|
||||
let usage = extractTokenUsage(response)
|
||||
tokensUsed = usage.totalTokens
|
||||
|
||||
const content = response.choices[0]?.message?.content
|
||||
if (!content) {
|
||||
throw new Error('Empty response from AI')
|
||||
// Parse with retry logic
|
||||
while (true) {
|
||||
try {
|
||||
const content = response.choices[0]?.message?.content
|
||||
if (!content) {
|
||||
throw new Error('Empty response from AI')
|
||||
}
|
||||
aiResponse = JSON.parse(content) as AIResponsePayload
|
||||
break
|
||||
} catch (parseError) {
|
||||
if (parseError instanceof SyntaxError && parseAttempts < MAX_PARSE_RETRIES) {
|
||||
parseAttempts++
|
||||
console.warn(`[AI Evaluation Summary] JSON parse failed, retrying (${parseAttempts}/${MAX_PARSE_RETRIES})`)
|
||||
|
||||
// Retry the API call with hint
|
||||
const retryParams = buildCompletionParams(model, {
|
||||
messages: [
|
||||
{ role: 'user', content: prompt + '\n\nIMPORTANT: Please ensure valid JSON output.' },
|
||||
],
|
||||
jsonMode: true,
|
||||
temperature: 0.1,
|
||||
maxTokens: 2000,
|
||||
})
|
||||
response = await openai.chat.completions.create(retryParams)
|
||||
const retryUsage = extractTokenUsage(response)
|
||||
tokensUsed += retryUsage.totalTokens
|
||||
continue
|
||||
}
|
||||
|
||||
// If retry limit reached or non-syntax error
|
||||
if (parseError instanceof SyntaxError) {
|
||||
const parseErrorObj = createParseError((parseError as Error).message)
|
||||
logAIError('EvaluationSummary', 'generateSummary', parseErrorObj)
|
||||
|
||||
await logAIUsage({
|
||||
userId,
|
||||
action: 'EVALUATION_SUMMARY',
|
||||
entityType: 'Project',
|
||||
entityId: projectId,
|
||||
model,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: tokensUsed,
|
||||
itemsProcessed: 0,
|
||||
status: 'ERROR',
|
||||
errorMessage: parseErrorObj.message,
|
||||
})
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to parse AI response. Please try again.',
|
||||
})
|
||||
}
|
||||
throw parseError
|
||||
}
|
||||
}
|
||||
|
||||
aiResponse = JSON.parse(content) as AIResponsePayload
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
const parseError = createParseError(error.message)
|
||||
logAIError('EvaluationSummary', 'generateSummary', parseError)
|
||||
|
||||
await logAIUsage({
|
||||
userId,
|
||||
action: 'EVALUATION_SUMMARY',
|
||||
entityType: 'Project',
|
||||
entityId: projectId,
|
||||
model,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: tokensUsed,
|
||||
itemsProcessed: 0,
|
||||
status: 'ERROR',
|
||||
errorMessage: parseError.message,
|
||||
})
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to parse AI response. Please try again.',
|
||||
})
|
||||
if (error instanceof TRPCError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const classified = classifyAIError(error)
|
||||
@@ -359,11 +402,11 @@ export async function generateSummary({
|
||||
|
||||
const summary = await prisma.evaluationSummary.upsert({
|
||||
where: {
|
||||
projectId_stageId: { projectId, stageId },
|
||||
projectId_roundId: { projectId, roundId },
|
||||
},
|
||||
create: {
|
||||
projectId,
|
||||
stageId,
|
||||
roundId,
|
||||
summaryJson: summaryJsonValue,
|
||||
generatedById: userId,
|
||||
model,
|
||||
@@ -395,7 +438,7 @@ export async function generateSummary({
|
||||
return {
|
||||
id: summary.id,
|
||||
projectId: summary.projectId,
|
||||
stageId: summary.stageId,
|
||||
roundId: summary.roundId,
|
||||
summaryJson: summaryJson as AIResponsePayload & { scoringPatterns: ScoringPatterns },
|
||||
generatedAt: summary.generatedAt,
|
||||
model: summary.model,
|
||||
|
||||
Reference in New Issue
Block a user