GuidesSummarize long documents

Summarize long documents

Contracts, reports, meeting transcripts and policy manuals are often too long to read in full but too important to skim. This guide turns long documents into structured summaries with the Syntic API: first for documents that fit in a single request, then for ones that do not, and finally for whole collections.

You will need: an API key in SYNTIC_API_KEY, Python 3.9+ with the openai package (an OpenAI-compatible client), and some way to turn your documents into plain text (see below).

Decide what the summary is for

“Summarize this” produces a different answer every time because it does not say what matters. Before writing code, write down:

  • Who reads it. A lawyer, a sales manager and an engineer want different things from the same contract.
  • What they must find. For a supplier contract, for example: parties, term and renewal, price and payment terms, termination rights, liability cap, anything unusual.
  • The format. Fixed headings make summaries easy to compare across documents.
  • How you will check it. Pick five documents, write the ideal summary for each by hand, and compare model output against them.

Get the text out

The API accepts text only: send document content as the text of a message. Convert files to text in your own code before calling it, for example:

FormatOne way to extract text in Python
PDF with a text layerpypdf (PdfReader(path).pages[i].extract_text())
Wordpython-docx
HTMLbeautifulsoup4 (get_text("\n"))
Scanned imagesAn OCR tool, before anything else

Keep headings and numbered clauses when you can; they help the model and make references in the summary meaningful.

Documents that fit in one request

The Amara models have a 262,144-token context window, enough for several hundred pages of plain English. Most single documents fit, and one request is simpler and more coherent than splitting.

import os
 
from openai import OpenAI
 
client = OpenAI(base_url="https://app.syntic.ai/v1", api_key=os.environ["SYNTIC_API_KEY"])
 
INSTRUCTIONS = """
You summarize supplier contracts for a procurement team that will decide whether to sign.
Use exactly these headings, in this order:
 
## Parties
## Term and renewal
## Price and payment
## Termination
## Liability and indemnity
## Unusual or one-sided terms
 
Under each heading write short bullet points. Quote clause numbers in brackets, e.g. [7.2].
If the contract does not cover a heading, write "Not addressed." Do not add advice.
""".strip()
 
 
def summarize(text: str, model: str = "Amara S1.2") -> str:
    reply = client.chat.completions.create(
        model=model,
        stream=False,
        max_tokens=1500,
        messages=[
            {"role": "system", "content": INSTRUCTIONS},
            {"role": "user", "content": f"<contract>\n{text}\n</contract>"},
        ],
    )
    return reply.choices[0].message.content
 
 
text = open("supplier-agreement.txt", encoding="utf-8").read()
print(summarize(text))

Why it is built this way:

  • Fixed headings and “Not addressed” make a missing clause visible instead of silently omitted.
  • Clause references let a reader jump to the source and check.
  • Document inside tags, instructions in the system message keeps the two apart, so text in the document cannot pass itself off as instructions.
  • max_tokens bounds the length (and cost) of the summary.

For a document over about 50 pages, consider Amara Deep, which is aimed at work that needs several steps of reasoning, and compare results on your evaluation set. See Choosing a model.

Documents that do not fit

When a document is longer than the context window, or the request would exceed the 2 MB body limit, summarize in two stages: summarize each part, then summarize the summaries.

Split on natural boundaries

Cut at chapter or section headings rather than at a fixed character count, so no clause is cut in half. Aim for parts of 50,000 to 100,000 tokens (roughly 200,000 to 400,000 characters of English).

Summarize each part with the same headings

Ask for the same headings as the final summary so the partial results line up.

Combine

Send all partial summaries in one request and ask for a single summary that merges them, removes repeats and resolves conflicts (for example, a later amendment changing an earlier price).

import re
from concurrent.futures import ThreadPoolExecutor
 
MAX_PART_CHARS = 300_000
 
 
def split_sections(text: str) -> list[str]:
    """Split before lines that look like section headings, then pack sections into parts."""
    sections = re.split(r"\n(?=(?:SECTION|Section|ARTICLE|Article|\d+\.)\s)", text)
    parts, current = [], ""
    for section in sections:
        if current and len(current) + len(section) > MAX_PART_CHARS:
            parts.append(current)
            current = ""
        current += section + "\n"
    if current:
        parts.append(current)
    return parts
 
 
def summarize_long(text: str) -> str:
    parts = split_sections(text)
    if len(parts) == 1:
        return summarize(text)
 
    # Stage 1: parts in parallel (keep the pool small to respect rate limits).
    with ThreadPoolExecutor(max_workers=4) as pool:
        partials = list(pool.map(summarize, parts))
 
    # Stage 2: merge.
    joined = "\n\n".join(f"<part n={i + 1}>\n{p}\n</part>" for i, p in enumerate(partials))
    reply = client.chat.completions.create(
        model="Amara S1.2",
        stream=False,
        max_tokens=2000,
        messages=[
            {"role": "system", "content": INSTRUCTIONS},
            {"role": "user", "content": (
                "Below are summaries of consecutive parts of ONE contract. Merge them into a single "
                "summary with the same headings. Remove repetition. Where parts conflict, prefer the "
                "later part and say so.\n\n" + joined
            )},
        ],
    )
    return reply.choices[0].message.content

Stage 1 sends the same INSTRUCTIONS system message on every request, so after the first part it is served from cache at the lower cached-input rate.

Asking follow-up questions

Readers usually want to ask more after reading a summary. Keep the document as the first user message and add questions after it:

history = [
    {"role": "system", "content": "Answer questions about the contract below. Quote clause numbers. "
                                   "If the contract does not say, answer 'The contract does not say.'"},
    {"role": "user", "content": f"<contract>\n{text}\n</contract>"},
]
 
def ask(question: str) -> str:
    history.append({"role": "user", "content": question})
    reply = client.chat.completions.create(model="Amara S1.2", stream=False, max_tokens=600, messages=history)
    answer = reply.choices[0].message.content
    history.append({"role": "assistant", "content": answer})
    return answer
 
print(ask("Can we terminate early if the supplier misses delivery dates?"))
print(ask("Is there a cap on our liability?"))

Because the long document sits at the start and never changes, every follow-up reuses it from cache. The first question pays full input price for the document; later ones pay the cached rate for it.

Collections of documents

For hundreds of documents, produce one structured summary per document and store it alongside the file. Then:

  • Search the summaries, not the full documents, to find the few that matter.
  • Answer cross-document questions (“which contracts renew automatically in Q1?”) by sending the relevant summaries in one request.
  • Re-summarize only what changed when a document is updated.

Process a large collection with a queue and a fixed number of workers, and handle 429 and 402 responses as shown in Handle usage limits.

Check the results

Summaries can be fluent and still wrong. Before relying on them:

  • Compare model summaries with your hand-written ones for the five reference documents, heading by heading.
  • Spot-check every clause reference in a sample of summaries.
  • Ask a second, short request to list any statement in the summary that the document does not support; review anything it flags.
  • For high-stakes documents, treat the summary as a reading aid and keep a person responsible for the decision.

Cost

A 60-page contract is around 30,000 tokens. Summarizing it once on Amara S1.2 with a 1,200-token summary costs about $0.09 from usage credits (30,000 × $2.50 + 1,200 × $12.00, per million tokens). Each follow-up question that reuses the cached document adds roughly $0.01 to $0.02. See Pricing.

Was this page helpful?