Skip to content
echogeco
Prompt and context engineering on real data · Context assembly

Chunk the policy manual

Split the policy wording into chunks a retriever can cite by clause number, capped at 400 tokens, without mistaking a sentence for a heading.

codeeasyExercise · 3 min · 36 of 64
On this page

The gold set scores the pilot by clause number, and the prototype's chunker made that impossible. Its replacement is about forty lines, and the line that matters is the one deciding what counts as a heading.

The situation

The insurer's 340-page policy set, the motor, property and commercial wordings plus their endorsements, arrives as text extracted from the PDFs: one string per printed line, cover and contents pages removed. Sections are numbered and titled, 4 Exclusions, 4.2 Escape of water, 4.2.1 Frozen pipes. The prototype split on any line beginning with a digit and kept each section whole, and three things broke. Body lines such as "14 days after the leak is found" became sections of their own, 61 of them. Commercial sections of up to 2,700 words, about 3,600 tokens, went into the prompt whole, three times the 1,200-token clause band from context assembly. And chunks were numbered by position, so the QA lead's 200 gold labels, written as clause numbers, had to be matched by hand. The retriever puts three chunks in each prompt, so the cap is 400 tokens. Assume 0.75 words per token: a chunk's tokens are its words ÷ 0.75, rounded up.

The question

Implement solve so that every chunk is citable by the clause number the handlers use, never mixes two sections, and never exceeds the cap unless a single paragraph does.

def solve(lines, max_tokens):
    """Split one policy document into chunks a retriever can cite.

    lines       one string per printed line; the first line is a heading.
                A line is a heading when it is a section number, a space and a
                title starting with a capital letter ('4.2 Escape of water'), AND
                the number is one the document can have next: the first child of
                the current section (4.2 -> 4.2.1), or the next sibling of the
                current section or of one of its ancestors (4.2 -> 4.3 or 5).
                Any other line is body text. A blank line ends a paragraph; a
                wrapped line continues it.
    max_tokens  the cap, counting body words at 0.75 words per token, rounded up.

    Returns [(chunk_id, heading_path, text, tokens), ...] in document order:
      chunk_id      the section number, '4.2'; when a section needs more than
                    one chunk, '4.2#1', '4.2#2', ...
      heading_path  titles from the top-level section down, joined by ' > '
      text          the chunk's paragraphs, lines joined by single spaces,
                    paragraphs separated by newlines
      tokens        the chunk's body tokens
    Consecutive paragraphs share a chunk while it fits the cap; a paragraph
    over the cap is a chunk on its own and is never cut. A heading with no
    body text of its own produces no chunk.
    """
lines = [
    "4 Exclusions",
    "4.1 Wear and tear",
    "We do not cover gradual deterioration.",
    "4.2 Escape of water",
    "We cover sudden escape of water",
    "from a fixed installation.",
    "",
    "14 days after the leak is found, cover ends",
    "unless the leak has been reported to us.",
    "5 Claims",
    "5.1 Reporting",
    "Tell us within 30 days.",
]
solve(lines, 400) == [
    ("4.1", "Exclusions > Wear and tear", "We do not cover gradual deterioration.", 8),
    ("4.2", "Exclusions > Escape of water",
     "We cover sudden escape of water from a fixed installation.\n"
     "14 days after the leak is found, cover ends unless the leak has been reported to us.", 36),
    ("5.1", "Claims > Reporting", "Tell us within 30 days.", 7),
]
# At a 20-token cap, 4.2 becomes '4.2#1' (14 tokens) and '4.2#2' (23 tokens):
# the second paragraph is over the cap and stays whole.

A strong answer · Rubric · Where answers go wrong · Solution

Write your answer first, then open