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)
MIT licence. Python 3.10+, no other installs. Unzip, then follow README.md.
Run this one
- Set up the starter (the steps on the starter page).
- python -m aios run review_replies
- Edit config.json: point "review_replies" at your own files (paths relative to config.json).
Check it yourself
- Each draft must use the reviewer's name, be 20-90 words and never promise a refund, discount or anything free in public.
- Every 1-3 star reply asks the customer to call or email; a draft that breaks a rule is retried once, then marked CHECK.
- The rules are review_replies.check() - read it, change it.
"""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.