In the previous lesson we protected DocuBot from those trying to manipulate it. This lesson looks in the other direction: what DocuBot does with the data entrusted to it. Every call to the provider's API packages up the system prompt, the conversation history, the retrieved internal documentation and project data, and sends it to a third party's servers. For a developer this is just another HTTPS call; for the organization it is a data transfer with contractual and, sometimes, legal implications. In module 1 (01-04) we flagged privacy at a high level with a pointer to this lesson; it's time to keep that promise: understand exactly what leaves, minimize what leaves, know the contractual guarantees available, and place all of this within the framework of the GDPR and the EU AI Act.
⚠️ Warning: this lesson is educational material and does not constitute legal advice. How the GDPR, the AI Act or any other regulation applies depends on the specific case (type of data, purpose, industry, countries involved). Before deploying to production a system that processes personal data or has regulatory implications, review the design with your organization's legal, compliance, data protection (DPO) and security teams.
Contents
- What leaves your organization on every call
- The data lifecycle at the provider
- Data minimization: DocuBot's redaction function
- Contractual guarantees and alternative architectures
- GDPR at a high level: what a developer should know
- The EU AI Act and DocuBot's transparency
- Data decisions for DocuBot: the travels / doesn't travel table
What Leaves Your Organization on Every Call
Let's start by making the invisible visible. When docubot.py::answer (or agent_loop) calls the API, the request body contains much more than "the user's question":
| Prompt component | Origin in DocuBot | What it may contain |
|---|---|---|
| System prompt | prompts.py (SYSTEM_DOCUBOT_AGENT) |
Internal instructions, tone, policies — intellectual property, rarely personal data |
| Conversation history | Conversation / SessionManager (03-03), up to 10 turns |
Everything the user has typed: names, complaints, customer data they pasted without thinking |
| Retrieved documentation | rag.py::retrieve → chunks from docs_nubelia |
Internal wiki content: procedures, internal pricing, sometimes employee or customer names |
| Tool definitions | tools.py::TOOLS |
Names and descriptions of your internal APIs (they reveal your architecture) |
| Tool results | tool_result from query_project, etc. |
Operational project data: statuses, owners, dates, incidents |
| The current question | The user | Anything at all |
Three observations that change the perspective:
- RAG multiplies the exposure. Before module 4, what left was what the user typed. Since module 4, what the wiki contains leaves too — even if the user never sees it, because the chunks travel in the prompt even when the answer doesn't cite them.
- The history accumulates. With the 10-turn truncation from 03-03, a piece of personal data mentioned in turn 2 travels again on every call until it falls out of the window. Data sent once is sent many times.
- Tools open a third door.
query_projectpulls data out of Nubelia's internal API and sends it toward the provider without anyone having typed it in the chat.
The basic discipline is being able to answer, at any moment, the question: "if I print the full prompt of this call, what's in there that shouldn't leave Nubelia?". A healthy exercise is literally that — dump the final prompt to the console in a development environment and read it with an auditor's eyes.
The Data Lifecycle at the Provider
Data "leaving" is not a binary event; what happens afterwards matters. The questions whose answers live in your provider's terms of service and agreements (read them, don't assume them — they differ between providers, between plans of the same provider, and over time):
- Retention: how long does the provider keep prompts and responses? Is there a zero- or reduced-retention option? Is retention different for requests flagged by their abuse filters?
- Use for training: does the provider use your data to train models? On paid, business-oriented APIs the norm is that they don't by default, unlike some free consumer products — but that's a clause you verify in the contract, not a rumor you repeat.
- Sub-processors and location: in which region is the data processed and stored? Which third parties are involved?
- Human access: under what circumstances can the provider's staff read your prompts (support, abuse review)?
Two engineering habits that follow:
- Document the answers to these questions for the provider and plan you use, with a date. It's the kind of evidence legal and compliance will ask you for, and the basis for re-evaluating if you switch providers (the 01-03 landscape).
- Treat your own logs as part of the lifecycle. There's no point in the provider retaining for 30 days if you keep the full prompts indefinitely in your traces. This thread — personal data in logs — is picked up again in 06-04.
Data Minimization: DocuBot's Redaction Function
The most cost-effective principle of the whole lesson is minimization: don't send what the task doesn't need. Guiding question with a concrete example: when a support employee asks DocuBot for help drafting a ticket resolution, does the LLM need to know the customer's name? Almost never. "The customer can't export the report" produces exactly the same answer as the version with name, email and phone number — and exposes no one.
When the data comes embedded in free text (the user pastes the whole ticket into the chat), minimization is implemented with redaction or pseudonymization before sending. A pedagogical version using regular expressions:
# redaction.py
import re
# Each pattern: (name, compiled regex, replacement marker)
PII_PATTERNS = [
("email", re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+"), "[EMAIL]"),
("phone", re.compile(r"(?:\+?\d{1,3}[ .-]?)?(?:\d[ .-]?){9,12}"), "[PHONE]"),
("iban", re.compile(r"\b[A-Z]{2}\d{2}(?:[ ]?[\dA-Z]{4}){3,7}\b"), "[IBAN]"),
("card", re.compile(r"\b(?:\d[ -]?){13,19}\b"), "[CARD]"),
]
def redact(text: str) -> tuple[str, dict[str, int]]:
"""Replaces personal data patterns with markers.
Returns the redacted text and a count per type (useful for metrics)."""
counts = {}
for name, pattern, marker in PII_PATTERNS:
text, n = pattern.subn(marker, text)
if n:
counts[name] = n
return text, countsAnd its usage, with fictional, generic data:
user_message = (
"The customer (contact: [email protected], "
"phone 600 000 000) can't export the project's weekly report."
)
clean_text, counts = redact(user_message)
print(clean_text)
# The customer (contact: [EMAIL], phone [PHONE]) can't export
# the project's weekly report.
print(counts) # {'email': 1, 'phone': 1}Where does it hook in? At the boundary, before anything travels: in docubot.py::answer (and at the entry of agent_loop), as the first step over the user's message; and optionally over the tool_result if the internal API returns sensitive fields. The counts value isn't decorative: logged as a metric (without the content), in 06-04 it will tell you how much personal data tries to sneak in through the chat every week.
Nuances a professional should know:
- Redaction ≠ pseudonymization. Redacting replaces with a generic marker (
[EMAIL]) and is irreversible. Pseudonymizing replaces with a consistent identifier ([CUSTOMER_7]) while keeping a mapping table that never travels; it lets the LLM reason about "the same customer" across turns and lets you re-personalize the answer when you receive it. More useful, more delicate (the table is itself data to protect). - Regexes have a ceiling. They catch formats (emails, phone numbers, IBANs) but not proper names or contextual data ("the CFO of the company in Zaragoza who called us yesterday"). That's what NER systems (named entity recognition) and specialized PII-detection libraries are for; the architecture is the same — a filter at the boundary — with a better detector.
- Minimization also applies to RAG: the best redaction is the one that isn't needed. If Nubelia's wiki shouldn't contain customer data, the moment to check is the 04-03 ingestion — the same hook where 06-01 placed the suspicious-pattern detector can run
redact()or queue for review.
Contractual Guarantees and Alternative Architectures
Minimization reduces what leaves; guarantees govern what leaves anyway. From less to more control:
| Option | What it provides | Cost/trade-off |
|---|---|---|
| Standard API + DPA (Data Processing Agreement) | Contractual framework: the provider acts as processor, with security and confidentiality obligations and declared sub-processors | The bare minimum if personal data travels; check that it's signed, not just published |
| Data residency / EU region | Processing and/or storage happens in the chosen region; simplifies the international-transfer analysis | Not all providers/models offer it; sometimes with added latency or a reduced catalog |
| Enterprise plans / via cloud (the big cloud providers offer the same models under their own contractual frameworks) | Configurable retention, strengthened commitments, integration with the cloud contractual umbrella the company has already signed | Cost and procurement complexity |
| Self-hosting open models (the 01-03 landscape) | Data never leaves your infrastructure: this lesson's conversation almost disappears | GPU and operations cost, generally less capable models, and security becomes 100% your problem |
The decision isn't global but per data flow: DocuBot can use the commercial API for general questions about public documentation and require the EU region (or aggressive redaction) for the flow that touches tickets with customer data. The course's modular architecture (the client encapsulated in client.py, with client and MODEL as single points of configuration) is what makes that decision cheap to implement.
GDPR at a High Level: What a Developer Should Know
The GDPR (General Data Protection Regulation) applies whenever personal data is processed: any information about an identified or identifiable natural person. Important note: corporate names and emails ("[email protected]") are also personal data — the GDPR does not distinguish between "business data" and "consumer data". The generally accepted points a DocuBot developer should keep on the radar:
- Roles: Nubelia decides the purposes and means of the processing → it's the controller; the LLM provider processes the data on Nubelia's behalf → typically a processor (hence the DPA from the previous section).
- Legal basis: all processing needs a legal basis (consent, performance of a contract, legitimate interest...). Which one applies to a specific internal assistant is a decision for the DPO/legal, not the developer — your job is to be able to explain precisely what data is processed, for what purpose and where it flows. The data-flow diagrams you're building in this lesson are exactly the input legal needs.
- Minimization and purpose limitation are literal principles of the regulation — the redaction section wasn't just good engineering practice.
- Data subject rights (access, rectification, erasure...): if prompts containing personal data end up in your logs and traces, those logs fall within the scope of a rights request ("delete what you have about me"). Designing logs with pseudonymization and defined retention (06-04) is what makes those rights exercisable without archaeology.
- International transfers: sending personal data to servers outside the EEA requires adequate safeguards (hence the practical value of EU residency).
- Impact assessments (DPIA): for high-risk processing, a formal prior assessment may be mandatory. If your assistant is going to process special categories of data (health, for example), stop and consult before writing code.
We insist: this is a map for talking with the experts, not the whole conversation.
The EU AI Act and DocuBot's Transparency
The AI Act (the European Union's Artificial Intelligence Regulation) regulates AI systems with a risk-based approach: obligations proportional to the system's category.
| Category | General idea | Typical examples cited by the regulation |
|---|---|---|
| Unacceptable risk | Prohibited | Harmful subliminal manipulation, generalized social scoring |
| High risk | Strict requirements (risk management, documentation, human oversight, logging) | AI in hiring, credit, critical infrastructure, certain uses in education/healthcare/justice |
| Limited risk | Transparency obligations | Chatbots and conversational assistants |
| Minimal risk | No specific obligations | Spam filters, AI in video games |
An internal documentation assistant like DocuBot fits in principle in the territory of transparency obligations — but the specific classification depends on the case and the actual use, and deciding it is up to legal/compliance, not this lesson: the same engine, applied to making decisions about people (hiring, evaluations), would change boxes. In addition, the providers of general-purpose models have their own obligations under the regulation, distinct from yours as an integrator.
What is actionable for DocuBot right now, and cheap:
- The user must know they're talking to an AI. No dressing DocuBot up as a human colleague: the interface identifies it as an automated assistant.
- Honesty about limits: the course's anti-hallucination phrase — "I can't find that information in the available documentation." — and the
confidencefield of the JSON contract are, besides quality, transparency: the system communicates its uncertainty instead of feigning omniscience. - Human oversight wherever there's action: the human-in-the-loop of
create_with_confirmation()(05-02) is exactly the oversight pattern the regulation favors. Good engineering practice and regulatory expectations point in the same direction — that's no coincidence.
# In DocuBot's interface: transparency as a requirement, not an ornament
WELCOME_MESSAGE = (
"Hi, I'm DocuBot, Nubelia's automated documentation assistant. "
"I'm an AI system: I can make mistakes and my answers may require "
"verification. I always cite the sources I rely on."
)Data Decisions for DocuBot: The Travels / Doesn't Travel Table
The operational synthesis of the lesson is a flow inventory with an explicit decision per data item. This table (adapted to your case) is also an excellent deliverable for the conversation with the DPO:
| Data | Travels to the provider? | How | Rationale |
|---|---|---|---|
| User's question | Yes, redacted | redact() at the boundary of answer |
Needed for the task; the embedded personal data is not |
| History (10 turns) | Yes, redacted at the source | Redacted on entering Conversation, so it never travels again uncleaned |
The history re-sends every data item many times |
| System prompt | Yes, as is | prompts.py |
No personal data by design; reviewed on every PROMPT_VERSION change |
| Wiki chunks | Yes, as is | rag.py |
The wiki must not contain customer data; verified at ingestion (04-03) |
| Name/email of the employee asking | No | The session is identified with a pseudonym (SessionManager); the real identity never enters the prompt |
The LLM doesn't need to know who is asking |
| Customer contact details in tickets | No (the redacted text travels) | redact() + pseudonymization if consistency is needed |
The LLM doesn't need the name to draft the resolution |
Results from query_project |
Yes, filtered | The tool returns only the necessary fields (status, milestones), not the full record | Minimization applied to the tool's design |
| API keys, secrets | Never | Environment variables, never in prompts or code | No exceptions |
| Your own logs and traces | Don't travel to the provider, but they are processing | Pseudonymized and with defined retention | Developed further in 06-04 |
Notice the pattern in the "No" rows: the solution is almost never a heroic last-minute filter, but designing the flow so the data never gets near the prompt (the tool returns fewer fields; the session uses pseudonyms). The best redaction is the one with nothing to redact.
Common Mistakes and Tips
- Believing that "only the user's question leaves". The full prompt leaves: system, history, RAG chunks, tool definitions and results. Do the exercise of dumping a real (development) prompt and reading it in full.
- Assuming the provider's terms instead of reading them. "Surely they don't train on my data" is not a privacy policy. Read them for your specific plan, document the answer with a date, and review it when you change providers or plans.
- Redacting the input and forgetting the other doors. The history (which re-sends old data), the
tool_resultand the RAG chunks are entry paths just as real as the user's message. Redact at the source, not just at the front door. - Confusing redaction with anonymization. A text with
[EMAIL]can still identify a person through context ("the director of the Cuenca office"). Pattern-based redaction reduces the risk; it doesn't eliminate it, and it rarely makes the data anonymous in the strict sense (which is why the prudent term is pseudonymization). - Treating compliance as a final sprint. Minimization affects the design of tools, sessions and logs; retrofitting it is far more expensive than designing it in. And conversely: don't let regulatory paralysis block everything — most of this lesson (minimize, redact, DPA, transparency) is industry-accepted table stakes.
- Playing lawyer. Your deliverable as a developer is a minimizing system and a precise map of data flows; the legal basis, the classification under the AI Act and the DPIA are legal/DPO decisions. Show up to that meeting with the travels/doesn't-travel table done and you'll be their favorite developer.
Exercises
Exercise 1: Auditing a Prompt
A support employee has this conversation with DocuBot (fictional data):
Turn 1: "The customer Example Ltd. (fictional tax ID, contact: [email protected]) says we charged them twice for the March fee." Turn 2: "Can you tell me how a refund is processed?"
In turn 2, DocuBot uses RAG (retrieves 3 chunks from the billing wiki) and answers. List everything that travels to the provider in the turn 2 call, point out which elements contain data that minimization should have handled, and explain why the turn 1 problem is still present in turn 2.
Exercise 2: Extending Redaction with Consistent Pseudonymization
Modify redaction.py so that emails are not all replaced with [EMAIL], but with consistent pseudonyms ([EMAIL_1], [EMAIL_2]...) within the same text: the same email always gets the same marker, different emails get different markers. Also return the mapping table (which would never travel to the provider).
Exercise 3: Classifying Data Flows
For each new data item, decide (as in the travels/doesn't-travel table) whether it should travel, not travel, or travel redacted, and justify it: (a) the internal project name ("Atlas") in a question about its status; (b) an employee's salary mentioned in a ticket of the billing category; (c) the session identifier that SessionManager uses to group the conversation.
Solutions
Exercise 1:
Travels in turn 2: (1) the full system prompt (SYSTEM_DOCUBOT_TOOLS or whichever applies); (2) the history: turn 1 in its entirety — with the customer company name, tax ID and email — plus the assistant's previous answer, because Conversation re-sends the last 10 turns; (3) the turn 2 question; (4) the 3 chunks from the billing wiki; (5) if operating as an agent, the tool definitions.
What should have been handled: the customer data from turn 1 (name, tax ID, email → redaction/pseudonymization). The key point is the third one: even though turn 2 is innocent, turn 1 travels again inside the history on every call until it falls out of the 10-turn window. That's why redaction must be applied when the message enters the conversation (at the source), not just to the last message: data that entered unredacted contaminates all subsequent calls.
Exercise 2:
import re
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
def pseudonymize_emails(text: str) -> tuple[str, dict[str, str]]:
"""Replaces each email with a consistent [EMAIL_n] marker.
Returns the text and the mapping table (private: it does NOT travel)."""
table: dict[str, str] = {}
def _replacement(match: re.Match) -> str:
email = match.group(0)
if email not in table:
table[email] = f"[EMAIL_{len(table) + 1}]"
return table[email]
return EMAIL_RE.sub(_replacement, text), table
text = ("[email protected] and [email protected] wrote in; "
"reply to [email protected] and copy the second one.")
clean, table = pseudonymize_emails(text)
print(clean)
# [EMAIL_1] and [EMAIL_2] wrote in; reply to [EMAIL_1] and copy the second one.How it works: re.sub accepts a replacement function; the function looks up/fills in table, so the same email always gets the same marker. This way the LLM can reason "reply to [EMAIL_1]" coherently, and your code can re-personalize the answer by inverting the table — which stays in your infrastructure, because it is itself personal data to protect.
Exercise 3:
- (a) Travels as is. "Atlas" is an internal project identifier, not personal data; without it, neither the RAG nor
query_projectcan work. Residual risk: business confidentiality (not privacy), covered by the DPA. - (b) Doesn't travel. It's personal data of a sensitive category in practice (economic information about an identifiable person) and the LLM doesn't need it to process anything: it gets redacted (
[AMOUNT]or removal of the passage) before sending, and its presence in a ticket should also trigger a process review (why are there salaries in support tickets?). - (c) Doesn't travel (and doesn't need to). The session identifier is plumbing on your side:
SessionManageruses it to retrieve the history, but the prompt has no reason to contain it. If one day correlation were needed (e.g. to debug with the provider), a pseudonymous, opaque identifier would travel, never one revealing the employee's identity — and that correlation reappears, done properly, in the 06-04 logs.
Conclusion
DocuBot now has both sides of the helmet: 06-01 protects it from those who want to manipulate it; this lesson protects everyone else from what DocuBot could tell. The ideas that should stay with you: on every call the full prompt travels — system, history that re-sends the past, RAG chunks, tool data — not "the question"; the data lifecycle at the provider (retention, training, region) is read in the terms and documented, not assumed; minimization is the developer's lever — the redact() function at the boundary, tools that return only the necessary fields, pseudonymous sessions — and the best redaction is that of the data that never got near the prompt; DPAs, EU residency and self-hosting are the ladder of guarantees when the data travels anyway; and the regulatory framework (GDPR and the AI Act, without playing lawyers) rewards exactly what the course already practiced: transparency that one is talking to an AI, honesty about uncertainty, and human oversight over actions. One uncomfortable question remains that neither security nor privacy answers: this is all very nice, but does DocuBot work? How do you know yesterday's prompt change didn't break twenty answers that used to come out right? In 02-04 and 04-05 we evaluated by hand and promised to automate it; the next lesson settles that debt: unit tests for the deterministic code, contract tests for the model's outputs, the runner that executes the complete set — adversarials included — and an LLM acting as a judge calibrated against your gold standard.
Generative AI and LLMs for Developers Course
Module 1: Generative AI Fundamentals
- What generative AI is and why it matters to developers
- How an LLM works: tokens, embeddings and attention
- The model and provider landscape
- Limitations and risks: hallucinations, context and costs
Module 2: Prompt Engineering
- Anatomy of a prompt: roles, instructions and context
- Prompting techniques: few-shot, chain of thought and templates
- Structured outputs: JSON and format control
- Prompt iteration and evaluation
Module 3: Integrating LLMs into Applications
- First integration with an LLM API
- Streaming, errors and retries
- Conversations and context management
- Costs, latency and caching
Module 4: RAG - Retrieval-Augmented Generation
- Embeddings and semantic search
- Vector databases
- Document ingestion and chunking
- Building a complete RAG pipeline
- Evaluating RAG systems
Module 5: Function Calling and Agents
- Function calling: connecting the LLM to your code
- From LLM to agent: the reasoning and action loop
- Orchestration frameworks
