297 lines
11 KiB
Markdown
297 lines
11 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: 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
|