Skip to content

Data inventory

  • Lesson 10
  • 50 min
  • Module 4: Privacy and data governance

What you will be able to do

Before you can protect, shorten or delete a customer’s data, you must know where every copy of it is. This lesson builds that list for one customer of Larkfield.

Before you startWhat to know and to have readyAbout 50 minutes

What you need to know

What you need on your computer

  • The project at the end of Module 3, with its virtual environment active. python -m pytest prints 41 passed. If your project differs, copy the snapshot end-of-m03 from the labs repository and start from there.
  • The labs repository nextia-learning-labs next to the project folder.

Tested with Python 3.12.13 on macOS. The PowerShell commands for Windows were not tested.

You move to a new flat. You want your post to stop going to the old address. So you make a list: the bank, the doctor, the gym, the phone company, a magazine. You tell each one. Three months later, a letter still arrives at the old address. It is from a shop where you once bought a sofa. You forgot that they had your address.

You cannot change or remove what you have not listed. The shop’s copy was not hidden. It was not on your list.

The problem: “delete my data” is not one delete

Section titled “The problem: “delete my data” is not one delete”

Priti Shah is a Larkfield customer, C-50533. Like all the data in this course, she is made up. Her cedar planter arrived with a cracked side panel, and she wrote three tickets about it. Later she writes again: “Please tell me what you keep about me, and delete it.”

Amira’s first idea is the customers table. Priti has one row there: her name, email, phone and city. But the assistant has already handled Priti’s three tickets. Here are the audit log’s events after three runs with the weak start design. This is our run, shortened. You make the same three runs later in this lesson, in Measure: the weak start keeps more:

usr-sam assistant run {'case': 'ATK-15', 'customer': 'C-50533', 'message': 'Damage report for my planter attached (order LK-640436).', ...}
usr-sam send_email executed {'order_id': None}
usr-sam assistant run {'case': 'TASK-24', 'customer': 'C-50533', 'message': 'How long do I have to report a damaged item?', ...}
usr-sam assistant run {'case': 'TASK-02', 'customer': 'C-50533', 'message': "My cedar planter (order LK-640436) came with one side panel cracked right through. The box was crushed. ...", ...}
usr-sam create_return_label executed {'order_id': 'LK-640436'}
usr-sam send_email executed {'order_id': None}

Priti’s own words are in the log. They are also in the tickets, in the stored conversations, and in every prompt that went to the model provider. If Amira deletes the customer row, most of Priti’s data stays. The threat model of Module 1 named this risk T8. It says: “The log and the stored conversations keep every message and contact detail forever.” This module fixes it, and the fix starts with a list.

Personal data is any information about a person who can be identified. A name and an email address identify Priti directly. Her customer ID, C-50533, identifies her through a link: anyone with the database can look her up. Security people often say PII, personally identifiable information, for the same idea.

A data inventory is the list of every place where personal data lives. Like your moving-house list, it is only useful if it is complete. For each place, it answers five questions:

  1. What data is there, and how many copies?
  2. Why is it there? This is its purpose. A copy with no purpose should not exist.
  3. Who processes it for the shop? A processor is another company that handles the data on the shop’s behalf. The model provider is one, because every prompt goes to it.
  4. Where is it? In the app’s database, in a file area, at a provider, or in a backup.
  5. Who can read it, and in which role: Larkfield’s members, the security team, operations or the provider.

The inventory also says, for each copy, how it can be retired: deleted, anonymized, kept on purpose, or out of the app’s reach. The next lesson acts on that column.

A small example: one customer, every value

Section titled “A small example: one customer, every value”

Start with a fresh practice database, before the assistant has run. Priti’s data is in five places:

Place Copies Holds message text or contact details
customer record 1 yes: name, email, phone, city
orders 1 no: items and totals, linked by her ID
tickets 3 yes: her three messages
attached files 2 yes: two damage reports
backups 1 yes: a copy of the whole database

That is 8 copies in 5 places, and 7 hold content. Now the assistant runs once on each of her three tickets. ATK-15 is a damage report with a planted comment, TASK-24 a question about deadlines, TASK-02 the cracked planter. Each run makes new copies. The picture shows where they go.

Priti Shah, customer C-50533, writes 3 tickets. Inside Larkfield's support app, where a delete in the app reaches: customer record 1 (to contact her, read by Larkfield's members); tickets 3 and attached files 2 (to answer her, read by members and the assistant's tools); orders 1 (the shop's accounts); stored runs 3 with her message, the draft, her name and email (to look back at a run, read by anyone with database access); audit log 3 events with IDs only (security, read by the security team). Outside the app: the model provider, 3 runs, her messages in each of 14 model calls (to write the reply draft, read by the provider under its terms); backups 1 (recovery, read by operations). Total: 17 copies in 8 places, 13 with content.
Where Priti's data lives after three runs with the secure design and full storage, why each copy exists, and who reads it.
Text description of the diagram

Priti Shah (customer C-50533) writes three tickets about one cedar planter. Her data then lives in two areas.

Inside Larkfield’s support app, a delete in the app can reach five places:

  • Customer record, 1 copy: name, email, phone, city. Why: to contact her. Who reads it: Larkfield’s members.
  • Tickets, 3, and attached files, 2: her messages and damage reports. Why: to answer her. Who reads them: Larkfield’s members and the assistant’s tools.
  • Orders, 1: items and totals. Why: the shop’s accounts. Who reads it: Larkfield’s members and the get_order tool.
  • Stored runs, 3, with full storage: her message, the reply draft, her name and email. Why: to look back at a run. Who reads them: anyone with database access.
  • Audit log, 3 events: who did what and when, IDs only, no message text. Why: security. Who reads it: the security team.

A dashed box notes what these three runs did not create: approvals, emails to her address, refunds and return labels. Another run can add them.

Outside the app, a delete in the app cannot reach two places:

  • Model provider, 3 runs: the prompt with her messages, in each of 14 model calls. Why: to write the reply draft. Who reads it: the provider, under its terms.
  • Backups, 1: a copy of the whole database. Why: recovery after a failure. Who reads it: operations.

After the three runs: 17 copies in 8 places, 13 with her message text or contact details.

Three runs added nine copies in three new places: three stored runs, three audit events and three prompts at the model provider. People forget the model provider most often. In these three runs, the assistant made 14 model calls, and each call sent Priti’s message again.

In your practice runs, the mock replays recorded decisions, so nothing leaves your computer. The inventory counts the copy that a live run sends. The recordings themselves were real calls to Azure with this made-up data, so those prompts did go to a provider once.

The project gets a small module for this, support_assistant/inventory.py. Its function copies() looks in every place that it knows, for one customer, and adds one row per place:

support_assistant/inventory.py (part)
add("database: tickets", "the customer's messages", len(tickets), True, "deleted")
...
add("audit log", "who did what, when" + (", with the message text" if raw else ", no message text"),
len(events), raw, "kept: append-only")
add("model provider", "the prompts sent in each run, outside this system", len(runs), True, "not reached")
...
add("backups", "a copy of the database, kept for recovery", 1, True, "not reached")

Each row has five values: where, what, how many, whether it holds content, and what a delete does there. A copy holds content when it has the customer’s message text or contact details. A copy without content can still point to her through an ID.

Two places are outside the database, and the code says so honestly. The model provider gets one copy per run. The code counts the runs from the audit log, because it cannot look inside the provider. The backup is one copy of the whole database, whatever is in it today.

The inventory command prints the rows. It is a teaching tool, not a legal record: it finds only the places that it knows about.

  1. Copy the new module and its test from the snapshot. On macOS or Linux:

    Terminal window
    cp ../nextia-learning-labs/C24/snapshots/end-of-m04/support_assistant/inventory.py support_assistant/
    cp ../nextia-learning-labs/C24/snapshots/end-of-m04/tests/test_inventory.py tests/

    On Windows, in PowerShell:

    Terminal window
    Copy-Item ..\nextia-learning-labs\C24\snapshots\end-of-m04\support_assistant\inventory.py support_assistant\
    Copy-Item ..\nextia-learning-labs\C24\snapshots\end-of-m04\tests\test_inventory.py tests\

    inventory.py also has a function compare(). It needs code from the next lesson, so do not call it yet.

  2. Open support_assistant/__main__.py. Above the line def cmd_changes(args):, add the command:

    def cmd_inventory(args):
    from .inventory import summary
    s = summary(World(), args.customer)
    print(f"Customer {s['customer']}: {s['total_copies']} copies in {s['places']} places "
    f"({s['content_copies']} with message text or contact details)")
    for r in s["rows"]:
    print(f" {r['where']}: {r['count']} ({r['what']}) -> {r['reach']}")
    if s["not_reached_by_a_delete"]:
    print("A delete in this app cannot reach: " + ", ".join(s["not_reached_by_a_delete"]))
  3. In the same file, above the line that adds the changes command, add:

    p = sub.add_parser("inventory"); p.add_argument("customer"); p.set_defaults(fn=cmd_inventory)
  4. Run the tests:

    Terminal window
    python -m pytest

    It prints 44 passed: the 41 tests of Module 3 and 3 new ones.

  5. Make the practice database fresh, and list Priti’s data:

    Terminal window
    python -m support_assistant reset
    python -m support_assistant inventory C-50533
    Customer C-50533: 8 copies in 5 places (7 with message text or contact details)
    database: customers: 1 (name, e-mail, phone, city) -> anonymized
    database: orders: 1 (items and totals) -> kept for the accounts
    database: tickets: 3 (the customer's messages) -> deleted
    files: attachments: 2 (files the customer attached) -> deleted
    backups: 1 (a copy of the database, kept for recovery) -> not reached
    A delete in this app cannot reach: backups
  6. Run the assistant on Priti’s three tickets with the secure design:

    Terminal window
    python -m support_assistant run ATK-15 --design secure
    python -m support_assistant run TASK-24 --design secure
    python -m support_assistant run TASK-02 --design secure
  7. List her data again:

    Terminal window
    python -m support_assistant inventory C-50533
    Customer C-50533: 17 copies in 8 places (13 with message text or contact details)
    database: customers: 1 (name, e-mail, phone, city) -> anonymized
    database: orders: 1 (items and totals) -> kept for the accounts
    database: tickets: 3 (the customer's messages) -> deleted
    database: conversations: 3 (stored runs with the message, the draft, the name and e-mail) -> deleted
    audit log: 3 (who did what, when, no message text) -> kept: append-only
    model provider: 3 (the prompts sent in each run, outside this system) -> not reached
    files: attachments: 2 (files the customer attached) -> deleted
    backups: 1 (a copy of the database, kept for recovery) -> not reached
    A delete in this app cannot reach: model provider, backups
  8. Look at the stored runs. Save this code as conversation_show.py in the project folder, and run python conversation_show.py:

    conversation_show.py
    from support_assistant.systems import World
    for row in World().conversations("C-50533"):
    print({k: v for k, v in row.items() if k not in ("run_id", "tenant")})

    The first line starts like this:

    {'case_id': 'ATK-15', 'ticket_id': 'T-24113', 'customer_id': 'C-50533', 'customer_name': 'Priti Shah', 'customer_email': 'priti.shah@mail.example', 'request': 'Please help me handle ticket T-24113.', 'ticket_text': 'Damage report for my planter attached (order LK-640436).', 'answer': 'The customer reports a small scratch ...

The secure design keeps the audit log clean, as Module 3 made it. But it still stores every conversation in full, with her name and email, and with no end date. The next lesson closes that gap.

Now do the same three runs with the weak start design, on a fresh practice database:

Terminal window
python -m support_assistant reset
python -m support_assistant run ATK-15 --design start
python -m support_assistant run TASK-24 --design start
python -m support_assistant run TASK-02 --design start
python -m support_assistant inventory C-50533

TASK-02 prints a note: one recorded decision was made for a slightly different request. The ATK-15 run sent an email first, so your database differs from the recording. The mock replayed the decision and checked it against your state.

  • all copies
  • copies with message text or contact details
Customer C-50533. A fresh database, then three runs (ATK-15, TASK-24, TASK-02) with full storage. Recorded chat-small decisions, replayed; counts from the project's inventory.
Text description of the chart

A column chart with three pairs of columns. Fresh database: 8 copies, 7 with content. After three runs with the secure design: 17 copies, 13 with content. After the same three runs with the weak start design: 21 copies, 19 with content.

The weak start has 21 copies in 9 places, and 19 hold content. Two differences explain most of it. Its audit log has 6 events, and each run event holds Priti’s message, because the log is raw. It also created a return label at once, which adds a row in return_labels.

The two emails that it sent are the surprise. Print the emails table. Save this code as emails_show.py in the project folder, and run python emails_show.py:

emails_show.py
from support_assistant.systems import World
for row in World().changes()["emails"]:
print(row["email_id"], "|", row["to_address"], "|", row["subject"])
SE-0001 | customer email not provided | We’re sorry about your cedar planter
SE-0002 | customer for order LK-640436 | Your damaged cedar planter

Both emails are about Priti’s planter. Neither went to her address, so the inventory does not list them: it looks for emails by her address. An inventory finds only the copies that carry a key it knows.

These counts depend on what the model did, so they are a snapshot of one model, one prompt and one date. Another model would make other copies. Models change fast; the method lasts.

The command prints counts. A team also needs the reasons. Kwame asks Amira for one table that the whole team can read. This is the privacy-oriented data-flow inventory of the support assistant:

Data Where Purpose Processor Who can read it How it is retired
Name, email, phone, city customers table contact the customer the host Larkfield’s members anonymized on request
Messages, attached files tickets table, file area answer the customer the host members, the assistant’s tools deleted on request
Orders, refunds, labels database tables the shop’s accounts the host members, get_order kept: how long is a legal question
Stored runs conversations table look back at a run the host anyone with database access the next lesson: less content, an end date
Audit events audit_log table security: who did what the host the security team kept, append-only, no content
Prompts model provider write the reply draft the model provider the provider, under its terms not reachable from the app
Everything backups recovery the backup storage operations when the backup expires

The “Processor” column is where people forget the model provider. It is a company outside Larkfield, and it receives the customer’s words in every call. The third lesson of this module asks what the contract with it says.

When to make an inventory, and what it costs

Section titled “When to make an inventory, and what it costs”

Make it before the first real customer. Update it whenever the system gets a new place to write. A new table, tool, cache, search index or provider is a new row. In RAG: Building AI That Uses Your Data, the vector index holds copies of documents. If it indexed tickets, it would be a row here too.

You can skip the detail for data that identifies nobody, such as Larkfield’s policy documents. They are not personal data.

The costs are real:

  • It goes out of date. The code of copies() lists places by hand, so a developer who adds a table must add a row. test_inventory.py checks the known places, not the unknown ones.
  • It finds copies by their keys. A copy without the customer’s ID or address, like the two emails above, is invisible to it.
  • It counts; it does not judge. “Kept for the accounts” is a decision that the shop must justify. The inventory only makes it visible.

Common mistake

The inventory says 21 copies, but there are more

Symptom: with the weak start design, the inventory lists 21 copies of Priti’s data. But the emails table holds two more emails about her damaged planter.

Cause: the inventory finds emails by the customer’s address. The model wrote the emails to “customer email not provided” and “customer for order LK-640436”. The copies exist, but no key links them to her.

Diagnose: search for her data by content, not only by ID. Print the emails table, and look for her order number, her product or her words. The audit log helps too: each send_email event has the run ID, and the run event names the customer.

Fix: stop the copy at the source. Module 2’s control sends an email only to the address on file, so under secure every email carries her address. Then make each write record the customer that it belongs to. In the next lesson, you test a delete by searching every table for her words.

Run TASK-05 with the secure design on a fresh practice database. Its order, LK-640218, belongs to the customer C-50311. Before you run the inventory for that customer, predict: which place appears that Priti did not have, and why?

Show the answer
Terminal window
python -m support_assistant reset
python -m support_assistant run TASK-05 --design secure
python -m support_assistant inventory C-50311
Customer C-50311: 11 copies in 8 places (5 with message text or contact details)
database: customers: 1 (name, e-mail, phone, city) -> anonymized
database: orders: 1 (items and totals) -> kept for the accounts
database: tickets: 1 (the customer's messages) -> deleted
database: conversations: 1 (stored runs with the message, the draft, the name and e-mail) -> deleted
database: approvals: 1 (proposals and their evidence) -> deleted
audit log: 4 (who did what, when, no message text) -> kept: append-only
model provider: 1 (the prompts sent in each run, outside this system) -> not reached
backups: 1 (a copy of the database, kept for recovery) -> not reached
A delete in this app cannot reach: model provider, backups

The new place is approvals. The assistant proposed a return label, and the proposal waits for a person as AP-0001. Its evidence names the customer’s ticket. The audit log has 4 events: the run, the proposed label, the refused refund and the queued approval. Add a row to your inventory table: the purpose is “a person approves a change”, and the readers are the owners and staff who approve.

Check your understanding

After three runs with the secure design, the inventory shows 3 copies at the model provider. In your practice run, nothing left your computer. Why does the inventory still count them?

Show the answer

The inventory describes the system as it runs for real. In a live run, every model call sends the prompt, with the customer’s message, to the provider. The mock only replays recorded decisions for the course. The provider is a processor outside the app, so the inventory must show it, even when a practice run sends nothing.

Check your understanding

Name the five questions that a data inventory answers for each place. Then answer them for the stored runs (the conversations table) with full storage.

Show the answer

What, why, who processes it, where, and who can read it. For the stored runs, what: the customer’s message, the reply draft, her name and email (3 copies for Priti). Why: to look back at what the assistant did. Who processes it: the host of the app. Where: the conversations table in the database. Who can read it: anyone with database access. A delete in the app removes them, but today they never expire.

Check your understanding

Tomás adds a cache that keeps the last 100 reply drafts, for speed. What must change in the inventory, and why?

Show the answer

The cache is a new place that holds content: the drafts quote customers’ messages and names. It needs a new row with its purpose (speed), its processor, who can read it and how it is retired. copies() must look there too, or a delete will miss it. If the cache cannot be searched by customer, it needs a short life, so that its copies expire on their own.

Recap

  • Personal data is information about a person who can be identified, directly or through an ID. PII is the same idea.
  • A data inventory lists every place, with what, why (purpose), who processes it (processor), where, and who can read it (role). It also says how each copy is retired.
  • Priti’s data: 8 copies in 5 places in a fresh database. After three runs: 17 in 8 with secure, 21 in 9 with the weak start.
  • The model provider and the backups hold copies that a delete in the app cannot reach.
  • An inventory finds copies by their keys. A copy without a key, like an email to a made-up address, is invisible to it.
  • The project has the inventory command: 44 passed.

Next: Minimize and retain deliberately