GEMINI LABJP
VIDEO — Agentic video understanding reached 3.7 Flash, 3.6 Flash, and 3.5 Flash-Lite on September 1. The model navigates the timeline itself rather than sampling frames at a fixed rateTOKENS — Because it pulls transcripts, frames, or audio only when it needs them, Google measures up to 88% fewer tokens on long-form contentSCOPE — It works across both the Interactions and GenerateContent APIs. If you have costed out long-video work before, the assumptions have movedMUSIC — Lyria 3.5 entered public preview on September 3, generating full-length songs at 44.1 kHz stereoCONTROL — Lyria 3.5 accepts text and image inputs, with better musical coherence, more natural vocals, and finer control over duration and structureROBOTICS — gemini-robotics-er-2-streaming-preview is tuned for real-time streaming over the Live API, with function calling that blocks on physical robot actionsVIDEO — Agentic video understanding reached 3.7 Flash, 3.6 Flash, and 3.5 Flash-Lite on September 1. The model navigates the timeline itself rather than sampling frames at a fixed rateTOKENS — Because it pulls transcripts, frames, or audio only when it needs them, Google measures up to 88% fewer tokens on long-form contentSCOPE — It works across both the Interactions and GenerateContent APIs. If you have costed out long-video work before, the assumptions have movedMUSIC — Lyria 3.5 entered public preview on September 3, generating full-length songs at 44.1 kHz stereoCONTROL — Lyria 3.5 accepts text and image inputs, with better musical coherence, more natural vocals, and finer control over duration and structureROBOTICS — gemini-robotics-er-2-streaming-preview is tuned for real-time streaming over the Live API, with function calling that blocks on physical robot actions
Articles/Dev Tools
Dev Tools/2026-08-20Beginner

A minimal pre-send guard for Gemini-drafted app review replies

Store replies are risky after you hit send, not before. Here is a short Python guard that blocks unkeepable promises, leaked contact details, and register slips, plus the two false positives it produced on the first run.

Gemini API234Indie development3App operationsStructured output2Python45

I once started writing a reply to a one-star review late at night.

"It crashes every time I open settings." I knew the bug. Before I noticed, I had typed "we will fix this in the next update," and I stopped with my cursor on the send button. Nothing at that moment guaranteed the fix would make the next build.

Gemini can draft that reply in seconds. The faster the drafting gets, the less recoverable the send becomes. Store replies are public, and a promise stays readable until the next version actually ships.

So before making the drafting smarter, I built the part that stops a reply from going out. The code is short and unglamorous. But when I ran it, the thing that turned out to be wrong was the guard, not the draft — and that part is worth showing.

Decide whether to reply before writing anything

If you answer reviews in the order they arrive, you run out of time for the ones that matter. So the sorting happens before any text is generated.

BucketConditionWhat happens next
priority2 stars or fewer, with something resembling repro stepsDraft it, and read the draft yourself
ack_only3 stars or fewer, no repro stepsSend a short acknowledgement
no_reply4 stars or moreDon't spend a reply here

Skipping happy reviewers may look cold. But the people whose opinion actually moves when they read a reply are the ones currently stuck. That is where the limited time goes.

import re
 
REPRO = r"(when I|after I|every time|crash|freeze|won't open|can't save|doesn't load)"
 
def triage(stars: int, review: str) -> str:
    repro = re.search(REPRO, review, re.IGNORECASE)
    if stars >= 4:
        return "no_reply"        # don't spend a reply here
    if stars <= 2 and repro:
        return "priority"        # repro steps present, handle first
    return "ack_only"            # acknowledge and move on

The only signals here are the star rating and symptom words. Emotional intensity is deliberately ignored. A calmly written report of exact steps usually carries more fixable information than an angry one.

Receive the draft as structure, not prose

Ask Gemini for "a reply" and you get one continuous paragraph. Feeding that into a guard means re-parsing which sentence is an apology and which one is a commitment, every single time.

Splitting it at the source makes everything downstream easier.

from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_API_KEY")
 
REPLY_SCHEMA = {
    "type": "object",
    "properties": {
        "acknowledgement": {"type": "string"},   # we heard you
        "status": {"type": "string"},            # only what is known today
        "next_step": {"type": "string"},         # what we ask of the reviewer
    },
    "required": ["acknowledgement", "status"],
}
 
def draft(review: str, mode: str) -> dict:
    prompt = (
        "Draft a reply to this app store review. Stay polite and concise, "
        "and never state a fix date that has not been committed.\n"
        f"Bucket: {mode}\nReview: {review}"
    )
    res = client.models.generate_content(
        model="gemini-3.7-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            response_mime_type="application/json",
            response_schema=REPLY_SCHEMA,
        ),
    )
    return res.parsed

Keeping status separate is the point. "We are investigating the cause" is a fact; "we will fix it" is a commitment. Blend them into one paragraph and you can no longer tell them apart afterwards. If the schema itself gives you trouble, common Structured Output validation errors and how to resolve them collects the usual failure shapes.

And telling the model not to promise a date does not mean it never will. That is why the guard exists.

Three checks before send

Only three things need to be stopped: promises you cannot keep (A), contact details leaking into a public reply (B), and tone or length problems (C).

MAX_CHARS = 350  # my own readability limit, separate from any store limit
 
PROMISE = [r"次の(アップデート|バージョン)で", r"必ず(修正|対応)", r"すぐに(修正|対応)",
           r"\d+\s*(|週間|)以内に", r"来週(まで)?に", r"今月中に", r"予定です"]
# Japanese has an explicit polite register. Confirm it first, then suspect the plain form.
POLITE_END = re.compile(r"(です|でした|ます|ました|ません|ましょう|ください|ございます)$")
PLAIN_END = re.compile(r"(|である|した|ない|いる|する||)$")
 
def mask(draft: str) -> str:
    """Blank out emails and URLs first, or the handle pattern fires twice on the same span"""
    d = re.sub(r"[\w.+-]+@[\w-]+\.[\w.]+", "\u3000", draft)
    return re.sub(r"https?://\S+", "\u3000", d)
 
def check(draft: str) -> list:
    hits = []
    for p in PROMISE:
        if re.search(p, draft):
            hits.append(("A_promise", p))
    for pattern, target in ((r"[\w.+-]+@[\w-]+\.[\w.]+", draft),
                            (r"https?://\S+", draft),
                            (r"@[A-Za-z0-9_]{3,}", mask(draft)),
                            (r"0\d{1,4}-\d{1,4}-\d{3,4}", mask(draft))):
        m = re.search(pattern, target)
        if m:
            hits.append(("B_contact", m.group(0)))
    for sentence in re.split(r"(?<=)", draft):
        sentence = sentence.strip()
        if sentence and not POLITE_END.search(sentence) and PLAIN_END.search(sentence):
            hits.append(("C_plain", sentence))
            break
    if len(draft) > MAX_CHARS:
        hits.append(("C_length", f"{len(draft)} chars"))
    return hits

Check B exists because a public reply that routes someone to a support address leaves that address sitting in public forever. The instinct to help one person individually is right; the place to do it is not the store listing.

Running it exposed two bugs — in the guard

I prepared six review-and-draft pairs and ran the first version as written. The drafts are Japanese, since that is the locale I actually reply in, so check C evaluates the Japanese polite register. The output was this:

★  bucket     verdict  hits
1  priority   STOP     A_promise:次の(アップデート|バージョン)で
1  ack_only   STOP     B_contact:support@example.com / B_contact:@example
2  ack_only   OK       —
2  priority   STOP     C_plain:保存が完了しない事象を確認しました。
5  no_reply   —        reply slot not used
3  ack_only   STOP     C_plain:ご意見に感謝する。
 
6 drafts → 5 in scope / 1 passed / 4 blocked / 1 skipped

Two of those four blocks were the guard's own mistakes.

The first is on line two. B_contact fires twice for support@example.com, because the handle pattern picked up @example inside the email address. One span, counted twice.

The second one actually hurts. On line four, a perfectly polite sentence was blocked as plain-form, because the rule searched for a plain-form ending (した。) that also sits inside the polite ending (ました。). Ordering was the problem: the guard has to confirm the polite form first and only then suspect the plain one, not the other way around. The same trap exists in any language where the formal marker is a superset of the informal one.

Two fixes: mask emails and URLs before looking for handles, and evaluate the polite ending before the plain one. Re-running gives:

1  priority   STOP     A_promise:次の(アップデート|バージョン)で
1  ack_only   STOP     B_contact:support@example.com
2  ack_only   OK       —
2  priority   OK       —
5  no_reply   —        reply slot not used
3  ack_only   STOP     C_plain:ご意見に感謝する。
 
6 drafts → 5 in scope / 2 passed / 3 blocked / 1 skipped
MetricBeforeAfter
Blocked43
False positives2 (one double count, one register misread)0
Passed12

All three remaining blocks were correct: an uncommitted fix date, an address I did not want published, and a sentence that dropped out of the register I use with reviewers.

A guard that cries wolf gets ignored, and an ignored guard eventually gets deleted. Laying six cases out by hand while the numbers are still small is cheap insurance against that path.

What to delegate, and what to keep

Drafting and mechanical checking are automated on my side. Pressing send is not.

The reason is simple: a reply contains commitments. "We are investigating" reports a fact, but "we will fix this" agrees to a development plan. Only the person who knows what gets worked on next can make that call, and that has nothing to do with how well the sentence is written.

The same logic keeps judgement about the review itself in human hands. The buckets inform me; they do not decide for me. A reply written without reading the review reads exactly like one.

Once you are replying in several languages, other concerns appear — terminology consistency, pacing the volume, register per locale. I wrote up an actual multilingual run in building Google Play replies in eleven languages with Gemini, which is worth a look once your reply volume grows.

One thing to try next

Add a single phrase to the PROMISE list: one you have personally regretted sending. A generic banned-words list helps less than removing one of your own habits. Mine was "shortly."

It is a small guard. But having one stage in front of the send button means the version of you that writes at midnight gets checked by the version that reads at breakfast.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-05-03
Build a CSV Insight App with Gemini API and Streamlit — A Production-Ready Dashboard with Auto-Insights and Visualization
A production-grade implementation guide for a Streamlit + Gemini API data analysis app. Upload a CSV, get auto-insights and visualizations in seconds. Covers schema inference, structured output, and real-world rate-limit handling.
API / SDK2026-08-27
Your Spreadsheet Breaks Before Gemini Ever Sees It
Merged cells and two-row headers quietly strip rows of their keys during extraction, long before the model reads anything. Here is what gets lost, measured, plus the Python that flattens the table and catches the total row.
Dev Tools2026-09-06
Icon-Only Buttons Can Stay Silent for Screen Readers, Even When Every Translation Is in Place
A fully translated app can still read as silence. Here is how I collect icon elements and labels from iOS and Android, settle everything mechanical locally, and send only the wording to Gemini — with the actual output from each stage.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links