Capstone: ad-copy reviewer
Combine files, functions, JSON and an LLM call into one small tool.
Why it matters
This is the shape of most useful AI tools. Plain code does everything that can be checked with rules, cheaply and the same way every time. A model handles the one step that needs judgment or writing. Agents, evals and content pipelines in Phase 1 are bigger versions of what you build here. And you'll check a model's output with your own code constantly.
What you'll be able to do
- Break a real task into load, check, report and generate steps
- Write rule checks as small functions that return a list of problems
- Save results as a JSON report other tools can read
- Use one LLM call for the part rules can't do, and check what comes back
What it covers
- What you're building
- Step 1: load the data
- Step 3: a report other tools can read
- Worked example: steps 1 to 3 on subject lines
- What code should do, and what the model should do
What you're building
Before ads go live, someone checks the copy. Is each headline short enough for the placement? Does it avoid phrases that get ads rejected or sound spammy? You're going to automate that review for a CSV export of headlines. Then you'll ask a model to rewrite the worst one.
The program has four steps:
- Load the headlines from
ads.csvinto a list of dictionaries. - Check each headline against two rules: at most 40 characters, and none of the banned phrases
"click here","guaranteed","act now"or"!!!". - Report: save every headline with its problems to
report.json. - Generate: ask the model for a better version of the headline with the most problems.
Each step is a small piece you already know. The skill you're practicing is putting them together, so each one does one job and hands clean data to the next.
For marketers: this is the same split you'd use with a junior copywriter and a style guide. The checklist catches the mechanical problems every time. The person is there for the rewrite. Here the checklist is code, and the rewrite comes from a model.
Step 1: load the data
csv.DictReader from the files lesson turns each row into a dictionary, keyed by the header row. Here it is on a different, smaller file:
import csv with open("subjects.csv", "w") as f: f.write("""subject,audience Last chance: spring sale ends tonight,customers Your March report is ready,partners """) with open("subjects.csv") as f: rows = list(csv.DictReader(f)) print(len(rows)) print(rows[0]) print(rows[1]["subject"])
list(...) collects the rows while the file is still open. After the with block ends, the file is closed, but rows keeps the data.
Step 2: rules as functions that return problems
A good shape for a checker is a function that takes one item and returns a list of problems. An empty list means it passed. That's easy to test, easy to count, and easy to add new rules to later.
A comprehension builds that list in one line. Lowercasing the text first makes the check ignore capitals, so "FREE" and "free" are both caught:
SPAMMY = ["free", "winner", "$$$"] def check_subject(subject): problems = [f"spammy word: {word}" for word in SPAMMY if word in subject.lower()] if len(subject) > 50: problems.append(f"too long ({len(subject)} characters)") return problems print(check_subject("Your March report is ready")) print(check_subject("FREE gift for our WINNER")) print(len(check_subject("FREE gift for our WINNER")))
Step through and watch problems fill up for the second subject, then become one entry in results.
BANNED = ["guaranteed", "act now"] headline = "Act Now: Results Guaranteed" [p for p in BANNED if p in headline.lower()]
Show the answer
['guaranteed', 'act now']
.lower() makes the headline "act now: results guaranteed", which contains both phrases. The comprehension goes through BANNED in order, so "guaranteed" comes first: ['guaranteed', 'act now'].
check_subject returns [] for one subject line. What does that mean?Show the answer
It passed: no rule found a problem
A checker that returns a list of problems uses an empty list to mean "all clear". len(problems) then counts how bad an item is.
Step 3: a report other tools can read
Printing results is fine while you're working. But a file is what another script, a dashboard or a teammate can use. JSON is the natural format. It holds lists and dictionaries exactly as they are in Python, and every language and AI tool reads it.
import json results = [ {"subject": "Your March report is ready", "problems": [], "ok": True}, {"subject": "FREE gift for our WINNER", "problems": ["spammy word: free", "spammy word: winner"], "ok": False}, ] with open("subject_report.json", "w") as f: json.dump(results, f, indent=2) with open("subject_report.json") as f: print(f.read())
json.dump (no "s") writes straight to an open file, and indent=2 makes it readable.
Step 4: one model call, used carefully
Finding the worst headline is a job for a loop that remembers the best candidate so far:
results = [
{"subject": "Report ready", "problems": []},
{"subject": "FREE gift for our WINNER", "problems": ["free", "winner"]},
{"subject": "Free shipping", "problems": ["free"]},
]
worst = results[0]
for r in results:
if len(r["problems"]) > len(worst["problems"]):
worst = r
print(worst["subject"])
Using > rather than >= means a tie keeps the earlier item.
Then the prompt. A useful prompt gives the model the same context you'd give a person: what the text is, what's wrong with it, the rules, and exactly what to return.
subject = "FREE gift for our WINNER" problems = ["spammy word: free", "spammy word: winner"] prompt = f"""Rewrite this email subject line. Subject: {subject} Problems found: {", ".join(problems)} Rules: at most 50 characters, and avoid the words free and winner. Reply with the new subject line only.""" print(prompt)
"Reply with the new subject line only" matters. Without it, models often add an introduction or several options, and your code would have to dig the answer out.
Finally, don't trust the output blindly. You already have a function that checks headlines, so run the suggestion through it. If it still has problems, you know before anyone else does. Checking model output with code is the seed of what Phase 1 calls evals.
review() function?Show the answer
The model can break the rules too, and your checks catch that automatically
Models follow instructions most of the time, not every time. Reusing your rule-based checks on generated text is cheap insurance.
Worked example: steps 1 to 3 on subject lines
The same load, check and report steps on a small file of email subject lines, ending with a count of how many passed for each audience.
import csv import json with open("subjects.csv", "w") as f: f.write("""subject,audience Last chance: spring sale ends tonight,customers FREE gift for every WINNER,customers Your March report is ready,partners You're a winner: free upgrade for partners who renew this week,partners """) SPAMMY = ["free", "winner", "$$$"] def check_subject(subject): problems = [f"spammy word: {word}" for word in SPAMMY if word in subject.lower()] if len(subject) > 50: problems.append(f"too long ({len(subject)} characters)") return problems with open("subjects.csv") as f: rows = list(csv.DictReader(f)) report = [{"subject": r["subject"], "audience": r["audience"], "problems": check_subject(r["subject"])} for r in rows] with open("subject_report.json", "w") as f: json.dump(report, f, indent=2) passed = {} for r in report: passed[r["audience"]] = passed.get(r["audience"], 0) + (1 if not r["problems"] else 0) for r in report: print(len(r["problems"]), r["subject"]) print(passed)
It prints the problem count for each subject line: 0, 2, 0 and 3. Then {'customers': 1, 'partners': 1}: one subject line passed for each audience.
The last partner line breaks all three rules. It mentions "winner" and "free", and it's 62 characters long. Each step hands clean data to the next: rows from the CSV, a report of dictionaries, then a count built from the report. Change a rule in check_subject, and the report and the counts update with it.
What code should do, and what the model should do
The most useful habit in this capstone is deciding who does what. Your headline checks, such as length, banned phrases and counting, run in plain Python. A model could do those too, but it would be slower, it would cost money on every call, and it would sometimes be wrong. Code gets them right every time, for free.
Save the model for what code can't do, such as rewriting a headline so it sounds better.
Then don't trust the rewrite. A model asked to fix a headline might produce one that's too long, or that uses a banned phrase in a new way. Run its answer back through the same rule functions you wrote in Step 2. If it fails, keep the original. That loop is a small version of how real AI products are built: the model suggests, and code checks.
Watch for two small bugs along the way. Forgetting .lower() means your banned-phrase check misses FREE and Free. And when you track the worst headline, compare with > and store the text you want to report, not the whole row. Test both with a made-up row that should fail. A check you've never seen fail may not be checking anything.
Practice what you just read
- 6 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
Why have review() return a list of problems instead of just True or False?