Two moves you already have: batch-loop a list through an agent, and constrain an agent's output with Literal. What happens when you combine them to produce not a list of labels, but counts of labels?
Loop over results, classify each snippet as positive/negative/neutral, then tally how many fell into each bucket — a dict with three counts?
Exactly. The key move is a plain counter dict — initialize three zeros, increment on each label. The smallest version:
counts = {"positive": 0, "negative": 0, "neutral": 0}
agent = Agent(model, result_type=Literal["positive", "negative", "neutral"])
for r in results:
label = agent.run_sync(r["snippet"]).output
counts[label] += 1
print(counts)The Literal guarantees the label is always one of the three keys you already initialized — no KeyError risk, no defensive normalization.
So because Literal locks the output to exactly those three strings, the dict-increment is safe without any if label in counts guard?
Exactly. The type system does the guard for you. If you tried this with an unconstrained classifier, you'd need a defensive .get(label, 0) or an if check. With Literal, the contract is already enforced upstream. Full function:
def classify_all_results(query: str, count: int) -> dict:
results = search(query, count=count)
agent = Agent(model, result_type=Literal["positive", "negative", "neutral"])
counts = {"positive": 0, "negative": 0, "neutral": 0}
for r in results:
label = agent.run_sync(r["snippet"]).output
counts[label] += 1
counts["total"] = len(results)
return countsWhy build the agent once outside the loop instead of per-iteration?
Agent(...) is cheap to construct but still allocates. Building once and reusing inside the loop is cleaner and faster. The agent holds no state between calls — each run_sync is a fresh request — so reuse is both safe and idiomatic. This is true for every batch pattern you'll see from here on.
So one function now does what would have been a spreadsheet full of manual classifications — search ten results and get three counts back.
Exactly the Week 2 payoff. Retrieval feeds the classifier; the classifier feeds the tally. By Friday you've seen the full loop — search, reason, aggregate. Next week sharpens the retrieval side with embeddings and caching.
TL;DR: Literal locks the label to your keyset; a plain counter dict tallies safely without guards.
Literal["a", "b", "c"] — three-value classifier, Pydantic-validatedcounts = {"a": 0, ...} — pre-initialize with exactly the same keys| Classifier output | Need .get()? |
|---|---|
| Free-form string | yes — defensive |
Literal[...] | no — keys match |
Literal upstream removes defensive code downstream — the shape is already validated before your tally runs.
Two moves you already have: batch-loop a list through an agent, and constrain an agent's output with Literal. What happens when you combine them to produce not a list of labels, but counts of labels?
Loop over results, classify each snippet as positive/negative/neutral, then tally how many fell into each bucket — a dict with three counts?
Exactly. The key move is a plain counter dict — initialize three zeros, increment on each label. The smallest version:
counts = {"positive": 0, "negative": 0, "neutral": 0}
agent = Agent(model, result_type=Literal["positive", "negative", "neutral"])
for r in results:
label = agent.run_sync(r["snippet"]).output
counts[label] += 1
print(counts)The Literal guarantees the label is always one of the three keys you already initialized — no KeyError risk, no defensive normalization.
So because Literal locks the output to exactly those three strings, the dict-increment is safe without any if label in counts guard?
Exactly. The type system does the guard for you. If you tried this with an unconstrained classifier, you'd need a defensive .get(label, 0) or an if check. With Literal, the contract is already enforced upstream. Full function:
def classify_all_results(query: str, count: int) -> dict:
results = search(query, count=count)
agent = Agent(model, result_type=Literal["positive", "negative", "neutral"])
counts = {"positive": 0, "negative": 0, "neutral": 0}
for r in results:
label = agent.run_sync(r["snippet"]).output
counts[label] += 1
counts["total"] = len(results)
return countsWhy build the agent once outside the loop instead of per-iteration?
Agent(...) is cheap to construct but still allocates. Building once and reusing inside the loop is cleaner and faster. The agent holds no state between calls — each run_sync is a fresh request — so reuse is both safe and idiomatic. This is true for every batch pattern you'll see from here on.
So one function now does what would have been a spreadsheet full of manual classifications — search ten results and get three counts back.
Exactly the Week 2 payoff. Retrieval feeds the classifier; the classifier feeds the tally. By Friday you've seen the full loop — search, reason, aggregate. Next week sharpens the retrieval side with embeddings and caching.
TL;DR: Literal locks the label to your keyset; a plain counter dict tallies safely without guards.
Literal["a", "b", "c"] — three-value classifier, Pydantic-validatedcounts = {"a": 0, ...} — pre-initialize with exactly the same keys| Classifier output | Need .get()? |
|---|---|
| Free-form string | yes — defensive |
Literal[...] | no — keys match |
Literal upstream removes defensive code downstream — the shape is already validated before your tally runs.
Create a free account to get started. Paid plans unlock all tracks.