Project: A human-in-the-loop reply assistant
Parse a chat transcript, redact sensitive data, draft replies through a replaceable model adapter, and require human approval before anything is sent.
By the end, you’ll be able to…
- Define and parse a small transcript format.
- Redact personal and secret data before sending text to a model.
- Hide the model behind an interface with timeouts and a fake test double.
- Keep a human approval step and an audit trail.
01Define the transcript contract
A useful assistant starts with a predictable transcript format. For example, each line contains a time, a speaker, and a message: [09:14] Ada: Can we move the review? Parse the line into a small record rather than passing raw text around. Reject malformed lines with a clear message so the user can fix the export.
Keep the parser independent of any model. The parser decides what a message is; the model only drafts a response. That separation lets you test the parser with fixtures and swap providers without changing the input rules.
02Redact before you send
A chat transcript can contain email addresses, phone numbers, tokens, and names that should not leave the user's machine. Redaction is a policy, not a magic filter: decide which patterns to replace and test them. Replace with stable placeholders such as [EMAIL] so the model can still understand the sentence.
Do not rely on the model to ignore secrets. Remove them before the request is built, and keep the original transcript local unless the user explicitly approves sending it. Log the redaction count, not the removed values.
03Put the model behind an interface
Define a ReplyGenerator protocol with one method: generate(transcript, instruction) returns text. A real adapter can call an HTTP API; a fake adapter can return a deterministic draft. The assistant should not know which one it is using.
Set a timeout, handle rate limits, and avoid retrying a request that may already have succeeded unless the operation is idempotent. Treat model output as untrusted text: never execute it, never treat it as a shell command, and never send it without review.
04Keep a human in the loop
A draft is not a sent message. Show the generated text, let the user edit or reject it, and require an explicit approval before sending. Record who approved it, when, and which draft version was sent. This audit trail is more valuable than an automatic reply that cannot be explained later.
The safest default is no automatic sending at all. If you add an auto-send mode, scope it to a test channel, require an allowlist of recipients, and cap the number of messages. A reply assistant should reduce effort, not remove responsibility.
The ideas worth keeping.
- Parse and validate the transcript before any model call.
- Redact personal data before it leaves the local boundary.
- Draft, review, approve, and log; never auto-send by default.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Parse a transcript
Write parse_transcript(text) that returns a list of Message records. Raise ValueError with the line number for malformed input. Demonstrate it on two valid lines.
Explore a worked solution
from dataclasses import dataclass@dataclass(frozen=True)class Message: time: str speaker: str text: strdef parse_transcript(text): messages = [] for number, line in enumerate(text.splitlines(), start=1): if not line.strip(): continue try: stamp, rest = line.split("] ", 1) speaker, message = rest.split(": ", 1) except ValueError as exc: raise ValueError(f"malformed line {number}") from exc messages.append(Message(stamp.removeprefix("["), speaker, message)) return messagestranscript = "[09:14] Ada: Review?\n[09:15] Sam: After lunch."print(parse_transcript(transcript))Skipping blank lines makes exported chat files easier to handle. The line number in the error points to the original input, which is more useful than an error from the middle of a split.
Redact secrets before drafting
Write redact(text) that replaces email addresses and long digit sequences with [EMAIL] and [NUMBER]. Test it on a sentence containing both.
Explore a worked solution
import reEMAIL = re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.]+\b")NUMBER = re.compile(r"\b\d{6,}\b")def redact(text): return NUMBER.sub("[NUMBER]", EMAIL.sub("[EMAIL]", text))print(redact("Write to ada@example.com or use token 123456789"))The output is 'Write to [EMAIL] or use token [NUMBER]'. Replacing the email first prevents the number pattern from touching digits inside the address.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Parse a transcript
Write parse_transcript(text) that returns a list of Message records. Raise ValueError with the line number for malformed input. Demonstrate it on two valid lines.
parse_transcript(transcript)
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.