Finding personal data before it is sent
Personal data is any fact about a living person who could be identified from it. Finding it in a prompt matters, because a prompt can leave your computer and cannot be called back.
A name, an email address and a phone number all count. An app that sends messages to a model may send these without meaning to. You cannot protect what you cannot find, so finding it comes first.
By the end of this lesson
Note
I can find personal data in a prompt with rules and say what the rules miss.
Key words
| Word | What it means | Example |
|---|---|---|
| Personal data | a fact that points to one living person | |
| Rule | a test that a piece of text passes or fails | has an @ sign |
| Shape | how a kind of value is written | 07700900123 |
| False alarm | text the rule flags that is not personal | |
| Miss | personal data the rules did not flag |
The cells students fill in are left blank in this preview.
Rules find data that has a shape
Some personal data is always written the same way. An email address has an at sign and a dot. A UK mobile number is 11 digits starting 07.
A rule tests one shape. A finder splits a message into words and tests each word against every rule. It lists what it finds in the order the words appear.
Worked example: four rules
| Kind | Rule | Invented example |
|---|---|---|
| An at sign, then a dot later | [email protected] | |
| phone | 11 digits, starting 07 | 07700900123 |
| ni | Two capitals, six digits, one capital | QQ123456C |
| card | 16 digits | 4000000000000002 |
The cells students fill in are left blank in this preview.
Rules miss things and raise false alarms
A rule only knows shapes. A name has no shape, so no rule finds it. A phone number typed with spaces becomes three words, and none fits.
A false alarm is the opposite fault. A 16 digit order number has the card shape, so the rule flags it. Both faults are normal. Count them so you know how far to trust the rules.
Worked example: a miss and a false alarm
| Message part | A person says | The rules say | Result |
|---|---|---|---|
| Ben Sample | Personal | Nothing found | Miss |
| 07700 900 456 | Personal | Nothing found | Miss |
| 4000000000000002 (an order number) | Not personal | Card | False alarm |
| [email protected] | Personal | Correct |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think a finder that prints nothing means the message is clean. It does not. It means no rule matched. A name or an address written in words has no shape. Say "no known shape found" and never "no personal data".
Write the model helper, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the model helper, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' '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 || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' '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 finder, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const messages = [' ' \"Please email [email protected] about the refund.\",' ' \"Call me on 07700900123 after five.\",' ' \"My number is 07700 900 456, ring any time.\",' ' \"NI number QQ123456C is on the form.\",' ' \"Order 4000000000000002 shipped to Ben Sample.\",' '];' 'const marked = [[\"[email protected]\"], [\"07700900123\"], [\"07700 900 456\"], [\"QQ123456C\"], [\"Ben Sample\"]];' > finder.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the finder, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function clean(word) {' ' let w = word;' ' while (w.length > 0 && \".,;:?()\".includes(w[w.length - 1])) w = w.slice(0, -1);' ' return w;' '}' 'function digits(w, n) {' ' return w.length === n && w.split(\"\").every(function (c) { return c >= \"0\" && c <= \"9\"; });' '}' >> finder.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the finder, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function kindOf(w) {' ' const at = w.indexOf(\"@\");' ' if (at > 0 && w.indexOf(\".\", at) > at + 1) return \"email\";' ' if (digits(w, 11) && w.startsWith(\"07\")) return \"phone\";' ' const ni = w.length === 9 && /[A-Z][A-Z]/.test(w.slice(0, 2)) && /[A-Z]/.test(w[8]);' ' if (ni && digits(w.slice(2, 8), 6)) return \"ni\";' ' if (digits(w, 16)) return \"card\";' ' return \"\";' '}' >> finder.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the finder, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function find(text) {' ' const out = [];' ' text.split(\" \").forEach(function (word) {' ' const w = clean(word);' ' if (kindOf(w).length > 0) out.push({ kind: kindOf(w), value: w });' ' });' ' return out;' '}' 'function same(a, b) { return JSON.stringify(a) === JSON.stringify(b); }' >> finder.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the finder, part 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' messages.forEach(function (text, i) {' ' const values = find(text).map(function (f) { return f.value; });' ' const missed = marked[i].filter(function (v) { return values.indexOf(v) < 0; });' ' const extra = values.filter(function (v) { return marked[i].indexOf(v) < 0; });' ' console.log((i + 1) + \" found \" + JSON.stringify(find(text)) + \" missed \" + JSON.stringify(missed) + \" false alarm \" + JSON.stringify(extra));' ' });' ' const q = \"In one short sentence, why can a rule not find a name?\";' ' const sample = \"A name has no fixed shape, so a rule has nothing to match.\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"model said: \" + reply.text);' '}' 'main();' >> finder.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the finder
docker exec -w /home/node/project agent node finder.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the Run prints
| Line | What it shows |
|---|---|
| 1 found email ... missed [] false alarm [] | A correct find |
| 3 found [] missed ["07700 900 456"] | A number with spaces is missed |
| 5 found card ... missed ["Ben Sample"] | A name is missed |
| 5 ... false alarm ["4000000000000002"] | An order number looks like a card |
| model said: ... | A sentence. The words differ each run |
The cells students fill in are left blank in this preview.