Coding Round
Write a function that returns the top-k most similar documents by cosine similarity.
Example
Plain Python, no numpy. Sort by score descending and break ties by ID so the result is stable.
import math
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
na, nb = math.sqrt(sum(x * x for x in a)), math.sqrt(sum(y * y for y in b))
return 0.0 if na == 0 or nb == 0 else dot / (na * nb)
def top_k(query_vec, index, k=2):
scored = [(round(cosine(query_vec, vec), 3), doc_id) for doc_id, vec in index.items()]
scored.sort(key=lambda t: (-t[0], t[1]))
return scored[:k]
index = {"returns": [0.9, 0.1, 0.0], "refunds": [0.7, 0.6, 0.1],
"shipping": [0.0, 0.2, 0.9], "warranty": [0.5, 0.0, 0.4]}
print(top_k([1.0, 0.3, 0.0], index, k=3))
print(top_k([0.0, 0.0, 0.0], index, k=1)) # zero vector edge caseOutput
[(0.984, 'returns'), (0.909, 'refunds'), (0.748, 'warranty')] [(0.0, 'refunds')]
Follow-ups: normalise vectors once at insert time so search is just a dot product; use heapq.nlargest for big indexes; mention ANN (HNSW) beyond about a million vectors.
Split a text into chunks of N words with an overlap of M words.
Solution
def chunk_words(text, size, overlap):
if not 0 <= overlap < size:
raise ValueError("need 0 <= overlap < size")
words = text.split()
step = size - overlap
chunks = []
for start in range(0, len(words), step):
chunks.append(" ".join(words[start:start + size]))
if start + size >= len(words):
break
return chunks
text = ("Most items can be returned within 10 days of delivery. Electronics have a "
"7 day replacement only policy. Refunds reach the original payment method in 5-7 working days.")
for i, c in enumerate(chunk_words(text, size=10, overlap=3)):
print(i, c)Output
0 Most items can be returned within 10 days of delivery. 1 days of delivery. Electronics have a 7 day replacement only 2 day replacement only policy. Refunds reach the original payment method 3 original payment method in 5-7 working days.
Edge cases interviewers check: overlap >= size (infinite loop), empty text, and the last chunk not being dropped. In real systems you chunk by tokens and by headings/paragraphs first.
Implement a BM25 scorer.
Solution
import math
from collections import Counter
def bm25_scores(query, docs, k1=1.5, b=0.75):
toks = [d.lower().split() for d in docs]
n, avgdl = len(toks), sum(len(t) for t in toks) / len(toks)
df = Counter(w for t in toks for w in set(t))
scores = []
for t in toks:
tf, s = Counter(t), 0.0
for w in query.lower().split():
if w not in tf:
continue
idf = math.log((n - df[w] + 0.5) / (df[w] + 0.5) + 1)
s += idf * tf[w] * (k1 + 1) / (tf[w] + k1 * (1 - b + b * len(t) / avgdl))
scores.append(round(s, 3))
return scores
docs = ["cod available up to 50000 rupees",
"refund to original payment method in 5 to 7 days",
"cod orders get refund to bank account"]
print(bm25_scores("cod refund", docs))Output
[0.521, 0.413, 0.978]
Explain the three parts: IDF (rare words count more), saturating term frequency (k1), and length normalisation (b). The third document wins because it has both words.
Trim a chat history so it fits a token budget, keeping the system prompt and the newest messages.
Example
Token counts are estimated as about 4 characters per token here; in production use the provider's token counting endpoint.
def est_tokens(text):
return max(1, len(text) // 4)
def trim_history(system, messages, budget):
used = est_tokens(system)
kept = []
for msg in reversed(messages): # newest first
cost = est_tokens(msg["content"])
if used + cost > budget:
break
kept.append(msg)
used += cost
kept.reverse()
while kept and kept[0]["role"] != "user": # history must start with a user turn
kept.pop(0)
return kept, used
system = "You are ShopEase support. Be brief."
messages = [
{"role": "user", "content": "Hi, I ordered a pressure cooker last week, order SE1077."},
{"role": "assistant", "content": "Thanks! SE1077 is out for delivery and should arrive today."},
{"role": "user", "content": "Also, what is the return window for shoes?"},
{"role": "assistant", "content": "Shoes can be returned within 10 days with tags attached."},
{"role": "user", "content": "And for earbuds?"},
]
kept, used = trim_history(system, messages, budget=45)
print("kept", len(kept), "of", len(messages), "messages, ~", used, "tokens")
for m in kept:
print(f" {m['role']}: {m['content']}")Output
kept 3 of 5 messages, ~ 36 tokens user: Also, what is the return window for shoes? assistant: Shoes can be returned within 10 days with tags attached. user: And for earbuds?
Better versions summarise the dropped messages into one short "conversation so far" note instead of discarding them, and never drop a tool_use without its matching tool_result.
Parse JSON from a model reply, repair common problems, validate it, and retry with the error.
Example
fake_model is a stand-in: the first reply has a code fence and a trailing comma, the second misses a field, the third is valid.
import json, re
REPLIES = iter([
'Sure! Here it is:\n~~~json\n{"intent": "refund_status", "order_id": "SE1042",}\n~~~',
'{"intent": "refund_status"}',
'{"intent": "refund_status", "order_id": "SE1042", "urgent": false}',
])
def fake_model(prompt): # stand-in for the model
return next(REPLIES)
REQUIRED = {"intent": str, "order_id": str, "urgent": bool}
def repair(text):
m = re.search(r"\{.*\}", text, re.S) # keep only the outermost object
if not m:
raise ValueError("no JSON object found")
return re.sub(r",\s*([}\]])", r"\1", m.group()) # remove trailing commas
def validate(obj):
for key, typ in REQUIRED.items():
if not isinstance(obj.get(key), typ):
raise ValueError(f"field '{key}' missing or not {typ.__name__}")
return obj
def extract(prompt, max_attempts=3):
for attempt in range(1, max_attempts + 1):
reply = fake_model(prompt)
try:
result = validate(json.loads(repair(reply)))
print(f"attempt {attempt}: ok")
return result
except (ValueError, json.JSONDecodeError) as e:
print(f"attempt {attempt}: {e}")
prompt += f"\nYour last reply was invalid: {e}. Return only valid JSON."
raise RuntimeError("could not get valid JSON")
print(extract("Classify this ticket as JSON: 'SE1042 refund?'"))Output
attempt 1: field 'urgent' missing or not bool
attempt 2: field 'order_id' missing or not str
attempt 3: ok
{'intent': 'refund_status', 'order_id': 'SE1042', 'urgent': False}Mention that with structured outputs (output_config.format or messages.parse) the API guarantees schema-valid JSON, so this loop is the fallback for models or providers without it.
Write a minimal agent loop that executes tool calls until the model gives a final answer.
Example
scripted_model is a stand-in for the API: it returns blocks shaped like Claude's tool_use / text content.
def get_order(order_id):
return {"SE1042": "delivered on 12 Oct"}.get(order_id, "not found")
def days_since(date_text):
return "9 days" # fixed for the demo
TOOLS = {"get_order": get_order, "days_since": days_since}
def scripted_model(messages): # stand-in for client.messages.create
n_results = sum(1 for m in messages if m["role"] == "user" and isinstance(m["content"], list))
if n_results == 0:
return "tool_use", [{"type": "tool_use", "id": "t1", "name": "get_order", "input": {"order_id": "SE1042"}}]
if n_results == 1:
return "tool_use", [{"type": "tool_use", "id": "t2", "name": "days_since", "input": {"date_text": "12 Oct"}}]
return "end_turn", [{"type": "text", "text": "SE1042 was delivered 9 days ago, so you still have 1 day to return it."}]
def run_agent(question, max_steps=5):
messages = [{"role": "user", "content": question}]
for step in range(max_steps):
stop, content = scripted_model(messages)
messages.append({"role": "assistant", "content": content})
if stop == "end_turn":
return next(b["text"] for b in content if b["type"] == "text")
results = []
for b in content:
if b["type"] == "tool_use":
fn = TOOLS.get(b["name"])
out = fn(**b["input"]) if fn else f"unknown tool {b['name']}"
print(f"step {step + 1}: {b['name']}({b['input']}) -> {out}")
results.append({"type": "tool_result", "tool_use_id": b["id"], "content": out,
"is_error": fn is None})
messages.append({"role": "user", "content": results})
return "Stopped: step limit reached."
print(run_agent("Can I still return SE1042?"))Output
step 1: get_order({'order_id': 'SE1042'}) -> delivered on 12 Oct
step 2: days_since({'date_text': '12 Oct'}) -> 9 days
SE1042 was delivered 9 days ago, so you still have 1 day to return it.Points to say aloud: append the full assistant content, return every result in one user message with the matching tool_use_id, mark failures with is_error, and cap the steps.
Implement a semantic cache with a similarity threshold.
Example
Bag-of-words vectors stand in for an embedding model.
import math, re
from collections import Counter
def embed(text): # stand-in for an embedding model
stop = {"what", "is", "the", "a", "for", "how", "do", "i", "my", "to", "can"}
return Counter(w for w in re.findall(r"[a-z]+", text.lower()) if w not in stop)
def cosine(a, b):
dot = sum(a[w] * b[w] for w in a)
norm = math.sqrt(sum(v * v for v in a.values())) * math.sqrt(sum(v * v for v in b.values()))
return dot / norm if norm else 0.0
class SemanticCache:
def __init__(self, threshold):
self.threshold, self.entries = threshold, []
def lookup(self, question):
q = embed(question)
scored = [(cosine(q, vec), ans) for vec, ans in self.entries]
best = max(scored, default=(0.0, None))
return (best[1] if best[0] >= self.threshold else None), round(best[0], 3)
def store(self, question, answer):
self.entries.append((embed(question), answer))
calls = 0
def answer(question, cache):
global calls
hit, sim = cache.lookup(question)
if hit:
return f"cache ({sim}) -> {hit}"
calls += 1
reply = f"model answer #{calls}" # stand-in for the LLM call
cache.store(question, reply)
return f"model ({sim}) -> {reply}"
cache = SemanticCache(threshold=0.75)
for q in ["What is the return window for shoes?", "return window for shoes",
"What is the refund time for UPI?", "UPI refund time"]:
print(f"{q:38} {answer(q, cache)}")
print("model calls:", calls)Output
What is the return window for shoes? model (0.0) -> model answer #1 return window for shoes cache (1.0) -> model answer #1 What is the refund time for UPI? model (0.0) -> model answer #2 UPI refund time cache (1.0) -> model answer #2 model calls: 2
Discuss the threshold trade-off, TTLs for changing data, and never sharing cached answers to personal questions across users.
Implement softmax with temperature and show its effect.
Solution
import math
def softmax(logits, temperature=1.0):
if temperature <= 0:
raise ValueError("temperature must be > 0 (use argmax for greedy decoding)")
scaled = [x / temperature for x in logits]
m = max(scaled) # subtract max for numerical stability
exps = [math.exp(x - m) for x in scaled]
total = sum(exps)
return [e / total for e in exps]
tokens = ["delivered", "shipped", "lost", "banana"]
logits = [3.0, 2.0, 0.5, -1.0]
for t in (0.2, 1.0, 2.0):
probs = softmax(logits, t)
print(f"T={t}: " + " ".join(f"{tok}={p:.3f}" for tok, p in zip(tokens, probs)))
print("overflow-safe:", [round(p, 3) for p in softmax([1000.0, 999.0])])Output
T=0.2: delivered=0.993 shipped=0.007 lost=0.000 banana=0.000 T=1.0: delivered=0.681 shipped=0.251 lost=0.056 banana=0.012 T=2.0: delivered=0.493 shipped=0.299 lost=0.141 banana=0.067 overflow-safe: [0.731, 0.269]
Low temperature concentrates probability on the top token; high temperature spreads it out. Subtracting the max avoids math.exp overflow.
Compute precision@k, recall@k and MRR for a retrieval run.
Solution
def precision_at_k(ranked, relevant, k):
return sum(1 for d in ranked[:k] if d in relevant) / k
def recall_at_k(ranked, relevant, k):
return sum(1 for d in ranked[:k] if d in relevant) / len(relevant) if relevant else 0.0
def mrr(runs):
total = 0.0
for ranked, relevant in runs:
total += next((1 / i for i, d in enumerate(ranked, 1) if d in relevant), 0.0)
return total / len(runs)
runs = [
(["d3", "d1", "d7"], {"d1"}),
(["d2", "d5", "d9"], {"d2", "d9"}),
(["d4", "d6", "d8"], {"d1"}),
]
for ranked, rel in runs:
print(ranked, sorted(rel), f"P@2={precision_at_k(ranked, rel, 2):.2f}", f"R@2={recall_at_k(ranked, rel, 2):.2f}")
print(f"MRR={mrr(runs):.3f}")Output
['d3', 'd1', 'd7'] ['d1'] P@2=0.50 R@2=1.00 ['d2', 'd5', 'd9'] ['d2', 'd9'] P@2=0.50 R@2=0.50 ['d4', 'd6', 'd8'] ['d1'] P@2=0.00 R@2=0.00 MRR=0.500
MRR only looks at the first relevant result, so it fits "one right answer" search; for RAG with several useful chunks, report recall@k as well.