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
| Word | What it means | Example |
|---|---|---|
| Extract | to pull named facts out of text | |
| Field | one named fact in a record | date |
| Null | the value that says a field is not there | |
| Source text | the paragraph the facts must come from | |
| Grounded | found in the source text, not guessed | |
| Completeness | the 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
| Field | In the message? | The right value |
|---|---|---|
| name | Yes: "my name is Priya Shah" | Priya Shah |
| destination | Yes: "a trip to Leeds" | Leeds |
| date | Yes: "2026-11-20" | 2026-11-20 |
| students | Yes: "24 students" | 24 |
| phone | No | null |
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
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
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
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
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
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
docker exec -w /home/node/project agent node extract4.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
One possible output
| Line | What it may show |
|---|---|
| name, destination, date, students | Each followed by a value and (kept) |
| phone | null (missing), or a value marked (not in the text, dropped) |
| Filled | Filled: 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.