Skip to content

Repository files navigation

Supermarket Ops Agent

A Telegram-only conversational agent that runs an Indian kirana / supermarket store end-to-end. Receives stock, cuts GST-correct bills, runs khata (customer credit), checks stock, closes the day, and generates PDF invoices and PPTX analysis decks.

Live bot: @kiranaBuddyBot (message it on Telegram)

Watch the product walkthrough


Harness

Layer Choice
Agent harness Vercel AI SDK (ai v7)
Model Google Gemini via @ai-sdk/google
Hosting Vercel Function one webhook POST /api/telegram
Database Neon Postgres

Because Vercel functions are stateless, all state lives in Postgres such as stock, bills, khata, preferences, and the conversation history + processed update-ids. This provides durable persistence and idempotency across restarts.

Control loop

api/telegram.ts (webhook) → dedupe update_idrunAgent (src/lib/agent/agent.ts) → generateText({ model, system, messages, tools, stopWhen: stepCountIs(10) }) → persist the turn → reply via Telegram. History + preferences are reloaded from Postgres each call; the system prompt (prompt.ts) injects the owner's remembered settings.

Skill / tool design

Tools are thin, Zod-validated, and each enforces its own rule inside a DB transaction. The model orchestrates; business rules live in the tools, not the prompt.

  • inventory (tools/inventory.ts): add_product, receive_stock, get_stock, list_products, low_stock
  • billing (tools/billing.ts): add_bill_items (preferred, multi-line), add_bill_item, edit_bill_item, remove_bill_item, view_bill, finalize_bill, includes strict validation:
    • Upfront stock checks: Rejects adding items if requested quantity exceeds stock (returns insufficient_stock).
    • Packaged item checks: Validates non-loose items (isLoose: false) to ensure quantities are whole numbers (integers), rejecting fractional quantities (returns invalid_quantity).
  • khata (tools/khata.ts): khata_charge, khata_payment, khata_balance
  • analytics (tools/analytics.ts): sales_summary, close_day
  • memory (tools/memory.ts): set_preference, get_preferences ; allows open-ended preferences with descriptive keys and coherent, self-contained values.
  • documents (tools/documents.ts): generate_invoice_pdf, generate_analysis_pptx

A free-text product resolver (domain/products.ts) scores catalog matches; ties come back as ambiguous so the model asks "which atta ; Aashirvaad 5kg or loose?" rather than guessing.

How the hard parts are handled

  1. Grounding ; every price/slab/stock comes from a tool DB read; the prompt forbids inventing data; unknown items return not_found and the model asks.
  2. Oversell guard ; Checked at draft time (in add_bill_item/edit_bill_item using applyItem) and transactionally enforced at finalize with UPDATE products SET qty = qty - $n WHERE id = $id AND qty >= $n RETURNING. A 0-row result rolls back the whole bill. This is fully business-logic enforced in the tool layer, not the prompt.
  3. GST correctness ; domain/gst.ts (pure, unit-tested): per-item HSN + slab, CGST = SGST = slab/2, round-half-up per line, tax grouped by slab on the bill. Seed data uses real HSN codes and slabs (0/5/12/18%).
  4. Multi-turn bills ; the bill is a draft row; add/edit/remove mutate it; stock only moves at finalize_bill.
  5. Idempotency ; Telegram update_id is claimed via processed_updates (insert-on-conflict-do- nothing); redeliveries are skipped. finalize_bill is a one-way draft → final transition, so a retried finalize returns the same bill unchanged.
  6. Concurrency ; finalize_bill runs in a Neon WebSocket-pool transaction with SELECT … FOR UPDATE on the bill and products, plus the atomic conditional decrement. Two bills, or a sale + a stock-in, can't corrupt or negative stock.
  7. Guardrails ; below-cost sale → needs_confirmation (model must reconfirm with allow_below_cost); khata payment with no account → no_account; no destructive deletes.
  8. Real artifacts ; pdfkit GST invoice (shop/GSTIN header, HSN, CGST/SGST breakup, amount in words) and a pptxgenjs deck (KPIs, top-items bar chart, payment-mix pie), sent via Telegram sendDocument.
  9. Memory across sessions ; Custom preferences are fully open-ended. The model saves self-contained, coherent statements (e.g., "Okay with either Ghee or Butter, whichever is in stock") under descriptive keys. The prompt builder (prompt.ts) dynamically iterates through and injects all saved preferences into system instructions. /new clears chat messages but leaves preferences intact.
  10. Duplicate checks on creation ; add_product verifies duplicates by performing an exact case-insensitive match on name, brand, and pack size, avoiding fuzzy-token search overlap issues (allowing variants like Aashirvaad Atta 10kg and Aashirvaad Atta 5kg to coexist).

GST base assumption: a product's sell_price is the GST-exclusive taxable value; tax is added on top so the bill shows a clean breakup. (An MRP-inclusive back-calc is a documented alternative.)

Run it

Prerequisites (all free, no credit card)

  1. Telegram bot ; talk to @BotFather, /newbot, copy the token.
  2. Gemini key ; Google AI Studio.
  3. Neon Postgres ; neon.tech → copy the pooled DATABASE_URL.

Local / setup

npm install
cp .env.example .env.local          # fill in the 4 secrets + a random TELEGRAM_WEBHOOK_SECRET
npm run db:push                      # create tables in Neon
npm run db:seed                      # load realistic SKUs
npm run test                         # GST engine unit tests
npm run typecheck                    # full type check
npm run smoke                        # OPTIONAL: drive the agent end-to-end without Telegram

Deploy (Vercel) + register the webhook

# 1. Push to GitHub, import into Vercel (Hobby), set the same env vars in the dashboard.
# 2. After the first deploy, point Telegram at it:
PUBLIC_URL=https://your-app.vercel.app npm run set-webhook

Then message the bot.

Try these examples

  • 50 packets of Maggi came in, cost ₹12, MRP ₹14
  • make a bill: 2kg loose atta, 1 Aashirvaad atta 5kg, 4 Maggi, 1 Amul butter, UPIdrop the butter, make it 6 Maggi
  • add atta (alone, no brand) → agent asks "Aashirvaad 5kg or loose?" instead of guessing
  • bill more than is in stock → refused
  • put ₹500 on Ramesh's credit · Ramesh paid ₹300 · Ramesh's balance?
  • today's sales / close the day
  • send me that bill as a PDF · make this week's analysis deck
  • always assume UPI unless I say cash/new → bill without a stated mode → still UPI

Project layout

api/telegram.ts               the ONLY web surface ; Vercel Function webhook
src/lib/
  agent/  agent.ts, prompt.ts, tools/*      the control loop + tool surface
  domain/ gst.ts, products.ts, billing.ts, preferences.ts, history.ts, idempotency.ts
  db/     schema.ts, client.ts, seed.ts     Drizzle + Neon
  telegram.ts, env.ts
scripts/ set-webhook.ts, smoke.ts
vercel.json                   declares the function (maxDuration 60s)

About

A Telegram-only conversational agent that runs supermarket store end-to-end. Receives stock, cuts GST-correct bills, runs customer credit, checks stock, and generates PDF invoices and PPTX analysis decks.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages