Index Ai Academy

Build an AI ad-copy reviewer in Python

A small Python tool that checks ad headlines against your rules, saves a JSON report, and asks a language model to rewrite the worst one.

What you'll build

Most useful AI tools are mostly ordinary code with one model call in the right place. This one reads a CSV of ad headlines, checks each against rules you set, and writes a report. Only the part that needs judgment, rewriting a bad headline, goes to a language model.

What you'll learn

  • 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 you need

Basic Python: functions, lists and dictionaries. In the lesson everything runs in your browser. On your own machine you need pip install requests and an Anthropic API key in ANTHROPIC_API_KEY, and API calls cost money.

Step 1: Set up the data and the rules

Start with the imports, five sample headlines and two rules: a length limit and a list of banned phrases. Writing the CSV from the script keeps the example self-contained. In real use you'd export it from your ad platform. With the rules as constants at the top, changing a rule never means touching the logic.

import csv
import json
import os
import requests

ADS = """headline,channel
Spring Sale: 30% Off Everything,search
Click Here Now!!! Best Deals Guaranteed,display
Organic Cotton Tees Made to Last Through Years of Weekend Wear,social
Free Shipping This Weekend,email
Results Guaranteed or Your Money Back,search
"""
with open("ads.csv", "w") as f:
    f.write(ADS)

MAX_LENGTH = 40
BANNED = ["click here", "guaranteed", "act now", "!!!"]

Step 2: Write a helper that calls the model

ask sends one message to the Messages API and returns the reply's text and why it stopped. raise_for_status turns a failed request into an error you can see, rather than a confusing failure later on. The key comes from an environment variable, so it never ends up in your code or your git history.

# Your helper from the last lesson
def ask(prompt, system=None, max_tokens=1000):
    body = {"model": "claude-opus-5-5", "max_tokens": max_tokens,
            "messages": [{"role": "user", "content": prompt}]}
    if system:
        body["system"] = system
    response = requests.post(
        "https://api.anthropic.com/v1/messages",
        headers={"x-api-key": os.environ.get("ANTHROPIC_API_KEY", "added-by-the-academy-server"),
                 "anthropic-version": "2026-10-21", "content-type": "application/json"},
        json=body, timeout=60)
    response.raise_for_status()
    data = response.json()
    text = ""
    for block in data["content"]:
        if block["type"] == "text":
            text += block["text"]
    return {"text": text, "stop_reason": data["stop_reason"]}

Rather write it yourself?

In the lesson you write this from a starter, in your browser with nothing to install, and checks tell you what's wrong as you go. It needs no account.

Open the lesson

Step 3: Load the headlines and check each one

A rule that code can check should be code: it's instant, free and gives the same answer every time. review returns a list of problems rather than True or False, so the report can say what's wrong. An empty list means a clean headline.

def load_headlines(path):
    with open(path) as f:
        return list(csv.DictReader(f))


def review(headline):
    problems = [f"banned phrase: {p}" for p in BANNED if p in headline.lower()]
    if len(headline) > MAX_LENGTH:
        problems.append(f"too long ({len(headline)} characters)")
    return problems

Step 4: Build the report and save it

Each row of the report keeps the headline, its channel, its problems and a simple ok flag. Saving it as JSON means other tools can read it, such as a dashboard or a nightly job.

report = []
for row in load_headlines("ads.csv"):
    problems = review(row["headline"])
    report.append({
        "headline": row["headline"],
        "channel": row["channel"],
        "problems": problems,
        "ok": len(problems) == 0,
    })

with open("report.json", "w") as f:
    json.dump(report, f, indent=2)

ok_count = len([r for r in report if r["ok"]])

Step 5: Ask the model to fix the worst headline

Rewriting takes judgment, so this is the one place the program calls the model. The prompt carries the headline, its problems and the rules, and asks for the new headline only, so there's nothing to clean up afterwards.

worst_row = report[0]
for r in report:
    if len(r["problems"]) > len(worst_row["problems"]):
        worst_row = r
worst = worst_row["headline"]

prompt = f"""Rewrite this ad headline.
Headline: {worst}
Problems found: {", ".join(worst_row["problems"])}
Rules: at most {MAX_LENGTH} characters, and none of these phrases: {", ".join(BANNED)}.
Reply with the new headline only."""

suggestion = ask(prompt)["text"].strip()
print(ok_count, "of", len(report), "headlines passed")
print("Worst:", worst)
print("Suggestion:", suggestion)
What it prints
2 of 5 headlines passed
Worst: Click Here Now!!! Best Deals Guaranteed

It also prints a Suggestion: line with the model's rewrite, which changes from run to run.

Where to take it next

  • Run each suggestion back through review, and ask again if it fails. Never trust a model's output just because you asked nicely.
  • Give each channel its own length limit.
  • Read your real export instead of the sample CSV, and write the report somewhere your team will see it.

Mistakes to watch for

  • Asking the LLM to do checks that plain code does exactly and for free.
  • Trusting the model's rewrite without checking it against the same rules.
  • Forgetting .lower(), so the banned-phrase check misses capitalized phrases.
  • Storing the whole row in worst instead of the headline text, or using >= so ties pick the later row.
  • Not asking for the answer only, then getting an introduction or several options back.
The complete program
import csv
import json
import os
import requests

ADS = """headline,channel
Spring Sale: 30% Off Everything,search
Click Here Now!!! Best Deals Guaranteed,display
Organic Cotton Tees Made to Last Through Years of Weekend Wear,social
Free Shipping This Weekend,email
Results Guaranteed or Your Money Back,search
"""
with open("ads.csv", "w") as f:
    f.write(ADS)

MAX_LENGTH = 40
BANNED = ["click here", "guaranteed", "act now", "!!!"]

# Your helper from the last lesson
def ask(prompt, system=None, max_tokens=1000):
    body = {"model": "claude-opus-5-5", "max_tokens": max_tokens,
            "messages": [{"role": "user", "content": prompt}]}
    if system:
        body["system"] = system
    response = requests.post(
        "https://api.anthropic.com/v1/messages",
        headers={"x-api-key": os.environ.get("ANTHROPIC_API_KEY", "added-by-the-academy-server"),
                 "anthropic-version": "2026-10-21", "content-type": "application/json"},
        json=body, timeout=60)
    response.raise_for_status()
    data = response.json()
    text = ""
    for block in data["content"]:
        if block["type"] == "text":
            text += block["text"]
    return {"text": text, "stop_reason": data["stop_reason"]}


def load_headlines(path):
    with open(path) as f:
        return list(csv.DictReader(f))


def review(headline):
    problems = [f"banned phrase: {p}" for p in BANNED if p in headline.lower()]
    if len(headline) > MAX_LENGTH:
        problems.append(f"too long ({len(headline)} characters)")
    return problems


report = []
for row in load_headlines("ads.csv"):
    problems = review(row["headline"])
    report.append({
        "headline": row["headline"],
        "channel": row["channel"],
        "problems": problems,
        "ok": len(problems) == 0,
    })

with open("report.json", "w") as f:
    json.dump(report, f, indent=2)

ok_count = len([r for r in report if r["ok"]])

worst_row = report[0]
for r in report:
    if len(r["problems"]) > len(worst_row["problems"]):
        worst_row = r
worst = worst_row["headline"]

prompt = f"""Rewrite this ad headline.
Headline: {worst}
Problems found: {", ".join(worst_row["problems"])}
Rules: at most {MAX_LENGTH} characters, and none of these phrases: {", ".join(BANNED)}.
Reply with the new headline only."""

suggestion = ask(prompt)["text"].strip()
print(ok_count, "of", len(report), "headlines passed")
print("Worst:", worst)
print("Suggestion:", suggestion)

Write it yourself

In the lesson you write this from a starter, in your browser with nothing to install, and checks tell you what's wrong as you go. It needs no account.

Open the lesson