Chapter 112 — GenAI Interview: Hands-On Coding Tasks (with Solutions)
📖 What to Expect
Practical GenAI interviews often ask you to write code live: call an LLM API, stream a response, get structured JSON, chunk text, or build a mini RAG. Below are common tasks in TypeScript (Node.js) and Python, with solutions you can adapt. Always mention API keys come from environment variables, never hard-coded.
1. Call an LLM API (Node.js / TypeScript)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from the environment
export async function summarise(text: string): Promise<string> {
const res = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 300,
system: "You summarise text in 3 bullet points, simple English.",
messages: [{ role: "user", content: text }],
});
return res.content.find((b) => b.type === "text")?.text ?? "";
}2. Same in Python
import anthropic
client = anthropic.Anthropic() # uses ANTHROPIC_API_KEY
def summarise(text: str) -> str:
res = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=300,
system="You summarise text in 3 bullet points, simple English.",
messages=[{"role": "user", "content": text}],
)
return "".join(b.text for b in res.content if b.type == "text")3. Streaming endpoint with Express (Server-Sent Events)
import express from "express";
import Anthropic from "@anthropic-ai/sdk";
const app = express();
const client = new Anthropic();
app.use(express.json());
app.post("/api/chat/stream", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
const stream = client.messages.stream({
model: "claude-sonnet-5-5",
max_tokens: 1024,
messages: req.body.messages, // validate in real code
});
req.on("close", () => stream.abort()); // stop paying if the user leaves
stream.on("text", (delta) => res.write(`data: ${JSON.stringify({ delta })}\n\n`));
stream.on("error", () => { res.write(`event: error\ndata: {}\n\n`); res.end(); });
await stream.finalMessage().catch(() => {});
res.write("event: done\ndata: {}\n\n");
res.end();
});4. React client that reads the stream
import { useState } from "react";
export function Chat() {
const [answer, setAnswer] = useState("");
const [loading, setLoading] = useState(false);
async function ask(question: string) {
setAnswer("");
setLoading(true);
const res = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: [{ role: "user", content: question }] }),
});
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const events = buffer.split("\n\n");
buffer = events.pop() ?? ""; // keep incomplete event
for (const evt of events) {
const line = evt.split("\n").find((l) => l.startsWith("data: "));
if (!line || evt.startsWith("event:")) continue;
const { delta } = JSON.parse(line.slice(6));
if (delta) setAnswer((a) => a + delta);
}
}
setLoading(false);
}
return (
<div>
<button onClick={() => ask("Explain RAG in 3 lines")} disabled={loading}>Ask</button>
<p style={{ whiteSpace: "pre-wrap" }}>{answer}</p>
</div>
);
}5. Structured JSON output with validation and retry
interface Ticket { category: "billing" | "technical" | "account" | "other"; urgency: "low" | "medium" | "high"; summary: string }
function isTicket(v: any): v is Ticket {
return (
v && ["billing", "technical", "account", "other"].includes(v.category) &&
["low", "medium", "high"].includes(v.urgency) && typeof v.summary === "string"
);
}
export async function classifyTicket(text: string): Promise<Ticket> {
const res = await client.messages.create({
model: "claude-haiku-5-5", // small, fast model is enough
max_tokens: 300,
tools: [{
name: "save_ticket",
description: "Save the classified support ticket.",
input_schema: {
type: "object",
properties: {
category: { type: "string", enum: ["billing", "technical", "account", "other"] },
urgency: { type: "string", enum: ["low", "medium", "high"] },
summary: { type: "string", description: "One sentence" },
},
required: ["category", "urgency", "summary"],
},
}],
tool_choice: { type: "tool", name: "save_ticket" }, // force structured output
messages: [{ role: "user", content: `<ticket>${text}</ticket>` }],
});
const block = res.content.find((b) => b.type === "tool_use");
if (!block || !isTicket(block.input)) throw new Error("Invalid classification");
return block.input;
}6. Chunk text with overlap (Python)
def chunk_text(text: str, chunk_size: int = 800, overlap: int = 100) -> list[str]:
"""Split by paragraphs, then pack paragraphs into chunks of ~chunk_size characters."""
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
chunks, current = [], ""
for p in paragraphs:
if len(current) + len(p) + 2 <= chunk_size:
current = f"{current}\n\n{p}" if current else p
else:
if current:
chunks.append(current)
tail = current[-overlap:] if current else "" # carry overlap into next chunk
current = f"{tail}\n\n{p}" if tail else p
while len(current) > chunk_size: # very long paragraph → hard split
chunks.append(current[:chunk_size])
current = current[chunk_size - overlap:]
if current:
chunks.append(current)
return chunksMention: production code usually counts tokens (tokenizer) instead of characters and splits by headings.
7. Cosine similarity search over embeddings (Python, NumPy)
import numpy as np
def top_k(query_vec: np.ndarray, doc_vecs: np.ndarray, k: int = 5) -> list[int]:
q = query_vec / np.linalg.norm(query_vec)
d = doc_vecs / np.linalg.norm(doc_vecs, axis=1, keepdims=True)
scores = d @ q
return list(np.argsort(-scores)[:k])For large data, use a vector database instead of brute force.
8. Mini RAG in Python (in-memory)
import anthropic
import numpy as np
client = anthropic.Anthropic()
# uses chunk_text (task 6) and top_k (task 7) from above
class MiniRAG:
def __init__(self, embed_fn):
self.embed = embed_fn # function: list[str] -> np.ndarray (n, dim)
self.chunks: list[str] = []
self.vectors = None
def add_documents(self, docs: list[str]):
for doc in docs:
self.chunks.extend(chunk_text(doc))
self.vectors = self.embed(self.chunks)
def ask(self, question: str, k: int = 4) -> str:
q = self.embed([question])[0]
ids = top_k(q, self.vectors, k)
context = "\n\n".join(f"[{i}] {self.chunks[i]}" for i in ids)
res = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=500,
system="Answer only from the context. Cite chunk numbers like [3]. "
"If the answer is not in the context, say you don't know.",
messages=[{"role": "user", "content": f"<context>\n{context}\n</context>\n\nQuestion: {question}"}],
)
return "".join(b.text for b in res.content if b.type == "text")9. Retry with exponential backoff for rate limits (TypeScript)
async function withRetry<T>(fn: () => Promise<T>, retries = 4): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err: any) {
const status = err?.status;
const retryable = status === 429 || (status >= 500 && status < 600);
if (!retryable || attempt >= retries) throw err;
const delay = Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random()); // jitter
await new Promise((r) => setTimeout(r, delay));
}
}
}(Official SDKs already retry some errors automatically — mention that and configure maxRetries.)
10. Count tokens / estimate cost before sending
const count = await client.messages.countTokens({
model: "claude-sonnet-5-5",
messages: [{ role: "user", content: longText }],
});
const estimatedInputCost = (count.input_tokens / 1_000_000) * INPUT_PRICE_PER_MILLION;11. Simple evaluation script (Python)
import json
def evaluate(test_file: str, answer_fn, judge_fn) -> float:
"""test_file: JSONL with {"question": ..., "expected": ...}."""
results = []
with open(test_file) as f:
for line in f:
case = json.loads(line)
answer = answer_fn(case["question"])
verdict = judge_fn(case["question"], case["expected"], answer) # returns True/False
results.append({**case, "answer": answer, "pass": verdict})
score = sum(r["pass"] for r in results) / len(results)
print(f"Pass rate: {score:.1%} ({len(results)} cases)")
for r in results:
if not r["pass"]:
print("FAIL:", r["question"])
return score12. Conversation memory: keep recent turns, summarise older ones
async function buildHistory(messages: Anthropic.MessageParam[], keepLast = 8) {
if (messages.length <= keepLast) return messages;
const older = messages.slice(0, -keepLast);
const summary = await summarise(older.map((m) => `${m.role}: ${JSON.stringify(m.content)}`).join("\n"));
return [
{ role: "user" as const, content: `Summary of earlier conversation:\n${summary}` },
{ role: "assistant" as const, content: "Understood." },
...messages.slice(-keepLast),
];
}🧠 What Interviewers Look For
- Clean API usage with keys from environment variables.
- Error handling: timeouts, retries, invalid outputs.
- Validation of model output before using it.
- Security awareness: permissions in code, untrusted input in tags.
- Thinking about cost and latency (model choice, streaming, caching).
← Scenario & Design Questions | Index | Next: Coding Question Tracker →