Structured Extraction
Sometimes the LLM’s job is not to answer a question or hold a conversation but to turn an unstructured input into a structured record such as:
- Extracting names, addresses, and emails from a customer message.
- Turning meeting notes into calendar events.
- Converting a paragraph of product description into fields for a database.
This class of task is covered in this chapter.
We will use the LlamaIndex primitive llm.structured_predict(SchemaClass, prompt_template, **vars). Supply a Pydantic model and a prompt template and then get back a validated instance of structured data. The framework handles the JSON-schema plumbing, the “here is the schema, please output JSON” prompt engineering, and the retry loop when validation fails.
Everything lives in source-code/llama_index_extract/ with the usual setup:
1 $ cd source-code/llama_index_extract
2 $ uv sync
3 $ ollama pull qwen3.5:4b
One-shot extraction
We will start with a simple example in the script 01_structured_predict.py:
1 from llama_index.core.prompts import PromptTemplate
2 from llama_index.llms.ollama import Ollama
3 from pydantic import BaseModel, Field
4
5
6 class Person(BaseModel):
7 """A person mentioned in text."""
8
9 name: str = Field(description="The person's full name.")
10 address: str | None = Field(default=None, description="Street address if given.")
11 email: str | None = Field(default=None, description="Email address if given.")
12
13
14 llm = Ollama(model="qwen3.5:4b", temperature=0,
15 request_timeout=120.0, thinking=False)
16
17 prompt = PromptTemplate(
18 "Extract structured information about the person mentioned in the following text. "
19 "If a field is not mentioned, leave it null.\n\n"
20 "Text: {input_text}"
21 )
22
23 text = (
24 "Mark Johnson enjoys living in Berkeley, California at 102 Dunston Street "
25 "and can be reached at mjess@foobar.com."
26 )
27
28 person: Person = llm.structured_predict(Person, prompt, input_text=text)
29
30 print(f"name = {person.name}")
31 print(f"address = {person.address}")
32 print(f"email = {person.email}")
Two design points worth being explicit about.
The docstrings on Field(description=...) are part of the prompt. The model reads them along with the JSON schema, and the quality of your field descriptions directly affects the quality of the extraction. Vague descriptions produce sloppy extraction; precise descriptions produce reliable extraction.
The Optional (str | None) with default=None is how you tell the model a field may not be present. Without it, the model tends to hallucinate values for missing fields.
Here is the eoutput:
1 $ uv run 01_structured_predict.py
2 name = Mark Johnson
3 address = 102 Dunston Street, Berkeley, California
4 email = mjess@foobar.com
Batch extraction
Now we use the same primitive in a loop over input data sources in the example script 02_batch_extract.py:
1 class Event(BaseModel):
2 """A calendar-style event described in text."""
3
4 title: str = Field(description="Short title of the event.")
5 date: str = Field(description="Date in YYYY-MM-DD format.")
6 location: str | None = Field(default=None, description="Location if mentioned.")
7
8
9 llm = Ollama(model="qwen3.5:4b", temperature=0, request_timeout=120.0, thinking=False)
10
11 prompt = PromptTemplate(
12 "Extract calendar event information from the text below. "
13 "If the location is not mentioned, leave it null.\n\n"
14 "Text: {input_text}"
15 )
16
17 notes = [
18 "Meeting with Carol on March 15, 2026 at the Sedona office.",
19 "Team lunch on April 3, 2026.",
20 "Book launch party April 20, 2026 at the downtown bookstore in Flagstaff.",
21 ]
22
23 events: list[Event] = []
24 for note in notes:
25 event = llm.structured_predict(Event, prompt, input_text=note)
26 events.append(event)
27
28 for e in events:
29 print(f"{e.date} {e.title!r} location={e.location!r}")
This is the shape of nearly every “migrate a folder of unstructured notes into a database” workflow. If throughput matters, replace the sequential loop with llm.astructured_predict and an asyncio.gather; for local models with limited concurrency, keeping it sequential when using local faster than trying to parallelize. When using commercial inference AIs then please do paralyze these operations to get faster throughput.
When to reach for this vs a chat model with tools
structured_predict and tool-calling look similar: both make the model output structured data. The difference is what you do with the result.
structured_predictreturns a Pydantic object directly. Use it when the LLM’s job is to produce structured data as the final answer.- Tool calling (Chapters “Building a ReAct Agent with LangGraph + Ollama” and “Building an Agent as a Workflow”) returns tool call requests that you then execute. Use it when the LLM’s job is to decide to do something that produces data.
There is overlap. A ReAct agent whose final answer is a Pydantic object is a valid pattern. But if all you need is “text in, structured record out,” structured_predict is dramatically simpler.
What we covered
llm.structured_predict(SchemaClass, prompt, **vars)extracts a validated Pydantic object from unstructured text in one call.- Field descriptions on the Pydantic model are part of the prompt, so treat them carefully.
- Optional fields with
default=Noneprevent the model from hallucinating missing values. - Batch extraction is just the same primitive in a loop; use
astructured_predictfor concurrency when needed.
The next chapter “Serving a Workflow with FastAPI” wraps up Part II by deploying a workflow as a service with plain FastAPI: one process, no cloud.