The ticket with a phone number in it
Keeping personal data out of prompts means a model only sees what it needs to do the task. It matters because a prompt is sent to another service, and it often ends up in a log.
A support tool sends each ticket to a model to draft a reply. The ticket holds a name, an email address and a phone number. The reply needs none of them. The tool can swap them for placeholders and swap them back.
By the end of this lesson
Note
I can replace personal data with placeholders before a model call and restore them afterwards.
Key words
| Word | What it means | Example |
|---|---|---|
| Personal data | any detail that identifies a living person | |
| Data minimisation | sending only what the task needs | |
| Redact | to replace a value with a stand-in | |
| Placeholder | a label that stands for a removed value | [EMAIL_1] |
| Mapping | the table that links each placeholder to its value |
The cells students fill in are left blank in this preview.
Send only what the task needs
Ask first whether the model needs the value at all. A reply to a complaint needs the complaint, not the phone number. Remove what is not needed.
If the reply must mention a person, use a placeholder. The model writes "Dear [NAME_1]" and your own code puts the real name back afterwards.
Worked example: before and after
| Value | Sent to the model | Kept in the mapping |
|---|---|---|
| Amira | [NAME_1] | [NAME_1] is Amira |
| [email protected] | [EMAIL_1] | [EMAIL_1] is [email protected] |
| 07700900123 | [PHONE_1] | [PHONE_1] is 07700900123 |
| Amira, again | [NAME_1] | The same value gets the same placeholder |
The cells students fill in are left blank in this preview.
Redaction has limits
A rule finds the shapes you wrote: a word with an at sign, a long run of digits, a name from a list. It misses a street name, a nickname or a number written with spaces.
So redaction lowers the risk and never removes it. Test it on messy examples, and keep a check that looks for leftovers.
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 redaction script, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const names = [\"Amira\", \"Tom\"];' 'const ticket = \"Hi, I am Amira. Email [email protected] or call 07700900123. Tom helped me last week.\";' 'function allDigits(text) {' ' return text.length >= 8 && text.split(\"\").every(function (c) { return c >= \"0\" && c <= \"9\"; });' '}' > redact.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the redaction script, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function redact(text, names) {' ' const map = {};' ' const seen = {};' ' const words = text.split(\" \");' ' for (let i = 0; i < words.length; i++) {' ' let bare = words[i];' ' while (bare.endsWith(\".\") || bare.endsWith(\",\")) bare = bare.slice(0, -1);' ' const tail = words[i].slice(bare.length);' ' let kind = \"\";' ' if (bare.includes(\"@\")) kind = \"EMAIL\";' ' if (allDigits(bare)) kind = \"PHONE\";' ' if (names.includes(bare)) kind = \"NAME\";' >> redact.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the redaction script, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (kind === \"\") continue;' ' if (seen[bare] === undefined) {' ' const n = Object.keys(map).filter(function (k) { return k.startsWith(\"[\" + kind); }).length + 1;' ' seen[bare] = \"[\" + kind + \"_\" + n + \"]\";' ' map[seen[bare]] = bare;' ' }' ' words[i] = seen[bare] + tail;' ' }' ' return { text: words.join(\" \"), map: map };' '}' >> redact.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the redaction script, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function restore(text, map) {' ' let out = text;' ' for (const key of Object.keys(map)) out = out.split(key).join(map[key]);' ' return out;' '}' 'async function main() {' ' const safe = redact(ticket, names);' ' console.log(\"Sent to the model: \" + safe.text);' ' console.log(\"An @ is left in it: \" + safe.text.includes(\"@\"));' ' const rule = \"Write a two-sentence reply to the customer. Keep every placeholder in square brackets exactly as written.\";' ' const messages = [{ role: \"system\", content: rule }, { role: \"user\", content: safe.text }];' ' const sample = \"Dear [NAME_1], thank you for your message. We will write to [EMAIL_1] today.\";' >> redact.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the redaction script, part 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const reply = await ask(messages, sample, 100);' ' console.log(\"Reply with placeholders: \" + reply);' ' console.log(\"Restored for the customer: \" + restore(reply, safe.map));' ' console.log(\"Log line: redacted \" + Object.keys(safe.map).length + \" items, reply length \" + reply.length);' '}' 'main();' >> redact.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the redaction script
docker exec -w /home/node/project agent node redact.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 shows |
|---|---|
| Sent to the model | The ticket with [NAME_1], [EMAIL_1], [PHONE_1] and [NAME_2] in place of the values |
| An @ is left in it | false |
| Reply with placeholders | Two sentences that keep the placeholders. The words differ each run |
| Restored for the customer | The reply with the real values put back |
| Log line | Counts and a length, with no personal text |
The cells students fill in are left blank in this preview.
What a log may hold
A log is read by more people than the app, and it lives longer. Log what you need to find a fault: a request id, a user id, a count, a time, a decision.
Do not log the prompt or the reply in full. If you must keep one to debug, keep the redacted copy, for a short time, behind access control.
A common mistake
Warning
Many people think that once the prompt is redacted, the logs are safe. The reply, the error messages and the original request may still hold the real values. Check every place the text goes: the prompt, the reply, the logs and any cache.