Subzero Labs

Free code · Subzero Labs

Google review reply drafts for local businesses

From the Subzero Labs video "Your AI Has Amnesia. This Free Code Makes It Remember".

Free, no sign-up. Copy it, use it, share it.

For: Local service businesses

The job: Every review deserves a reply, and the 1-star ones are the hardest to write without making it worse.

You give it: new reviews pasted into one text file (name | stars | date, the text, then ---) and your business notes in Markdown.

It is one agent of the free AI OS starter (https://subzero.bodymindbeing.me/free/ai-os-starter/) - download the whole starter, it runs on sample data out of the box. Nothing is sent anywhere: replies come out as drafts.

Download the AI OS starter (zip) 65 KB

MIT licence. Python 3.10+, no other installs. Unzip, then follow README.md.

Run this one

  1. Set up the starter (the steps on the starter page).
  2. python -m aios run review_replies
  3. Edit config.json: point "review_replies" at your own files (paths relative to config.json).

Check it yourself

  1. Each draft must use the reviewer's name, be 20-90 words and never promise a refund, discount or anything free in public.
  2. Every 1-3 star reply asks the customer to call or email; a draft that breaks a rule is retried once, then marked CHECK.
  3. The rules are review_replies.check() - read it, change it.

agents/review_replies.py

"""LOCAL SERVICE BUSINESSES - review reply drafts. Paste your new Google reviews into one text file (name | stars | date,
the text, then a line of ---). The AI reads your business notes from memory first (policies, tone, how to sign) and
drafts a reply for each review. Plain code then checks every draft: it uses the reviewer's name, stays short
(20-90 words), never promises a refund, discount or anything free in public, and for 1-3 stars asks the customer to
call or email - so the problem moves out of the public page. Low ratings go to your attention list.
Nothing is posted: drafts are in data/drafts/reviews/ for you to paste."""
import re

from aios import llm, memory
from aios.config import path

from ._common import blocks, save_draft

TITLE = "Review reply drafts"
PROMISES = re.compile(r"\b(refund|money back|discount|free of charge|for free|compensat|reimburs|voucher|credit)\w*", re.I)
CONTACT = re.compile(r"\b(call|phone|email|e-mail|reach out|contact|office)\b", re.I)


def parse(block):
    head, _, text = block.partition("\n")
    parts = [p.strip() for p in head.split("|")]
    try:
        stars = int(parts[1])
    except (IndexError, ValueError):
        stars = 0
    return {"name": parts[0], "stars": stars, "date": parts[2] if len(parts) > 2 else "", "text": text.strip()}


def check(reply, review):
    """-> list of problems (empty = the draft passes)."""
    out, words = [], len(reply.split())
    first = review["name"].split()[0] if review["name"] else ""
    if first and first.lower() not in reply.lower():
        out.append("does not use the reviewer's name")
    if not 20 <= words <= 90:
        out.append(f"{words} words (keep it 20-90)")
    if PROMISES.search(reply):
        out.append(f"promises '{PROMISES.search(reply).group(0)}' in public")
    if review["stars"] <= 3 and not CONTACT.search(reply):
        out.append("low rating but no 'call or email us'")
    return out


def run(cfg, data, s):
    memory.sync_folder(data, s["business_project"], path(cfg, s["business_notes"]))
    notes = memory.notes_text(data, s["business_project"])
    items, attention = [], []
    reviews = [parse(b) for b in blocks(cfg, s["reviews"])]
    for n, rv in enumerate(reviews, 1):
        reply = ""
        for attempt in range(2):  # one retry with the problems named
            fix = f"\nYour last draft had these problems, fix them: {'; '.join(problems)}" if attempt else ""
            reply = llm.ask(f"OUR BUSINESS NOTES:\n{notes}\n\nNEW GOOGLE REVIEW ({rv['stars']} stars) from {rv['name']}:\n"
                            f"{rv['text']}\n\nWrite our public reply. Follow our notes exactly. Use their first name, "
                            "20-90 words, no refund/discount/free offers in public; for 1-3 stars thank them, say what "
                            f"we changed if our notes say so, and invite them to call or email.{fix}\nJust the reply text.")
            problems = check(reply, rv)
            if not problems:
                break
        f = save_draft(data, "reviews", n, f"{rv['name']} ({rv['stars']} stars)", f"Reply to {rv['name']}", reply)
        status = "ok" if not problems else "CHECK: " + "; ".join(problems)
        items.append(f"{rv['stars']}* {rv['name']}: draft {f.name} - {status}")
        if rv["stars"] <= 3:
            attention.append(f"{rv['stars']}-star review from {rv['name']}: \"{rv['text'][:70]}...\" - reply draft ready")
    low = sum(1 for r in reviews if r["stars"] <= 3)
    avg = sum(r["stars"] for r in reviews) / len(reviews) if reviews else 0
    return {"summary": f"{len(reviews)} new reviews (average {avg:.1f} stars, {low} at 3 stars or less). "
                       "Reply drafts saved - nothing posted.", "items": items, "attention": attention}

Shared as shown in the video. Replace any <PLACEHOLDERS> with your own values.