docs: sync from backend 8fc2309 — M43/M44 missing FKs + H37 dispute enums
This commit is contained in:
296
10 - Services/amanat-assist.md
Normal file
296
10 - Services/amanat-assist.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# 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: npm ci + npm run build (Vite)
|
||||
2. deploy:
|
||||
- Copy dist/ to /opt/amanat-assist/dist/ (nginx bind-mount)
|
||||
- Rebuild amanat-llm-proxy Docker image in-place
|
||||
- docker compose up -d --no-deps llm-proxy
|
||||
3. notify: Telegram CI notification
|
||||
```
|
||||
|
||||
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 |
|
||||
| `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
|
||||
Reference in New Issue
Block a user