Joe Archondis
July 2, 2026 · 10 min read
Business AI Automation
Building an AI Operations Assistant for a Restaurant Chain
The Shawar'Mama owner was mid-investor meeting when someone asked about last week's revenue split across locations. He opened Telegram, typed the question. Nine seconds later he had the answer — pulled live from Zelty, normalized across all 6 Paris locations, written in French. No dashboard. No app switching. No "let me check later."
I built that system. A Telegram agent connected directly to his POS that does two things: sends an automated digest every morning at 8 AM, and answers any operations question in natural language on demand. Here's the architecture, the decisions that mattered, and where the real complexity turned out to be.
Why Dashboards Don't Work for Restaurant Operators
The owner of a restaurant chain doesn't sit at a computer. He's doing a location walk-through at 11 AM, on a supplier call at noon, managing service rush at 7 PM. A dashboard assumes free time and a screen — neither of which a working restaurant owner reliably has.
Zelty had a perfectly good analytics interface. He'd built reports. Given the managers logins. He still wasn't using it. Not because it was bad — because opening a browser tab during service felt like a context switch he couldn't afford.
The problem wasn't the data. It was the interface. He needed information to come to him, in the tool he already had open all day. That tool was Telegram.
What the System Does
The operations assistant has two modes.
Morning Digest. Every day at 8:00 AM Paris time, the bot pulls yesterday's sales from Zelty, normalizes it across all 6 locations, and sends a narrative summary to Telegram. Total revenue. Covers. Average ticket. Top and bottom items. Week-over-week delta. In French. Delivered before service starts.
Ad-Hoc Q&A. The owner types any question in plain language: "How did Saturday compare to last Saturday?" or "Which location had the highest average ticket this month?" The agent decides what to fetch, queries Zelty live, and returns a clear answer. Conversation history persists so follow-up questions work naturally.
This isn't a chatbot wrapping a static report. The agent decides what data to pull at query time. If he asks about a specific menu item, it filters by SKU. If he asks about a date range, the API params adjust. Every answer is live.
Connecting to the POS: Zelty Integration
Zelty is a French restaurant POS with a well-documented REST API covering orders, items, covers, and revenue. The main complexity is that Zelty organizes data per location. Any cross-location aggregation happens on your side.
The integration layer fetches per location and aggregates before passing anything to the AI:
async def get_daily_sales_summary(
location_ids: list[str],
date: str
) -> dict:
summaries = []
for location_id in location_ids:
response = await zelty_client.get(
"/orders",
params={
"location_id": location_id,
"date_from": f"{date}T00:00:00",
"date_to": f"{date}T23:59:59",
"status": "completed"
}
)
orders = response.json()["orders"]
revenue = sum(o["total_ttc"] for o in orders)
covers = sum(o["covers"] for o in orders)
summaries.append({
"location_id": location_id,
"revenue": revenue,
"covers": covers,
"avg_ticket": revenue / covers if covers > 0 else 0,
"items": aggregate_items(orders)
})
total_revenue = sum(s["revenue"] for s in summaries)
total_covers = sum(s["covers"] for s in summaries)
return {
"locations": summaries,
"total_revenue": total_revenue,
"total_covers": total_covers,
"avg_ticket": total_revenue / total_covers if total_covers > 0 else 0
} One thing Zelty doesn't give you for free: week-over-week comparisons. That means fetching the same date from last week and computing the delta yourself. Simple math — but worth designing for upfront. Retrofit it later and you're touching every function that returns a number.
For the Q&A mode, Zelty calls are wrapped as tools the model can invoke. The model sees the question, decides what to fetch, calls the tool, and formulates the answer. No pre-cached data, no staleness. The tradeoff is latency — a live API call adds 300 to 600ms versus reading from cache. At conversational speed, he doesn't notice.
The Morning Digest Format
The digest runs on APScheduler at 8:00 AM. Three versions of the format before settling on one the owner actually reads every day.
Version 1: paragraphs of numbers. Stopped reading by day 3. Version 2: four bullet lines. Too sparse, no context. Version 3 — what's running now — uses narrative sections with directional signals:
📊 Lundi 30 juin — Récapitulatif des 6 établissements
💰 Chiffre d'affaires total : 8 240 € (+12% vs lundi dernier)
👥 Couverts : 412 · Ticket moyen : 20,00 €
📍 Par établissement :
• République : 1 920 € ✅ (+8%)
• Châtelet : 1 840 € ✅ (+15%)
• Opéra : 1 710 € ✅ (+11%)
• Nation : 1 320 € ⚠️ (-3%)
• Bastille : 890 € ⚠️ (-7%)
• Marais : 560 € 🔴 (-22%)
🔝 Top articles : Shawarma bœuf (187), Assiette mixte (142), Ayran (98)
The signals (✅ / ⚠️ / 🔴) are generated dynamically: positive growth, 0 to -10%, worse than -10%. The owner spots which location needs attention without doing mental math.
The numbers come directly from the Zelty API. The narrative sentences are generated by the AI model. That split matters — structured data from the source, prose from AI — so numbers are always accurate even if the phrasing varies slightly day to day.
Natural Language Q&A Over Live POS Data
The agent has four tools it can call at runtime:
| Tool | What It Fetches | Example Trigger |
|---|---|---|
get_daily_sales | Revenue, covers, ticket for a date | "What was revenue yesterday?" |
get_period_sales | Aggregated data over a date range | "How did we do this week?" |
get_top_items | Best and worst items by period/location | "What sold best last Saturday?" |
compare_periods | Side-by-side of two date ranges | "How does this month compare to last?" |
The model decides which tool to call. No routing logic on my side — just tool definitions passed to the model at inference time. The model maps natural language to tool parameters. If the question doesn't match any tool, it says so rather than guessing.
Conversation history is stored in PostgreSQL. The last 10 exchanges per session are fetched and included in the model context on each request. This makes follow-up questions work: "What was revenue by location this week?" then "And how does that compare to last week?" — the second question resolves correctly because the prior exchange is in context.
Cloud Run scales to zero when idle. In-memory conversation state would be wiped on every cold start. PostgreSQL adds a few milliseconds and keeps conversation continuity across restarts. Not a clever choice — just the right one.
Handling Failures Without Going Silent
The Zelty API goes down occasionally. Maintenance windows, post-update instability. The first version of the system timed out silently. The owner would send a question and get nothing. No idea if the bot was broken or just slow.
The fix was explicit error messaging to Telegram on every failure type:
try:
data = await zelty_client.get_daily_sales(location_ids, date)
response = await generate_response(data, question, history)
await bot.send_message(chat_id, response)
except ZeltyAPIError as e:
await bot.send_message(
chat_id,
f"⚠️ Connexion Zelty indisponible ({e.status_code}). "
f"Les données de ce jour ne sont pas accessibles. "
f"Réessayez dans quelques minutes."
)
except Exception as e:
logger.error(f"Unhandled error in ops bot: {e}")
await bot.send_message(
chat_id,
"⚠️ Erreur inattendue. Si le problème persiste, contactez le support."
) The rule is simple: never let the owner wonder if the system is working. A clear error with a retry suggestion is better than silence. He treats it the same as a "network unavailable" notification — tries again in a few minutes, moves on.
Architecture Decisions Worth Thinking Through
A few choices I'd make the same way again, and one I'd revisit.
Per-location fetching vs. aggregate endpoints
Zelty has aggregate endpoints that return totals across all locations in one call. I chose per-location fetching because it lets the agent answer location-specific questions without a second round-trip. If you're building for a single location, skip the complexity. For a chain, per-location is the right default.
Scheduled polling vs. real-time webhooks
APScheduler running in-process is simple and has been reliable for months. For a daily digest at 8 AM, polling works fine. If you want real-time alerts on anomalies mid-service — unusually low covers, a location going dark — you'd want Zelty webhooks with event-driven processing. More complex setup, clear use case. Didn't need it here.
Access control
The bot responds only to pre-authorized Telegram user IDs: the owner, two partners, one general manager. An unknown user ID gets ignored — no response, no error. Simple, effective, zero auth overhead. I've built more elaborate auth flows for other systems and regretted the maintenance cost. For a tight user list, allowlisting IDs is the right call.
What I'd change: the AI model
This runs on Gemini 2.0 Flash — a client preference, and it works well. Starting fresh, I'd use Claude Haiku. The Anthropic SDK's tool use API is cleaner to work with, and prompt behavior for structured data queries is more predictable from my experience with both. Either model handles this task. The model is not the hard part of this system.
Frequently Asked Questions
What data can an AI operations assistant pull from a restaurant POS system?
From Zelty, the agent pulls daily revenue by location, cover counts, average ticket size, order-level detail, and item-level sales ranked by volume. Most restaurant POS systems expose similar data via REST APIs. The key is per-location granularity — aggregate endpoints are easy to query but won't help you identify which specific location had a bad night.
Is a Telegram bot better than a dashboard for restaurant analytics?
For a working restaurant owner managing multiple locations from their phone, yes. Dashboards require you to log in and navigate to a report. A Telegram bot sends data proactively at 8 AM and answers questions in the chat you already have open. The Shawar'Mama owner stopped logging into Zelty's analytics dashboard entirely within two weeks of the bot going live.
How do you connect a Telegram bot to a restaurant POS system?
The bot doesn't talk to the POS directly. A FastAPI backend handles the Telegram webhook, decides what data to fetch, calls the POS API, and formats the response. The Telegram bot is the interface; the Python backend is the integration layer. This keeps POS integration logic testable independently of the bot.
How much does it cost to run an AI operations assistant for a restaurant?
Monthly infrastructure for a 6-location chain runs at roughly $30 to $40: Cloud Run always-warm instance (~$5), Cloud SQL PostgreSQL (~$15), and AI model API calls (~$10 to $15 at typical query volume). Build time is 2 to 3 weeks for a full implementation. For a chain doing meaningful monthly revenue, these are not significant costs.
What questions can a restaurant owner ask the AI operations bot?
Anything the POS data can answer. Revenue yesterday. Best-selling items last week. How a specific location performed on Saturday versus the previous Saturday. Average ticket by location this month. The agent translates natural language into live POS API calls — it's not a pre-built report. If the data exists in your POS, you can ask about it conversationally.
Working on something similar?
I build AI agents and low-latency systems. If you're trying to solve a version of this, let's talk.
Get in touchAuthor: Joe Archondis — AI systems engineer and HFT infrastructure builder.
Last updated: 2026-07-02