AI EngineeringZero to ProductionHome·About·What’s new·Contact
AWS AI Automation · Chapter W10

Textract & Comprehend

Textract (OCR, forms, tables) and Comprehend (entities, PII, sentiment) are cheap, deterministic AI APIs. Chain Textract → Claude for structured extraction beyond fixed fields.

⏱️ ~2 hours🧪 3 labs🎯 Intermediate
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need:
  • AWS credentials (aws configure) + Bedrock model access enabled in your region + pip install boto3
  • AWS credentials (aws configure) + pip install boto3
Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Extract text, forms, and tables from documents with Textract.
  • Pull entities, PII, key phrases, and sentiment with Comprehend.
  • Chain Textract → Claude-on-Bedrock for structured extraction beyond fixed fields.
  • Recognize when a pre-built AI service beats an LLM (and vice versa).
▶ Runnable companionEvery code block in this lesson is also a standalone file under code/aws10-docs-textract-comprehend/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.

Pre-built AI services: no ML required intermediate

Here's a mistake almost everyone makes their first week with LLMs. A ticket lands: "extract the names and email addresses from these 50,000 support messages." You reach for the tool you just learned — you pipe every message through Claude with a clever prompt, and it works beautifully on your ten test cases. Then the bill arrives, the latency is measured in seconds per document, and one message in fifty comes back with a hallucinated email that was never in the text. You've used a Ferrari to deliver pizza. The job — "find the emails" — was a solved problem years before you got there, and Amazon will do it for a fraction of a cent per call, deterministically, in milliseconds.

That is the whole reason this section exists. Textract and Comprehend are what AWS calls pre-built (or "managed") AI services: fully trained models behind a plain API call. You don't train anything, you don't pick a model, you don't manage a GPU — you send text or a document and get back a structured answer. Textract is the OCR specialist (turn pixels into text, forms, and tables); Comprehend is the NLP specialist (pull sentiment, entities, PII, language, and key phrases out of text you already have). Think of them as the microwave in your kitchen: it does exactly one narrow thing, it does it instantly and identically every time, and you'd never dream of buying a full restaurant kitchen just to reheat soup.

Now the precise idea, because the intuition needs a rule behind it. A pre-built service is a fixed-function model: it answers one well-defined question from a closed set of outputs (Comprehend's PII detector only ever returns categories AWS defined; it will never invent a new one). An LLM like Claude is open-ended: it will answer almost anything, in almost any shape, which is exactly its strength and exactly why it is slower, pricier, and occasionally wrong. So the deciding question for every document task is: "Is what I want on the fixed menu?" If yes — sentiment, PII, OCR — use the pre-built service; it is cheaper, faster, and can't hallucinate a category that doesn't exist. If no — arbitrary line items, your custom invoice fields, reasoning across the page — you need the LLM.

The common trap is treating this as a religious "LLM vs. old AI" choice. It isn't either/or. The pattern the rest of this lesson builds toward is the hybrid: let each tool do what it's best at. Textract deterministically turns the PDF into clean text — the one job an LLM is genuinely bad at (reading pixels) — and then Claude does the flexible reasoning the fixed-function API cannot. You get the reliability and price of the specialist for the mechanical part, and the intelligence of the LLM only where you actually need it.

Textract and Comprehend are pay-per-use APIs that solve narrow problems extremely well: OCR and NLP. They are cheaper and more deterministic than an LLM for what they do. The modern pattern is a hybrid: Textract turns a PDF into text, then Claude does the flexible reasoning the fixed-function API cannot.

for any document task, ask one question first Is it on the fixed menu? OCR · sentiment · PII · entities? yes no Pre-built service Textract / Comprehend — cheap, fast,can't hallucinate a category LLM · Claude on Bedrock arbitrary fields, any layout— pricier, slower Hybrid — chain both tools One question routes every document task. If the answer is on a closed menu (OCR, sentiment, PII, known entities) a pre-built service is cheaper, faster, and can't invent a category; only arbitrary, open-ended fields need the LLM — and the hybrid uses both.
🗺️ How to read this diagram

Start at the blue box at the top — the single question you ask before touching any tool — then follow the branch that matches your answer down to the coloured box below it.

  • The "yes" arrow (your task is a fixed, known job) lands on the green box — a pre-built service (Textract for OCR, Comprehend for sentiment/PII/entities): cheap, deterministic, and unable to hallucinate a category that isn't on its menu.
  • The "no" arrow (you need arbitrary, document-specific fields) lands on the amber box — Claude on Bedrock: it will answer in any shape, which is why it costs more and is occasionally wrong.
  • The two dashed arrows converging on the purple box are the payoff: it's rarely either/or. The hybrid chains both — the specialist does the mechanical part, the LLM does the reasoning part.

In short: "Is it on the fixed menu?" — yes → pre-built service, no → LLM, and in practice → the hybrid that uses each for what it's best at.

Textract: OCR, forms, tables intermediate

You have almost certainly hit this wall. Someone hands you a folder of scanned invoices — actual photographs of paper — and asks you to "just pull the totals into a spreadsheet." You open one and realize the computer sees no "total" at all: it sees a grid of coloured dots. The text you can read with your eyes is, to the machine, a picture. Turning that picture back into characters is OCR — optical character recognition — and it is a genuinely hard problem (skewed scans, coffee stains, two-column layouts, handwriting) that took decades of research to get right. Textract is the trained result of that research, rented to you by the call.

But here's the part that makes Textract more than a plain OCR engine, and it's the reason this section matters. Reading the characters is only half the battle; the harder half is understanding structure. On an invoice, "Total" and "$99.00" aren't just two random strings — one is a label and the other is its value, and they belong together even though they might sit inches apart on the page. A row of numbers under "Qty / Item / Price" isn't loose text — it's a table. Textract can reconstruct both. Picture the difference between a human who can only read words aloud and one who can also say "that's a form, and the answer to 'Total' is ninety-nine dollars" — that second understanding is what you're paying for.

You control which understanding you get with one parameter, FeatureTypes, and this is the single most common point of confusion. Call analyze_document with no feature types and you get flat text — the LINE and WORD blocks, useful but dumb. Add FORMS and Textract also links label/value pairs (the key/value structure); add TABLES and it also reconstructs rows and columns as cells. You ask for the analysis you need, because you pay for what you ask for.

One more mental model that saves hours of confusion downstream: Textract does not hand you a tidy nested document. It returns a flat list under Blocks where each block is a typed node — a PAGE, a LINE, a WORD, a KEY_VALUE_SET, a CELL — and the blocks point at each other by ID to describe relationships. In other words the response is a graph, not a tree you can index into. That's why the lab below filters (if b["BlockType"] == "LINE") rather than reaching for resp["lines"] — there is no such key. Expect to iterate and select the block types you care about; that flat-graph shape is deliberate, because it lets one response carry pages, words, forms, and tables all at once without forcing a single rigid structure on documents that come in every shape imaginable.

the FeatureTypes you ask for decides how much structure comes back analyze_document a page or PDF FeatureTypes = [FORMS, TABLES] (no features) LINE + WORD — flat text + FORMS KEY_VALUE_SET — label/value + TABLES CELL — rows & columns one flat graph Blocks[] linked by ID You pay for what you ask for — and it all comes back flat. No features gives bare LINE/WORD text; FORMS adds label/value KEY_VALUE_SET pairs; TABLES adds CELL blocks — but every level is delivered as one flat graph of typed Blocks, not a nested tree.
🗺️ How to read this diagram

Read left to right: the box on the left is your analyze_document call, the middle column is what each FeatureTypes setting buys you, and the purple box on the right is the shape it all arrives in.

  • The three solid brand arrows fan out by how much you request. Ask for nothing and you get the plain LINE/WORD text (top box); add FORMS and Textract also links label/value pairs as KEY_VALUE_SET (green); add TABLES and it also reconstructs rows and columns as CELL blocks (amber).
  • Each level is additive — FORMS+TABLES gives you all three kinds at once, which is why the left box requests both.
  • The dashed arrows all point at the purple box: whatever you asked for, the reply is one flat graph of typed Blocks linked by ID — the "aha" that explains why the lab filters on BlockType instead of indexing a nested tree.

In short: more FeatureTypes → more structure recognized, but always the same flat Blocks graph you iterate and filter.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab W10.1
textract.pyimport boto3
tx = boto3.client("textract", region_name="us-east-1")

# analyze_document with FORMS + TABLES extracts key/value pairs and cells,
# not just raw text. Point it at a document already in S3.
resp = tx.analyze_document(
    Document={"S3Object": {"Bucket": "my-docs", "Name": "invoice.pdf"}},
    FeatureTypes=["FORMS", "TABLES"],
)
# blocks are a graph; pull the raw lines as a quick check
lines = [b["Text"] for b in resp["Blocks"] if b["BlockType"] == "LINE"]
print("\n".join(lines[:5]))
▶ How this works

Textract is Amazon's OCR (optical character recognition) service: give it a scanned page or PDF and it reads the text back out — and with the right options it also understands forms (label/value pairs like "Total: $99") and tables (rows and columns). This lab points Textract at a file sitting in S3 and prints the first few lines it read.

  1. tx = boto3.client("textract", ...) — boto3 is the AWS SDK for Python. This line builds a client object you use to call the Textract service in the us-east-1 region. (It needs AWS credentials to actually run.)
  2. tx.analyze_document(...) sends the request. Document={"S3Object": ...} tells Textract which file to read — here invoice.pdf in the bucket my-docs. You don't upload the file in the call; it's already in S3.
  3. FeatureTypes=["FORMS", "TABLES"] asks for the smart extraction — key/value pairs and table cells — not just loose text. Leave it out and you only get raw lines.
  4. The reply, resp, is a big graph of blocks. The last line is a list comprehension: it walks every block b and keeps b["Text"] only where b["BlockType"] == "LINE" — i.e. just the text lines — then lines[:5] takes the first five to print as a sanity check.

What the output means: You'd see the first five lines of text Textract found on the page, one per line — proof the OCR worked. The forms and tables are also in resp, waiting to be pulled out.

Try this: Change lines[:5] to lines[:20] to preview more, or drop FeatureTypes entirely and notice you can still read raw LINE blocks but lose the form/table structure.

Async for big documentsanalyze_document is synchronous (single page-ish). For multi-page PDFs use start_document_analysis → poll get_document_analysis. W13 wires that into an event-driven pipeline so you never poll by hand.

Comprehend: entities, PII, sentiment advanced

Textract gave you the words; now you have to make sense of them. Imagine ten thousand support emails just landed in a bucket. Compliance needs to know which ones contain personal data before they can be archived. Product wants to know which customers are angry. Support wants to know which companies and cities are being complained about. You could read all ten thousand yourself over a very long weekend, or write brittle regexes that catch jane@acme.com but miss jane [at] acme dot com and flag x@y in a URL by mistake. Comprehend is the trained specialist that does these narrow reading-comprehension jobs for you, per call, without you owning a model.

The key thing to internalize — and it's why this sits in its own section rather than blurring into "just use an LLM" — is that Comprehend is not one clever model; it's a family of narrow, single-purpose classifiers, each answering one fixed question and each drawing from a closed, AWS-defined set of answers. detect_sentiment returns one of a fixed handful of moods (POSITIVE, NEGATIVE, NEUTRAL, MIXED). detect_pii_entities returns categories from a predefined list (NAME, EMAIL, ADDRESS, CREDIT_DEBIT_NUMBER, and so on). detect_entities tags real-world things with fixed types (PERSON, LOCATION, ORGANIZATION, DATE). You don't train these and you can't teach them a new category — which sounds like a limitation but is exactly the point. Because the menu is fixed, the output is deterministic and reliable: the same sentence classified today and next month gives the same answer, with a confidence score you can threshold on.

Because each call is independent and narrow, the honest way to think about Comprehend is as a box of specialized tools rather than a single brain: you compose the ones you need for a task (run sentiment and entities on the same review and merge the results) rather than asking one call to do everything. That composition — several cheap, deterministic classifiers stitched together — is often all a real pipeline needs, and it costs a rounding error compared to sending the same text to an LLM.

Which brings us to the boundary you must keep straight, because it's the single most useful thing in this lesson: Comprehend can only find categories it already knows. Ask it for sentiment or emails and it's perfect. Ask it for "the invoice number and the three line-item totals on this purchase order" and it simply has no such category — there is nothing on its menu to return. That is not a bug to work around; it is the exact seam where you hand off to Claude. Hold that line firmly: known category → Comprehend; arbitrary, document-specific fields → LLM. The next section is that handoff.

Comprehend is not one brain — it's several narrow classifiers, each with a fixed menu one text you already have detect_sentiment the mood detect_pii_entities personal data present detect_entities real-world things POSITIVE · NEGATIVE NEUTRAL · MIXED NAME · EMAIL · ADDRESS CREDIT_DEBIT_NUMBER … PERSON · LOCATION ORGANIZATION · DATE A box of specialized tools, not one brain. The same text fans out to independent single-purpose calls, and each one can only answer from its own closed, AWS-defined set of categories — which is exactly what makes the output deterministic and impossible to hallucinate a new category into.
🗺️ How to read this diagram

Read left to right: one piece of text on the left, the three Comprehend calls in the middle column, and — crucially — the fixed set of answers each call can ever return on the right.

  • The solid brand arrows show the same text being sent to three separate classifiers. They don't chain or share work — each is an independent call you choose to make.
  • The dashed arrows point to the green boxes, which list each call's closed menu of outputs: sentiment is one of four moods, PII is a predefined category list, entities are fixed real-world types.
  • The payoff is that green column: because the menu is fixed and AWS-defined, you can't teach it a new category — but the same input always yields the same, thresholdable answer, which is why it's reliable and cheap.

In short: one text → several narrow classifiers → each answers only from its own closed category set; compose the ones you need.

Lab W10.2
comprehend.pyimport boto3
cp = boto3.client("comprehend", region_name="us-east-1")
text = "Contact Jane Doe at jane@acme.com about the Seattle outage. Very frustrated."

print(cp.detect_sentiment(Text=text, LanguageCode="en")["Sentiment"])
print([e["Type"] for e in cp.detect_pii_entities(Text=text, LanguageCode="en")["Entities"]])
print([(e["Text"], e["Type"]) for e in cp.detect_entities(Text=text, LanguageCode="en")["Entities"]])
NEGATIVE
['NAME', 'EMAIL']
[('Jane Doe', 'PERSON'), ('Seattle', 'LOCATION')]
▶ How this works

Comprehend is Amazon's NLP (natural-language processing) service. Instead of reading pixels like Textract, it reads meaning out of text you already have: is it positive or negative? Does it contain personal data (PII)? What people, places, and things are mentioned? This lab runs one sample sentence through three different Comprehend calls.

  1. cp = boto3.client("comprehend", ...) makes the client for the Comprehend service, just like the Textract client in the last lab.
  2. text = "Contact Jane Doe at jane@acme.com ..." is the sentence we analyze. It was deliberately written to contain a name, an email, a place, and an emotion.
  3. cp.detect_sentiment(Text=text, LanguageCode="en") returns an overall mood; ["Sentiment"] pulls just the label (e.g. POSITIVE/NEGATIVE) out of the reply.
  4. detect_pii_entities(...) finds personal data — the list comprehension keeps each hit's ["Type"] (like NAME, EMAIL). detect_entities(...) finds real-world things and keeps a (text, type) pair for each, e.g. a person or a location.

What the output means: Three lines print. NEGATIVE is the sentiment ("Very frustrated"). ['NAME', 'EMAIL'] are the PII types found. The last line lists the entities with their categories: ('Jane Doe', 'PERSON') and ('Seattle', 'LOCATION').

Try this: Swap in your own sentence — try a happy one and watch the sentiment flip to POSITIVE, or add a phone number and see a new PII type appear.

The hybrid: Textract → Claude expert

This is where the two halves of the lesson click together, and it starts with a failure you can feel. Finance hands you invoices from three hundred different vendors and wants vendor, total, and due_date out of each. The naive answer — a template parser that reads "the total is always in the box at bottom-right" — works for exactly the vendors you tested and shatters the first time a supplier moves their total to the top, renames it "Amount Due," or splits it across two lines. Templates are brittle because real documents are infinitely various; you cannot enumerate every layout on Earth. Comprehend can't rescue you either: "invoice total" and "due date on this vendor's form" aren't on its fixed menu. You've walked into the exact gap the pre-built services leave open.

The resolution is to stop asking one tool to do a job it's bad at, and instead chain two tools, each doing what it's best at. Textract is superb at the mechanical, deterministic part an LLM is genuinely weak at — reading pixels reliably into clean text. Claude is superb at the flexible, judgment part a fixed-function API cannot do — looking at that text, regardless of layout, and understanding "this string is the vendor, that number is the total." Neither could do the whole job alone; together they cover it. Think of it as an assembly line: a fast, tireless scanner (Textract) feeds a smart analyst (Claude), and you pay the analyst only for the thinking, not for squinting at scans.

The mechanism that makes the analyst reliable — and this is the piece worth studying — is forced tool use, which you met in W3. Left to prose, an LLM might reply "The vendor appears to be Acme and the total is around $99." That's useless to a program: you'd have to parse English back into fields, and you'd be back to brittle regexes. Instead you hand Claude a tool schema: a JSON shape naming the exact fields and types you want (vendor: string, total: number, which are required), and you set toolChoice to force Claude to answer by calling that tool. Now the model must return schema-valid structured data — a clean dict — instead of a paragraph. The LLM supplies the flexibility (it maps any layout into your fields); the schema supplies the discipline (the output shape never varies). That combination — deterministic OCR, flexible reasoning, rigid output contract — is the pattern that powers real document-intelligence pipelines, and it's the seam every serious system builds on.

Comprehend finds known entity types. When you need arbitrary structured extraction — line items, totals, custom fields — hand the Textract text to Claude with a tool schema (W3). Deterministic OCR + flexible reasoning.

each stage does the one job the other is bad at PDF in S3 pixels Textract deterministic OCR Claude · Bedrock flexible reasoning dict schema-valid clean text forced tool schema (W3) An assembly line: scanner feeds analyst. Textract handles the pixels-to-text step an LLM is weak at; Claude — constrained by a forced tool schema — maps that text, whatever the layout, into a fixed structured dict. Deterministic OCR, flexible reasoning, rigid output contract.
🗺️ How to read this diagram

Follow the solid arrow left to right — it's the flow of one document from raw file to finished data.

  • The white box is the raw PDF in S3 — to the machine, just pixels. The brand arrow carries it into the green Textract box, which does the deterministic OCR and emits clean text (the arrow's label).
  • That text flows into the amber Claude-on-Bedrock box — the flexible reasoner that reads the text regardless of layout and pulls out the fields you asked for.
  • The dashed arrow from below is the discipline: a forced tool schema (from W3) constrains Claude so its answer isn't prose but the purple schema-valid dict on the right — the same shape no matter how the invoice was laid out.

In short: PDF → Textract (deterministic OCR) → text → Claude + forced schema → a clean, layout-independent dict.

Lab W10.3
extract.pyimport boto3
brt = boto3.client("bedrock-runtime", region_name="us-east-1")

def extract_invoice(doc_text):
    schema = {"tools": [{"toolSpec": {
        "name": "record_invoice",
        "description": "Record structured invoice fields.",
        "inputSchema": {"json": {"type": "object", "properties": {
            "vendor": {"type": "string"},
            "total": {"type": "number"},
            "due_date": {"type": "string"},
        }, "required": ["vendor", "total"]}},
    }}]}
    resp = brt.converse(
        modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
        messages=[{"role": "user", "content": [{"text": f"Invoice text:\n{doc_text}"}]}],
        toolConfig={**schema, "toolChoice": {"tool": {"name": "record_invoice"}}},
    )
    for b in resp["output"]["message"]["content"]:
        if "toolUse" in b:
            return b["toolUse"]["input"]
▶ How this works

Comprehend only finds pre-defined categories. When you need custom fields — a vendor name, an invoice total, a due date — you hand the OCR'd text to Claude (an LLM on Amazon Bedrock) and ask it to fill in a fixed shape. This is the "hybrid": Textract reads the document, Claude does the flexible reasoning. The trick is a tool schema that forces Claude to answer as clean structured data instead of prose.

  1. brt = boto3.client("bedrock-runtime", ...) is the client for Bedrock, the AWS service that hosts Claude and other models.
  2. schema defines a tool called record_invoice. Its inputSchema is the JSON shape we want back: a vendor string, a total number, an optional due_date — and required lists the fields Claude must not skip.
  3. brt.converse(...) sends the request. modelId picks the Claude model; messages passes the document text inside an f-string (f"Invoice text:\n{doc_text}"); toolChoice with {"tool": {"name": "record_invoice"}} forces Claude to reply by calling that tool — so you get structured fields, not a paragraph.
  4. The final loop scans the reply's content blocks; when it finds one with a "toolUse" key it returns b["toolUse"]["input"] — the dictionary Claude filled in, e.g. {"vendor": "Acme", "total": 99.0}.

What the output means: Calling extract_invoice(doc_text) returns a Python dict of the fields Claude pulled from the text — the same clean shape no matter how the invoice was laid out, which a rigid template parser could never manage.

Try this: Add a "currency" property to the schema (and to required) and re-run — Claude will start returning that field too, without any other code change.

Exercise W10.1 — Document pipeline

Context: Fixed-template parsers break the moment a vendor rearranges their invoice. The durable pattern is OCR-then-LLM: Textract reads the pixels and Claude maps whatever layout it sees into one stable schema — exactly what a template cannot do.

Your task: Build the pipeline pdf_in_s3 → Textract text → Claude structured extract → dict, then run it on three different invoice layouts and confirm the same schema comes out of all three.

Requirements:

  • OCR each PDF from S3 with Textract, collecting the text (e.g. the LINE blocks)
  • Pass that text to Claude on Bedrock with a forced tool / fixed schema so the output is a validated dict
  • Define one target schema (e.g. invoice number, total, date) and reuse it across all three documents
  • Run all three differing layouts through the identical pipeline
  • Assert the same keys come out of every layout — the win over a template parser
  • Handle a field a document genuinely lacks (null/flag) rather than crashing

💡 Hint: Let the model absorb the layout differences: keep one schema and one forced-tool call, and only the OCR input changes per document.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · OCR a document with TextractBeginner

Context: Most enterprise data still arrives as PDFs, scans, and photos of forms. Textract turns that pixel soup into structured text you can query, and it understands tables and key/value form fields, not just flat OCR.

Your task: Use Textract analyze_document on an S3 object with the FORMS and TABLES feature types, then print every readable line of text the document contains.

Requirements:

  • Call analyze_document on a boto3 textract client, pointing Document at an S3Object (Bucket/Name)
  • Request FeatureTypes=["FORMS", "TABLES"] so form and table structure is analyzed, not just raw text
  • Textract returns a flat graph under Blocks; iterate it rather than expecting nested text
  • Print only the human-readable lines by keeping blocks where BlockType == "LINE" and reading their Text

💡 Hint: Textract hands back a graph of typed Blocks; the readable prose lives on the LINE blocks, so filter before you print.

Show solution

Textract returns a graph of Blocks; filter on BlockType == "LINE" for readable text.

import boto3

tx = boto3.client("textract", region_name="us-east-1")
resp = tx.analyze_document(
    Document={"S3Object": {"Bucket": "my-docs", "Name": "invoice.png"}},
    FeatureTypes=["FORMS", "TABLES"],
)
for b in resp["Blocks"]:
    if b["BlockType"] == "LINE":
        print(b["Text"])
Exercise 2 · Detect PII with ComprehendIntermediate

Context: Before you store or forward extracted text, you often must know whether it contains personal data. Comprehend ships a managed PII detector that flags emails, names, card numbers and more without any model of your own.

Your task: Run Comprehend detect_pii_entities on a string and print the Type of every PII entity it finds.

Requirements:

  • Call detect_pii_entities on a boto3 comprehend client, passing Text and LanguageCode="en"
  • Iterate the returned Entities list
  • Print each entity's Type (a fixed category such as EMAIL or CREDIT_DEBIT_CARD_NUMBER)
  • Recognize the categories are a closed, service-defined set — you do not train or configure them

💡 Hint: The categories are predefined; each entry in Entities already carries its Type, so there is nothing to classify yourself.

Show solution

Comprehend returns fixed entity categories; each has a Type like EMAIL or NAME.

import boto3

cp = boto3.client("comprehend", region_name="us-east-1")
text = "Email jane@x.com or call about card 4111111111111111."
resp = cp.detect_pii_entities(Text=text, LanguageCode="en")
for e in resp["Entities"]:
    print(e["Type"])   # e.g. EMAIL, CREDIT_DEBIT_CARD_NUMBER
Exercise 3 · Sentiment + entities togetherAdvanced

Context: Comprehend is really a family of narrow, deterministic classifiers. Real tasks usually want more than one at once — how a customer feels and what they named — so you compose several single-purpose calls into one record.

Your task: Call both detect_sentiment and detect_entities on a product review and combine their results into a single structured dict.

Requirements:

  • Issue two separate Comprehend calls on the same text, each with LanguageCode="en"
  • Read the overall label from detect_sentiment's Sentiment field (POSITIVE/NEGATIVE/…)
  • Pull (Text, Type) for each item in detect_entities' Entities list
  • Merge both into one dict keyed by e.g. sentiment and entities for a quick structured summary

💡 Hint: Each call is an independent classifier returning its own JSON; you are just stitching two responses together, not making one smarter call.

Show solution

Each Comprehend call is a narrow, deterministic classifier; combine them for a quick structured summary.

import boto3

cp = boto3.client("comprehend", region_name="us-east-1")
text = "Loved the Seattle store, but Acme support was slow."
s = cp.detect_sentiment(Text=text, LanguageCode="en")
e = cp.detect_entities(Text=text, LanguageCode="en")
summary = {
    "sentiment": s["Sentiment"],                          # POSITIVE/NEGATIVE/...
    "entities": [(x["Text"], x["Type"]) for x in e["Entities"]],
}
print(summary)
Exercise 4 · Textract to Claude for arbitrary extractionExpert

Context: Comprehend's fixed categories can't extract your invoice number or line-item total. The production pattern is a hybrid: let Textract produce clean text, then let a Claude tool on Bedrock coerce that text into a schema you define.

Your task: OCR a document with Textract, then force a Claude tool via Bedrock converse to pull custom fields (invoice number, total). Show the forced-tool call so the model must return schema-valid JSON.

Requirements:

  • Feed the Textract-derived text to converse on a bedrock-runtime client
  • Define a tool under toolConfig.tools with an inputSchema naming the custom fields and their types (e.g. invoice_number: string, total: number)
  • Force that tool with toolChoice={"tool": {"name": …}} so the reply is guaranteed structured, not prose
  • Recover the fields from the toolUse.input block in the response content
  • Explain why this beats Comprehend here: arbitrary, per-document fields Comprehend has no category for

💡 Hint: Textract gives you the text; the forced toolChoice is what turns free text into a validated object — read the answer off toolUse.input, not the model's prose.

Show solution

The hybrid: Textract gives clean text, then a forced tool gives schema-valid custom fields Comprehend can't produce.

import boto3

brt = boto3.client("bedrock-runtime", region_name="us-east-1")
ocr_text = "Invoice #INV-42  Total: $1,299.00"   # from Textract LINEs
TOOLS = {"tools": [{"toolSpec": {"name": "record_invoice",
  "description": "Extract invoice fields.",
  "inputSchema": {"json": {"type":"object","properties":{
     "invoice_number":{"type":"string"},
     "total":{"type":"number"}},"required":["invoice_number","total"]}}}}],
  "toolChoice": {"tool": {"name": "record_invoice"}}}

r = brt.converse(modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
    messages=[{"role":"user","content":[{"text": ocr_text}]}],
    toolConfig=TOOLS)
for b in r["output"]["message"]["content"]:
    if "toolUse" in b:
        print(b["toolUse"]["input"])   # {'invoice_number':'INV-42','total':1299.0}
Exercise 5 · Prebuilt vs LLM routing to control costProfessional

Context: Managed classifiers are pennies and milliseconds; a general LLM is neither. Sending sentiment or PII to Claude when Comprehend would do burns budget, so mature systems route each task to the cheapest service that can actually do it.

Your task: Encode route(task) that dispatches each task to the cheapest capable surface: sentiment/PII/known-entities to Comprehend, OCR to Textract, and arbitrary structured extraction to Claude on Bedrock.

Requirements:

  • Keep the set of Comprehend-suitable tasks (sentiment, PII, entities, language) as data, not scattered ifs
  • Return "comprehend" for those narrow, deterministic tasks
  • Return "textract" for OCR
  • Fall through to "claude-bedrock" only for open-ended/custom extraction
  • Runs fully offline — it is a routing decision, no AWS calls

💡 Hint: The rule is capability-and-cost: reserve the LLM for what the prebuilt services genuinely cannot express, and default everything narrow to Comprehend.

Show solution

Pre-built services are cheaper and faster for narrow tasks; reserve the LLM for open-ended extraction.

COMPREHEND_TASKS = {"sentiment", "pii", "entities", "language"}

def route(task):
    if task in COMPREHEND_TASKS:
        return "comprehend"      # cheap, deterministic
    if task == "ocr":
        return "textract"
    return "claude-bedrock"      # arbitrary/custom extraction

for t in ("sentiment", "ocr", "custom_line_items"):
    print(t, "->", route(t))
Exercise 6 · An invoice pipeline with a confidence gateIndustry scenario

Context: Finance wants invoices posted automatically, but a wrong total is worse than a slow one. The accepted pattern is to automate the confident cases and escalate the rest: OCR, extract, then a gate that decides auto-post vs. human review.

Your task: Build the document pipeline — Textract OCR → Claude extraction → a gate that routes to human review whenever a required field is missing or OCR confidence is low. Model the gate so it runs offline.

Requirements:

  • Define the required fields (e.g. invoice_number, total) in one place
  • The gate flags any required field that is missing or falsy in the extracted dict
  • The gate also fails documents whose OCR confidence is below a threshold (e.g. min_conf=0.90)
  • Return human_review (with what's missing / the confidence) when either check fails, else auto_post with the data
  • Demonstrate both a clean auto-post case and an escalated case offline

💡 Hint: The gate is a pure function of (extracted fields, ocr_confidence); make it return a route plus the reason so a reviewer sees why it was escalated.

Show solution

Automate the clean cases; escalate the risky ones. The gate protects downstream systems from bad extractions.

REQUIRED = ["invoice_number", "total"]

def gate(extracted, ocr_confidence, min_conf=0.90):
    missing = [f for f in REQUIRED if not extracted.get(f)]
    if missing or ocr_confidence < min_conf:
        return {"route": "human_review",
                "missing": missing, "confidence": ocr_confidence}
    return {"route": "auto_post", "data": extracted}

good = gate({"invoice_number":"INV-42","total":1299.0}, 0.98)
bad  = gate({"invoice_number":"INV-42"}, 0.97)   # total missing
print(good["route"], "|", bad["route"])   # auto_post | human_review

✓ Checkpoint — you can move on when you can…

  • Extract forms and tables with Textract.
  • Detect entities, PII, and sentiment with Comprehend.
  • Explain the Textract→Claude hybrid and when to use it.
  • Decide between a pre-built AI service and an LLM for a task.

Knowledge check check yourself

✓ Knowledge check

What is the Textract -> Claude hybrid pattern, and why not just use Comprehend for everything?

Show answer
Textract does deterministic OCR (text, forms, tables) and Claude does the flexible reasoning to pull arbitrary structured fields (line items, totals, custom fields). Comprehend only finds pre-defined entity types, so it can't do open-ended structured extraction the way a tool-schema'd LLM can.
✓ Knowledge check

Why is analyze_document with FeatureTypes=['FORMS','TABLES'] not enough for a large multi-page PDF, and what is the production shape?

Show answer
analyze_document is synchronous and essentially single-page. For multi-page documents you use the async start_document_analysis and then get_document_analysis; in production (W13) that is wired event-driven via SNS so you never poll by hand.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in