Katkat

Load a Support Chat Transcript by Pages, Then Trim to a Token Budget

SierraSoftware Engineer · Technical Screen

Last updated

Build the transcript-loading layer for a customer support chat agent: assemble a full conversation from a paginated backend into one ordered list, then trim it down whenever it grows past a token budget by dropping the oldest turns first.

You're building the part of a support chat agent that manages its view of the conversation before handing it to a language model. Two pieces of this are covered here: loading a transcript through a paginated API, and trimming it back down once it grows past a token budget. (The exercise continued into further parts not covered by this practice question.)

Assume this interface:

from dataclasses import dataclass
from typing import Callable, Optional

@dataclass(frozen=True)
class Turn:
    id: str
    speaker: str       # e.g. "customer" or "agent"
    text: str
    token_count: int   # precomputed token count for this turn

# fetch_page(cursor) returns one page of turns plus the cursor for the next
# page, or None once there are no more pages.
FetchPage = Callable[[Optional[str]], tuple[list[Turn], Optional[str]]]

Constraints and Clarifications

  • Each turn's token count is already computed and given; you never tokenize text yourself.
  • The token budget passed to the trimming step is always a positive whole number supplied by whoever calls it.
  • Turns within a page, and pages themselves, are returned in chronological order.

Clarifying Questions

  • Could fetch_page ever return the same cursor twice, or otherwise misbehave, and if so how should the loader react?
  • Should turns be assembled into one list eagerly, or is a lazy, page-at-a-time generator preferred so a caller can stop early?
  • When trimming, is a total exactly equal to the budget acceptable, or must it be strictly under?
  • If a single turn's token count alone exceeds the budget, should it be kept anyway (as the only context available) or dropped?

Part 1 — Pagination

Implement:

def load_transcript(fetch_page: FetchPage) -> list[Turn]:
    """Call fetch_page repeatedly, starting with cursor=None, until it
    reports no further pages, and return every turn seen, in order."""
Your stopping condition is a None cursor, not an empty page — a page can legitimately come back empty while more pages still follow. Also decide what your loop does if a cursor it already used comes back again.

What This Part Should Cover

  • A loop with a termination condition based on the cursor, not page emptiness
  • Correct ordering of turns across pages and within a page
  • A guard against a cursor repeating, so a misbehaving source can't loop forever

Part 2 — Trimming to a token budget

Implement:

def trim_to_budget(turns: list[Turn], token_budget: int) -> list[Turn]:
    """Return the longest chronological suffix of turns whose token counts
    sum to at most token_budget, dropping the oldest turns first."""
Recomputing the sum after every removal makes this quadratic for no reason. Think about how a running total should change as you drop turns from the front one at a time.

What This Part Should Cover

  • Correctly dropping from the oldest end first, keeping the most recent context
  • A running total maintained incrementally rather than resummed after each drop
  • What happens when even the single most recent turn exceeds the budget on its own

Hints

Hint 1

Loop on the cursor being non-None, not on the page being non-empty — those are different conditions and only one of them is the real stopping signal.

Hint 2

Keep a set of cursors you've already consumed so a source that repeats a cursor can be caught instead of looping forever.

Hint 3

For trimming, maintain a running token total and pop from the front of a deque while the total exceeds budget, updating the total by subtraction rather than re-summing.

Hint 4

Check what trim_to_budget should return when the budget is smaller than even the newest single turn's token count.

Follow-up questions

  • How would you turn load_transcript into a lazy generator that a caller could stop consuming early, and what would that change about the pagination guard?
  • If turns could be edited or deleted after being fetched, how would you keep a cached transcript consistent across repeated calls?
  • How would you change trim_to_budget if certain turns (like a leading system prompt) must never be dropped regardless of budget?
  • How would you support fetching pages concurrently to speed up loading a very long transcript, while still assembling turns in the correct order?