Bonus: Top up now and we'll double your first deposit — get x2 credits instantly.
LLM Tips

How to Build a Python Text Summarizer

Aug 07, 2026

Some text enters your app looking completely harmless.

Then you open it.

Suddenly it is 4,000 words of meeting notes, a support thread with six people arguing politely, a research article with three levels of “therefore,” or a customer review dump where the actual useful point is hiding somewhere near paragraph eleven.

Nobody wants to read all of that manually every time.

That is where a Python text summarizer becomes useful. We can take a wall of text, send it through a summarization workflow, and return something cleaner: a short summary, bullet points, action items, key risks, or a version written for a specific reader.

In this guide, we’ll build a simple API-powered Python text summarizer. We’ll use LLMAPI for flexible summaries, add a local Hugging Face option for comparison, handle long text without panicking, and add practical guardrails so the summary does not quietly invent things.

Because summarization is easy to demo.

Reliable summarization takes a bit more care.

What are we actually building?

We’re building a Python workflow that accepts text and returns a structured summary.

The basic version looks like this:

Input text
→ Python function
→ LLMAPI summarization request
→ clean summary
→ optional validation
→ app response

Example input:

The customer contacted support three times this week about duplicate billing. 
They were charged twice for the Pro plan and said the billing page did not show 
the second charge. Support asked for screenshots, but the customer said they 
already sent them in the previous ticket. They are frustrated and asked for a 
refund or manager follow-up.

Example output:

{
  "summary": "The customer is frustrated after being charged twice for the Pro plan and wants a refund or manager follow-up.",
  "key_points": [
    "Customer contacted support three times this week.",
    "They report a duplicate Pro plan charge.",
    "They say screenshots were already sent in a previous ticket."
  ],
  "recommended_action": "Route to billing support and review the previous ticket attachments."
}

That output is much more useful than a plain paragraph because the app can actually do something with it.

Why text summarization is trickier than it looks

A summarizer has to decide what matters.

That sounds simple until we remember that “what matters” changes by use case.

A support agent wants the customer issue.
A lawyer wants obligations and risks.
A product manager wants complaints and feature requests.
A researcher wants methods, findings, and limitations.
An executive wants the three-line version with no emotional damage.

So before writing code, we should decide what kind of summary we want.

Summary typeBest for
Short paragraphQuick reading
Bullet summarySupport, notes, documents
Executive summaryReports and business docs
Action-item summaryMeetings and project updates
Risk summaryLegal, finance, compliance
Technical summaryResearch and developer docs
Customer summarySupport tickets and reviews
Structured JSON summaryApps, automation, dashboards

The research side also agrees that summarization has layers. A 2025 survey on abstractive text summarization describes summarization systems across techniques, architectures, evaluation methods, and datasets. Another 2024 survey on abstractive summarization challenges highlights factual inconsistency, domain-specific summarization, multilingual summarization, long documents, and noisy data as important research areas.

Translation for us: the summary format should match the product problem, and we should not trust summaries blindly just because they sound smooth.

Extractive vs abstractive summarization

There are two classic summarization styles.

TypeWhat it doesExample
Extractive summarizationSelects important sentences from the original textPulls 3 key sentences from an article
Abstractive summarizationWrites a new shorter version in fresh wordingGenerates a concise paragraph summary

Extractive summarization is safer when we need exact wording because it reuses source sentences. Abstractive summarization is usually nicer to read because it can combine, shorten, and rephrase ideas.

A comprehensive 2024 review on automatic text summarization describes extractive summarization as selecting important sentences from the source and abstractive summarization as generating new shorter text based on the source. Hugging Face’s summarization task guide also describes summarization as creating a shorter version of a document or article while preserving important information.

For product apps, we often want abstractive summaries because they read better. But for sensitive domains, we should preserve evidence, quotes, or source references.

Why we can write this guide

We’ve spent around 6 years working with AI APIs, NLP workflows, Python automation, document processing, RAG, structured outputs, and production-style LLM integrations. We also checked current docs and research from LLMAPI, Hugging Face, ACL Anthology, TACL, ScienceDirect, and recent summarization evaluation surveys while preparing this guide.

One theme keeps coming up: summaries need faithfulness checks. A 2025 ACL-linked survey called Trust but Verify reviews faithfulness evaluation methods for abstractive summarization, including human evaluation, QA-based methods, NLI-based methods, graph-based approaches, and LLM-based evaluation. A 2024 ACL survey on explainability and text summarization also focuses on how summarization systems can be made more interpretable.

So yes, we can build a summarizer fast.

We should also build it like people may rely on the output.

The tools we’ll use

For this tutorial, we’ll use:

ToolWhy
PythonSimple backend and scripting language
LLMAPIAPI-powered summarization and structured outputs
OpenAI Python clientOpenAI-compatible request pattern
python-dotenvEnvironment variables
PydanticResponse validation
FastAPIOptional API endpoint
Hugging Face TransformersLocal summarization option
tiktoken or simple chunkingLong-text handling

LLMAPI is the main API-powered path. The LLMAPI quick-start docs show an OpenAI-compatible chat completions pattern, which means we can use familiar SDK-style code and point it at LLMAPI’s base URL.

Step 1: Set up the project

Create a folder:

mkdir python-text-summarizer
cd python-text-summarizer
python -m venv .venv

Activate the environment.

On macOS/Linux:

source .venv/bin/activate

On Windows PowerShell:

.venv\Scripts\Activate.ps1

Install the basic packages:

pip install openai python-dotenv pydantic fastapi uvicorn

Create a .env file:

LLMAPI_API_KEY=your_api_key_here
LLMAPI_BASE_URL=https://api.llmapi.ai/v1

Keep the API key on the backend. Please do not ship it inside frontend JavaScript and then act surprised when it escapes into the wild.

Step 2: Create a basic LLMAPI client

Create llm_client.py:

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
    api_key=os.environ["LLMAPI_API_KEY"],
    base_url=os.environ.get("LLMAPI_BASE_URL", "https://api.llmapi.ai/v1")
)

This gives us one shared client for the app.

Step 3: Write the simplest summarizer

Create summarizer.py:

from llm_client import client


def summarize_text(text: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "Summarize the user's text clearly and accurately."
            },
            {
                "role": "user",
                "content": text
            }
        ],
        temperature=0.2
    )

    return response.choices[0].message.content

Test it with main.py:

from summarizer import summarize_text

text = """
The customer contacted support three times this week about duplicate billing.
They were charged twice for the Pro plan and said the billing page did not show
the second charge. Support asked for screenshots, but the customer said they
already sent them in the previous ticket. They are frustrated and asked for a
refund or manager follow-up.
"""

summary = summarize_text(text)

print(summary)

Run it:

python main.py

This works as a tiny summarizer.

But it is too vague for a real app. We should shape the output.

Step 4: Make the summary format useful

A summary can be more than one paragraph.

For many apps, we want:

  1. Short summary.
  2. Key points.
  3. Action items.
  4. Risks or warnings.
  5. Suggested next step.

Let’s ask for that directly.

from llm_client import client


def summarize_for_support(text: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
You summarize customer support text for an agent.

Return:
- A 1-sentence summary
- 3 to 5 key points
- Any action items
- Any missing information

Stay faithful to the source text. Do not invent facts.
"""
            },
            {
                "role": "user",
                "content": text
            }
        ],
        temperature=0.2
    )

    return response.choices[0].message.content

This is already better because the summary has a job.

A generic summary is nice.

A support-ready summary is useful.

Step 5: Return structured JSON

If this summarizer feeds an app, structured JSON is better than free text.

Create schemas.py:

from typing import List
from pydantic import BaseModel, Field


class SummaryResult(BaseModel):
    summary: str
    key_points: List[str] = Field(default_factory=list)
    action_items: List[str] = Field(default_factory=list)
    risks_or_warnings: List[str] = Field(default_factory=list)
    missing_information: List[str] = Field(default_factory=list)

Now update summarizer.py:

import json
from schemas import SummaryResult
from llm_client import client


def summarize_to_json(text: str) -> SummaryResult:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
You summarize text into valid JSON.

Return exactly this JSON shape:
{
  "summary": "string",
  "key_points": ["string"],
  "action_items": ["string"],
  "risks_or_warnings": ["string"],
  "missing_information": ["string"]
}

Rules:
- Return only valid JSON.
- Do not wrap the JSON in markdown.
- Do not invent facts.
- If there are no action items, return an empty array.
- If information is missing, list it in missing_information.
"""
            },
            {
                "role": "user",
                "content": text
            }
        ],
        temperature=0
    )

    raw_content = response.choices[0].message.content
    parsed = json.loads(raw_content)

    return SummaryResult.model_validate(parsed)

Test it:

from summarizer import summarize_to_json

text = """
The customer contacted support three times this week about duplicate billing.
They were charged twice for the Pro plan and said the billing page did not show
the second charge. Support asked for screenshots, but the customer said they
already sent them in the previous ticket. They are frustrated and asked for a
refund or manager follow-up.
"""

result = summarize_to_json(text)

print(result.model_dump_json(indent=2))

Example output:

{
  "summary": "The customer is frustrated after being charged twice for the Pro plan and wants a refund or manager follow-up.",
  "key_points": [
    "Customer contacted support three times this week.",
    "They report a duplicate Pro plan charge.",
    "They say the billing page did not show the second charge.",
    "They say screenshots were already sent in a previous ticket."
  ],
  "action_items": [
    "Review previous ticket attachments.",
    "Route the case to billing support.",
    "Consider refund or manager follow-up."
  ],
  "risks_or_warnings": [
    "Customer frustration is high due to repeated contact and unresolved billing issue."
  ],
  "missing_information": [
    "Original charge IDs or invoice numbers are not included."
  ]
}

Now the output is ready for a UI, dashboard, CRM, support queue, or database.

Step 6: Add input validation

Before sending text to any model, check it.

Create validation.py:

def validate_text_input(text: str, min_length: int = 20, max_length: int = 50000) -> list[str]:
    errors = []

    if not text or not text.strip():
        errors.append("Text is required.")

    if len(text.strip()) < min_length:
        errors.append(f"Text must be at least {min_length} characters.")

    if len(text) > max_length:
        errors.append(f"Text is too long. Maximum length is {max_length} characters.")

    return errors

Use it:

from validation import validate_text_input
from summarizer import summarize_to_json


def safe_summarize(text: str):
    errors = validate_text_input(text)

    if errors:
        return {
            "status": "error",
            "errors": errors
        }

    result = summarize_to_json(text)

    return {
        "status": "success",
        "result": result.model_dump()
    }

This prevents your app from sending empty strings, tiny snippets, or giant surprise novels to the API.

Step 7: Turn it into a FastAPI endpoint

Create app.py:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from summarizer import summarize_to_json
from validation import validate_text_input

app = FastAPI(
    title="Python Text Summarizer",
    description="Summarize text with Python and LLMAPI.",
    version="1.0.0"
)


class SummarizeRequest(BaseModel):
    text: str = Field(..., min_length=20, max_length=50000)


@app.get("/health")
def health_check():
    return {
        "status": "ok"
    }


@app.post("/summarize")
def summarize(request: SummarizeRequest):
    errors = validate_text_input(request.text)

    if errors:
        raise HTTPException(
            status_code=400,
            detail=errors
        )

    result = summarize_to_json(request.text)

    return {
        "status": "success",
        "result": result.model_dump()
    }

Run it:

uvicorn app:app --reload

Test it:

curl -X POST "http://127.0.0.1:8000/summarize" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "The customer contacted support three times this week about duplicate billing. They were charged twice for the Pro plan and asked for a refund or manager follow-up."
  }'

Now we have a simple API-powered summarizer.

Step 8: Add summary styles

Different users need different summary styles.

Instead of one summarizer, we can support modes:

ModeBest for
shortTiny summary
bulletsQuick scanning
executiveBusiness reports
supportTickets and customer messages
researchPapers and technical docs
action_itemsMeetings and planning
riskLegal, finance, compliance review

Update the request model:

from typing import Literal
from pydantic import BaseModel, Field


class SummarizeRequest(BaseModel):
    text: str = Field(..., min_length=20, max_length=50000)
    mode: Literal[
        "short",
        "bullets",
        "executive",
        "support",
        "research",
        "action_items",
        "risk"
    ] = "bullets"

Create mode instructions:

SUMMARY_MODE_INSTRUCTIONS = {
    "short": "Write a concise 2-sentence summary.",
    "bullets": "Write 4 to 6 clear bullet points.",
    "executive": "Write an executive summary with business impact and decisions needed.",
    "support": "Summarize the customer issue, urgency, and recommended next step.",
    "research": "Summarize the objective, method, findings, and limitations.",
    "action_items": "Extract decisions, action items, owners if mentioned, and deadlines if mentioned.",
    "risk": "Summarize key risks, unresolved questions, and items that need review."
}

Use it in the model call:

def summarize_with_mode(text: str, mode: str = "bullets") -> str:
    instruction = SUMMARY_MODE_INSTRUCTIONS.get(
        mode,
        SUMMARY_MODE_INSTRUCTIONS["bullets"]
    )

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""
You summarize text for a real application.

Summary mode:
{instruction}

Rules:
- Stay faithful to the source.
- Do not invent facts.
- Mention uncertainty when the text is unclear.
- Keep the summary easy to scan.
"""
            },
            {
                "role": "user",
                "content": text
            }
        ],
        temperature=0.2
    )

    return response.choices[0].message.content

This makes the summarizer feel less one-size-fits-all.

Step 9: Handle long text with chunking

Long text is where summarizers start sweating.

If the text is too long for the model or too expensive to process in one request, chunk it.

A simple chunking function:

def chunk_text(text: str, max_chars: int = 6000) -> list[str]:
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    chunks = []
    current = ""

    for paragraph in paragraphs:
        if len(current) + len(paragraph) + 2 <= max_chars:
            current += paragraph + "\n\n"
        else:
            if current.strip():
                chunks.append(current.strip())
            current = paragraph + "\n\n"

    if current.strip():
        chunks.append(current.strip())

    return chunks

Now we can summarize chunks first, then summarize the summaries.

from summarizer import summarize_with_mode


def summarize_long_text(text: str, mode: str = "bullets") -> str:
    chunks = chunk_text(text)

    if len(chunks) == 1:
        return summarize_with_mode(text, mode=mode)

    partial_summaries = []

    for index, chunk in enumerate(chunks, start=1):
        partial = summarize_with_mode(
            f"Chunk {index} of {len(chunks)}:\n\n{chunk}",
            mode="bullets"
        )
        partial_summaries.append(partial)

    combined = "\n\n".join(
        f"Chunk {i + 1} summary:\n{summary}"
        for i, summary in enumerate(partial_summaries)
    )

    final_summary = summarize_with_mode(
        f"Create one final summary from these chunk summaries:\n\n{combined}",
        mode=mode
    )

    return final_summary

This is often called map-reduce summarization.

The “map” step summarizes each chunk.

The “reduce” step combines the smaller summaries.

Long-document summarization is an active research area. The 2024 ACL paper SumSurvey notes that longer inputs create a need for better long-document summarization datasets, especially as LLMs can handle longer contexts. A 2025 NAACL survey on summarization datasets also points out that summarization research depends heavily on dataset design, data cards, and clearer dataset documentation.

So we should treat long-text summarization as its own workflow, not just “send more text.”

Step 10: Add faithfulness checks

A summary should not add new facts.

This is the annoying part because generated summaries can sound confident even when they drift from the source.

Add a lightweight check:

def check_summary_faithfulness(source_text: str, summary: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
You check whether a summary is faithful to the source text.

Return valid JSON:
{
  "faithful": true,
  "unsupported_claims": ["string"],
  "missing_major_points": ["string"],
  "notes": "string"
}

Rules:
- Mark unsupported claims that are not clearly supported by the source.
- Do not be overly strict about wording.
- Focus on factual consistency.
"""
            },
            {
                "role": "user",
                "content": f"Source text:\n{source_text}\n\nSummary:\n{summary}"
            }
        ],
        temperature=0
    )

    import json
    return json.loads(response.choices[0].message.content)

Use it:

summary = summarize_with_mode(text, mode="executive")
check = check_summary_faithfulness(text, summary)

print(check)

Example:

{
  "faithful": true,
  "unsupported_claims": [],
  "missing_major_points": [],
  "notes": "The summary accurately reflects the main issue and does not add unsupported details."
}

This kind of check is useful for:

  1. Customer support.
  2. Legal notes.
  3. Research summaries.
  4. Financial documents.
  5. HR workflows.
  6. Compliance reviews.

For high-risk workflows, a model-based check is still not enough by itself. Add human review when the summary affects serious decisions.

Step 11: Add source quotes for trust

One way to make summaries more trustworthy is to include evidence.

Ask the model to return key points with source quotes:

def summarize_with_evidence(text: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
Summarize the text with evidence.

Return only valid JSON:
{
  "summary": "string",
  "key_points": [
    {
      "point": "string",
      "source_quote": "exact short quote from the text"
    }
  ],
  "action_items": ["string"],
  "warnings": ["string"]
}

Rules:
- Each source_quote must be copied exactly from the source text.
- Do not invent facts.
- Use empty arrays when there are no action items or warnings.
"""
            },
            {
                "role": "user",
                "content": text
            }
        ],
        temperature=0
    )

    import json
    return json.loads(response.choices[0].message.content)

Example output:

{
  "summary": "The customer is frustrated about a duplicate Pro plan charge and wants follow-up.",
  "key_points": [
    {
      "point": "The customer contacted support multiple times.",
      "source_quote": "contacted support three times this week"
    },
    {
      "point": "They report being charged twice.",
      "source_quote": "They were charged twice for the Pro plan"
    }
  ],
  "action_items": [
    "Review billing history and previous ticket attachments."
  ],
  "warnings": []
}

This makes the summary easier to audit.

It also reduces the chance that a smooth-sounding summary quietly drifts away from the source.

Step 12: Add a local Hugging Face summarizer

API summarization is convenient, especially when we want custom formats, reasoning, or structured output.

But local models can be useful too.

Install:

pip install transformers torch

Use the Hugging Face summarization pipeline:

from transformers import pipeline

local_summarizer = pipeline(
    "summarization",
    model="facebook/bart-large-cnn"
)


def summarize_locally(text: str) -> str:
    result = local_summarizer(
        text,
        max_length=130,
        min_length=30,
        do_sample=False
    )

    return result[0]["summary_text"]

Test:

text = """
The customer contacted support three times this week about duplicate billing.
They were charged twice for the Pro plan and asked for a refund or manager follow-up.
"""

print(summarize_locally(text))

Hugging Face’s summarization guide walks through summarization with Transformer models and explains how summarization creates a shorter version of a document while keeping important information.

Local summarization is useful when:

  1. You need offline processing.
  2. You want more control over the model.
  3. You process sensitive internal text.
  4. You want predictable per-run costs.
  5. You can handle model hosting and performance.

LLMAPI is usually easier when:

  1. You need flexible output formats.
  2. You want quick setup.
  3. You want stronger instruction following.
  4. You want structured JSON.
  5. You want model routing.
  6. You want production workflow flexibility.

Both can exist in the same app.

Step 13: Choose summary length carefully

Users often ask for “a short summary,” but “short” is vague.

Better options:

LengthBest for
1 sentenceInbox previews
3 bulletsSupport ticket cards
5 bulletsDocument overview
1 paragraphArticle/report summary
Executive summaryBusiness docs
Detailed summaryResearch and legal notes
Section-by-sectionLong documents

Add parameters:

def summarize_custom(text: str, audience: str, length: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""
Summarize the text for this audience: {audience}.
Desired length: {length}.

Rules:
- Keep the summary faithful to the source.
- Use plain language.
- Do not invent facts.
- Mention uncertainty when the source is unclear.
"""
            },
            {
                "role": "user",
                "content": text
            }
        ],
        temperature=0.2
    )

    return response.choices[0].message.content

Use it:

summary = summarize_custom(
    text,
    audience="busy customer support manager",
    length="5 bullet points"
)

This is more useful than a generic summary because the model knows who the summary is for.

Step 14: Make summaries less boring

Summaries can easily become technically correct but painfully bland.

For internal apps, bland is fine.

For user-facing apps, tone matters.

Try modes like:

ToneUse case
NeutralReports, legal, business
FriendlySupport agents, user-facing summaries
ExecutiveLeadership dashboards
TechnicalDeveloper docs, research
Plain EnglishGeneral users
Action-orientedMeetings, project notes

Example:

def summarize_with_tone(text: str, tone: str = "neutral") -> str:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""
Summarize this text in a {tone} tone.

Rules:
- Stay accurate.
- Keep it easy to read.
- Do not add claims that are not in the source.
"""
            },
            {
                "role": "user",
                "content": text
            }
        ],
        temperature=0.3
    )

    return response.choices[0].message.content

Tone is helpful, but it should never override accuracy.

A summary can be friendly without becoming fan fiction.

Step 15: Add a CLI tool

Sometimes we just want to summarize a text file from the terminal.

Create summarize_file.py:

import argparse
from pathlib import Path
from summarizer import summarize_long_text


def main():
    parser = argparse.ArgumentParser(description="Summarize a text file with LLMAPI.")
    parser.add_argument("file", help="Path to a .txt file")
    parser.add_argument("--mode", default="bullets", help="Summary mode")

    args = parser.parse_args()

    text = Path(args.file).read_text(encoding="utf-8")
    summary = summarize_long_text(text, mode=args.mode)

    print(summary)


if __name__ == "__main__":
    main()

Run it:

python summarize_file.py meeting_notes.txt --mode action_items

This is great for internal workflows, quick experiments, or batch processing.

Step 16: Add batch summarization

If we have many texts, process them one by one and store results.

from pathlib import Path
from summarizer import summarize_long_text


def summarize_folder(input_folder: str, output_folder: str, mode: str = "bullets"):
    input_path = Path(input_folder)
    output_path = Path(output_folder)
    output_path.mkdir(parents=True, exist_ok=True)

    for file_path in input_path.glob("*.txt"):
        text = file_path.read_text(encoding="utf-8")
        summary = summarize_long_text(text, mode=mode)

        output_file = output_path / f"{file_path.stem}_summary.txt"
        output_file.write_text(summary, encoding="utf-8")

        print(f"Summarized {file_path.name} → {output_file.name}")

Use it:

summarize_folder(
    input_folder="documents",
    output_folder="summaries",
    mode="executive"
)

For serious batch jobs, add:

  1. Queues.
  2. Rate-limit handling.
  3. Retries with backoff.
  4. Logging.
  5. Cost tracking.
  6. Failure records.
  7. Resume-from-last-file behavior.

Batch summarization can get expensive quickly if we pretend every document is tiny.

Step 17: Handle rate limits and retries

AI APIs can return rate-limit errors when too many requests arrive too quickly.

Use retries with exponential backoff.

import time
from openai import RateLimitError, APIError


def call_with_retries(fn, max_attempts: int = 5):
    for attempt in range(max_attempts):
        try:
            return fn()
        except RateLimitError:
            wait_seconds = min(2 ** attempt, 30)
            time.sleep(wait_seconds)
        except APIError:
            wait_seconds = min(2 ** attempt, 30)
            time.sleep(wait_seconds)

    raise RuntimeError("API request failed after retries.")

Use it around the model call:

def summarize_text_with_retries(text: str) -> str:
    def request():
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {
                    "role": "system",
                    "content": "Summarize the text accurately in 5 bullet points."
                },
                {
                    "role": "user",
                    "content": text
                }
            ],
            temperature=0.2
        )

        return response.choices[0].message.content

    return call_with_retries(request)

For production, also add jitter, queueing, and provider-specific retry-after handling.

Step 18: Store summaries

If summaries are part of your product, save them.

A simple JSON record:

{
  "document_id": "doc_123",
  "summary": "The customer is frustrated about duplicate billing.",
  "mode": "support",
  "model": "gpt-4o-mini",
  "prompt_version": "support_summary_v1",
  "created_at": "2026-08-22T01:00:00-05:00"
}

Track:

  1. Source document ID.
  2. Summary text.
  3. Summary mode.
  4. Model used.
  5. Prompt version.
  6. Creation date.
  7. Validation status.
  8. Faithfulness check result.
  9. User edits.
  10. Review status.

This helps when someone asks why a summary changed after a prompt update.

And yes, they will ask.

Step 19: Evaluate summary quality

Do not evaluate your summarizer with one happy test case.

Create a small test set.

Include:

  1. Short support tickets.
  2. Long support threads.
  3. Meeting notes.
  4. Product reviews.
  5. Research snippets.
  6. Reports.
  7. Mixed-positive/negative feedback.
  8. Text with missing details.
  9. Text with conflicting claims.
  10. Very long documents.

Score summaries on:

MetricWhat it checks
FaithfulnessDoes the summary avoid unsupported claims?
CoverageDoes it include the important points?
ConcisionIs it shorter without becoming useless?
ClarityCan a human understand it fast?
FormatDoes it follow the requested structure?
UsefulnessDoes it help the workflow?
EvidenceAre key points backed by source quotes?
ConsistencyAre similar inputs summarized similarly?

Traditional summarization evaluation often uses metrics like ROUGE, but ROUGE does not catch every important issue. The 2025 Trust but Verify survey highlights the broader range of faithfulness evaluation approaches, including QA-based, NLI-based, graph-based, LLM-based, and human evaluation methods. In plain terms: automatic scores help, but human review still matters for serious workflows.

Best practices for Python text summarizers

Here is the checklist we would use before shipping.

  • Define the summary type before prompting.
  • Use structured JSON when the app needs automation.
  • Keep temperature low for factual summaries.
  • Tell the model not to invent facts.
  • Include source quotes for important claims.
  • Use chunking for long documents.
  • Summarize chunks, then summarize chunk summaries.
  • Add a faithfulness check for sensitive workflows.
  • Validate JSON with Pydantic.
  • Store model and prompt version metadata.
  • Add retries and rate-limit handling.
  • Cache summaries for unchanged documents.
  • Let users choose summary length or mode.
  • Add human review for legal, finance, HR, medical, and compliance summaries.
  • Track user edits to improve prompts.
  • Test on real documents, not only cute examples.

The boring parts are what make the summarizer trustworthy.

Common mistakes

MistakeBetter approach
Asking for “a summary” with no formatDefine audience, length, and summary type
Sending huge text directlyChunk long documents
Trusting smooth wordingAdd faithfulness checks
No schema validationUse Pydantic for JSON outputs
Ignoring rate limitsAdd retries and queues
No prompt versioningTrack prompt changes
Using one summary style for everyoneAdd modes
No source evidenceAsk for quotes or references
Summarizing sensitive docs with no reviewAdd human review
Evaluating on one exampleBuild a real test set

A summarizer should reduce reading time, not create a new job where people have to fact-check every sentence.

Where LLMAPI fits

LLMAPI fits well when we want an API-powered summarizer that can support different models, structured outputs, workflow logic, and reusable summarization modes.

Use LLMAPI for:

NeedHow it helps
Quick summarizationSend text and get a clean summary
Structured summariesReturn JSON for apps and workflows
Multi-style summariesSupport executive, support, research, risk, and action modes
Long-text workflowsCombine chunk summaries into final summaries
Review notesExplain risks or missing information
Model routingUse different models for different summarization tasks
FallbacksRoute around provider/model issues
Product automationFeed summaries into dashboards, CRMs, queues, or reports

A practical LLMAPI summarization workflow looks like this:

raw text
→ choose summary mode
→ call LLMAPI
→ validate output
→ check faithfulness if needed
→ save or return result

That gives us a summarizer that can start small and grow into a real product feature.

The practical takeaway

We can build a Python text summarizer pretty quickly with LLMAPI, the OpenAI-compatible Python client, and a clean prompt. The basic version sends text to the model and returns a short summary. The better version supports modes, structured JSON, long-text chunking, source quotes, validation, retries, and faithfulness checks.

Use API-powered summarization when we want flexible, polished summaries fast. Use local Hugging Face models when we need offline control or custom hosting. Use chunking for long documents. Use Pydantic when summaries feed an app. Use evidence and review when the content is sensitive.

The final workflow is simple:

wall of text
→ Python summarizer
→ LLMAPI summary
→ validation
→ cleaner output

That is how we turn “please read this giant thing” into something users can actually work with.

Deploy in minutes