# procurement_ai_agent — Bale chatbot for the procurement ERP A **Bale messenger bot** that turns Persian **chat messages and voice notes** into procurement actions in the [procurement ERP](https://git.bennu-api-bahrestan.ir/ali_mohaghegh/ERP) — draft-first: it captures what it understood, tells you **what it did** and **what is still empty**, and **commits only after you approve** with a ✅ button. ``` you (Bale, text/voice) ┌─────────────────────────────────┐ │ getUpdates (long poll) │ procurement_ai_agent │ ▼ │ ffmpeg → STT (Persian) │ ┌──────────────────┐ POST {base}/bot │ LLM → intent + entities │ │ tapi.bale.ai │◄───────────────────────►│ fuzzy resolve → draft payload │ └──────────────────┘ sendMessage / buttons │ review + «چه چیزهایی خالی مونده»│ └───────────────┬─────────────────┘ │ HTTP + X-Actor-Id ▼ ┌─────────────────────────────────┐ │ ERP api (/api/v1) │ │ quotes · suppliers · materials│ └─────────────────────────────────┘ ``` ## What it can do | Activity | Example (voice or text) | Committed via | |---|---|---| | Register a supplier quote | «سازان نو برای پروژه برج آسمان ۵۰۰ کیلو میلگرد A3 سایز ۱۴، هر کیلو ۴۵ تومن، پرداخت ۳ فاز» | `POST /quotes` | | 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 1. Send a message or voice note. Voice → ffmpeg → STT; text/STT text → LLM extracts a typed **intent + entities** (JSON mode, Persian digits normalized). 2. Names are fuzzy-matched against the ERP. Several candidates → inline **pick buttons**; no match → listed as missing. 3. The bot replies with a **review card**: everything captured + a ✅/⚠️/❌ list of what is still empty. 4. Keep chatting to refine («قیمت میلگرد ۴۷ شد») — the open draft updates and re-renders. Drafts survive bot restarts (postgres-backed, TTL-swept). 5. **Nothing is written to the ERP until you tap ✅.** After commit you get the created quote id and the fields left empty. ### Team access & attribution Each member links their Bale chat once: `/start` → Casdoor username → password (validated via the ERP's real `POST /auth/login`, used once, never stored). Every mutating call carries `X-Actor-Id: ` so `quote_events.actor` records the **human** who asked. Commands: `/start` · `/help` · `/drafts` · `/cancel` · `/whoami` · `/unlink` ## Quick start (Docker) ```bash cp .env.example .env # fill PROCUREMENT_BOT_BOT_TOKEN + AI_API_KEY docker build -t procurement-ai-agent . docker run -d --name procurement-bot --env-file .env \ --add-host host.docker.internal:host-gateway \ procurement-ai-agent docker logs -f procurement-bot ``` Or as part of the ERP stack — see [`integration/`](integration/README.md) (compose service + the one tiny ERP-side patch). Full setup, env-var reference, flows and troubleshooting: **[docs/SETUP.md](docs/SETUP.md)**. ## Architecture ``` app/ ├── main.py ensure bot tables → sweeper task → getUpdates long-poll loop ├── config.py pydantic-settings, PROCUREMENT_BOT_ prefix ├── bale/client.py Telegram-compatible Bale API (poll, send, edit, buttons, files) ├── api/client.py ERP /api/v1 client (X-Actor-Id attribution, error envelope) ├── stt.py ffmpeg (OGG/Opus → MP3) → OpenAI-compatible /audio/transcriptions ├── nlu.py Persian intent+entity extraction (JSON-mode chat completions) ├── resolve.py fuzzy name→id matching (difflib over ilike search), TTL caches ├── 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 bot_links / bot_drafts / bot_messages / bot_meta (own schema) └── handlers/ dispatch · commands · callbacks · links · quote · supplier · material · queries · decisions ``` Design notes: - **Telegram-compatible Bale API** (`tapi.bale.ai`): getUpdates long polling — no public port, no webhook, resumable offset persisted across restarts. - **Draft-first**: `bot_drafts.payload` holds a talking-friendly JSON that is converted into the exact `POST /quotes` request body (temp_id cross-references) only on approval; the API deliberately tolerates incomplete quotes, so partial commits are explicit, never accidental. - **Accept flow echoes the termsheet's existing `schedule_steps`** back to `PATCH /quotes/{id}/accept` (the API replaces steps with what it receives). - **Persian-aware matching**: Persian/Arabic digit normalization, ZWNJ folding, Arabic ya/ka folding, difflib ranking on top of the API's ilike search. - **Voice notes** arrive as OGG/Opus; ffmpeg converts to mono 16 kHz MP3 before STT. The LLM provider and the STT provider are configured independently (e.g. DeepSeek for NLU + any whisper-compatible endpoint for voice). ## Testing An offline end-to-end harness stubs Bale/ERP/AI and drives the real handler pipeline with fake updates — no token, DB or API key needed: ```bash pip install -r requirements.txt python tools/smoke_test.py # → ALL PASSED: 48 passed ```