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:
| Format | One way to extract text in Python |
|---|---|
| PDF with a text layer | pypdf (PdfReader(path).pages[i].extract_text()) |
| Word | python-docx |
| HTML | beautifulsoup4 (get_text("\n")) |
| Scanned images | An 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_tokensbounds 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.contentStage 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.
Related pages
- Limits: context window and request size.
- Prompting tips: separating instructions from material.
- Control costs: keeping long-document work affordable.