- Update backend, frontend, scanner, deployment, amanat-assist service docs - Update System Overview, Scanner Architecture, Telegram Mini App flow - Update 10 - Services/README.md - Add Tenant data model, Tenant API reference, Tenant Storefront Flow - Add Multi-Shop Branch Project Scan (2026-06-10) - Add tenant.md service doc - Append activity log entry - Reflects archived/search/stats route fix and new E2E test suite Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
amanat-assist — AI Request Assistant
Status: Live at assist.amn.gg (v1.1.1)
Repo: /amanat-assist (separate repo, no Amanat DB or internal-service access)
Owner: Amanat Platform
PRD: PRD — AI Request Assistant Mini App
1. Overview
amanat-assist is a Telegram Mini App and standalone web app that guides buyers through creating a purchase request on the Amanat escrow marketplace using a conversational LLM interface. The user describes what they want in plain language; the assistant asks clarifying questions, suggests price and delivery windows, then with one tap posts the structured request to the Amanat backend.
The user never sees a form. The LLM handles categorisation, field normalisation, and the API call.
2. Architecture
┌─────────────────────────────────────────────────────────┐
│ Telegram / Browser │
│ assist.amn.gg (nginx, static React/Vite bundle) │
│ → auth (Telegram SSO or web redirect to dev.amn.gg) │
│ → UI: multi-turn chat, photo upload, review card │
└──────────────────────┬──────────────────────────────────┘
│ POST /api/llm
▼
┌─────────────────────────────────────────────────────────┐
│ amanat-llm-proxy (Node.js 18+, port 3001) │
│ Providers: Mistral → fallback DeepSeek on 429 │
│ Also: Kimi, OpenCode proxy │
│ API keys server-side only, never in browser │
└──────────────────────┬──────────────────────────────────┘
│ Bearer JWT
▼
┌─────────────────────────────────────────────────────────┐
│ Amanat Backend (api.amn.gg / dev.amn.gg) │
│ /api/auth/telegram /api/categories /api/requests │
└─────────────────────────────────────────────────────────┘
Docker Compose
| Service | Image | Container | Notes |
|---|---|---|---|
frontend |
nginx:alpine |
amanat-frontend |
Serves dist/ — static bundle |
llm-proxy |
Built from ./llm-proxy/ |
amanat-llm-proxy |
Port 3001 |
Both services join the external escrow-dev_default docker network (alias escrow_net).
3. Tech Stack
| Layer | Tech |
|---|---|
| Frontend | React 18, TypeScript, Vite 5 |
| Styling | CSS variables + Telegram theme tokens |
| LLM Proxy | Plain Node.js 18+ (http module, native fetch) — zero npm deps |
| State | React state machine + useSlotFilling hook |
| Persistence | localStorage via useChatSessions hook |
| Auth (Telegram) | window.Telegram.WebApp.initData → /api/auth/telegram |
| Auth (Web) | Redirect to dev.amn.gg → ?access_token=... callback |
| CI | Woodpecker CI on ARM64 agent co-located with assist.amn.gg |
4. State Machine
stateDiagram-v2
[*] --> INIT
INIT --> AUTH : Telegram initData present
INIT --> GREETING : Dev mode (skip auth)
INIT --> GREETING : Web — stored session valid
INIT --> GREETING : Web — OAuth callback received
AUTH --> GREETING : silentSSO success
AUTH --> ERROR : silentSSO failure
GREETING --> COLLECT : user sends first message
COLLECT --> COLLECT : LLM asks follow-up
COLLECT --> REVIEW : all required slots filled
REVIEW --> SUBMITTING : user taps Submit
REVIEW --> COLLECT : user taps Edit
SUBMITTING --> DONE : POST /api/requests 200
SUBMITTING --> ERROR : submit failed
ERROR --> AUTH : retry (Telegram)
ERROR --> GREETING : retry (dev/web)
COLLECT --> HISTORY : user taps History
HISTORY --> COLLECT : user loads session
HISTORY --> GREETING : new chat
5. Auth
5.1 Telegram Mini App (primary)
User opens bot
→ window.Telegram.WebApp.initData (injected by Telegram)
→ POST https://dev.amn.gg/api/auth/telegram
{ initData: "<raw string>", role: "buyer" }
← { data: { tokens: { accessToken, refreshToken }, user, isNewUser } }
→ Store accessToken in memory (not localStorage — ephemeral session)
On any 401, the app transparently POSTs /api/auth/refresh-token and retries.
5.2 Web Browser
- Check for
?access_token=...in URL (OAuth callback redirect fromdev.amn.gg) - Check
localStoragefor a stored valid session (calls/api/auth/meto verify) - If no session → redirect to
dev.amn.gg?redirect_uri=<current-origin>for login
5.3 Development Mode
Skips all auth, uses mock tokens + mock user.
6. LLM Service
6.1 Providers
| Provider | Model | Key env var | Notes |
|---|---|---|---|
mistral |
mistral-large-latest |
MISTRAL_API_KEY |
Primary |
mistral (vision) |
pixtral-12b-2409 |
MISTRAL_API_KEY |
Image analysis |
kimi |
moonshot-v1-8k |
KIMI_API_KEY |
Optional |
deepseek |
deepseek-chat |
DEEPSEEK_API_KEY |
Auto-fallback on 429 |
opencode |
claude-3-sonnet |
— | OpenCode local proxy |
6.2 Proxy API
POST /api/llm
Content-Type: application/json
{
"messages": [{ "role": "user", "content": "..." }, ...],
"provider": "mistral", // optional, defaults to mistral
"model": "mistral-large-latest" // optional
}
Response: { "content": "...", "model": "..." }
| { "content": "...", "model": "...", "fallback": true } // on auto-failover
6.3 Slot Filling
The system prompt instructs the LLM to:
-
Extract ALL info from the user's message before asking anything
-
Ask at most one question at a time
-
When all required slots are filled, output a fenced JSON block:
```request { "title": "...", "description": "...", "categoryId": "...", ... } ```
Required fields: title, description, categoryId, urgency, deliveryInfo.deliveryType
Optional: productLink, attachments[], budget{min,max,currency}, quantity, size, color
The app detects the ```request fence, parses the JSON, and transitions to REVIEW.
6.4 Vision (Image Upload)
Uses pixtral-12b-2409. The user uploads a photo; the LLM returns structured JSON with name, category, color, description, quantity. Result is merged into slots; categoryId is never set from vision (names aren't valid ObjectIds).
6.5 Price Suggestion
If slots.budget is unset at REVIEW time, the app calls the LLM with a structured price-suggestion prompt. Result tagged high/medium/low confidence; only high and medium are auto-applied.
7. Request Slots Schema
interface RequestSlots {
title?: string;
description?: string;
categoryId?: string; // must be a valid ObjectId from /api/categories
productLink?: string;
attachments?: string[]; // image URLs or base64 (base64 stripped before storage)
budget?: { min?: number; max?: number; currency: string };
urgency?: 'low' | 'medium' | 'high' | 'urgent';
quantity?: number;
size?: string;
color?: string;
deliveryInfo?: {
deliveryType: 'physical' | 'online';
email?: string;
};
}
8. Frontend Components
| Component | Description |
|---|---|
App.tsx |
State machine root — renders one screen per state |
ChatUI |
Scrollable message list + text/photo input + category chips |
ChatHistory |
localStorage-persisted past sessions list |
ReviewCard |
Final structured view of filled slots + Submit/Edit buttons |
AuthScreen |
Loading spinner shown during SSO |
ErrorScreen |
Error message + Retry button |
Hooks
| Hook | Description |
|---|---|
useSlotFilling |
Manages LLM conversation, slot extraction, greeting, session load |
useChatSessions |
Read/write/delete chat sessions from localStorage |
9. Amanat API Calls
| Method | Endpoint | Auth | Purpose |
|---|---|---|---|
POST |
/api/auth/telegram |
— | Exchange Telegram initData for JWT |
POST |
/api/auth/refresh-token |
— | Refresh expired access token |
GET |
/api/auth/me |
Bearer | Validate stored session |
GET |
/api/categories |
Bearer | Load category list for slot filling |
POST |
/api/requests |
Bearer | Submit completed purchase request |
All requests from src/services/api.ts use amanatApi() from auth.ts, which auto-refreshes on 401.
Submitted requests include aiGenerated: true.
10. Deployment
CI Pipeline (.woodpecker/ci.yml)
trigger: push/manual to main
agent: linux/arm64 (same host as assist.amn.gg)
steps:
1. build-frontend (node:22-alpine):
- npm ci + npm run build (Vite)
- Bakes VITE_ env vars into the static bundle at build time
2. deploy (docker:27-cli, docker socket volume-mounted — no registry push):
- Copy dist/ to /opt/amanat-assist/dist/ (nginx bind-mount)
- Sync docker-compose.yml to /opt/amanat-assist/
- Rebuild `amanat-assist-llm-proxy` Docker image in-place (locally, never pushed)
- docker compose up -d llm-proxy (recreates llm-proxy container only)
3. notify (node:22-alpine):
- Runs scripts/ci/tg-notify.cjs on success or failure
- Uses TG_TOKEN + TG_USERS secrets
Nginx picks up new static files from the bind-mount without restart. The proxy container is recreated with the new image.
Environment Variables
| Variable | Scope | Description |
|---|---|---|
VITE_AMANAT_API_BASE |
Frontend build-time | Backend URL (e.g. https://dev.amn.gg) |
VITE_LLM_PROVIDER |
Frontend build-time | Default LLM provider (mistral) |
VITE_LLM_API_URL |
Frontend build-time | Proxy URL (e.g. https://assist.amn.gg/api/llm) |
MISTRAL_API_KEY |
llm-proxy runtime | Mistral API key (server-side only) |
KIMI_API_KEY |
llm-proxy runtime | Optional Kimi API key |
DEEPSEEK_API_KEY |
llm-proxy runtime | Optional DeepSeek API key (auto-fallback) |
OPENCODE_PROXY_URL |
llm-proxy runtime | OpenCode local proxy URL (default http://127.0.0.1:3456) |
ALLOWED_ORIGINS |
llm-proxy runtime | CORS whitelist (comma-separated) |
PORT |
llm-proxy runtime | Port (default 3001) |
11. Integration with dev.amn.gg Frontend
The dev.amn.gg frontend (Next.js) includes a native AI Assistant page at /dashboard/assist that:
- Proxies
/api/llmcalls toamanat-llm-proxyvia an internal Next.js API route - Uses the existing
dev.amn.ggsession (no re-auth needed) - Allows buyers to start an AI-assisted request flow from within the main dashboard
- The "New Request" page includes a button to launch the AI assistant
See src/sections/assist/ in the frontend repo for the implementation.
12. Known Limitations / Open Items
- No voice input — text and photo only (MVP)
- Single-item only — one purchase request per conversation
- No post-submit editing — requests posted via the assistant cannot be edited through the assistant
- Session storage is local only — history lives in
localStorage, not synced to backend - Vision model not streaming — responses may feel slow for image analysis
- categoryId from vision disabled — vision returns category names, not ObjectIds; name→ID matching is left to the LLM in the follow-up turn
- llm-proxy is zero-dependency —
llm-proxy/index.mjsuses only Node.js built-ins (http, nativefetch); no npm packages. Logs rotate at 10 MB. - No registry push — CI builds the llm-proxy image directly on the host via a docker socket volume mount;
docker pullwill always fail (intentional — image is local-only) - Telegram theme override is --primary accent only — applying full Telegram theme tokens causes invisible text on cream backgrounds; only the primary accent colour is overridden from the Telegram theme
- iframe auth handoff — when embedded via iframe, auth is delivered via
access_token+user_jsonURL params; the app decodes the JWT client-side as a fallback when the backend/api/auth/mecall is not possible - slotsRef stale-closure guard — review submit uses a ref (
slotsRef) instead of state directly to avoid a stale-closure bug that could cause the wrong slot values to be submitted