# AI-агент для ресторанного склада

`02 · ai-warehouse · MVP`

AI-агент ресторанного склада с two-tier Gemini-роутингом и draft→Confirm safety boundary. Чеки, заготовки и списания стартуют в Telegram; WebApp держит review, WAC и dashboard impact.

**Скоуп:** Соло · 1 неделя  
**Роль:** AI-assisted ресторанный склад

**Видео:** [YouTube](https://www.youtube.com/watch?v=u8hGuM5KK7w) · [RuTube](https://rutube.ru/video/private/93dbabcc9f3972c282c5ab792fa07745/?p=TDvJVMYjOIOeL0nbBj848Q)

## Разбор видео

Складской учёт ресторана разделён между Telegram (грязный ввод — фото чеков, заготовки, списания текстом или кухонным чеком) и WebApp, где каждый черновик проверяется до того как склад сдвинется. Агент читает чеки, раскладывает блюда на ингредиенты, обновляет средневзвешенную себестоимость по подтверждению и отвечает на свободные вопросы по складу с рекомендациями для следующей закупки.

AI-система складского учёта для ресторанов — чеки, заготовки и списания начинаются в Telegram, а WebApp оставляет каждое число проверяемым.

Дашборд показывает день, неделю и месяц: выручку, food cost, закупки, списания и стоимость склада, с компактными графиками рабочего дня.

Склад подсвечивает, что требует внимания. Две замороженные позиции закончились — команда докупает продукты и отправляет боту фото чека из магазина.

AI читает фото, сопоставляет продукты, напитки и расходники со складскими категориями и создаёт черновик поставки. Проверяем его в WebApp и подтверждаем — остатки и средневзвешенная себестоимость обновляются сразу.

Заготовки тоже учитываются. Когда кухня делает тесто для пиццы, томатный соус, котлеты для бургеров или маринованную курицу, команда может написать агенту, что приготовлено, или вручную добавить партию в WebApp. Ингредиенты списываются, готовая заготовка приходит на склад, а себестоимость остаётся связанной.

Для продаж или списаний можно отправить кухонный чек либо просто описать операцию текстом. AI распознаёт блюда, раскладывает их на ингредиенты и готовит списание. Подтверждаем — и дашборд снова обновляется.

После этого можно спросить агента что угодно про склад — например, как прошёл месяц. Он анализирует историю поставок, списаний, выручку, food cost, движение склада и out-of-stock позиции, а затем даёт рекомендации для следующей закупки.

Telegram принимает грязный ввод. WebApp отвечает за проверку. Дашборд показывает влияние на бизнес.

---

## Контекст

> Модель делает черновик. Человек подтверждает. Dashboard показывает эффект.

Кухонный склад уходит от записей быстрее, чем у кого-то на кухне есть время его внести. Чеки приходят на бумаге или голосом; поставки, списания блюд и заготовки на полуфабрикаты — у каждой своя математика; средневзвешенная себестоимость остаётся корректной только если каждая операция доехала до журнала вовремя. Цена пропущенных записей невидима до первых перекосов остатков — а к этому моменту следов уже нет.

MVP закрывает это узкое место end-to-end: Telegram принимает messy stock operations, WebApp оставляет каждый черновик проверяемым, а dashboard переводит подтверждения в revenue, gross profit, food cost, stock value, low-stock risk и next-order recommendations.

## Факты

| | |
|---|---|
| **Скоуп** | 1 неделя соло |
| **Поверхности** | Telegram-бот (Aiogram) + WebApp SPA (React 18) + browser fallback |
| **Домен** | 100 inventory · 40 menu · 12 заготовок · 18 таблиц · deterministic 30-day history |
| **AI** | Gemini 3 Flash-workflows · Pro-style operating analysis · per-call $ трекаются |
| **Бизнес** | Revenue · gross profit · food cost · stock value · low-stock risk · next-order list |
| **Статус** | MVP · Telegram-бот + WebApp · multimodal intake live · stock, WAC и dashboard end-to-end |

## Архитектура

### Операторский loop · dashboard → Telegram → review → impact

```text
 1  WebApp Dashboard
        │  Today / Week / Month
        │  stock value · revenue · gross profit · purchases
        │  deductions · preparations · movement charts
        ▼
 2  Stock view
        │  категории · количество · стоимость остатка · цена за единицу
        │  Frozen flags: Green Peas Frozen + Potato Wedges Frozen = out
        ▼
 3  Telegram: фото чека поставки
        │  📷 Receipt received
        │  👁 Vision pass: reading receipt lines and totals
        │  🧠 Matching products to stock categories
        │  📦 Calculating quantities and weighted costs
        │  ✅ Draft supply ready for review
        ▼
 4  Draft supply result
        │  19 stock items matched · 15 650.24 RSD
        │  actions: Review · Confirm · Cancel
        ▼
 5  WebApp review + Confirm
        │  food · beverages · operating supplies
        │  confirm commits stock_levels + WAC + stock_history
        ▼
 6  Preparations
        │  pizza dough batch: ingredients out, prepared item in
        │  preparation cost cascades through WAC
        ▼
 7  Telegram: фото kitchen order slip
        │  📷 Kitchen slip received
        │  👁 Vision pass: reading dishes and quantities
        │  🧠 Matching dishes to menu recipes
        │  🥘 Expanding dishes into ingredient deductions
        │  ✅ Draft deduction ready for review
        ▼
 8  Draft deduction result
        │  Chicken Shawarma ×3 · Shawarma Plate ×2
        │  confirm writes off ingredients at current WAC
        ▼
 9  Agent monthly analysis
        │  last 30 days · revenue · food cost · supplies
        │  preparations · top deductions · out-of-stock · next order
        ▼
10  Dashboard impact
        │  Telegram принимает messy input
        │  WebApp даёт review / confirm
        │  Dashboard показывает business effect
```

**Progress — часть продукта.** Каждое AI-действие редактирует одно Telegram-сообщение по видимым стадиям: received, vision pass, matching, cost calculation или recipe expansion, затем draft ready. Оператор видит работу модели, а не пустой spinner.

**AI делает draft, Confirm делает commit.** Supply, deduction и preparation workflows возвращают структурированные черновики. Backend сохраняет их со status = draft; stock, WAC, history и dashboard totals двигаются только после нажатия Confirm.

**Review живёт в Telegram и WebApp.** Telegram-результат несёт Review, Confirm и Cancel. Review открывает WebApp для line-level проверки; Confirm коммитит тот же draft через backend confirm endpoint.

### Мультимодальный intake → draft → confirm

```text
 1  Пользователь                  text  /  фото  /  голос
        │
 2  Бот  (Aiogram)
        │  base64-кодирует фото/голос  +  выбранный workflow
        │  POST /api/agent/message              [X-Telegram-User-Id]
        ▼
 3  Backend.router  (route_message)
        │  dispatch по workflow:
        │     supply | deduction | preparation  →  Flash workflow
        │     agent analysis                    →  monthly operating analysis
        ▼
 4  Flash workflow  (gemini-3-flash, ≤5 итераций)
        │  inject inventory snapshot в system_prompt
        │  call client.generate_content(tools=[...])
        ▼
 5  Gemini  ──►  text  /  function_call
                │
                ▼
              tool_create_supply_draft  (или _deduction_, _preparation_)
                │  parse args, нормализует price_type
                ▼
              insert строку в supplies / deductions / preparations
              со status = 'draft'
        │
 6  Backend  ──►  AgentMessageResponse(text, actions=[
        │           AgentAction(type=callback, label='Confirm',
        │                       data='confirm_supply:<uuid>'),
        │           AgentAction(type=callback, label='Delete',  ...),
        │         ])
        ▼
 7  Бот  ──►  inline keyboard рендерится из actions
        │
 8  Юзер тапает Confirm
        │  POST /api/supplies/{id}/confirm
        ▼
 9  Backend
        │  пересчёт stock_levels  +  WAC
        │  insert в stock_history
        │  snapshot cost_per_unit на lines
        ▼
10  Done  ·  status = 'confirmed'  ·  audit_log записан
```

**Двухфазно by design.** AI workflow создаёт черновик (никаких побочек). Confirm — отдельный POST, который обновляет stock_levels и пишет stock_history. AI никогда не трогает живой склад; нажатие кнопки человеком — трогает.

**Backend владеет смыслом action.** Бот — тонкий Telegram adapter: загружает media, показывает progress, рендерит actions от backend и форвардит callbacks. Business meaning остаётся в backend.

**Явные lanes держат MVP предсказуемым.** Оператор выбирает Поставка, Списание, Заготовка или Агент до отправки messy input. Это убирает false intent classification из критичного stock path; auto-router можно поставить над тем же dispatcher позже.

### Компонентная схема

```text
   Inputs              docker-compose  (4 сервиса)               External
   ──────              ────────────────────────────              ────────
   Telegram chat ────► bot  (Aiogram 3.4)                        Gemini API
                            │  POST /api/agent/message            google-genai
                            │  X-Telegram-User-Id                      ▲
                            ▼                                          │
   Telegram WebApp     ┌──────────────────────────────────┐            │
   ─initData──────────►│  backend  (FastAPI · async)      │────────────┘
   browser  ─JWT──────►│  9 routers · 18 tables · 9 mods  │
                       │  GeminiClient + token_usage      │
                       └────┬───────────┬─────────────────┘
                            │           │
                            ▼           ▼
                      postgres 16    receipts_data
                      (asyncpg WAL)  shared volume:
                                     bot writes · backend reads

                            ▲ HTTP /api/*
                            │
                       frontend  (Vite · React 18 SPA · :5173)
                       react-i18next 5 langs × 10 ns
                       @tanstack/react-query
```

**Бот — только HTTP.** Бот держит long-poll к Telegram и POSTит каждое событие в backend. Прямого доступа к БД у бота нет — это держит его stateless через рестарты.

**Фото шарятся через FS, не через API.** Бот пишет загруженные фото в volume receipts_data; backend читает оттуда же. Без re-upload многомегабайтных файлов между контейнерами.

**Frontend — dual-mode.** Тот же Vite SPA монтируется внутри Telegram WebApp (initData в заголовке) и как обычное browser-приложение (deep-link JWT). Один бандл, два пути авторизации в lib/auth.ts; остальное приложение их не различает.

### Доменное ядро · 18 таблиц в 9 модулях

```text
INVENTORY                            SUPPLIES
─────────                            ────────
inventory_items                      suppliers
  · status: active|archived          supplies  (status: draft|confirmed)
       │                               └── supply_lines
       ▼                                     · qty · price_per_unit
  stock_levels                              · expiry_date
    · qty · avg_cost  ◄────── WAC update on confirm
       │
       ▼
  stock_history  (журнал каждого движения)


PREPARATIONS                         DEDUCTIONS
────────────                         ──────────
prep_recipes                         deductions  (status: draft|confirmed)
  · default_multiplier                 └── deduction_items
  · portion_size · portion_unit              · type: dish | item
  └── prep_recipe_ingredients               └── deduction_lines
                                                  · cost_per_unit
preparations                                       (snapshot at confirm)
  · multiplier  (e.g. 3× base broth)
  · cost = sum(ingredient × avg_cost) / output_qty   ← cascade WAC


MENU                                 AI · CHAT
────                                 ─────────
menu_items  (с archived flag)        chat_sessions
  └── menu_item_ingredients          chat_messages  (last 40 в контексте)
                                     user_settings
                                     token_usage
                                       · workflow_type · model
                                       · input/output/thinking_tokens
                                       · cost_usd


AUDIT
─────
audit_logs  ·  receipt_images  (общий FS-путь с ботом)
```

**WAC живёт в stock_levels.avg_cost.** Каждый supply confirm пересчитывает (old_qty*old_avg + new_qty*new_price) / total_qty в stock.avg_cost. Каскадирует через preparation: prep cost = sum(ingredient_qty * avg_cost) / output_qty, и затем сам входит в output_stock.avg_cost через тот же WAC.

**Cost-snapshot на deduction confirm.** Когда deduction подтверждается, deduction_lines.cost_per_unit = stock.avg_cost на тот момент. Последующие поставки по другой цене не переписывают исторические списания.

**Dual-mode списания.** deduction_items.type = dish (разворачивает рецепт; ингредиенты становятся lines) или item (прямая stock-line). Одно списание может мешать оба — покрывает случай, когда блюдо подали и повар съел ещё два сверху.

## Ключевые инженерные решения

### 01 · Двухуровневый Gemini-3 с явными workflow lanes

**Решение.** Два уровня Gemini-3 покрывают две разные формы работы. Дешёвый Flash (gemini-3-flash-preview, thinking_budget=0, ≤5 итераций) гоняет stateless workflow tool-loops — один из {create_supply_draft, create_deduction_draft, create_preparation}. Pro-style analysis отвечает на более широкие операционные вопросы по 30-дневной истории, low-stock watchlist, dashboard-метрикам и next-order list. В MVP дорожка явная — кнопки Поставка / Списание / Заготовка / Агент в боте. Per-call usage парсится из response.usage_metadata и пишется в token_usage с workflow_type — Settings строит срезы по тиру и workflow.

**Почему.** Single-tier на Pro стоит примерно в 4× за токен и тащит full chat-history overhead на каждый dispatch фото-чека. Single-tier на Flash подходит для узких tool-loop, но не для широкой операционной аналитики. Разнести по форме работы — черновик конкретной операции vs broader business analysis — попадает в реальный cost/benefit envelope Gemini-3 на сегодня. Явные lanes также уменьшают ложное определение intent, пока core MVP доказывает stock-operation path.

**Цена.** Два семейства промптов на поддержку. Tool declarations частично дублируются между узкими workflow и широкой аналитикой. Явный выбор дорожки — UX-цена на стороне бота: лишняя кнопка перед intake. Auto-intent router может убрать это позже, но он должен лечь поверх тех же cost/workflow telemetry, а не заменить tier split.

### 02 · Draft → Confirm на каждой мутирующей операции

**Решение.** Поставки, списания и заготовки двухфазны. Первый POST вставляет строку со status = draft — никаких побочек на склад. Второй POST на /confirm пессимистично лочит соответствующие stock_levels-строки, пересчитывает WAC, пишет строку в stock_history на каждое движение, снапшотит cost_per_unit на deduction-lines и переводит status в confirmed. AI никогда не трогает живой склад; человек подтверждает inline-кнопкой.

**Почему.** AI-агент, мутирующий склад напрямую, — одна неудачная транскрипция от того, чтобы повредить недели данных по остаткам. Двухфазность делает каждое предложение модели обратимым (удалить черновик) и инспектируемым (открыть WebApp, поправить строку, потом подтвердить). Тот же паттерн даёт оператору-человеку ту же возможность — стартовать черновик в боте, закрыть через WebApp.

**Цена.** Каждая мутирующая поверхность — два endpoint (`/...` и `/.../confirm`). Stock обязан принимать draft-строки, которые игнорирует в тоталах. UX обязан коммуницировать, что только что созданная supply ещё не живая. Concurrent confirms на тот же stock_level требуют лока — добавил pessimistic `SELECT FOR UPDATE` на каждое чтение stock в confirm-path (supplies + deductions + cascade в preparations); read-only-запросы стока остаются lock-free.

### 03 · Server-driven review actions вместо bot-side business logic

**Решение.** Backend эмитит AgentAction внутри AgentMessageResponse — каждый action несёт type (callback или webapp), label и data-строку. Telegram рендерит Review, Confirm и Cancel из этого списка; backend владеет смыслом confirm_supply:<uuid> и тем, какой WebApp-экран открывает Review. Тот же контракт работает в Telegram, в chat-pane WebApp-fallback и в любом текстовом канале с tap-a-button.

**Почему.** Бот, который владеет business actions, превращается в параллельную UI-кодобазу: каждый новый тип черновика, каждый review path, каждый workflow требует bot-side handler change. Бот как transport adapter означает, что новые stock flows едут только в backend. Оператор всё равно получает богатую Telegram-поверхность — progress edits, result summary, Review/Confirm/Cancel — но business meaning остаётся централизованным.

**Цена.** Backend обязан знать про Telegram-специфические лимиты (callback_data ≤ 64 байт; некоторые type — только inline-mode). Кнопки — dispatch-only, inline-формы они не несут. Что-то богаче confirm/delete (например, правка количества в строке) уходит в WebApp.

### 04 · Weighted Average Cost с каскадом через preparations

**Решение.** Каждый supply confirm крутит WAC по затронутому stock_level: new_avg = (old_qty*old_avg + new_qty*new_price) / total_qty. Preparations применяют WAC дважды — сначала считают unit-стоимость заготовки (sum(ingredient_qty * ingredient.avg_cost) / output_qty), потом мержат её в выходной stock_level через тот же WAC. Deduction-lines снапшотят cost_per_unit = stock.avg_cost на момент confirm; последующие изменения цены не переписывают прошлое.

**Почему.** FIFO/LIFO потребовали бы lot-level трекинга — каждая supply-line как дискретная партия со своим остатком — и pop из очереди/стека на каждом списании. Для одной кухни ресторана с замешанными ингредиентами (один мешок риса, не пять различимых партий) WAC попадает в то, как повар реально думает о себестоимости. Snapshot на confirm защищает исторические отчёты от последующего ценового drift.

**Цена.** WAC-математика прячет batch-level вариативность — нельзя ответить, из какой именно поставки приехало это блюдо. Preparations превращают одно списание в цепочку WAC-вычислений; debug неверной cost — ходить по цепи руками. С нулём тестов в репо каждое изменение в WAC проверяется вручную.

## Стек

| | |
|---|---|
| **Backend** | Python · FastAPI · SQLAlchemy 2.0 Mapped (async) · asyncpg · Alembic · Pydantic v2 |
| **Frontend** | React 18 · Vite · TypeScript · Tailwind · @tanstack/react-query · react-i18next |
| **Bot** | Aiogram 3.4 (long polling) · httpx |
| **AI** | google-genai 1.61 · Gemini 3 Flash + Pro · мультимодал (text/photo/voice) · cost-матрица |
| **Инфра** | PostgreSQL 16 · Docker Compose (4 сервиса · 1 shared volume для чеков) |
| **Объём** | 9 backend-модулей · 18 таблиц · 13 страниц · 5 langs × 10 i18n namespaces · 6 валют · ~50 routes |

## Уроки и статус

### Оставлю как есть

- Per-call cost из usage_metadata персистится с workflow_type — Settings-страница режет траты по модели, workflow и дню. Сделать это до первого run’а дешевле, чем добавлять postmortem после первого vendor-инвойса; этот же сигнал — то, что должно питать smart-router, когда он встанет.
- Draft → Confirm с cost-snapshot — каждый mutating AI output проверяется до commit. Product safety boundary живёт в форме workflow, не в точности модели: плохой OCR может создать плохой draft, но не может изменить живой склад без human confirmation.
- Server-driven review actions — Telegram показывает progress, result summaries и Review/Confirm/Cancel, а backend владеет action contract. Тот же контракт работает в Telegram, любом text channel и WebApp chat pane. Новый workflow едет backend-side.
- Компактный design system + hot-reload во всех трёх контейнерах — UI-STANDARDS.md (~600 строк) фиксирует receipt-like карточки, 28×28 квадратные действия, 11–14 px типографику, Telegram CSS-vars замаплены на Tailwind tokens. Bind-mount’ы + uvicorn --reload + vite HMR — цикл «правка → видно» в секундах для backend, frontend и бота.

### Поменял бы

- Стартовал без тестов — приемлемо для скорости MVP, но workflow routing и WAC confirm-paths теперь требуют pytest-фикстур до расширения на больше операторов. Рисковая поверхность маленькая: draft creation, confirm mutation, WAC cascade и dashboard totals.
- HMAC-валидация Telegram WebApp initData — hmac импортирован в auth.py и нигде не вызывается. Trust сейчас опирается на client-side гарантии Telegram WebApp, а не на server-side проверку; закрытие — ~50 строк добавкой.
- init_db() параллельно с Alembic — Base.metadata.create_all() стреляет на старте, миграции живут рядом. Норм для соло-итерации на пустой БД, становится foot-gun при первом же клонировании, если ни одна из дорожек не приводит к чистой схеме. Либо alembic stamp head после create_all, либо отказаться от create_all.

MVP · Telegram-бот + WebApp · multimodal intake live · поставки, заготовки, списания, WAC и dashboard работают end-to-end.

---

Источник: https://ilyadev.xyz/cases/ai-warehouse (HTML) · /cases/ai-warehouse.ru.md (этот файл)
Назад: 01 — AI CRM · Недвижимость → https://ilyadev.xyz/cases/ai-crm.ru.md
Дальше: 03 — AI-видеоредактор → https://ilyadev.xyz/cases/ai-video-editor.ru.md
Индекс: https://ilyadev.xyz/llms-ru.txt — полный список кейсов
Автор: Илья Казанцев — https://ilyadev.xyz/index.ru.md
