AI Feedback & LLM Engine Blueprint
This page provides the architecture and code blueprints for Mockrithm's assessment engine, which evaluates candidate transcript metrics and generates structured feedback reports.
1. Engine Architecture & Dual-Route Flow
The assessment engine uses a dual-route pipeline to ensure feedback is generated with low latency and high structural reliability.
- Primary Route (Groq): Generates fast assessments using the
llama-3.3-70b-versatilemodel. - Fallback Route (Gemini): If Groq experiences rate limits or fails JSON validation, the pipeline falls back to Google's
gemini-2.0-flash-001orgemini-2.5-flashmodel, enforcing structured outputs using schema validation.
graph TD
Trigger[Interview Session Completed] --> BuildPrompt[Aggregate Interview Metadata & Transcript]
BuildPrompt --> CallGroq{Call Groq API: Llama-3.3-70b}
CallGroq -- Success & Valid JSON --> WriteDB[Write feedback record to Firestore]
CallGroq -- Failure / JSON Glitch --> CallGemini{Call Gemini API: Gemini-2.0-flash}
CallGemini -- Success --> WriteDB
CallGemini -- Fail --> Alert[Dispatch Error Logging Alert]
WriteDB --> AutoUpdate[Trigger Asynchronous Candidate Profile Metrics Update]2. Structured JSON Output Schema
The engine relies on a strict Zod schema to validate the output format returned by the LLMs. This ensures that the frontend dashboard can parse and render the scoring metrics without encountering runtime errors.
import { z } from "zod";
export const feedbackReportSchema = z.object({
totalScore: z.number().min(0).max(100),
categoryScores: z.array(
z.object({
name: z.string(),
score: z.number().min(0).max(100),
comment: z.string(),
})
),
strengths: z.array(z.string()).min(2),
areasForImprovement: z.array(z.string()).min(2),
finalAssessment: z.string().min(50),
averageWpm: z.number(),
topFillerWords: z.array(z.string()),
});
export type FeedbackReport = z.infer<typeof feedbackReportSchema>;3. Core Engine Action Implementation
Here is the implementation code of the Next.js Server Action (lib/actions/evaluate.ts) handling the evaluation process:
import { generateText, generateObject } from "ai";
import { groq } from "@ai-sdk/groq";
import { google } from "@ai-sdk/google";
import { db } from "@/firebase/admin";
import { feedbackReportSchema, FeedbackReport } from "@/types/feedback";
export async function evaluateSession(interviewId: string): Promise<{ success: boolean; data?: FeedbackReport }> {
try {
// 1. Fetch interview transcript from Firestore
const interviewDoc = await db.collection("interviews").doc(interviewId).get();
if (!interviewDoc.exists) throw new Error("Session record not found");
const sessionData = interviewDoc.data();
const formattedTranscript = sessionData?.transcript
.map((t: any) => `${t.role.toUpperCase()}: ${t.content}`)
.join("\n");
const prompt = `Evaluate the following interview transcript for a candidate targetting the role: ${sessionData?.role} (${sessionData?.experience} level).
Transcript:
${formattedTranscript}`;
// 2. Execute Primary Route: Groq
try {
const response = await generateObject({
model: groq("llama-3.3-70b-versatile"),
schema: feedbackReportSchema,
prompt: prompt,
temperature: 0.2,
});
if (response.object) {
await saveFeedback(interviewId, sessionData?.userId, response.object);
return { success: true, data: response.object };
}
} catch (groqError) {
console.warn("Groq validation failed, running Gemini fallback...", groqError);
}
// 3. Fallback Route: Gemini
const fallbackResponse = await generateObject({
model: google("gemini-2.0-flash-001"),
schema: feedbackReportSchema,
prompt: prompt,
temperature: 0.25,
});
await saveFeedback(interviewId, sessionData?.userId, fallbackResponse.object);
return { success: true, data: fallbackResponse.object };
} catch (error) {
console.error("Evaluation pipeline failed completely:", error);
return { success: false };
}
}
async function saveFeedback(interviewId: string, userId: string, feedback: FeedbackReport) {
const feedbackRef = db.collection("interviewsfeedback").doc();
await feedbackRef.set({
...feedback,
interviewId,
userId,
createdAt: new Date().toISOString()
});
}