Files
ERP/bot/docs/SETUP.md
whqreza 638076dbaa refactor(bot)!: rename 'ai procurement' -> 'bot/' so the compose build context is committed
The bot service in docker-compose.yml builds from ./bot, but the agent code lived in 'ai procurement/' and ./bot was only a Windows junction on reza.g's machine (and was gitignored) — exactly as the team reported: 'bot/ was never committed — prod-v7's bot is un-buildable from the repo.'

- junction removed; 'ai procurement/' renamed to 'bot/' via git mv (all 41 files keep their history as pure renames)
- 'bot/' dropped from .gitignore (voice/ stays ignored — runtime voice notes; bot/.env stays ignored by bot/.gitignore)
- compose needs no change: context: ./bot and ./bot/app now resolve to committed files
- bot/INTEGRATION-REPORT.md added: full story (problem, root cause, git state, security note on the committed BALE_BOT_TOKEN / AI_API_KEY compose defaults, and usage instructions for the team)

Verified: 41 renames staged with zero secret/cache files; voice/ and bot/.env still ignored.
2026-10-02 12:52:25 +03:30

8.5 KiB

Setup & Operations Guide

A Bale messenger bot that turns Persian chat messages and voice notes into procurement actions in the ERP. It works draft-first: it captures what it understood, tells you exactly what it did and what is still empty, and commits only after you approve with a ✅ button.

Bale's Bot API is a Telegram-compatible clone hosted at tapi.bale.ai, so the bot uses the familiar getUpdates long-polling — no public port, no webhook, no TLS setup.


What it can do (v1)

Activity Example (voice or text) Committed via
Register a supplier quote «سازان نو برای پروژه برج آسمان ۵۰۰ کیلو میلگرد A3 سایز ۱۴، هر کیلو ۴۵ تومن، پرداخت ۳ فاز» POST /quotes (nested create)
Add a supplier «تأمین‌کننده جدید پروفیل بر، دسته لوله و اتصالات، تلفن ۰۹۱۲…» POST /suppliers
Add a material «کالای جدید میلگرد A3 سایز ۱۸ با واحد کیلوگرم» POST /materials
Status queries «استعلام‌های در انتظار؟» / «کدام‌ها به‌زودی منقضی می‌شوند؟» GET /quotes?…
Accept a quote «استعلام سازان نو را قبول کن» PATCH /quotes/{id}/accept
Announce a delivery «اقلام سازان نو رسید، ۵۰۰ و ۲۰۰» PATCH /quotes/{id}/arrive-items

The draft → approve loop (the core idea)

  1. You send a message (or voice note). The bot converts voice to text (STT) and extracts an intent + entities (LLM).
  2. Names are fuzzy-matched against the ERP (supplier / material / unit / project). Multiple candidates → inline pick buttons; no match → it is listed as missing.
  3. The bot replies with a review card: everything it captured, plus a ✅/⚠️/❌ list of what is still empty («یادآوری چیزهای خالی»).
  4. You keep chatting to refine («قیمت میلگرد ۴۷ شد», «پروژه برج آسمان») — the open draft is updated and re-rendered.
  5. Nothing is written to the ERP until you tap ✅ ثبت نهایی. Partial commits are possible («ثبت فقط اقلام مشخص») because the API deliberately tolerates incomplete quotes. After commit you get the created quote id and the list of fields that were left empty.

Team access & attribution

  • Every user links their Bale chat to their ERP account once: /start → Casdoor username → password (validated through the real POST /auth/login; the password is used once and never stored).
  • Mutating API calls carry X-Actor-Id: <users.id> so quote_events.actor records the human who asked, not the bot. (The bot's container talks to the api in-network: http://api:8000/api/v1.)
  • BALE_ADMIN_CHAT_IDS may list chat ids that bypass linking.

Commands

/start · /help · /drafts (open drafts) · /cancel (abort draft/flow) · /whoami · /unlink


Setup

  1. Create the bot in Bale's BotFather and copy the token.

  2. Configure an AI provider — any OpenAI-compatible endpoint works for both STT (POST {base}/audio/transcriptions, Persian) and the LLM (POST {base}/chat/completions, JSON mode). Examples: OpenAI whisper-1

    • gpt-4o-mini, or any proxy/provider you already use.
  3. Compose (dev):

    # env passed at compose level (or a .env next to docker-compose.yml)
    $env:BALE_BOT_TOKEN = "<token>"
    $env:AI_BASE_URL    = "https://api.openai.com/v1"
    $env:AI_API_KEY     = "<key>"
    
    docker compose up -d --build bot
    docker compose logs -f bot
    

    For Coolify (docker-compose.coolify.yml) set the same variables as service-level env vars; the bot container needs no domain/labels.

Environment variables (prefix PROCUREMENT_BOT_)

Variable Default Purpose
BOT_TOKEN — (required) token from Bale's BotFather
BALE_API_BASE https://tapi.bale.ai Bot API base (overridable for tests)
API_BASE_URL http://localhost:8000/api/v1 ERP api (in Docker: http://api:8000/api/v1)
DATABASE_URL same postgres as the api bot's own tables (bot_*)
AI_BASE_URL / AI_API_KEY — OpenAI-compatible STT+LLM provider
STT_MODEL / LLM_MODEL whisper-1 / gpt-4o-mini model names at the provider
ADMIN_CHAT_IDS — comma-separated Bale chat ids allowed without linking
SERVICE_TOKEN — optional bearer for the day procurement endpoints enforce auth
DRAFT_TTL_HOURS 24 drafts older than this are marked expired

In compose these are fed from BALE_BOT_TOKEN, BALE_API_BASE, AI_BASE_URL, AI_API_KEY, AI_STT_MODEL, AI_LLM_MODEL, BALE_ADMIN_CHAT_IDS, BALE_SERVICE_TOKEN.


How it's built

bot/
├── Dockerfile                  python:3.13-slim + ffmpeg (voice conversion)
├── app/
│   ├── main.py                 ensure tables → sweeper task → poll loop
│   ├── config.py               pydantic-settings, PROCUREMENT_BOT_ prefix
│   ├── bale/client.py          getUpdates/sendMessage/edit/callbacks/getFile+download
│   ├── api/client.py           ERP /api/v1 client (X-Actor-Id attribution)
│   ├── stt.py                  ffmpeg (OGG/Opus → MP3) → /audio/transcriptions
│   ├── nlu.py                  Persian intent+entity extraction (JSON mode)
│   ├── resolve.py              fuzzy name→id matching (difflib + ilike search)
│   ├── drafts.py               draft store + QuoteCreate payload builder + missing-field logic
│   ├── keyboards.py            inline keyboards (callback_data ≤ 64 B op-codes)
│   ├── texts.py                Persian message templates
│   ├── runtime.py              context, link store, in-memory pending flows
│   ├── models.py / db_init.py  bot_links / bot_drafts / bot_messages / bot_meta (idempotent DDL)
│   └── handlers/               dispatch, commands, callbacks, links,
│                               quote, supplier, material, queries, decisions
└── tools/smoke_test.py         offline end-to-end harness (fake Bale/ERP/AI, in-memory store)

Design notes:

  • Draft state lives in postgres (bot_drafts.payload JSONB in the shape of the eventual API request), so drafts survive bot restarts; short-lived interactions (linking, quote picking, accept/arrive confirmations) are in-memory with a 30-minute TTL.
  • Callback grammar fits Bale's 64-byte callback_data: q:ok:{draft8} (commit), pk:{draft8}:{field}:{idx} (pick candidate), ac:ts:{i} (choose payment terms), ar:full / ar:ok (delivery)…
  • Persian handling: Persian/Arabic digits are normalized before parsing; fuzzy matching folds ZWNJ and Arabic ya/ka; all user-facing text is Persian.
  • Voice notes arrive as OGG/Opus; ffmpeg converts to mono 16 kHz MP3 before hitting the STT endpoint (accepted by essentially all providers).
  • The accept flow echoes the termsheet's existing schedule_steps back to PATCH /quotes/{id}/accept (the API replaces steps with what you send, so the bot must send the current ones).

Attribution tweak in the api (backward compatible)

backend/app/utils/security.py::get_actor now prefers an X-Actor-Id header over the raw bearer token when attributing quote_events.actor. Requests without that header behave exactly as before.


Testing

Offline harness (no Bale token, no DB, no AI key — stubs everything and drives the real handler pipeline with fake updates):

docker compose run --rm bot python tools/smoke_test.py
# or, with the requirements installed locally:
cd bot && python tools/smoke_test.py

Covers: linking, voice→draft, ambiguous-material pick buttons, unit fallback, draft refinement, commit payload shape (temp_id cross-references), supplier / material commits, status digest, accept payload (steps echoed, per-item price), arrival quantities («۵۰۰ و ۲۰۰»), cancel and unknown-intent handling.

Troubleshooting

Symptom Likely cause
Bot silent wrong BALE_BOT_TOKEN; check docker compose logs bot
«پردازش هوشمند تنظیم نشده است» AI_BASE_URL / AI_API_KEY missing
Voice → «نتوانستم پیام صوتی را تبدیل کنم» provider rejected the model name (STT_MODEL) or key
«ERP API unreachable» api container not up / wrong API_BASE_URL
Draft disappears TTL expiry (DRAFT_TTL_HOURS) or someone pressed ❌ / /cancel