Files
nick-doc/10 - Services/amanat-assist.md
Siavash Sameni 67244223ec docs: add sub-project service docs + sync vault 2026-06-08
Add 10 - Services/ docs for all sub-projects: backend, frontend, scanner,
deployment (new), update amanat-assist. Update Scanner Architecture,
Telegram Mini App flow, and Activity Log. Add payment safety edge cases.
2026-06-08 16:23:00 +04:00

304 lines
12 KiB
Markdown

# amanat-assist — AI Request Assistant
**Status:** Live at `assist.amn.gg` (v1.1.0)
**Repo:** `/amanat-assist` (separate repo, no Amanat DB or internal-service access)
**Owner:** Amanat Platform
**PRD:** [PRD — AI Request Assistant Mini App](../PRD%20-%20AI%20Request%20Assistant%20Mini%20App.md)
---
## 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
```mermaid
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
1. Check for `?access_token=...` in URL (OAuth callback redirect from `dev.amn.gg`)
2. Check `localStorage` for a stored valid session (calls `/api/auth/me` to verify)
3. 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:
1. Extract ALL info from the user's message before asking anything
2. Ask at most **one question** at a time
3. 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
```typescript
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`)
```yaml
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-llm-proxy Docker image in-place (locally, never pushed)
- docker compose up -d (recreates llm-proxy container)
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/llm` calls to `amanat-llm-proxy` via an internal Next.js API route
- Uses the existing `dev.amn.gg` session (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.mjs` uses only Node.js built-ins (`http`, native `fetch`); 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 pull` will always fail (intentional — image is local-only)