Files
ERP/bot/README.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

121 lines
6.8 KiB
Markdown

# 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<T> │ 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: <users.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
```