Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

How to extract data from text

After this lesson: I can turn a paragraph into a record without inventing a value.

60 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

The phone number nobody gave

Extracting a record from text means asking a model to read a paragraph and fill in a fixed set of fields. It matters because the model will fill a gap with a guess unless you tell it not to.

A school office gets a booking message with a name, a place and a date, but no phone number. The tool shows a phone number anyway. Nobody gave it: the model made it up.

By the end of this lesson

Note

I can turn a paragraph into a record without inventing a value.

Key words

WordWhat it meansExample
Extractto pull named facts out of text
Fieldone named fact in a recorddate
Nullthe value that says a field is not there
Source textthe paragraph the facts must come from
Groundedfound in the source text, not guessed
Completenessthe share of fields that have a value

The cells students fill in are left blank in this preview.

Name the fields and allow null

Say exactly which fields you want and the type of each. Then say what to do when a fact is not in the text: use null, and never guess.

Without that rule a model may fill every field, because a full record looks like a good answer. Give it permission to leave a gap.

Worked example: one message, five fields

FieldIn the message?The right value
nameYes: "my name is Priya Shah"Priya Shah
destinationYes: "a trip to Leeds"Leeds
dateYes: "2026-11-20"2026-11-20
studentsYes: "24 students"24
phoneNonull

The cells students fill in are left blank in this preview.

Check that each value is grounded

Do not trust the prompt alone. After the reply, check each value against the source text. If the value does not appear in it, treat it as invented and set it to null.

This check is simple and has a weakness. A wrong count of 2 students would be kept, because 2 appears inside 2026-11-20. A stronger check matches whole words. Even the simple check catches the made-up phone number.

Write the model helper

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    return (await res.json()).choices[0].message.content;' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' 'module.exports = { ask };' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the extractor, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const source = \"Hello, my name is Priya Shah. I would like to book the minibus for a trip to Leeds on 2026-11-20. There will be 24 students and 3 staff. Please write back by email.\";' 'const fields = [\"name\", \"destination\", \"date\", \"students\", \"phone\"];' > extract4.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the extractor, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function pullJson(text) {' '  const start = text.indexOf(\"{\");' '  const end = text.lastIndexOf(\"}\");' '  if (start === -1 || end < start) return null;' '  try { return JSON.parse(text.slice(start, end + 1)); } catch (error) { return null; }' '}' >> extract4.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the extractor, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Keep a value only if it really appears in the source text.' 'function grounded(record, source, fields) {' '  const result = {};' '  for (const name of fields) {' '    const value = record === null ? null : record[name];' '    if (value === null || value === undefined || value === \"\") {' '      result[name] = null;' '      console.log(name + \": null (missing)\");' '    } else if (source.toLowerCase().includes(String(value).toLowerCase())) {' '      result[name] = value;' '      console.log(name + \": \" + value + \" (kept)\");' '    } else {' '      result[name] = null;' '      console.log(name + \": \" + value + \" (not in the text, dropped)\");' '    }' '  }' '  return result;' '}' >> extract4.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the extractor, part 4

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const prompt = \"Extract these fields from the message as a JSON object: name (text), destination (text), date (text, year-month-day), students (a whole number), phone (text). If a field is not in the message, use null. Never guess. Reply with only the JSON.\n\nMessage: \" + source;' '  // Recorded sample, used only when no model answers. It invents a phone number on purpose.' '  const sample = JSON.stringify({ name: \"Priya Shah\", destination: \"Leeds\", date: \"2026-11-20\", students: 24, phone: \"07700 900123\" });' '  const reply = await ask([{ role: \"user\", content: prompt }], sample, 150);' '  const record = grounded(pullJson(reply), source, fields);' '  let filled = 0;' '  for (const name of fields) {' '    if (record[name] === null) continue;' '    filled = filled + 1;' '  }' '  console.log(\"Filled: \" + filled + \" of \" + fields.length);' '}' 'main();' >> extract4.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the extractor

Command
docker exec -w /home/node/project agent node extract4.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

One possible output

LineWhat it may show
name, destination, date, studentsEach followed by a value and (kept)
phonenull (missing), or a value marked (not in the text, dropped)
FilledFilled: 4 of 5 if the model found only the four facts

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think a filled field is better than an empty one. A made-up value looks just like a real one, and nobody knows to check it. A null says plainly that a person must look.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.