Lab 02 jev · typescript
jev()
Building
jev(accounts, "looks like a churn risk"). One call per row, a
yes-or-no question, a probability back. No embeddings, no index, no prompt.
- Type inside the quotes and press enter.
jev.rank()asks for a score instead. - Rows resolve as their call returns and sort themselves. Click one to see what the model saw.
- Threshold re-filters on screen. Repeating a query is cached and free.
Ana Ruiz at Lumen Labs (Madrid, Spain) · plan: team, 12 seats, $348/month · customer for 14 months · last seen 2 days ago · 9 sessions per week · 3 support tickets in the last 90 days · NPS 9 · last note: "asked about SSO twice this month"
Tom Becker at Becker & Sons (Leipzig, Germany) · plan: starter, 2 seats, $38/month · customer for 2 years · last seen 71 days ago · 0 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "invoice email bounced in June"
Priya Nair at Kestrel Health (Bengaluru, India) · plan: enterprise, 140 seats, $4900/month · customer for 2 years · last seen today · 41 sessions per week · 6 support tickets in the last 90 days · NPS 8 · last note: "renewal call booked for October"
Jonas Lindqvist at Fjord Analytics (Oslo, Norway) · plan: team, 8 seats, $232/month · customer for 9 months · last seen yesterday · 12 sessions per week · 1 support ticket in the last 90 days · NPS 10 · last note: "wants the API rate limit raised"
Mateo Álvarez at Freelance (Rosario, Argentina) · plan: free, 1 seat, $0/month · customer for 3 months · last seen 45 days ago · 0 sessions per week · 2 support tickets in the last 90 days · NPS not answered · last note: "asked if there is a student discount"
Chloé Martin at Atelier Nord (Lyon, France) · plan: starter, 3 seats, $57/month · customer for 18 months · last seen 4 days ago · 5 sessions per week · 0 support tickets in the last 90 days · NPS 7 · last note: "exports every report to Excel"
Derek Holloway at Holloway Logistics (Dallas, United States) · plan: team, 25 seats, $725/month · customer for 3 years · last seen today · 27 sessions per week · 2 support tickets in the last 90 days · NPS 9 · last note: "added five seats in August without asking"
Yuki Tanaka at Sora Games (Osaka, Japan) · plan: starter, 4 seats, $76/month · customer for 7 months · last seen 12 days ago · 2 sessions per week · 4 support tickets in the last 90 days · NPS 5 · last note: "ticket: dashboard slow on large projects"
Leah Cohen at Northbeam Capital (New York, United States) · plan: enterprise, 60 seats, $2400/month · customer for 22 months · last seen yesterday · 18 sessions per week · 0 support tickets in the last 90 days · NPS 8 · last note: "legal asked for a DPA addendum"
Samuel Okafor at Okafor Studio (Lagos, Nigeria) · plan: free, 1 seat, $0/month · customer for 11 months · last seen 6 days ago · 3 sessions per week · 0 support tickets in the last 90 days · NPS 9 · last note: "hit the free tier limit three times"
Ingrid Vos at Vos Bakkerij (Utrecht, Netherlands) · plan: starter, 1 seat, $19/month · customer for 2 years · last seen 88 days ago · 0 sessions per week · 1 support ticket in the last 90 days · NPS 6 · last note: "cancelled the newsletter"
Rahul Mehta at Verdant Farms (Pune, India) · plan: team, 6 seats, $174/month · customer for 5 months · last seen 3 days ago · 7 sessions per week · 5 support tickets in the last 90 days · NPS 4 · last note: "three tickets about the mobile app crashing"
Emma Walsh at Trinity College (Dublin, Ireland) · plan: free, 1 seat, $0/month · customer for 2 months · last seen yesterday · 6 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "signed up with a university email"
Lucas Ferreira at Ferreira Advogados (São Paulo, Brazil) · plan: team, 10 seats, $290/month · customer for 19 months · last seen 15 days ago · 4 sessions per week · 0 support tickets in the last 90 days · NPS 7 · last note: "usage dropped after their intern left"
Sofia Rossi at Rossi Interiors (Milan, Italy) · plan: starter, 2 seats, $38/month · customer for 12 months · last seen 2 days ago · 8 sessions per week · 0 support tickets in the last 90 days · NPS 9 · last note: "asked whether team plan includes shared folders"
Kwame Asante at Asante Ventures (Accra, Ghana) · plan: team, 5 seats, $145/month · customer for 8 months · last seen 29 days ago · 1 sessions per week · 2 support tickets in the last 90 days · NPS 5 · last note: "requested a pause on billing"
Hannah Schmidt at Schmidt Steuerberatung (Munich, Germany) · plan: enterprise, 35 seats, $1400/month · customer for 3 years · last seen today · 22 sessions per week · 1 support ticket in the last 90 days · NPS 9 · last note: "compliance audit passed in July"
Ben Carter at Carter Consulting (Austin, United States) · plan: starter, 1 seat, $19/month · customer for 4 months · last seen 33 days ago · 0 sessions per week · 3 support tickets in the last 90 days · NPS 2 · last note: "ticket: how do I export and delete my data"
Aiko Sato at Sato Design (Tokyo, Japan) · plan: free, 1 seat, $0/month · customer for 15 months · last seen 2 days ago · 4 sessions per week · 0 support tickets in the last 90 days · NPS 8 · last note: "shares public links every week"
Nikolai Petrov at Petrov Robotics (Tallinn, Estonia) · plan: team, 14 seats, $406/month · customer for 11 months · last seen today · 31 sessions per week · 2 support tickets in the last 90 days · NPS 10 · last note: "asked for a quote on 40 seats"
Olivia Brown at Brown & Partners (London, United Kingdom) · plan: enterprise, 80 seats, $3200/month · customer for 2 years · last seen 2 days ago · 15 sessions per week · 4 support tickets in the last 90 days · NPS 6 · last note: "procurement wants a two-year price lock"
Diego Castro at Castro Tacos (Mexico City, Mexico) · plan: starter, 2 seats, $38/month · customer for 6 months · last seen 58 days ago · 0 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "card expired, retry failed twice"
Fatima Zahra at Zahra Learning (Casablanca, Morocco) · plan: team, 9 seats, $261/month · customer for 13 months · last seen yesterday · 11 sessions per week · 1 support ticket in the last 90 days · NPS 8 · last note: "runs a training session on us every Monday"
Liam O'Connor at Solo (Galway, Ireland) · plan: free, 1 seat, $0/month · customer for 1 month · last seen today · 14 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "signed up yesterday, already built six projects"
Mei Chen at Chen Fabrication (Shenzhen, China) · plan: team, 18 seats, $522/month · customer for 2 years · last seen 5 days ago · 9 sessions per week · 0 support tickets in the last 90 days · NPS 7 · last note: "prefers invoices in CNY"
Pieter de Jong at de Jong Fietsen (Amsterdam, Netherlands) · plan: starter, 3 seats, $57/month · customer for 21 months · last seen 9 days ago · 3 sessions per week · 1 support ticket in the last 90 days · NPS 7 · last note: "asked to downgrade to one seat"
Grace Kim at Kim Robotics (Seoul, South Korea) · plan: enterprise, 50 seats, $2000/month · customer for 16 months · last seen today · 38 sessions per week · 3 support tickets in the last 90 days · NPS 9 · last note: "asked for the roadmap deck"
Marco Bianchi at Bianchi Vino (Verona, Italy) · plan: free, 1 seat, $0/month · customer for 9 months · last seen 120 days ago · 0 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "never completed onboarding"
Sarah Johnson at Johnson Dental (Denver, United States) · plan: starter, 2 seats, $38/month · customer for 2 years · last seen 3 days ago · 6 sessions per week · 0 support tickets in the last 90 days · NPS 8 · last note: "same usage every week for two years"
Ahmed Hassan at Hassan Trading (Dubai, United Arab Emirates) · plan: team, 7 seats, $203/month · customer for 10 months · last seen 41 days ago · 1 sessions per week · 2 support tickets in the last 90 days · NPS 3 · last note: "complained about pricing in the last survey"
Elena Popescu at Popescu Arhitectura (Bucharest, Romania) · plan: starter, 4 seats, $76/month · customer for 17 months · last seen yesterday · 10 sessions per week · 0 support tickets in the last 90 days · NPS 9 · last note: "asked about the team plan trial"
James Wilson at Wilson Analytics (Toronto, Canada) · plan: team, 11 seats, $319/month · customer for 6 months · last seen 2 days ago · 13 sessions per week · 7 support tickets in the last 90 days · NPS 4 · last note: "seven tickets in a quarter, mostly bugs"
Valentina Greco at Greco Moda (Florence, Italy) · plan: free, 1 seat, $0/month · customer for 5 months · last seen 27 days ago · 1 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "opened the pricing page four times"
Erik Nilsson at Nilsson Bygg (Gothenburg, Sweden) · plan: team, 15 seats, $435/month · customer for 3 years · last seen yesterday · 17 sessions per week · 0 support tickets in the last 90 days · NPS 9 · last note: "quiet customer, pays on time"
Camila Duarte at Duarte Films (Lisbon, Portugal) · plan: starter, 3 seats, $57/month · customer for 8 months · last seen 62 days ago · 0 sessions per week · 1 support ticket in the last 90 days · NPS 5 · last note: "asked how to export everything"
Noah Fischer at Fischer GmbH (Vienna, Austria) · plan: enterprise, 45 seats, $1800/month · customer for 3 years · last seen 3 days ago · 12 sessions per week · 2 support tickets in the last 90 days · NPS 8 · last note: "CFO joined the last call"
Zara Khan at Khan Tutoring (Karachi, Pakistan) · plan: free, 1 seat, $0/month · customer for 7 months · last seen 14 days ago · 2 sessions per week · 1 support ticket in the last 90 days · NPS 6 · last note: "asked for a nonprofit discount"
Oliver Smith at Competing Cloud Inc (San Francisco, United States) · plan: free, 1 seat, $0/month · customer for 1 month · last seen yesterday · 22 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "signed up from a competitor domain, poked at every feature"
Isabella Moreau at Moreau Patisserie (Brussels, Belgium) · plan: starter, 2 seats, $38/month · customer for 2 years · last seen 5 days ago · 4 sessions per week · 0 support tickets in the last 90 days · NPS 8 · last note: "sends a thank-you email every December"
Arjun Singh at Singh Logistics (Delhi, India) · plan: team, 20 seats, $580/month · customer for 14 months · last seen today · 29 sessions per week · 3 support tickets in the last 90 days · NPS 7 · last note: "asked for a bulk import tool"
Freya Hansen at Hansen Interiør (Copenhagen, Denmark) · plan: starter, 1 seat, $19/month · customer for 3 years · last seen 2 days ago · 5 sessions per week · 0 support tickets in the last 90 days · NPS 10 · last note: "our longest-standing customer"
Carlos Mendoza at Mendoza Agro (Córdoba, Argentina) · plan: team, 6 seats, $174/month · customer for 4 months · last seen 22 days ago · 2 sessions per week · 4 support tickets in the last 90 days · NPS 3 · last note: "trial ended, asked why they were charged"
Lin Wei at Wei Studio (Taipei, Taiwan) · plan: free, 1 seat, $0/month · customer for 20 months · last seen 3 days ago · 5 sessions per week · 0 support tickets in the last 90 days · NPS 9 · last note: "recommended us on a podcast"
Maximilian Weber at Weber Consulting (Zurich, Switzerland) · plan: enterprise, 30 seats, $1200/month · customer for 12 months · last seen 6 days ago · 8 sessions per week · 1 support ticket in the last 90 days · NPS 7 · last note: "asked about on-premise hosting"
Amara Okoye at Okoye Fashion (Nairobi, Kenya) · plan: starter, 2 seats, $38/month · customer for 3 months · last seen yesterday · 11 sessions per week · 0 support tickets in the last 90 days · NPS 9 · last note: "growing fast, added a second seat last week"
Jack Taylor at Taylor Plumbing (Sydney, Australia) · plan: starter, 1 seat, $19/month · customer for 15 months · last seen 97 days ago · 0 sessions per week · 0 support tickets in the last 90 days · NPS not answered · last note: "auto-renewed, no logins since March"
Anaïs Dubois at Dubois Édition (Paris, France) · plan: team, 9 seats, $261/month · customer for 23 months · last seen 2 days ago · 6 sessions per week · 2 support tickets in the last 90 days · NPS 6 · last note: "asked twice for a French interface"
Ryan Murphy at Murphy Ventures (Boston, United States) · plan: enterprise, 100 seats, $4000/month · customer for 8 months · last seen today · 35 sessions per week · 5 support tickets in the last 90 days · NPS 8 · last note: "wants a dedicated account manager"
See one call, exactly as the server sends it
{
"state": "Ana Ruiz at Lumen Labs (Madrid, Spain) · plan: team, 12 seats, $348/month · customer for 14 months · last seen 2 days ago · 9 sessions per week · 3 support tickets in the last 90 days · NPS 9 · last note: \"asked about SSO twice this month\"",
"questions": {
"match": {
"type": "boolean",
"instructions": "looks like a churn risk"
}
}
}
The state is one serialized row and match is the only question, so the answer is a single probability.
jev.rank() swaps it for a score question with five levels and reads the expected value:
{
"level": {
"type": "score",
"instructions": "how likely to add seats next quarter",
"criteria": [
"not at all",
"slightly",
"somewhat",
"very",
"extremely"
]
}
} The whole helper, 165 lines, no dependencies beyond the AI SDK
import { createGateway } from "@ai-sdk/gateway";
import {
experimental_evaluate as evaluate,
type Experimental_EvaluationQuestion,
type Experimental_EvaluationResult,
} from "ai";
export interface Settled<T> {
readonly item: T;
readonly index: number;
readonly latencyMs: number;
readonly inputTokens: number;
readonly cached: boolean;
}
export interface Match<T> extends Settled<T> {
readonly p: number;
}
export interface Ranked<T> extends Settled<T> {
readonly score: number;
}
export interface Labeled<T, L extends string> extends Settled<T> {
readonly label: L;
readonly p: number;
}
export interface JevOptions<T, R = Settled<T>> {
readonly threshold?: number;
readonly serialize?: (item: T) => string;
readonly concurrency?: number;
readonly signal?: AbortSignal | undefined;
readonly apiKey?: string | undefined;
readonly timeoutMs?: number;
readonly onResult?: (result: R) => void;
readonly onError?: (item: T, error: unknown) => void;
}
type Questions = Record<string, Experimental_EvaluationQuestion>;
type Answers<Q extends Questions> = Experimental_EvaluationResult<Q>["answers"];
interface Asked<Q extends Questions> {
readonly answers: Answers<Q>;
readonly latencyMs: number;
readonly inputTokens: number;
readonly cached: boolean;
}
export const jevModelId = "typesafe-ai/jev";
export const jevTimeoutMs = 4000;
export const rankLevels = ["not at all", "slightly", "somewhat", "very", "extremely"] as const;
const cache = new Map<string, { answers: unknown; inputTokens: number }>();
const cacheLimit = 5000;
function remember(key: string, answers: unknown, inputTokens: number): void {
if (cache.size >= cacheLimit) cache.delete(cache.keys().next().value as string);
cache.set(key, { answers, inputTokens });
}
function serializeByDefault(item: unknown): string {
return typeof item === "string" ? item : JSON.stringify(item);
}
function deadline(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
const timeout = AbortSignal.timeout(timeoutMs);
return signal ? AbortSignal.any([signal, timeout]) : timeout;
}
async function ask<Q extends Questions>(
state: string,
questions: Q,
options: Pick<JevOptions<unknown>, "signal" | "apiKey" | "timeoutMs">,
): Promise<Asked<Q>> {
const key = `${JSON.stringify(questions)}\n${state}`;
const hit = cache.get(key);
if (hit) return { answers: hit.answers as Answers<Q>, latencyMs: 0, inputTokens: 0, cached: true };
const gateway = createGateway(options.apiKey ? { apiKey: options.apiKey } : {});
const startedAt = performance.now();
const attempt = () =>
evaluate({
model: gateway.evaluationModel(jevModelId),
state,
questions,
maxRetries: 0,
abortSignal: deadline(options.signal, options.timeoutMs ?? jevTimeoutMs),
});
let result: Awaited<ReturnType<typeof attempt>>;
try {
result = await attempt();
} catch (error) {
if (options.signal?.aborted) throw error;
result = await attempt();
}
const inputTokens = result.usage.inputTokens ?? 0;
remember(key, result.answers, inputTokens);
return { answers: result.answers, latencyMs: Math.round(performance.now() - startedAt), inputTokens, cached: false };
}
async function fanOut<T, Q extends Questions, R extends Settled<T>>(
items: readonly T[],
questions: Q,
options: JevOptions<T, R>,
settle: (base: Settled<T>, answers: Answers<Q>) => R,
): Promise<R[]> {
const serialize = options.serialize ?? serializeByDefault;
const results: R[] = [];
let cursor = 0;
async function work(): Promise<void> {
while (cursor < items.length) {
if (options.signal?.aborted) return;
const index = cursor++;
const item = items[index] as T;
let asked: Asked<Q>;
try {
asked = await ask(serialize(item), questions, options);
} catch (error) {
if (options.signal?.aborted || !options.onError) throw error;
options.onError(item, error);
continue;
}
const result = settle({ item, index, latencyMs: asked.latencyMs, inputTokens: asked.inputTokens, cached: asked.cached }, asked.answers);
results[index] = result;
options.onResult?.(result);
}
}
const workers = Math.max(1, Math.min(options.concurrency ?? 8, items.length));
await Promise.all(Array.from({ length: workers }, work));
return results.filter((result): result is R => result !== undefined);
}
export async function jev<T>(items: readonly T[], predicate: string, options: JevOptions<T, Match<T>> = {}): Promise<Match<T>[]> {
const threshold = options.threshold ?? 0.5;
const questions = { match: { type: "boolean", instructions: predicate } } as const satisfies Questions;
const matches = await fanOut(items, questions, options, (base, answers) => ({ ...base, p: answers.match.probability }));
return matches.filter((match) => match.p >= threshold).sort((a, b) => b.p - a.p);
}
jev.rank = async function rank<T>(items: readonly T[], criterion: string, options: JevOptions<T, Ranked<T>> = {}): Promise<Ranked<T>[]> {
const questions = { level: { type: "score", instructions: criterion, criteria: [...rankLevels] } } as const satisfies Questions;
const top = rankLevels.length - 1;
const ranked = await fanOut(items, questions, options, (base, answers) => ({ ...base, score: Math.min(1, Math.max(0, answers.level.score / top)) }));
return ranked.sort((a, b) => b.score - a.score);
};
jev.classify = async function classify<T, L extends string>(
items: readonly T[],
instructions: string,
labels: Record<L, string>,
options: JevOptions<T, Labeled<T, L>> = {},
): Promise<Labeled<T, L>[]> {
const questions = { label: { type: "choice", instructions, criteria: labels } } as const satisfies Questions;
return fanOut(items, questions, options, (base, answers) => {
const label = answers.label.choice as L;
const probabilities = (answers.label.probabilities ?? {}) as Partial<Record<L, number>>;
return { ...base, label, p: probabilities[label] ?? 1 };
});
};
Limited concurrency, a serializer you control, an in-memory cache keyed by row and question, and an AbortSignal so a new predicate cancels the previous fan-out. Drop it in any TypeScript project with an AI Gateway key.