Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions kits/interview-scorecard-calibrator/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Flow Configurations
CALIBRATE_SCORECARD_FLOW_ID=your-lamatic-flow-uuid-here

# API Credentials
LAMATIC_API_KEY=your-lamatic-client-api-key-here
LAMATIC_PROJECT_ID=your-lamatic-project-id-here
LAMATIC_API_URL=https://your-organization-project-dev.lamatic.dev
5 changes: 5 additions & 0 deletions kits/interview-scorecard-calibrator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.lamatic/
node_modules/
.next/
.env
.env.local
110 changes: 110 additions & 0 deletions kits/interview-scorecard-calibrator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Interview Scorecard Calibrator

Reconcile multi-interviewer panel feedback into a calibrated hiring scorecard.

## Problem

After a panel interview, hiring managers receive fragmented notes — different formats, uneven scoring rigor, and conflicting opinions. Reconciling them into a fair hire/no-hire decision is slow and inconsistent.

## Solution

Paste a role rubric and interviewer notes. The Lamatic flow returns:

- Per-competency calibrated scores with evidence
- Interviewer disagreement map
- Hire / lean-hire / lean-no / no-hire recommendation + confidence
- Follow-up questions for missing evidence
- Internal decision-summary email draft
- A markdown hiring-committee brief

## Why this kit is unique

Unlike resume screeners (`hiring`, `hiring-copilot-agent`, `candidate-screener`) or candidate self-reflection coaches (`interview-reflection-coach`), this kit focuses on **post-panel calibration** for hiring managers.

## Providers & Prerequisites

- Lamatic account + deployed flow (import or rebuild from this kit)
- LLM provider configured in Lamatic Studio (Gemini / OpenAI / etc.)
- Node.js 20.9+

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## Setup in Lamatic Studio

1. Create a project in [studio.lamatic.ai](https://studio.lamatic.ai)
2. Recreate the flow from `flows/calibrate-scorecard.ts` (or import if your Studio supports kit import)
3. Attach prompts from `prompts/`, model configs from `model-configs/`, and the normalize script from `scripts/`
4. Bind a text generation model on both LLM nodes
5. Deploy the flow and copy the **Flow ID**

## Run locally

```bash
cd kits/interview-scorecard-calibrator/apps
cp .env.example .env.local
# fill in LAMATIC_API_URL, LAMATIC_PROJECT_ID, LAMATIC_API_KEY, CALIBRATE_SCORECARD_FLOW_ID
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000).

## Environment variables

Copy either kit-root `.env.example` or `apps/.env.example` into `apps/.env.local`.

| Variable | Description |
|---|---|
| `LAMATIC_API_URL` | Lamatic API endpoint |
| `LAMATIC_PROJECT_ID` | Project ID |
| `LAMATIC_API_KEY` | API key |
| `CALIBRATE_SCORECARD_FLOW_ID` | Deployed flow ID for `calibrate-scorecard` |

## Interviewer notes format

Provide **at least two** interviewer entries, separated by:

- a line containing only `---`, and/or
- headings like `Interviewer 1:` / `Interviewer 2 (Bob, Eng Manager):`

Requests with fewer than two interviewer blocks are rejected before the model runs.

## Sample input

**Job title:** Senior Backend Engineer
**Level:** L5

**Rubric:**
```
System Design (High): scalable services, tradeoffs, reliability
Coding (High): correctness, clarity, edge cases
Ownership (Medium): end-to-end delivery, communication
```

**Interviewer notes:**
```
Interviewer 1 (Alice, Staff Eng):
Strong system design around caching and failover. Coding solid but a bit slow. Ownership examples were concrete. Score: Design 4, Coding 3, Ownership 4.

---

Interviewer 2 (Bob, Eng Manager):
Design felt hand-wavy on consistency. Coding was clean. Concerned about stakeholder communication. Score: Design 2, Coding 4, Ownership 2.
```

## Deploy

Use the Vercel deploy link in `lamatic.config.ts` (`links.deploy`) with `root-directory=kits/interview-scorecard-calibrator/apps`.

## Project structure

```
kits/interview-scorecard-calibrator/
├── lamatic.config.ts
├── agent.md
├── README.md
├── constitutions/default.md
├── flows/calibrate-scorecard.ts
├── prompts/
├── model-configs/
├── scripts/
└── apps/ # Next.js UI
```
39 changes: 39 additions & 0 deletions kits/interview-scorecard-calibrator/agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Interview Scorecard Calibrator

## Overview
This kit helps hiring managers reconcile fragmented multi-interviewer feedback into a calibrated competency scorecard, disagreement map, hire/no-hire recommendation, and an internal decision-summary email draft. It is a single Lamatic flow invoked by the included Next.js app.

## Purpose
After panel interviews, notes arrive in different formats with uneven scoring rigor. Manual reconciliation is slow and inconsistent. This agent turns a shared rubric plus interviewer notes into a structured decision aid so committees can focus on evidence and disagreements instead of formatting.

## Flows

### `calibrate-scorecard`

- Trigger: on-demand API execution from the kit UI or any Lamatic SDK caller
- Inputs:
- `job_title` (required)
- `level` (optional)
- `rubric` (required) — competency list with weights/expectations
- `interviewer_notes` (required) — notes from 2–6 interviewers
- Pipeline:
1. **Normalize Inputs** (`codeNode`) — validates and packages context
2. **Calibrate Scorecard** (`InstructorLLMNode`) — structured JSON scorecard
3. **Compose Brief** (`LLMNode`) — markdown hiring-committee brief
4. **API Response** — returns `scorecard` + `brief`
- Outputs:
- `scorecard` — competencies, disagreements, recommendation, confidence, follow-ups, email draft
- `brief` — readable markdown summary for the hiring committee

## Guardrails
- Decision aid only — humans make the final hiring call
- Do not fabricate interview evidence
- Avoid protected-class inferences
- Keep candidate/interviewer content confidential
- See `constitutions/default.md`

## Environment
- `LAMATIC_API_URL`
- `LAMATIC_PROJECT_ID`
- `LAMATIC_API_KEY`
- `CALIBRATE_SCORECARD_FLOW_ID`
7 changes: 7 additions & 0 deletions kits/interview-scorecard-calibrator/apps/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Flow Configurations
CALIBRATE_SCORECARD_FLOW_ID=your-lamatic-flow-uuid-here

# API Credentials
LAMATIC_API_KEY=your-lamatic-client-api-key-here
LAMATIC_PROJECT_ID=your-lamatic-project-id-here
LAMATIC_API_URL=https://your-organization-project-dev.lamatic.dev
23 changes: 23 additions & 0 deletions kits/interview-scorecard-calibrator/apps/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
/coverage
/.next/
/out/
/build
.DS_Store
*.pem
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
.env*
!.env.example
.vercel
*.tsbuildinfo
next-env.d.ts
11 changes: 11 additions & 0 deletions kits/interview-scorecard-calibrator/apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Interview Scorecard Calibrator App

Next.js UI for the Interview Scorecard Calibrator kit.

```bash
cp .env.example .env.local
npm install
npm run dev
```

Required env vars are documented in the parent [README.md](../README.md).
128 changes: 128 additions & 0 deletions kits/interview-scorecard-calibrator/apps/actions/orchestrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"use server";

import { getLamaticClient } from "@/lib/lamatic-client";
import { parseScorecard, splitInterviewerNotes, type Scorecard } from "@/lib/scorecard";
import kitConfig from "../../lamatic.config";

export type CalibrateInput = {
jobTitle: string;
level: string;
rubric: string;
interviewerNotes: string;
};

export type CalibrateResult = {
success: boolean;
data?: {
scorecard: Scorecard;
brief: string;
raw: unknown;
};
error?: string;
};

/** Resolve the deployed calibrate-scorecard flow ID from kit-root lamatic.config. */
function getCalibrateFlowId() {
const step = kitConfig.steps.find((s) => s.id === "calibrate-scorecard");
if (!step?.envKey) {
throw new Error(
'lamatic.config has no step "calibrate-scorecard" with an envKey',
);
}

const flowId = process.env[step.envKey];
if (!flowId) {
throw new Error(
`${step.envKey} is not set. Set ${step.envKey} in the application environment.`,
);
}
return flowId;
}

/** Parse a JSON string when possible; otherwise return the original value. */
function parseMaybeJson(value: unknown) {
if (typeof value !== "string") return value;
try {
return JSON.parse(value);
} catch {
return value;
}
}

/**
* Invokes the Lamatic calibrate-scorecard flow and validates the structured response.
*/
export async function calibrateScorecard(
input: CalibrateInput,
): Promise<CalibrateResult> {
try {
const jobTitle = input.jobTitle.trim();
const rubric = input.rubric.trim();
const interviewerNotes = input.interviewerNotes.trim();
const level = input.level.trim();

if (!jobTitle) throw new Error("Job title is required");
if (!rubric) throw new Error("Competency rubric is required");
if (!interviewerNotes) throw new Error("Interviewer notes are required");

const interviewerBlocks = splitInterviewerNotes(interviewerNotes);
if (interviewerBlocks.length < 2) {
throw new Error(
"Provide notes from at least two interviewers, separated by '---' or 'Interviewer N:' headings",
);
}

const flowId = getCalibrateFlowId();
const lamaticClient = getLamaticClient();
const resData = await lamaticClient.executeFlow(flowId, {
job_title: jobTitle,
level: level || "Unspecified",
rubric,
interviewer_notes: interviewerNotes,
});

const result =
(resData as { result?: Record<string, unknown> })?.result || resData;
const scorecardRaw = parseMaybeJson(
(result as Record<string, unknown>)?.scorecard ??
(result as Record<string, unknown>)?.answer ??
result,
);
const scorecard = parseScorecard(scorecardRaw);
if (!scorecard) {
throw new Error(
"Flow returned an invalid scorecard payload. Check the deployed flow output schema.",
);
}

const brief =
typeof (result as Record<string, unknown>)?.brief === "string"
? ((result as Record<string, unknown>).brief as string)
: "";

return {
success: true,
data: {
scorecard,
brief,
raw: resData,
},
};
} catch (error) {
let errorMessage = "Unknown error occurred";
if (error instanceof Error) {
errorMessage = error.message;
if (error.message.includes("fetch failed")) {
errorMessage =
"Network error: Unable to connect to Lamatic. Check your internet connection and API URL.";
} else if (error.message.toLowerCase().includes("api key")) {
errorMessage = "Authentication error: Please check your Lamatic API key.";
}
}

return {
success: false,
error: errorMessage,
};
}
}
55 changes: 55 additions & 0 deletions kits/interview-scorecard-calibrator/apps/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";

:root {
--background: #f4f7fb;
--foreground: #0f172a;
--panel: #ffffff;
--accent: #0f766e;
--accent-soft: #ccfbf1;
--border: #dbe3ee;
--muted: #64748b;
--input: #e2e8f0;
--ring: #0f766e;
--primary: #0f766e;
--primary-foreground: #ffffff;
--secondary: #f1f5f9;
--secondary-foreground: #334155;
--destructive: #e11d48;
--rec-hire-bg: #d1fae5;
--rec-hire-fg: #065f46;
--rec-hire-border: #a7f3d0;
--rec-lean-hire-bg: #ccfbf1;
--rec-lean-hire-fg: #115e59;
--rec-lean-hire-border: #99f6e4;
--rec-lean-no-bg: #fef3c7;
--rec-lean-no-fg: #78350f;
--rec-lean-no-border: #fde68a;
--rec-no-hire-bg: #ffe4e6;
--rec-no-hire-fg: #9f1239;
--rec-no-hire-border: #fecdd3;
--rec-fallback-bg: #f1f5f9;
--rec-fallback-fg: #334155;
--rec-fallback-border: #e2e8f0;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-source-sans);
--font-display: var(--font-fraunces);
}

body {
background:
radial-gradient(circle at top left, rgba(15, 118, 110, 0.12), transparent 32%),
radial-gradient(circle at top right, rgba(14, 165, 233, 0.1), transparent 28%),
linear-gradient(180deg, #eef5f8 0%, var(--background) 48%, #e8eef5 100%);
color: var(--foreground);
font-family: var(--font-sans), "Segoe UI", sans-serif;
min-height: 100vh;
}

.display-font {
font-family: var(--font-display), Georgia, serif;
}
Loading
Loading