# 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)
