Spaces:
Running
Running
owlgebra-ai Claude Opus 4.6 (1M context) commited on
Commit ·
8c04fb6
1
Parent(s): 30ed904
Cart environment demo with real 29.6K product catalog
Browse files- CART-only interactive testing UI with modern Gradio theme
- Real stratified catalog (29,601 products, 6000 categories) with updated product types
- Prefers pre-built FAISS index, falls back to HF dataset
- Uses Alibaba-NLP/gte-modernbert-base (768-dim) embeddings
- Clean reward banner, persona display, tool execution panel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README.md +58 -5
- app.py +798 -0
- catalog.jsonl +0 -0
- policies.json +980 -0
- requirements.txt +9 -0
README.md
CHANGED
|
@@ -1,13 +1,66 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ShopRLVE Cart Environment
|
| 3 |
+
emoji: 🛒
|
| 4 |
+
colorFrom: green
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.0.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# ShopRLVE-GYM — Cart Environment Demo
|
| 14 |
+
|
| 15 |
+
Interactive demo for the Cart Building environment from ShopRLVE-GYM.
|
| 16 |
+
|
| 17 |
+
**You play the AI agent.** A persona-driven simulated customer asks you to find
|
| 18 |
+
and cart specific products. Use catalog search, cart tools, and chat to fulfill
|
| 19 |
+
the request, then submit your answer to see the verifiable reward breakdown.
|
| 20 |
+
|
| 21 |
+
## How it works
|
| 22 |
+
|
| 23 |
+
1. **Reset** an episode at your chosen difficulty (0-10)
|
| 24 |
+
2. Read the customer's request in the chat
|
| 25 |
+
3. Use **tools** (search, get_product, get_variants, cart.add, etc.)
|
| 26 |
+
4. **Chat** with the customer for clarification
|
| 27 |
+
5. **Submit** your answer with the product IDs in the cart
|
| 28 |
+
6. See the reward decomposition: r_task (75%), r_eff (15%), r_hall (10%)
|
| 29 |
+
|
| 30 |
+
## Data requirements
|
| 31 |
+
|
| 32 |
+
### Primary: Real Catalog (recommended)
|
| 33 |
+
|
| 34 |
+
The app prefers a **29.6K stratified real product catalog** with a pre-built
|
| 35 |
+
FAISS index using `Alibaba-NLP/gte-modernbert-base` (768-dim) embeddings.
|
| 36 |
+
|
| 37 |
+
Required files in `data/real_catalog_index/`:
|
| 38 |
+
|
| 39 |
+
| File | Size | Description |
|
| 40 |
+
|------|------|-------------|
|
| 41 |
+
| `catalog.jsonl` | 7.9 MB | 29,601 products (JSONL: id, title, category, brand, price, rating) |
|
| 42 |
+
| `index.faiss` | 87 MB | FAISS Flat index (768-dim, inner product) |
|
| 43 |
+
| `ids.txt` | 318 KB | Product ID list matching index order |
|
| 44 |
+
| `meta.json` | 138 B | Index metadata |
|
| 45 |
+
|
| 46 |
+
Build with: `python scripts/build_real_catalog_index.py`
|
| 47 |
+
|
| 48 |
+
Source: `data/real_product_catalog_stratified.jsonl` (29.6K products stratified
|
| 49 |
+
from 231K Amazebay catalog — 5 products per product type across 6000 categories).
|
| 50 |
+
|
| 51 |
+
### Fallback: HuggingFace Dataset
|
| 52 |
+
|
| 53 |
+
If the real catalog is not found, loads from `thebajajra/Amazebay-catalog` on
|
| 54 |
+
HuggingFace Hub (or a local Arrow dataset), limited to 5000 items. Builds a
|
| 55 |
+
FAISS Flat index at startup (~30s on CPU).
|
| 56 |
+
|
| 57 |
+
## Environment variables
|
| 58 |
+
|
| 59 |
+
| Variable | Default | Description |
|
| 60 |
+
|----------|---------|-------------|
|
| 61 |
+
| `REAL_CATALOG_PATH` | `data/real_catalog_index/catalog.jsonl` | Real catalog JSONL |
|
| 62 |
+
| `REAL_INDEX_DIR` | `data/real_catalog_index` | Directory with index.faiss + ids.txt |
|
| 63 |
+
| `CATALOG_PATH` | `data/amazebay-2M` | Fallback: HF dataset path |
|
| 64 |
+
| `CATALOG_MAX_ITEMS` | `5000` | Fallback: products to load |
|
| 65 |
+
| `EMBEDDING_MODEL` | `Alibaba-NLP/gte-modernbert-base` | Must match index embeddings |
|
| 66 |
+
| `EMBEDDING_DEVICE` | `cpu` | Device for query encoding |
|
app.py
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ShopRLVE-GYM · Cart Environment · Interactive Demo
|
| 3 |
+
|
| 4 |
+
Play as an AI shopping assistant. A persona-driven customer asks you
|
| 5 |
+
to find and cart products. Use catalog/cart tools, chat, then submit
|
| 6 |
+
your answer to see the verifiable reward breakdown.
|
| 7 |
+
|
| 8 |
+
Data requirements:
|
| 9 |
+
- Catalog: loaded from HuggingFace (thebajajra/Amazebay-catalog) or
|
| 10 |
+
local Arrow dataset (set CATALOG_PATH env var).
|
| 11 |
+
- FAISS index: built at startup from the loaded subset (~30s on CPU),
|
| 12 |
+
or loaded from disk if FAISS_INDEX_DIR is set and exists.
|
| 13 |
+
- Policies: policies.json bundled with the space.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
import logging
|
| 18 |
+
import os
|
| 19 |
+
import sys
|
| 20 |
+
import time
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
import gradio as gr
|
| 24 |
+
import numpy as np
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Path setup — find src/shop_rlve relative to this file
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
_app_dir = Path(__file__).resolve().parent
|
| 30 |
+
if (_app_dir / "src" / "shop_rlve").is_dir():
|
| 31 |
+
ROOT = _app_dir
|
| 32 |
+
else:
|
| 33 |
+
ROOT = _app_dir.parent
|
| 34 |
+
if not (ROOT / "src" / "shop_rlve").is_dir():
|
| 35 |
+
ROOT = ROOT.parent # two levels up
|
| 36 |
+
sys.path.insert(0, str(ROOT / "src"))
|
| 37 |
+
|
| 38 |
+
from shop_rlve.data.catalog_loader import load_catalog, load_real_catalog
|
| 39 |
+
from shop_rlve.server.openenv import ShopRLVEEnv
|
| 40 |
+
from shop_rlve.simulator.persona import PersonaWeights
|
| 41 |
+
|
| 42 |
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
| 43 |
+
logger = logging.getLogger("shoprlve-space")
|
| 44 |
+
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
# Configuration (override via env vars on HF Spaces)
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
# Real catalog: 29.6K stratified products from Amazebay with gte-modernbert embeddings
|
| 49 |
+
REAL_CATALOG_PATH = os.getenv(
|
| 50 |
+
"REAL_CATALOG_PATH",
|
| 51 |
+
str(ROOT / "data" / "real_catalog_index" / "catalog.jsonl"),
|
| 52 |
+
)
|
| 53 |
+
REAL_INDEX_DIR = os.getenv(
|
| 54 |
+
"REAL_INDEX_DIR",
|
| 55 |
+
str(ROOT / "data" / "real_catalog_index"),
|
| 56 |
+
)
|
| 57 |
+
# Fallback: original HF dataset (used if real catalog JSONL not found)
|
| 58 |
+
CATALOG_PATH = os.getenv("CATALOG_PATH", str(ROOT / "data" / "amazebay-2M"))
|
| 59 |
+
CATALOG_MAX_ITEMS = int(os.getenv("CATALOG_MAX_ITEMS", "5000"))
|
| 60 |
+
# Embedding model must match the index — gte-modernbert for real catalog
|
| 61 |
+
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "Alibaba-NLP/gte-modernbert-base")
|
| 62 |
+
EMBEDDING_DEVICE = os.getenv("EMBEDDING_DEVICE", "cpu")
|
| 63 |
+
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
# Global singleton
|
| 66 |
+
# ---------------------------------------------------------------------------
|
| 67 |
+
_env_singleton = None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _get_env() -> ShopRLVEEnv:
|
| 71 |
+
global _env_singleton
|
| 72 |
+
if _env_singleton is not None:
|
| 73 |
+
return _env_singleton
|
| 74 |
+
|
| 75 |
+
# Prefer real catalog (29.6K stratified products with pre-built FAISS index)
|
| 76 |
+
real_jsonl = Path(REAL_CATALOG_PATH)
|
| 77 |
+
real_index = Path(REAL_INDEX_DIR)
|
| 78 |
+
use_real = real_jsonl.is_file() and (real_index / "index.faiss").is_file()
|
| 79 |
+
|
| 80 |
+
if use_real:
|
| 81 |
+
logger.info("Loading real catalog from %s …", REAL_CATALOG_PATH)
|
| 82 |
+
products, variants = load_real_catalog(str(real_jsonl), seed=42)
|
| 83 |
+
logger.info("Loaded %d products, %d variants", len(products), len(variants))
|
| 84 |
+
faiss_path = str(real_index)
|
| 85 |
+
else:
|
| 86 |
+
logger.info(
|
| 87 |
+
"Real catalog not found at %s — falling back to HF dataset", REAL_CATALOG_PATH
|
| 88 |
+
)
|
| 89 |
+
products = load_catalog(CATALOG_PATH, max_items=CATALOG_MAX_ITEMS, seed=42)
|
| 90 |
+
variants = []
|
| 91 |
+
logger.info("Loaded %d products from HF dataset", len(products))
|
| 92 |
+
faiss_path = None
|
| 93 |
+
|
| 94 |
+
config = {
|
| 95 |
+
"embedding_model": EMBEDDING_MODEL,
|
| 96 |
+
"embedding_debug": False,
|
| 97 |
+
"embedding_device": EMBEDDING_DEVICE,
|
| 98 |
+
}
|
| 99 |
+
if faiss_path:
|
| 100 |
+
config["faiss_index_path"] = faiss_path
|
| 101 |
+
|
| 102 |
+
env = ShopRLVEEnv(
|
| 103 |
+
collection="C1", # force CART only
|
| 104 |
+
catalog=(products, variants),
|
| 105 |
+
config=config,
|
| 106 |
+
seed=int(time.time()) % 100_000,
|
| 107 |
+
)
|
| 108 |
+
_env_singleton = env
|
| 109 |
+
logger.info("Environment ready! (real_catalog=%s)", use_real)
|
| 110 |
+
return env
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# ---------------------------------------------------------------------------
|
| 114 |
+
# Tool definitions — CART-relevant subset
|
| 115 |
+
# ---------------------------------------------------------------------------
|
| 116 |
+
TOOLS: dict[str, dict] = {
|
| 117 |
+
"catalog.search": {
|
| 118 |
+
"icon": "search",
|
| 119 |
+
"description": "Search products by query and optional filters",
|
| 120 |
+
"args": {
|
| 121 |
+
"query": {"type": "str", "required": True, "description": "Search query"},
|
| 122 |
+
"top_k": {"type": "int", "required": False, "default": 20,
|
| 123 |
+
"description": "Number of results (1-500)"},
|
| 124 |
+
"filters": {"type": "json", "required": False, "default": None,
|
| 125 |
+
"description": 'Filters JSON e.g. {"price_max":50}'},
|
| 126 |
+
},
|
| 127 |
+
},
|
| 128 |
+
"catalog.rerank": {
|
| 129 |
+
"icon": "sort",
|
| 130 |
+
"description": "Re-rank candidates by query relevance",
|
| 131 |
+
"args": {
|
| 132 |
+
"query": {"type": "str", "required": True,
|
| 133 |
+
"description": "Reranking query"},
|
| 134 |
+
"candidate_product_ids": {"type": "list", "required": True,
|
| 135 |
+
"description": "Comma-separated product IDs"},
|
| 136 |
+
"top_k": {"type": "int", "required": False, "default": 10,
|
| 137 |
+
"description": "Results to return"},
|
| 138 |
+
},
|
| 139 |
+
},
|
| 140 |
+
"catalog.get_product": {
|
| 141 |
+
"icon": "package",
|
| 142 |
+
"description": "Get full product details by ID",
|
| 143 |
+
"args": {"product_id": {"type": "str", "required": True,
|
| 144 |
+
"description": "Product ID"}},
|
| 145 |
+
},
|
| 146 |
+
"catalog.get_variants": {
|
| 147 |
+
"icon": "layers",
|
| 148 |
+
"description": "Get colour/size variants for a product",
|
| 149 |
+
"args": {"product_id": {"type": "str", "required": True,
|
| 150 |
+
"description": "Product ID"}},
|
| 151 |
+
},
|
| 152 |
+
"cart.view": {
|
| 153 |
+
"icon": "shopping-cart",
|
| 154 |
+
"description": "View current cart contents",
|
| 155 |
+
"args": {},
|
| 156 |
+
},
|
| 157 |
+
"cart.add": {
|
| 158 |
+
"icon": "plus-circle",
|
| 159 |
+
"description": "Add product to cart",
|
| 160 |
+
"args": {
|
| 161 |
+
"product_id": {"type": "str", "required": True,
|
| 162 |
+
"description": "Product ID"},
|
| 163 |
+
"variant_id": {"type": "str", "required": False, "default": None,
|
| 164 |
+
"description": "Variant ID (if specific variant needed)"},
|
| 165 |
+
"quantity": {"type": "int", "required": False, "default": 1,
|
| 166 |
+
"description": "Quantity"},
|
| 167 |
+
},
|
| 168 |
+
},
|
| 169 |
+
"cart.remove": {
|
| 170 |
+
"icon": "trash-2",
|
| 171 |
+
"description": "Remove a cart line",
|
| 172 |
+
"args": {"line_id": {"type": "str", "required": True,
|
| 173 |
+
"description": "Cart line ID"}},
|
| 174 |
+
},
|
| 175 |
+
"cart.set_quantity": {
|
| 176 |
+
"icon": "edit-3",
|
| 177 |
+
"description": "Set quantity for a cart line",
|
| 178 |
+
"args": {
|
| 179 |
+
"line_id": {"type": "str", "required": True,
|
| 180 |
+
"description": "Cart line ID"},
|
| 181 |
+
"quantity": {"type": "int", "required": True,
|
| 182 |
+
"description": "New quantity (0 = remove)"},
|
| 183 |
+
},
|
| 184 |
+
},
|
| 185 |
+
"user.get_visit_history": {
|
| 186 |
+
"icon": "clock",
|
| 187 |
+
"description": "Get customer's recently viewed products",
|
| 188 |
+
"args": {},
|
| 189 |
+
},
|
| 190 |
+
"datetime.now": {
|
| 191 |
+
"icon": "calendar",
|
| 192 |
+
"description": "Get current date and time",
|
| 193 |
+
"args": {},
|
| 194 |
+
},
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# ---------------------------------------------------------------------------
|
| 199 |
+
# Session state
|
| 200 |
+
# ---------------------------------------------------------------------------
|
| 201 |
+
class SessionState:
|
| 202 |
+
def __init__(self):
|
| 203 |
+
self.env: ShopRLVEEnv | None = None
|
| 204 |
+
self.obs = None
|
| 205 |
+
self.done = False
|
| 206 |
+
self.reward = 0.0
|
| 207 |
+
self.conversation: list[dict] = []
|
| 208 |
+
self.tool_history: list[str] = []
|
| 209 |
+
self.turn = 0
|
| 210 |
+
self.difficulty: int = 3
|
| 211 |
+
self.persona_weights: PersonaWeights | None = None
|
| 212 |
+
self.goal_params: dict = {}
|
| 213 |
+
self.episode_info: dict = {}
|
| 214 |
+
self.hidden_goal_md: str = ""
|
| 215 |
+
self.initial_msg_source: str = "unknown"
|
| 216 |
+
self.user_sim_log: list[str] = []
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# ---------------------------------------------------------------------------
|
| 220 |
+
# CSS theme
|
| 221 |
+
# ---------------------------------------------------------------------------
|
| 222 |
+
CUSTOM_CSS = """
|
| 223 |
+
/* Global */
|
| 224 |
+
.gradio-container { max-width: 1440px !important; }
|
| 225 |
+
|
| 226 |
+
/* Reward banner */
|
| 227 |
+
.reward-banner {
|
| 228 |
+
border-radius: 16px; padding: 28px 24px; margin: 8px 0;
|
| 229 |
+
text-align: center; backdrop-filter: blur(8px);
|
| 230 |
+
border: 2px solid; transition: all 0.3s ease;
|
| 231 |
+
}
|
| 232 |
+
.reward-score { font-size: 52px; font-weight: 800; letter-spacing: -2px; }
|
| 233 |
+
.reward-label { font-size: 18px; margin: 4px 0 12px; font-weight: 600; }
|
| 234 |
+
.reward-grid {
|
| 235 |
+
display: flex; justify-content: center; gap: 36px;
|
| 236 |
+
flex-wrap: wrap; margin-top: 8px;
|
| 237 |
+
}
|
| 238 |
+
.reward-cell-label { font-size: 11px; text-transform: uppercase;
|
| 239 |
+
letter-spacing: 1px; opacity: 0.6; }
|
| 240 |
+
.reward-cell-value { font-size: 26px; font-weight: 700; }
|
| 241 |
+
|
| 242 |
+
/* Goal panel */
|
| 243 |
+
.goal-panel { font-size: 13px; line-height: 1.6; }
|
| 244 |
+
.goal-panel table { width: 100%; }
|
| 245 |
+
.goal-panel th, .goal-panel td { padding: 4px 8px; }
|
| 246 |
+
|
| 247 |
+
/* Episode sidebar */
|
| 248 |
+
.episode-stat { font-variant-numeric: tabular-nums; }
|
| 249 |
+
|
| 250 |
+
/* Tool output */
|
| 251 |
+
.tool-output { font-size: 13px; }
|
| 252 |
+
.tool-output pre { max-height: 300px; overflow-y: auto; }
|
| 253 |
+
|
| 254 |
+
/* Chat */
|
| 255 |
+
.chat-area { min-height: 400px; }
|
| 256 |
+
|
| 257 |
+
/* Persona bars */
|
| 258 |
+
.persona-bar { font-family: monospace; font-size: 13px; line-height: 1.8; }
|
| 259 |
+
|
| 260 |
+
/* Tool arg labels */
|
| 261 |
+
.tool-arg-label { font-size: 12px; font-weight: 600; }
|
| 262 |
+
"""
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# ---------------------------------------------------------------------------
|
| 266 |
+
# Formatting helpers
|
| 267 |
+
# ---------------------------------------------------------------------------
|
| 268 |
+
|
| 269 |
+
def _fmt_tool_result(result: dict) -> str:
|
| 270 |
+
name = result.get("name", "?")
|
| 271 |
+
error = result.get("error")
|
| 272 |
+
data = result.get("result")
|
| 273 |
+
ms = result.get("duration_ms", 0)
|
| 274 |
+
|
| 275 |
+
if error:
|
| 276 |
+
return f"**{name}** `{ms:.0f}ms` — Error: {error}"
|
| 277 |
+
|
| 278 |
+
if name == "datetime.now" and isinstance(data, dict):
|
| 279 |
+
return (f"**{name}** `{ms:.0f}ms`\n"
|
| 280 |
+
f"> {data.get('date','?')} ({data.get('day_of_week','?')}) "
|
| 281 |
+
f"{data.get('time','?')}")
|
| 282 |
+
|
| 283 |
+
if isinstance(data, list):
|
| 284 |
+
lines = [f"**{name}** `{ms:.0f}ms` — {len(data)} results"]
|
| 285 |
+
for i, item in enumerate(data[:15]):
|
| 286 |
+
if isinstance(item, dict):
|
| 287 |
+
title = item.get("title", item.get("product_title", "?"))[:60]
|
| 288 |
+
pid = item.get("product_id", item.get("id", "?"))
|
| 289 |
+
price = item.get("price", item.get("unit_price", ""))
|
| 290 |
+
rating = item.get("rating", "")
|
| 291 |
+
qty = item.get("qty", "")
|
| 292 |
+
parts = [f"`{i+1}.` **{title}**"]
|
| 293 |
+
if price:
|
| 294 |
+
parts.append(f"${price}")
|
| 295 |
+
if rating:
|
| 296 |
+
parts.append(f"{rating}")
|
| 297 |
+
if qty:
|
| 298 |
+
parts.append(f"x{qty}")
|
| 299 |
+
parts.append(f"`{pid}`")
|
| 300 |
+
lines.append(" " + " · ".join(parts))
|
| 301 |
+
if len(data) > 15:
|
| 302 |
+
lines.append(f" *… +{len(data)-15} more*")
|
| 303 |
+
return "\n".join(lines)
|
| 304 |
+
|
| 305 |
+
if isinstance(data, dict):
|
| 306 |
+
return (f"**{name}** `{ms:.0f}ms`\n"
|
| 307 |
+
f"```json\n{json.dumps(data, indent=2, default=str)[:1200]}\n```")
|
| 308 |
+
|
| 309 |
+
return f"**{name}** `{ms:.0f}ms` — {str(data)[:400]}"
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _fmt_reward_banner(reward: float, info: dict) -> str:
|
| 313 |
+
rb = info.get("reward_breakdown", {}) or {}
|
| 314 |
+
is_correct = rb.get("is_correct", False)
|
| 315 |
+
r_task = rb.get("r_task", 0)
|
| 316 |
+
r_eff = rb.get("r_eff", 0)
|
| 317 |
+
r_hall = rb.get("r_hall", 0)
|
| 318 |
+
fmt_ok = rb.get("format_valid", True)
|
| 319 |
+
tool_ok = rb.get("tool_valid", True)
|
| 320 |
+
|
| 321 |
+
if reward >= 0.8:
|
| 322 |
+
bg, border, fg = "#0d9488", "#14b8a6", "#f0fdfa" # teal
|
| 323 |
+
label = "EXCELLENT"
|
| 324 |
+
elif reward >= 0.5:
|
| 325 |
+
bg, border, fg = "#059669", "#10b981", "#ecfdf5" # emerald
|
| 326 |
+
label = "GOOD"
|
| 327 |
+
elif reward >= 0.2:
|
| 328 |
+
bg, border, fg = "#d97706", "#f59e0b", "#fffbeb" # amber
|
| 329 |
+
label = "FAIR"
|
| 330 |
+
elif reward >= 0:
|
| 331 |
+
bg, border, fg = "#ea580c", "#f97316", "#fff7ed" # orange
|
| 332 |
+
label = "POOR"
|
| 333 |
+
else:
|
| 334 |
+
bg, border, fg = "#dc2626", "#ef4444", "#fef2f2" # red
|
| 335 |
+
label = "FAILED"
|
| 336 |
+
|
| 337 |
+
correct_html = (
|
| 338 |
+
'<span style="color:#10b981;">CORRECT</span>' if is_correct
|
| 339 |
+
else '<span style="color:#ef4444;">INCORRECT</span>'
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
html = f"""
|
| 343 |
+
<div class="reward-banner" style="background:linear-gradient(145deg,{bg}18,{bg}30);
|
| 344 |
+
border-color:{border};">
|
| 345 |
+
<div class="reward-score" style="color:{border};">{reward:+.4f}</div>
|
| 346 |
+
<div class="reward-label" style="color:{border};">{label} · {correct_html}</div>
|
| 347 |
+
<hr style="border-color:{border}30;margin:14px 0;">
|
| 348 |
+
<div class="reward-grid">
|
| 349 |
+
<div>
|
| 350 |
+
<div class="reward-cell-label" style="color:{fg}99;">r_task (75%)</div>
|
| 351 |
+
<div class="reward-cell-value" style="color:{border};">{r_task:+.4f}</div>
|
| 352 |
+
</div>
|
| 353 |
+
<div>
|
| 354 |
+
<div class="reward-cell-label" style="color:{fg}99;">r_eff (15%)</div>
|
| 355 |
+
<div class="reward-cell-value" style="color:{border};">{r_eff:+.4f}</div>
|
| 356 |
+
</div>
|
| 357 |
+
<div>
|
| 358 |
+
<div class="reward-cell-label" style="color:{fg}99;">r_hall (10%)</div>
|
| 359 |
+
<div class="reward-cell-value" style="color:{border};">{r_hall:+.4f}</div>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
<div style="margin-top:14px;font-size:12px;opacity:0.5;">
|
| 363 |
+
Format {'Pass' if fmt_ok else 'Fail'} ·
|
| 364 |
+
Tools {'Pass' if tool_ok else 'Fail'}
|
| 365 |
+
</div>
|
| 366 |
+
</div>"""
|
| 367 |
+
|
| 368 |
+
details = rb.get("details", {})
|
| 369 |
+
if details:
|
| 370 |
+
html += "\n\n| Metric | Value |\n|--------|-------|\n"
|
| 371 |
+
for k, v in details.items():
|
| 372 |
+
html += f"| {k} | {v:.4f if isinstance(v, float) else v} |\n"
|
| 373 |
+
return html
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def _fmt_hidden_goal(ep_state) -> str:
|
| 377 |
+
if not ep_state or not ep_state.hidden_goal:
|
| 378 |
+
return "*Reset to generate a goal.*"
|
| 379 |
+
problem = ep_state.hidden_goal
|
| 380 |
+
extra = problem.extra or {}
|
| 381 |
+
|
| 382 |
+
lines = ["#### Target Cart"]
|
| 383 |
+
items = extra.get("item_details", [])
|
| 384 |
+
visit = extra.get("visit_history", [])
|
| 385 |
+
|
| 386 |
+
if items:
|
| 387 |
+
lines += ["", "| # | Product | Qty | Variant | ID |",
|
| 388 |
+
"|--:|---------|----:|---------|:---|"]
|
| 389 |
+
for i, d in enumerate(items, 1):
|
| 390 |
+
desc_parts = d.get("description", [])
|
| 391 |
+
desc_str = ", ".join(str(p) for p in desc_parts[:3]) if desc_parts else "—"
|
| 392 |
+
variant = d.get("variant_desc", "—")
|
| 393 |
+
lines.append(
|
| 394 |
+
f"| {i} | {desc_str[:45]} | {d.get('qty', 1)} "
|
| 395 |
+
f"| {variant} | `{d.get('product_id', '?')}` |"
|
| 396 |
+
)
|
| 397 |
+
lines.append("")
|
| 398 |
+
lines.append("**Hidden titles:**")
|
| 399 |
+
for i, d in enumerate(items, 1):
|
| 400 |
+
lines.append(f"{i}. {d.get('title', '?')[:65]}")
|
| 401 |
+
else:
|
| 402 |
+
lines.append("*No item details available.*")
|
| 403 |
+
|
| 404 |
+
if visit:
|
| 405 |
+
n_dist = max(0, len(visit) - len(items))
|
| 406 |
+
lines.append(f"\n**Visit history:** {len(visit)} items "
|
| 407 |
+
f"({len(items)} targets + {n_dist} distractors)")
|
| 408 |
+
|
| 409 |
+
return "\n".join(lines)
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def _fmt_episode(session: SessionState) -> str:
|
| 413 |
+
ep = session.env.get_episode_state() if session.env else None
|
| 414 |
+
extra = ep.hidden_goal.extra if ep and ep.hidden_goal else {}
|
| 415 |
+
t_max = extra.get("T_max", 14)
|
| 416 |
+
p_miss = extra.get("p_missing", 0)
|
| 417 |
+
p_noise = extra.get("p_noise", 0)
|
| 418 |
+
n_cart = len(ep.cart.lines) if ep else 0
|
| 419 |
+
n_seen = len(ep.seen_product_ids) if ep else 0
|
| 420 |
+
status = "Done" if session.done else "Active"
|
| 421 |
+
|
| 422 |
+
return f"""| | |
|
| 423 |
+
|---|---|
|
| 424 |
+
| **Difficulty** | {session.difficulty} / 10 |
|
| 425 |
+
| **Turn** | {session.turn} / {t_max} |
|
| 426 |
+
| **Status** | {status} |
|
| 427 |
+
| **Cart lines** | {n_cart} |
|
| 428 |
+
| **Seen products** | {n_seen} |
|
| 429 |
+
| **Tool calls** | {len(session.tool_history)} |
|
| 430 |
+
| **p_missing** | {p_miss:.2f} |
|
| 431 |
+
| **p_noise** | {p_noise:.3f} |"""
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def _fmt_chat(session: SessionState) -> list[dict]:
|
| 435 |
+
out = []
|
| 436 |
+
for m in session.conversation:
|
| 437 |
+
role = m.get("role", "user")
|
| 438 |
+
content = m.get("content", "")
|
| 439 |
+
out.append({"role": "user" if role == "user" else "assistant",
|
| 440 |
+
"content": content})
|
| 441 |
+
return out
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def _fmt_persona(w: PersonaWeights | None) -> str:
|
| 445 |
+
if w is None:
|
| 446 |
+
return "*Click Reset to start.*"
|
| 447 |
+
dims = [
|
| 448 |
+
("Price", w.w_price),
|
| 449 |
+
("Rating", w.w_rating),
|
| 450 |
+
("Shipping", w.w_ship),
|
| 451 |
+
("Brand", w.w_brand),
|
| 452 |
+
("Similarity", w.w_similarity),
|
| 453 |
+
]
|
| 454 |
+
lines = []
|
| 455 |
+
for label, val in dims:
|
| 456 |
+
filled = int(val * 20)
|
| 457 |
+
bar = "█" * filled + "░" * (20 - filled)
|
| 458 |
+
lines.append(f"**{label:>10}** `{bar}` {val:.2f}")
|
| 459 |
+
return "\n".join(lines)
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def _fmt_verbalization(session: SessionState) -> str:
|
| 463 |
+
src = session.initial_msg_source
|
| 464 |
+
src_label = {"llm": "LLM (Ollama)", "template": "Template"}.get(src, "—")
|
| 465 |
+
lines = [f"**Initial message:** {src_label}", ""]
|
| 466 |
+
if session.user_sim_log:
|
| 467 |
+
lines.append("**Response log:**")
|
| 468 |
+
for entry in session.user_sim_log[-10:]:
|
| 469 |
+
lines.append(f"- {entry}")
|
| 470 |
+
return "\n".join(lines)
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
# ---------------------------------------------------------------------------
|
| 474 |
+
# Core actions
|
| 475 |
+
# ---------------------------------------------------------------------------
|
| 476 |
+
|
| 477 |
+
def reset_episode(state, sel_diff):
|
| 478 |
+
env = _get_env()
|
| 479 |
+
s = SessionState()
|
| 480 |
+
s.env = env
|
| 481 |
+
diff = int(sel_diff)
|
| 482 |
+
s.difficulty = diff
|
| 483 |
+
|
| 484 |
+
obs = env.reset(env_id="CART", difficulty=diff)
|
| 485 |
+
s.obs, s.turn, s.conversation = obs, obs.turn, list(obs.conversation)
|
| 486 |
+
|
| 487 |
+
ep = env.get_episode_state()
|
| 488 |
+
if ep:
|
| 489 |
+
s.persona_weights = ep.persona_weights
|
| 490 |
+
s.goal_params = env._extract_goal_params(ep.hidden_goal, ep.env_id)
|
| 491 |
+
s.hidden_goal_md = _fmt_hidden_goal(ep)
|
| 492 |
+
s.initial_msg_source = "llm"
|
| 493 |
+
s.user_sim_log.append(f"Initial: LLM verbalization")
|
| 494 |
+
|
| 495 |
+
return (
|
| 496 |
+
s, # state
|
| 497 |
+
_fmt_chat(s), # chatbot
|
| 498 |
+
_fmt_persona(s.persona_weights), # persona_md
|
| 499 |
+
_fmt_episode(s), # episode_md
|
| 500 |
+
"", # reward_display
|
| 501 |
+
s.hidden_goal_md, # goal_md
|
| 502 |
+
_fmt_verbalization(s), # verb_md
|
| 503 |
+
"", # tool_out
|
| 504 |
+
gr.update(interactive=True), # exec_btn
|
| 505 |
+
gr.update(interactive=True), # asst_in
|
| 506 |
+
gr.update(interactive=True), # send_btn
|
| 507 |
+
gr.update(interactive=True), # done_btn
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
def execute_tool(state, tool_name, a1, a2, a3, a4, a5):
|
| 512 |
+
if state is None or state.done:
|
| 513 |
+
return state, [], "Reset first.", "", "", "", "", ""
|
| 514 |
+
env = state.env or _get_env()
|
| 515 |
+
|
| 516 |
+
# Parse args from the UI textboxes
|
| 517 |
+
args = {}
|
| 518 |
+
tdef = TOOLS.get(tool_name, {})
|
| 519 |
+
adefs = tdef.get("args", {})
|
| 520 |
+
vals = [a1, a2, a3, a4, a5]
|
| 521 |
+
for (aname, adef), v in zip(adefs.items(), vals):
|
| 522 |
+
if v is None or v == "":
|
| 523 |
+
if adef.get("required"):
|
| 524 |
+
return (state, _fmt_chat(state),
|
| 525 |
+
f"Required argument: `{aname}`", _fmt_episode(state),
|
| 526 |
+
"", state.hidden_goal_md, _fmt_verbalization(state), "")
|
| 527 |
+
continue
|
| 528 |
+
t = adef.get("type", "str")
|
| 529 |
+
if t == "int":
|
| 530 |
+
try:
|
| 531 |
+
args[aname] = int(v)
|
| 532 |
+
except ValueError:
|
| 533 |
+
args[aname] = adef.get("default", 10)
|
| 534 |
+
elif t == "json":
|
| 535 |
+
try:
|
| 536 |
+
args[aname] = json.loads(v) if v else None
|
| 537 |
+
except json.JSONDecodeError:
|
| 538 |
+
args[aname] = None
|
| 539 |
+
elif t == "list":
|
| 540 |
+
args[aname] = [x.strip() for x in v.split(",") if x.strip()]
|
| 541 |
+
else:
|
| 542 |
+
args[aname] = str(v)
|
| 543 |
+
|
| 544 |
+
action = json.dumps({
|
| 545 |
+
"assistant_message": f"[tool: {tool_name}]",
|
| 546 |
+
"tool_calls": [{"name": tool_name, "args": args}],
|
| 547 |
+
})
|
| 548 |
+
obs, reward, done, info = env.step(action)
|
| 549 |
+
state.obs, state.turn, state.done = obs, obs.turn, done
|
| 550 |
+
state.reward, state.conversation = reward, list(obs.conversation)
|
| 551 |
+
state.episode_info = info
|
| 552 |
+
|
| 553 |
+
if not done and obs.conversation and obs.conversation[-1].get("role") == "user":
|
| 554 |
+
state.user_sim_log.append(f"T{state.turn}: LLM response")
|
| 555 |
+
|
| 556 |
+
tout = ""
|
| 557 |
+
if obs.tool_results:
|
| 558 |
+
for tr in obs.tool_results:
|
| 559 |
+
tout += _fmt_tool_result(tr) + "\n\n"
|
| 560 |
+
state.tool_history.append(tool_name)
|
| 561 |
+
|
| 562 |
+
rmd = _fmt_reward_banner(reward, info) if done else ""
|
| 563 |
+
return (state, _fmt_chat(state), tout or "No output.",
|
| 564 |
+
_fmt_episode(state), rmd, state.hidden_goal_md,
|
| 565 |
+
_fmt_verbalization(state), "")
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def submit_response(state, msg):
|
| 569 |
+
if state is None or state.done:
|
| 570 |
+
return state, [], "", "", "", "", "", ""
|
| 571 |
+
if not msg.strip():
|
| 572 |
+
return (state, _fmt_chat(state), "Write a message first.",
|
| 573 |
+
_fmt_episode(state), "", state.hidden_goal_md,
|
| 574 |
+
_fmt_verbalization(state), "")
|
| 575 |
+
|
| 576 |
+
env = state.env or _get_env()
|
| 577 |
+
action = json.dumps({"assistant_message": msg, "tool_calls": []})
|
| 578 |
+
obs, reward, done, info = env.step(action)
|
| 579 |
+
state.obs, state.turn, state.done = obs, obs.turn, done
|
| 580 |
+
state.reward, state.conversation = reward, list(obs.conversation)
|
| 581 |
+
state.episode_info = info
|
| 582 |
+
|
| 583 |
+
if not done and obs.conversation and obs.conversation[-1].get("role") == "user":
|
| 584 |
+
state.user_sim_log.append(f"T{state.turn}: LLM response")
|
| 585 |
+
|
| 586 |
+
rmd = _fmt_reward_banner(reward, info) if done else ""
|
| 587 |
+
return (state, _fmt_chat(state), "", _fmt_episode(state),
|
| 588 |
+
rmd, state.hidden_goal_md, _fmt_verbalization(state), "")
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
def submit_answer(state, msg, pids):
|
| 592 |
+
if state is None or state.done:
|
| 593 |
+
return state, [], "", "", "Episode already ended.", "", "", ""
|
| 594 |
+
|
| 595 |
+
env = state.env or _get_env()
|
| 596 |
+
ids = [x.strip() for x in pids.split(",") if x.strip()] if pids else []
|
| 597 |
+
|
| 598 |
+
answer = {"env": "CART", "done": True, "recommended_product_ids": ids}
|
| 599 |
+
action = json.dumps({
|
| 600 |
+
"assistant_message": msg or "Here is my final cart.",
|
| 601 |
+
"tool_calls": [],
|
| 602 |
+
"answer": answer,
|
| 603 |
+
})
|
| 604 |
+
obs, reward, done, info = env.step(action)
|
| 605 |
+
state.obs, state.turn, state.done = obs, obs.turn, done
|
| 606 |
+
state.reward, state.conversation = reward, list(obs.conversation)
|
| 607 |
+
state.episode_info = info
|
| 608 |
+
|
| 609 |
+
return (state, _fmt_chat(state), "", _fmt_episode(state),
|
| 610 |
+
_fmt_reward_banner(reward, info), state.hidden_goal_md,
|
| 611 |
+
_fmt_verbalization(state), "")
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def update_tool_args(tool_name):
|
| 615 |
+
tdef = TOOLS.get(tool_name, {})
|
| 616 |
+
adefs = tdef.get("args", {})
|
| 617 |
+
names = list(adefs.keys())
|
| 618 |
+
updates = []
|
| 619 |
+
for i in range(5):
|
| 620 |
+
if i < len(names):
|
| 621 |
+
n = names[i]
|
| 622 |
+
d = adefs[n]
|
| 623 |
+
req = " *" if d.get("required") else f" (default: {d.get('default', '')})"
|
| 624 |
+
updates.append(gr.update(
|
| 625 |
+
label=f"{n}{req}", placeholder=d.get("description", ""),
|
| 626 |
+
value="", visible=True,
|
| 627 |
+
))
|
| 628 |
+
else:
|
| 629 |
+
updates.append(gr.update(label=f"arg{i+1}", value="", visible=False))
|
| 630 |
+
|
| 631 |
+
# Build description markdown
|
| 632 |
+
dl = [f"**{tool_name}** — {tdef.get('description', '')}"]
|
| 633 |
+
if adefs:
|
| 634 |
+
for an, ad in adefs.items():
|
| 635 |
+
r = "required" if ad.get("required") else f"default={ad.get('default')}"
|
| 636 |
+
dl.append(f"- `{an}` ({r}) — {ad.get('description', '')}")
|
| 637 |
+
else:
|
| 638 |
+
dl.append("*No arguments — click Execute.*")
|
| 639 |
+
return (*updates, gr.update(value="\n".join(dl)))
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
# ---------------------------------------------------------------------------
|
| 643 |
+
# Gradio UI
|
| 644 |
+
# ---------------------------------------------------------------------------
|
| 645 |
+
|
| 646 |
+
HEADER_MD = """
|
| 647 |
+
<div style="text-align:center;padding:12px 0 4px;">
|
| 648 |
+
<h1 style="font-size:28px;font-weight:800;margin:0;">
|
| 649 |
+
ShopRLVE-GYM · Cart Environment
|
| 650 |
+
</h1>
|
| 651 |
+
<p style="color:#6b7280;margin:4px 0 0;font-size:14px;">
|
| 652 |
+
You are the AI agent. A simulated customer asks for help building their cart.<br>
|
| 653 |
+
Use tools to search & add products, chat with the customer, then submit your answer.
|
| 654 |
+
</p>
|
| 655 |
+
</div>
|
| 656 |
+
"""
|
| 657 |
+
|
| 658 |
+
with gr.Blocks(
|
| 659 |
+
title="ShopRLVE Cart",
|
| 660 |
+
css=CUSTOM_CSS,
|
| 661 |
+
theme=gr.themes.Base(
|
| 662 |
+
primary_hue=gr.themes.colors.teal,
|
| 663 |
+
secondary_hue=gr.themes.colors.slate,
|
| 664 |
+
neutral_hue=gr.themes.colors.gray,
|
| 665 |
+
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
|
| 666 |
+
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
|
| 667 |
+
),
|
| 668 |
+
) as demo:
|
| 669 |
+
session_state = gr.State(value=None)
|
| 670 |
+
gr.HTML(HEADER_MD)
|
| 671 |
+
|
| 672 |
+
# ── Reward banner (full width, top) ──
|
| 673 |
+
reward_display = gr.HTML(value="")
|
| 674 |
+
|
| 675 |
+
with gr.Row(equal_height=False):
|
| 676 |
+
# ═════════��═════════════════════════
|
| 677 |
+
# LEFT SIDEBAR — Controls & Info
|
| 678 |
+
# ═══════════════════════════════════
|
| 679 |
+
with gr.Column(scale=1, min_width=280):
|
| 680 |
+
gr.Markdown("### Controls")
|
| 681 |
+
diff_sl = gr.Slider(
|
| 682 |
+
0, 10, step=1, value=3, label="Difficulty",
|
| 683 |
+
info="Higher = more items, variants, noise",
|
| 684 |
+
)
|
| 685 |
+
reset_btn = gr.Button(
|
| 686 |
+
"Reset Episode", variant="primary", size="lg",
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
gr.Markdown("### Episode")
|
| 690 |
+
episode_md = gr.Markdown("*Reset to start.*")
|
| 691 |
+
|
| 692 |
+
with gr.Accordion("Persona Weights", open=False):
|
| 693 |
+
persona_md = gr.Markdown("*Reset to start.*", elem_classes=["persona-bar"])
|
| 694 |
+
|
| 695 |
+
with gr.Accordion("Verbalization", open=False):
|
| 696 |
+
verb_md = gr.Markdown("")
|
| 697 |
+
|
| 698 |
+
# ═══════════════════════════════════
|
| 699 |
+
# CENTER — Chat + Answer
|
| 700 |
+
# ═══════════════════════════════════
|
| 701 |
+
with gr.Column(scale=2, min_width=460):
|
| 702 |
+
chatbot = gr.Chatbot(
|
| 703 |
+
[], height=440, type="messages",
|
| 704 |
+
placeholder="Reset an episode to begin the conversation.",
|
| 705 |
+
show_copy_button=True,
|
| 706 |
+
elem_classes=["chat-area"],
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
with gr.Row():
|
| 710 |
+
asst_in = gr.Textbox(
|
| 711 |
+
label="Your message", lines=2, interactive=False,
|
| 712 |
+
placeholder="Type your response to the customer…",
|
| 713 |
+
scale=4,
|
| 714 |
+
)
|
| 715 |
+
send_btn = gr.Button("Send", interactive=False, scale=1)
|
| 716 |
+
|
| 717 |
+
with gr.Accordion("Submit Final Answer", open=False):
|
| 718 |
+
gr.Markdown(
|
| 719 |
+
"*When the cart is ready, paste the product IDs you added "
|
| 720 |
+
"and submit.*"
|
| 721 |
+
)
|
| 722 |
+
ans_pids = gr.Textbox(
|
| 723 |
+
label="Product IDs in cart",
|
| 724 |
+
placeholder="B01ABC, B02DEF, …",
|
| 725 |
+
lines=1,
|
| 726 |
+
)
|
| 727 |
+
done_btn = gr.Button(
|
| 728 |
+
"Submit Answer", variant="stop", interactive=False,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
# ═══════════════════════════════════
|
| 732 |
+
# RIGHT SIDEBAR — Tools & Goal
|
| 733 |
+
# ═══════════════════════════════════
|
| 734 |
+
with gr.Column(scale=1, min_width=310):
|
| 735 |
+
gr.Markdown("### Tools")
|
| 736 |
+
tool_sel = gr.Dropdown(
|
| 737 |
+
list(TOOLS.keys()), value="catalog.search",
|
| 738 |
+
label="Select tool",
|
| 739 |
+
)
|
| 740 |
+
tool_desc = gr.Markdown("")
|
| 741 |
+
ta1 = gr.Textbox(label="arg1")
|
| 742 |
+
ta2 = gr.Textbox(label="arg2", visible=True)
|
| 743 |
+
ta3 = gr.Textbox(label="arg3", visible=True)
|
| 744 |
+
ta4 = gr.Textbox(label="arg4", visible=False)
|
| 745 |
+
ta5 = gr.Textbox(label="arg5", visible=False)
|
| 746 |
+
exec_btn = gr.Button("Execute", variant="secondary")
|
| 747 |
+
|
| 748 |
+
with gr.Accordion("Tool Output", open=True):
|
| 749 |
+
tool_out = gr.Markdown("", elem_classes=["tool-output"])
|
| 750 |
+
|
| 751 |
+
with gr.Accordion("Hidden Goal (ground truth)", open=True):
|
| 752 |
+
goal_md = gr.Markdown(
|
| 753 |
+
"*Reset to generate a goal.*",
|
| 754 |
+
elem_classes=["goal-panel"],
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
# ── Wiring ──
|
| 758 |
+
_out8 = [
|
| 759 |
+
session_state, chatbot, tool_out, episode_md,
|
| 760 |
+
reward_display, goal_md, verb_md, asst_in,
|
| 761 |
+
]
|
| 762 |
+
|
| 763 |
+
reset_btn.click(
|
| 764 |
+
reset_episode, [session_state, diff_sl],
|
| 765 |
+
[session_state, chatbot, persona_md, episode_md,
|
| 766 |
+
reward_display, goal_md, verb_md, tool_out,
|
| 767 |
+
exec_btn, asst_in, send_btn, done_btn],
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
tool_sel.change(
|
| 771 |
+
update_tool_args, [tool_sel],
|
| 772 |
+
[ta1, ta2, ta3, ta4, ta5, tool_desc],
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
exec_btn.click(
|
| 776 |
+
execute_tool,
|
| 777 |
+
[session_state, tool_sel, ta1, ta2, ta3, ta4, ta5],
|
| 778 |
+
_out8,
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
send_btn.click(
|
| 782 |
+
submit_response, [session_state, asst_in], _out8,
|
| 783 |
+
).then(lambda: "", outputs=[asst_in])
|
| 784 |
+
|
| 785 |
+
done_btn.click(
|
| 786 |
+
submit_answer,
|
| 787 |
+
[session_state, asst_in, ans_pids],
|
| 788 |
+
_out8,
|
| 789 |
+
)
|
| 790 |
+
|
| 791 |
+
|
| 792 |
+
if __name__ == "__main__":
|
| 793 |
+
demo.launch(
|
| 794 |
+
server_name="0.0.0.0",
|
| 795 |
+
server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
|
| 796 |
+
share=False,
|
| 797 |
+
show_error=True,
|
| 798 |
+
)
|
catalog.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
policies.json
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"policies": [
|
| 3 |
+
{
|
| 4 |
+
"rule_id": "pol_001",
|
| 5 |
+
"title": "Return window for electronics",
|
| 6 |
+
"category": "returns",
|
| 7 |
+
"conditions": [
|
| 8 |
+
{
|
| 9 |
+
"field": "cat",
|
| 10 |
+
"op": "eq",
|
| 11 |
+
"value": "electronics"
|
| 12 |
+
}
|
| 13 |
+
],
|
| 14 |
+
"answer_type": "numeric",
|
| 15 |
+
"answer": 15
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"rule_id": "pol_002",
|
| 19 |
+
"title": "Return window for clothing",
|
| 20 |
+
"category": "returns",
|
| 21 |
+
"conditions": [
|
| 22 |
+
{
|
| 23 |
+
"field": "cat",
|
| 24 |
+
"op": "eq",
|
| 25 |
+
"value": "clothing"
|
| 26 |
+
}
|
| 27 |
+
],
|
| 28 |
+
"answer_type": "numeric",
|
| 29 |
+
"answer": 30
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"rule_id": "pol_003",
|
| 33 |
+
"title": "Return window for furniture",
|
| 34 |
+
"category": "returns",
|
| 35 |
+
"conditions": [
|
| 36 |
+
{
|
| 37 |
+
"field": "cat",
|
| 38 |
+
"op": "eq",
|
| 39 |
+
"value": "furniture"
|
| 40 |
+
}
|
| 41 |
+
],
|
| 42 |
+
"answer_type": "numeric",
|
| 43 |
+
"answer": 30
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"rule_id": "pol_004",
|
| 47 |
+
"title": "Return window for groceries",
|
| 48 |
+
"category": "returns",
|
| 49 |
+
"conditions": [
|
| 50 |
+
{
|
| 51 |
+
"field": "cat",
|
| 52 |
+
"op": "eq",
|
| 53 |
+
"value": "groceries"
|
| 54 |
+
}
|
| 55 |
+
],
|
| 56 |
+
"answer_type": "numeric",
|
| 57 |
+
"answer": 0
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"rule_id": "pol_005",
|
| 61 |
+
"title": "Return window for jewelry",
|
| 62 |
+
"category": "returns",
|
| 63 |
+
"conditions": [
|
| 64 |
+
{
|
| 65 |
+
"field": "cat",
|
| 66 |
+
"op": "eq",
|
| 67 |
+
"value": "jewelry"
|
| 68 |
+
}
|
| 69 |
+
],
|
| 70 |
+
"answer_type": "numeric",
|
| 71 |
+
"answer": 30
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"rule_id": "pol_006",
|
| 75 |
+
"title": "Return window for books",
|
| 76 |
+
"category": "returns",
|
| 77 |
+
"conditions": [
|
| 78 |
+
{
|
| 79 |
+
"field": "cat",
|
| 80 |
+
"op": "eq",
|
| 81 |
+
"value": "books"
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
"answer_type": "numeric",
|
| 85 |
+
"answer": 14
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"rule_id": "pol_007",
|
| 89 |
+
"title": "Return window for toys",
|
| 90 |
+
"category": "returns",
|
| 91 |
+
"conditions": [
|
| 92 |
+
{
|
| 93 |
+
"field": "cat",
|
| 94 |
+
"op": "eq",
|
| 95 |
+
"value": "toys"
|
| 96 |
+
}
|
| 97 |
+
],
|
| 98 |
+
"answer_type": "numeric",
|
| 99 |
+
"answer": 30
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"rule_id": "pol_008",
|
| 103 |
+
"title": "Return window for sports",
|
| 104 |
+
"category": "returns",
|
| 105 |
+
"conditions": [
|
| 106 |
+
{
|
| 107 |
+
"field": "cat",
|
| 108 |
+
"op": "eq",
|
| 109 |
+
"value": "sports"
|
| 110 |
+
}
|
| 111 |
+
],
|
| 112 |
+
"answer_type": "numeric",
|
| 113 |
+
"answer": 30
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"rule_id": "pol_009",
|
| 117 |
+
"title": "Return window for beauty",
|
| 118 |
+
"category": "returns",
|
| 119 |
+
"conditions": [
|
| 120 |
+
{
|
| 121 |
+
"field": "cat",
|
| 122 |
+
"op": "eq",
|
| 123 |
+
"value": "beauty"
|
| 124 |
+
}
|
| 125 |
+
],
|
| 126 |
+
"answer_type": "numeric",
|
| 127 |
+
"answer": 14
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"rule_id": "pol_010",
|
| 131 |
+
"title": "Return window for automotive",
|
| 132 |
+
"category": "returns",
|
| 133 |
+
"conditions": [
|
| 134 |
+
{
|
| 135 |
+
"field": "cat",
|
| 136 |
+
"op": "eq",
|
| 137 |
+
"value": "automotive"
|
| 138 |
+
}
|
| 139 |
+
],
|
| 140 |
+
"answer_type": "numeric",
|
| 141 |
+
"answer": 15
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"rule_id": "pol_011",
|
| 145 |
+
"title": "Return fee for mail returns",
|
| 146 |
+
"category": "returns",
|
| 147 |
+
"conditions": [
|
| 148 |
+
{
|
| 149 |
+
"field": "return_method",
|
| 150 |
+
"op": "eq",
|
| 151 |
+
"value": "mail"
|
| 152 |
+
}
|
| 153 |
+
],
|
| 154 |
+
"answer_type": "numeric",
|
| 155 |
+
"answer": 5.99
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"rule_id": "pol_012",
|
| 159 |
+
"title": "Return fee for in-store returns",
|
| 160 |
+
"category": "returns",
|
| 161 |
+
"conditions": [
|
| 162 |
+
{
|
| 163 |
+
"field": "return_method",
|
| 164 |
+
"op": "eq",
|
| 165 |
+
"value": "in_store"
|
| 166 |
+
}
|
| 167 |
+
],
|
| 168 |
+
"answer_type": "numeric",
|
| 169 |
+
"answer": 0.0
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"rule_id": "pol_013",
|
| 173 |
+
"title": "Return fee for pickup returns",
|
| 174 |
+
"category": "returns",
|
| 175 |
+
"conditions": [
|
| 176 |
+
{
|
| 177 |
+
"field": "return_method",
|
| 178 |
+
"op": "eq",
|
| 179 |
+
"value": "pickup"
|
| 180 |
+
}
|
| 181 |
+
],
|
| 182 |
+
"answer_type": "numeric",
|
| 183 |
+
"answer": 0.0
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"rule_id": "pol_014",
|
| 187 |
+
"title": "Free mail returns for premium members",
|
| 188 |
+
"category": "returns",
|
| 189 |
+
"conditions": [
|
| 190 |
+
{
|
| 191 |
+
"field": "return_method",
|
| 192 |
+
"op": "eq",
|
| 193 |
+
"value": "mail"
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"field": "membership_tier",
|
| 197 |
+
"op": "eq",
|
| 198 |
+
"value": "premium"
|
| 199 |
+
}
|
| 200 |
+
],
|
| 201 |
+
"answer_type": "numeric",
|
| 202 |
+
"answer": 0.0
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"rule_id": "pol_015",
|
| 206 |
+
"title": "Free shipping threshold for non-members",
|
| 207 |
+
"category": "shipping",
|
| 208 |
+
"conditions": [
|
| 209 |
+
{
|
| 210 |
+
"field": "membership_tier",
|
| 211 |
+
"op": "eq",
|
| 212 |
+
"value": "none"
|
| 213 |
+
}
|
| 214 |
+
],
|
| 215 |
+
"answer_type": "numeric",
|
| 216 |
+
"answer": 50.0
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"rule_id": "pol_016",
|
| 220 |
+
"title": "Free shipping threshold for basic members",
|
| 221 |
+
"category": "shipping",
|
| 222 |
+
"conditions": [
|
| 223 |
+
{
|
| 224 |
+
"field": "membership_tier",
|
| 225 |
+
"op": "eq",
|
| 226 |
+
"value": "basic"
|
| 227 |
+
}
|
| 228 |
+
],
|
| 229 |
+
"answer_type": "numeric",
|
| 230 |
+
"answer": 35.0
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"rule_id": "pol_017",
|
| 234 |
+
"title": "Free shipping threshold for premium members",
|
| 235 |
+
"category": "shipping",
|
| 236 |
+
"conditions": [
|
| 237 |
+
{
|
| 238 |
+
"field": "membership_tier",
|
| 239 |
+
"op": "eq",
|
| 240 |
+
"value": "premium"
|
| 241 |
+
}
|
| 242 |
+
],
|
| 243 |
+
"answer_type": "numeric",
|
| 244 |
+
"answer": 0.0
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"rule_id": "pol_018",
|
| 248 |
+
"title": "Standard shipping cost",
|
| 249 |
+
"category": "shipping",
|
| 250 |
+
"conditions": [
|
| 251 |
+
{
|
| 252 |
+
"field": "shipping_method",
|
| 253 |
+
"op": "eq",
|
| 254 |
+
"value": "standard"
|
| 255 |
+
}
|
| 256 |
+
],
|
| 257 |
+
"answer_type": "numeric",
|
| 258 |
+
"answer": 7.99
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"rule_id": "pol_019",
|
| 262 |
+
"title": "Express shipping cost",
|
| 263 |
+
"category": "shipping",
|
| 264 |
+
"conditions": [
|
| 265 |
+
{
|
| 266 |
+
"field": "shipping_method",
|
| 267 |
+
"op": "eq",
|
| 268 |
+
"value": "express"
|
| 269 |
+
}
|
| 270 |
+
],
|
| 271 |
+
"answer_type": "numeric",
|
| 272 |
+
"answer": 14.99
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"rule_id": "pol_020",
|
| 276 |
+
"title": "Overnight shipping cost",
|
| 277 |
+
"category": "shipping",
|
| 278 |
+
"conditions": [
|
| 279 |
+
{
|
| 280 |
+
"field": "shipping_method",
|
| 281 |
+
"op": "eq",
|
| 282 |
+
"value": "overnight"
|
| 283 |
+
}
|
| 284 |
+
],
|
| 285 |
+
"answer_type": "numeric",
|
| 286 |
+
"answer": 24.99
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"rule_id": "pol_021",
|
| 290 |
+
"title": "Standard shipping delivery time",
|
| 291 |
+
"category": "shipping",
|
| 292 |
+
"conditions": [
|
| 293 |
+
{
|
| 294 |
+
"field": "shipping_method",
|
| 295 |
+
"op": "eq",
|
| 296 |
+
"value": "standard"
|
| 297 |
+
}
|
| 298 |
+
],
|
| 299 |
+
"answer_type": "categorical",
|
| 300 |
+
"answer": "5-7 business days"
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"rule_id": "pol_022",
|
| 304 |
+
"title": "Express shipping delivery time",
|
| 305 |
+
"category": "shipping",
|
| 306 |
+
"conditions": [
|
| 307 |
+
{
|
| 308 |
+
"field": "shipping_method",
|
| 309 |
+
"op": "eq",
|
| 310 |
+
"value": "express"
|
| 311 |
+
}
|
| 312 |
+
],
|
| 313 |
+
"answer_type": "categorical",
|
| 314 |
+
"answer": "2-3 business days"
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"rule_id": "pol_023",
|
| 318 |
+
"title": "Overnight shipping delivery time",
|
| 319 |
+
"category": "shipping",
|
| 320 |
+
"conditions": [
|
| 321 |
+
{
|
| 322 |
+
"field": "shipping_method",
|
| 323 |
+
"op": "eq",
|
| 324 |
+
"value": "overnight"
|
| 325 |
+
}
|
| 326 |
+
],
|
| 327 |
+
"answer_type": "categorical",
|
| 328 |
+
"answer": "Next business day"
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"rule_id": "pol_024",
|
| 332 |
+
"title": "Free express shipping for premium members on orders over $100",
|
| 333 |
+
"category": "shipping",
|
| 334 |
+
"conditions": [
|
| 335 |
+
{
|
| 336 |
+
"field": "membership_tier",
|
| 337 |
+
"op": "eq",
|
| 338 |
+
"value": "premium"
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
"field": "order_total",
|
| 342 |
+
"op": "gte",
|
| 343 |
+
"value": 100.0
|
| 344 |
+
}
|
| 345 |
+
],
|
| 346 |
+
"answer_type": "numeric",
|
| 347 |
+
"answer": 0.0
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"rule_id": "pol_025",
|
| 351 |
+
"title": "Heavy item shipping surcharge for furniture over 50 lbs",
|
| 352 |
+
"category": "shipping",
|
| 353 |
+
"conditions": [
|
| 354 |
+
{
|
| 355 |
+
"field": "cat",
|
| 356 |
+
"op": "eq",
|
| 357 |
+
"value": "furniture"
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"field": "weight_lbs",
|
| 361 |
+
"op": "gt",
|
| 362 |
+
"value": 50
|
| 363 |
+
}
|
| 364 |
+
],
|
| 365 |
+
"answer_type": "numeric",
|
| 366 |
+
"answer": 29.99
|
| 367 |
+
},
|
| 368 |
+
{
|
| 369 |
+
"rule_id": "pol_026",
|
| 370 |
+
"title": "International standard shipping cost",
|
| 371 |
+
"category": "shipping",
|
| 372 |
+
"conditions": [
|
| 373 |
+
{
|
| 374 |
+
"field": "shipping_method",
|
| 375 |
+
"op": "eq",
|
| 376 |
+
"value": "standard"
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"field": "destination",
|
| 380 |
+
"op": "eq",
|
| 381 |
+
"value": "international"
|
| 382 |
+
}
|
| 383 |
+
],
|
| 384 |
+
"answer_type": "numeric",
|
| 385 |
+
"answer": 19.99
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"rule_id": "pol_027",
|
| 389 |
+
"title": "Alaska/Hawaii shipping surcharge",
|
| 390 |
+
"category": "shipping",
|
| 391 |
+
"conditions": [
|
| 392 |
+
{
|
| 393 |
+
"field": "destination",
|
| 394 |
+
"op": "in",
|
| 395 |
+
"value": [
|
| 396 |
+
"alaska",
|
| 397 |
+
"hawaii"
|
| 398 |
+
]
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"field": "shipping_method",
|
| 402 |
+
"op": "eq",
|
| 403 |
+
"value": "standard"
|
| 404 |
+
}
|
| 405 |
+
],
|
| 406 |
+
"answer_type": "numeric",
|
| 407 |
+
"answer": 9.99
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"rule_id": "pol_028",
|
| 411 |
+
"title": "Price match window for in-store purchases",
|
| 412 |
+
"category": "pricing",
|
| 413 |
+
"conditions": [
|
| 414 |
+
{
|
| 415 |
+
"field": "purchase_channel",
|
| 416 |
+
"op": "eq",
|
| 417 |
+
"value": "in_store"
|
| 418 |
+
}
|
| 419 |
+
],
|
| 420 |
+
"answer_type": "numeric",
|
| 421 |
+
"answer": 14
|
| 422 |
+
},
|
| 423 |
+
{
|
| 424 |
+
"rule_id": "pol_029",
|
| 425 |
+
"title": "Price match window for online purchases",
|
| 426 |
+
"category": "pricing",
|
| 427 |
+
"conditions": [
|
| 428 |
+
{
|
| 429 |
+
"field": "purchase_channel",
|
| 430 |
+
"op": "eq",
|
| 431 |
+
"value": "online"
|
| 432 |
+
}
|
| 433 |
+
],
|
| 434 |
+
"answer_type": "numeric",
|
| 435 |
+
"answer": 7
|
| 436 |
+
},
|
| 437 |
+
{
|
| 438 |
+
"rule_id": "pol_030",
|
| 439 |
+
"title": "Price match eligible for electronics from authorized retailers",
|
| 440 |
+
"category": "pricing",
|
| 441 |
+
"conditions": [
|
| 442 |
+
{
|
| 443 |
+
"field": "cat",
|
| 444 |
+
"op": "eq",
|
| 445 |
+
"value": "electronics"
|
| 446 |
+
},
|
| 447 |
+
{
|
| 448 |
+
"field": "competitor_type",
|
| 449 |
+
"op": "eq",
|
| 450 |
+
"value": "authorized_retailer"
|
| 451 |
+
}
|
| 452 |
+
],
|
| 453 |
+
"answer_type": "categorical",
|
| 454 |
+
"answer": "eligible"
|
| 455 |
+
},
|
| 456 |
+
{
|
| 457 |
+
"rule_id": "pol_031",
|
| 458 |
+
"title": "Price match ineligible for marketplace sellers",
|
| 459 |
+
"category": "pricing",
|
| 460 |
+
"conditions": [
|
| 461 |
+
{
|
| 462 |
+
"field": "competitor_type",
|
| 463 |
+
"op": "eq",
|
| 464 |
+
"value": "marketplace"
|
| 465 |
+
},
|
| 466 |
+
{
|
| 467 |
+
"field": "cat",
|
| 468 |
+
"op": "neq",
|
| 469 |
+
"value": "electronics"
|
| 470 |
+
}
|
| 471 |
+
],
|
| 472 |
+
"answer_type": "categorical",
|
| 473 |
+
"answer": "ineligible"
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"rule_id": "pol_032",
|
| 477 |
+
"title": "5% bulk discount for 10+ identical items",
|
| 478 |
+
"category": "pricing",
|
| 479 |
+
"conditions": [
|
| 480 |
+
{
|
| 481 |
+
"field": "quantity",
|
| 482 |
+
"op": "gte",
|
| 483 |
+
"value": 10
|
| 484 |
+
},
|
| 485 |
+
{
|
| 486 |
+
"field": "quantity",
|
| 487 |
+
"op": "lt",
|
| 488 |
+
"value": 25
|
| 489 |
+
}
|
| 490 |
+
],
|
| 491 |
+
"answer_type": "numeric",
|
| 492 |
+
"answer": 5
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"rule_id": "pol_033",
|
| 496 |
+
"title": "10% bulk discount for 25+ identical items",
|
| 497 |
+
"category": "pricing",
|
| 498 |
+
"conditions": [
|
| 499 |
+
{
|
| 500 |
+
"field": "quantity",
|
| 501 |
+
"op": "gte",
|
| 502 |
+
"value": 25
|
| 503 |
+
},
|
| 504 |
+
{
|
| 505 |
+
"field": "quantity",
|
| 506 |
+
"op": "lt",
|
| 507 |
+
"value": 100
|
| 508 |
+
}
|
| 509 |
+
],
|
| 510 |
+
"answer_type": "numeric",
|
| 511 |
+
"answer": 10
|
| 512 |
+
},
|
| 513 |
+
{
|
| 514 |
+
"rule_id": "pol_034",
|
| 515 |
+
"title": "15% bulk discount for 100+ identical items",
|
| 516 |
+
"category": "pricing",
|
| 517 |
+
"conditions": [
|
| 518 |
+
{
|
| 519 |
+
"field": "quantity",
|
| 520 |
+
"op": "gte",
|
| 521 |
+
"value": 100
|
| 522 |
+
}
|
| 523 |
+
],
|
| 524 |
+
"answer_type": "numeric",
|
| 525 |
+
"answer": 15
|
| 526 |
+
},
|
| 527 |
+
{
|
| 528 |
+
"rule_id": "pol_035",
|
| 529 |
+
"title": "Maximum coupons per order",
|
| 530 |
+
"category": "pricing",
|
| 531 |
+
"conditions": [
|
| 532 |
+
{
|
| 533 |
+
"field": "order_type",
|
| 534 |
+
"op": "eq",
|
| 535 |
+
"value": "standard"
|
| 536 |
+
}
|
| 537 |
+
],
|
| 538 |
+
"answer_type": "numeric",
|
| 539 |
+
"answer": 2
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"rule_id": "pol_036",
|
| 543 |
+
"title": "Coupons not applicable on clearance items",
|
| 544 |
+
"category": "pricing",
|
| 545 |
+
"conditions": [
|
| 546 |
+
{
|
| 547 |
+
"field": "item_status",
|
| 548 |
+
"op": "eq",
|
| 549 |
+
"value": "clearance"
|
| 550 |
+
},
|
| 551 |
+
{
|
| 552 |
+
"field": "coupon_type",
|
| 553 |
+
"op": "eq",
|
| 554 |
+
"value": "percentage"
|
| 555 |
+
}
|
| 556 |
+
],
|
| 557 |
+
"answer_type": "categorical",
|
| 558 |
+
"answer": "not_applicable"
|
| 559 |
+
},
|
| 560 |
+
{
|
| 561 |
+
"rule_id": "pol_037",
|
| 562 |
+
"title": "Student discount on electronics",
|
| 563 |
+
"category": "pricing",
|
| 564 |
+
"conditions": [
|
| 565 |
+
{
|
| 566 |
+
"field": "customer_type",
|
| 567 |
+
"op": "eq",
|
| 568 |
+
"value": "student"
|
| 569 |
+
},
|
| 570 |
+
{
|
| 571 |
+
"field": "cat",
|
| 572 |
+
"op": "eq",
|
| 573 |
+
"value": "electronics"
|
| 574 |
+
}
|
| 575 |
+
],
|
| 576 |
+
"answer_type": "numeric",
|
| 577 |
+
"answer": 10
|
| 578 |
+
},
|
| 579 |
+
{
|
| 580 |
+
"rule_id": "pol_038",
|
| 581 |
+
"title": "Employee discount percentage",
|
| 582 |
+
"category": "pricing",
|
| 583 |
+
"conditions": [
|
| 584 |
+
{
|
| 585 |
+
"field": "customer_type",
|
| 586 |
+
"op": "eq",
|
| 587 |
+
"value": "employee"
|
| 588 |
+
}
|
| 589 |
+
],
|
| 590 |
+
"answer_type": "numeric",
|
| 591 |
+
"answer": 20
|
| 592 |
+
},
|
| 593 |
+
{
|
| 594 |
+
"rule_id": "pol_039",
|
| 595 |
+
"title": "Open-box item discount",
|
| 596 |
+
"category": "pricing",
|
| 597 |
+
"conditions": [
|
| 598 |
+
{
|
| 599 |
+
"field": "item_condition",
|
| 600 |
+
"op": "eq",
|
| 601 |
+
"value": "open_box"
|
| 602 |
+
}
|
| 603 |
+
],
|
| 604 |
+
"answer_type": "numeric",
|
| 605 |
+
"answer": 15
|
| 606 |
+
},
|
| 607 |
+
{
|
| 608 |
+
"rule_id": "pol_040",
|
| 609 |
+
"title": "Basic membership annual fee",
|
| 610 |
+
"category": "membership",
|
| 611 |
+
"conditions": [
|
| 612 |
+
{
|
| 613 |
+
"field": "membership_tier",
|
| 614 |
+
"op": "eq",
|
| 615 |
+
"value": "basic"
|
| 616 |
+
}
|
| 617 |
+
],
|
| 618 |
+
"answer_type": "numeric",
|
| 619 |
+
"answer": 29.99
|
| 620 |
+
},
|
| 621 |
+
{
|
| 622 |
+
"rule_id": "pol_041",
|
| 623 |
+
"title": "Premium membership annual fee",
|
| 624 |
+
"category": "membership",
|
| 625 |
+
"conditions": [
|
| 626 |
+
{
|
| 627 |
+
"field": "membership_tier",
|
| 628 |
+
"op": "eq",
|
| 629 |
+
"value": "premium"
|
| 630 |
+
}
|
| 631 |
+
],
|
| 632 |
+
"answer_type": "numeric",
|
| 633 |
+
"answer": 99.99
|
| 634 |
+
},
|
| 635 |
+
{
|
| 636 |
+
"rule_id": "pol_042",
|
| 637 |
+
"title": "Basic member points per dollar spent",
|
| 638 |
+
"category": "membership",
|
| 639 |
+
"conditions": [
|
| 640 |
+
{
|
| 641 |
+
"field": "membership_tier",
|
| 642 |
+
"op": "eq",
|
| 643 |
+
"value": "basic"
|
| 644 |
+
},
|
| 645 |
+
{
|
| 646 |
+
"field": "purchase_channel",
|
| 647 |
+
"op": "eq",
|
| 648 |
+
"value": "online"
|
| 649 |
+
}
|
| 650 |
+
],
|
| 651 |
+
"answer_type": "numeric",
|
| 652 |
+
"answer": 1
|
| 653 |
+
},
|
| 654 |
+
{
|
| 655 |
+
"rule_id": "pol_043",
|
| 656 |
+
"title": "Premium member points per dollar spent",
|
| 657 |
+
"category": "membership",
|
| 658 |
+
"conditions": [
|
| 659 |
+
{
|
| 660 |
+
"field": "membership_tier",
|
| 661 |
+
"op": "eq",
|
| 662 |
+
"value": "premium"
|
| 663 |
+
},
|
| 664 |
+
{
|
| 665 |
+
"field": "purchase_channel",
|
| 666 |
+
"op": "eq",
|
| 667 |
+
"value": "online"
|
| 668 |
+
}
|
| 669 |
+
],
|
| 670 |
+
"answer_type": "numeric",
|
| 671 |
+
"answer": 3
|
| 672 |
+
},
|
| 673 |
+
{
|
| 674 |
+
"rule_id": "pol_044",
|
| 675 |
+
"title": "Premium member free returns",
|
| 676 |
+
"category": "membership",
|
| 677 |
+
"conditions": [
|
| 678 |
+
{
|
| 679 |
+
"field": "membership_tier",
|
| 680 |
+
"op": "eq",
|
| 681 |
+
"value": "premium"
|
| 682 |
+
}
|
| 683 |
+
],
|
| 684 |
+
"answer_type": "categorical",
|
| 685 |
+
"answer": "Free returns on all items via any method"
|
| 686 |
+
},
|
| 687 |
+
{
|
| 688 |
+
"rule_id": "pol_045",
|
| 689 |
+
"title": "Premium member priority shipping",
|
| 690 |
+
"category": "membership",
|
| 691 |
+
"conditions": [
|
| 692 |
+
{
|
| 693 |
+
"field": "membership_tier",
|
| 694 |
+
"op": "eq",
|
| 695 |
+
"value": "premium"
|
| 696 |
+
}
|
| 697 |
+
],
|
| 698 |
+
"answer_type": "categorical",
|
| 699 |
+
"answer": "Free 2-day shipping on all orders"
|
| 700 |
+
},
|
| 701 |
+
{
|
| 702 |
+
"rule_id": "pol_046",
|
| 703 |
+
"title": "Basic to premium upgrade spending requirement",
|
| 704 |
+
"category": "membership",
|
| 705 |
+
"conditions": [
|
| 706 |
+
{
|
| 707 |
+
"field": "membership_tier",
|
| 708 |
+
"op": "eq",
|
| 709 |
+
"value": "basic"
|
| 710 |
+
},
|
| 711 |
+
{
|
| 712 |
+
"field": "annual_spend",
|
| 713 |
+
"op": "gte",
|
| 714 |
+
"value": 500
|
| 715 |
+
}
|
| 716 |
+
],
|
| 717 |
+
"answer_type": "categorical",
|
| 718 |
+
"answer": "Eligible for complimentary premium upgrade"
|
| 719 |
+
},
|
| 720 |
+
{
|
| 721 |
+
"rule_id": "pol_047",
|
| 722 |
+
"title": "Membership cancellation refund policy",
|
| 723 |
+
"category": "membership",
|
| 724 |
+
"conditions": [
|
| 725 |
+
{
|
| 726 |
+
"field": "days_since_signup",
|
| 727 |
+
"op": "lte",
|
| 728 |
+
"value": 30
|
| 729 |
+
}
|
| 730 |
+
],
|
| 731 |
+
"answer_type": "categorical",
|
| 732 |
+
"answer": "Full refund within 30 days of signup"
|
| 733 |
+
},
|
| 734 |
+
{
|
| 735 |
+
"rule_id": "pol_048",
|
| 736 |
+
"title": "Guest checkout order limit",
|
| 737 |
+
"category": "membership",
|
| 738 |
+
"conditions": [
|
| 739 |
+
{
|
| 740 |
+
"field": "membership_tier",
|
| 741 |
+
"op": "eq",
|
| 742 |
+
"value": "none"
|
| 743 |
+
}
|
| 744 |
+
],
|
| 745 |
+
"answer_type": "numeric",
|
| 746 |
+
"answer": 500.0
|
| 747 |
+
},
|
| 748 |
+
{
|
| 749 |
+
"rule_id": "pol_049",
|
| 750 |
+
"title": "Premium member birthday bonus points",
|
| 751 |
+
"category": "membership",
|
| 752 |
+
"conditions": [
|
| 753 |
+
{
|
| 754 |
+
"field": "membership_tier",
|
| 755 |
+
"op": "eq",
|
| 756 |
+
"value": "premium"
|
| 757 |
+
},
|
| 758 |
+
{
|
| 759 |
+
"field": "is_birthday_month",
|
| 760 |
+
"op": "eq",
|
| 761 |
+
"value": true
|
| 762 |
+
}
|
| 763 |
+
],
|
| 764 |
+
"answer_type": "numeric",
|
| 765 |
+
"answer": 500
|
| 766 |
+
},
|
| 767 |
+
{
|
| 768 |
+
"rule_id": "pol_050",
|
| 769 |
+
"title": "Standard warranty for electronics",
|
| 770 |
+
"category": "warranty",
|
| 771 |
+
"conditions": [
|
| 772 |
+
{
|
| 773 |
+
"field": "cat",
|
| 774 |
+
"op": "eq",
|
| 775 |
+
"value": "electronics"
|
| 776 |
+
}
|
| 777 |
+
],
|
| 778 |
+
"answer_type": "numeric",
|
| 779 |
+
"answer": 12
|
| 780 |
+
},
|
| 781 |
+
{
|
| 782 |
+
"rule_id": "pol_051",
|
| 783 |
+
"title": "Standard warranty for furniture",
|
| 784 |
+
"category": "warranty",
|
| 785 |
+
"conditions": [
|
| 786 |
+
{
|
| 787 |
+
"field": "cat",
|
| 788 |
+
"op": "eq",
|
| 789 |
+
"value": "furniture"
|
| 790 |
+
}
|
| 791 |
+
],
|
| 792 |
+
"answer_type": "numeric",
|
| 793 |
+
"answer": 24
|
| 794 |
+
},
|
| 795 |
+
{
|
| 796 |
+
"rule_id": "pol_052",
|
| 797 |
+
"title": "Standard warranty for appliances",
|
| 798 |
+
"category": "warranty",
|
| 799 |
+
"conditions": [
|
| 800 |
+
{
|
| 801 |
+
"field": "cat",
|
| 802 |
+
"op": "eq",
|
| 803 |
+
"value": "appliances"
|
| 804 |
+
}
|
| 805 |
+
],
|
| 806 |
+
"answer_type": "numeric",
|
| 807 |
+
"answer": 12
|
| 808 |
+
},
|
| 809 |
+
{
|
| 810 |
+
"rule_id": "pol_053",
|
| 811 |
+
"title": "Standard warranty for clothing",
|
| 812 |
+
"category": "warranty",
|
| 813 |
+
"conditions": [
|
| 814 |
+
{
|
| 815 |
+
"field": "cat",
|
| 816 |
+
"op": "eq",
|
| 817 |
+
"value": "clothing"
|
| 818 |
+
}
|
| 819 |
+
],
|
| 820 |
+
"answer_type": "numeric",
|
| 821 |
+
"answer": 3
|
| 822 |
+
},
|
| 823 |
+
{
|
| 824 |
+
"rule_id": "pol_054",
|
| 825 |
+
"title": "Standard warranty for jewelry",
|
| 826 |
+
"category": "warranty",
|
| 827 |
+
"conditions": [
|
| 828 |
+
{
|
| 829 |
+
"field": "cat",
|
| 830 |
+
"op": "eq",
|
| 831 |
+
"value": "jewelry"
|
| 832 |
+
}
|
| 833 |
+
],
|
| 834 |
+
"answer_type": "numeric",
|
| 835 |
+
"answer": 6
|
| 836 |
+
},
|
| 837 |
+
{
|
| 838 |
+
"rule_id": "pol_055",
|
| 839 |
+
"title": "Standard warranty for toys",
|
| 840 |
+
"category": "warranty",
|
| 841 |
+
"conditions": [
|
| 842 |
+
{
|
| 843 |
+
"field": "cat",
|
| 844 |
+
"op": "eq",
|
| 845 |
+
"value": "toys"
|
| 846 |
+
}
|
| 847 |
+
],
|
| 848 |
+
"answer_type": "numeric",
|
| 849 |
+
"answer": 6
|
| 850 |
+
},
|
| 851 |
+
{
|
| 852 |
+
"rule_id": "pol_056",
|
| 853 |
+
"title": "Standard warranty for sports",
|
| 854 |
+
"category": "warranty",
|
| 855 |
+
"conditions": [
|
| 856 |
+
{
|
| 857 |
+
"field": "cat",
|
| 858 |
+
"op": "eq",
|
| 859 |
+
"value": "sports"
|
| 860 |
+
}
|
| 861 |
+
],
|
| 862 |
+
"answer_type": "numeric",
|
| 863 |
+
"answer": 6
|
| 864 |
+
},
|
| 865 |
+
{
|
| 866 |
+
"rule_id": "pol_057",
|
| 867 |
+
"title": "Standard warranty for automotive",
|
| 868 |
+
"category": "warranty",
|
| 869 |
+
"conditions": [
|
| 870 |
+
{
|
| 871 |
+
"field": "cat",
|
| 872 |
+
"op": "eq",
|
| 873 |
+
"value": "automotive"
|
| 874 |
+
}
|
| 875 |
+
],
|
| 876 |
+
"answer_type": "numeric",
|
| 877 |
+
"answer": 12
|
| 878 |
+
},
|
| 879 |
+
{
|
| 880 |
+
"rule_id": "pol_058",
|
| 881 |
+
"title": "Extended warranty cost for electronics under $200",
|
| 882 |
+
"category": "warranty",
|
| 883 |
+
"conditions": [
|
| 884 |
+
{
|
| 885 |
+
"field": "cat",
|
| 886 |
+
"op": "eq",
|
| 887 |
+
"value": "electronics"
|
| 888 |
+
},
|
| 889 |
+
{
|
| 890 |
+
"field": "price",
|
| 891 |
+
"op": "lt",
|
| 892 |
+
"value": 200
|
| 893 |
+
}
|
| 894 |
+
],
|
| 895 |
+
"answer_type": "numeric",
|
| 896 |
+
"answer": 19.99
|
| 897 |
+
},
|
| 898 |
+
{
|
| 899 |
+
"rule_id": "pol_059",
|
| 900 |
+
"title": "Extended warranty cost for electronics $200-$500",
|
| 901 |
+
"category": "warranty",
|
| 902 |
+
"conditions": [
|
| 903 |
+
{
|
| 904 |
+
"field": "cat",
|
| 905 |
+
"op": "eq",
|
| 906 |
+
"value": "electronics"
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"field": "price",
|
| 910 |
+
"op": "gte",
|
| 911 |
+
"value": 200
|
| 912 |
+
}
|
| 913 |
+
],
|
| 914 |
+
"answer_type": "numeric",
|
| 915 |
+
"answer": 49.99
|
| 916 |
+
},
|
| 917 |
+
{
|
| 918 |
+
"rule_id": "pol_060",
|
| 919 |
+
"title": "Extended warranty cost for furniture over $500",
|
| 920 |
+
"category": "warranty",
|
| 921 |
+
"conditions": [
|
| 922 |
+
{
|
| 923 |
+
"field": "cat",
|
| 924 |
+
"op": "eq",
|
| 925 |
+
"value": "furniture"
|
| 926 |
+
},
|
| 927 |
+
{
|
| 928 |
+
"field": "price",
|
| 929 |
+
"op": "gte",
|
| 930 |
+
"value": 500
|
| 931 |
+
}
|
| 932 |
+
],
|
| 933 |
+
"answer_type": "numeric",
|
| 934 |
+
"answer": 79.99
|
| 935 |
+
},
|
| 936 |
+
{
|
| 937 |
+
"rule_id": "pol_061",
|
| 938 |
+
"title": "Premium member bonus warranty months for electronics",
|
| 939 |
+
"category": "warranty",
|
| 940 |
+
"conditions": [
|
| 941 |
+
{
|
| 942 |
+
"field": "membership_tier",
|
| 943 |
+
"op": "eq",
|
| 944 |
+
"value": "premium"
|
| 945 |
+
},
|
| 946 |
+
{
|
| 947 |
+
"field": "cat",
|
| 948 |
+
"op": "eq",
|
| 949 |
+
"value": "electronics"
|
| 950 |
+
}
|
| 951 |
+
],
|
| 952 |
+
"answer_type": "numeric",
|
| 953 |
+
"answer": 6
|
| 954 |
+
},
|
| 955 |
+
{
|
| 956 |
+
"rule_id": "pol_062",
|
| 957 |
+
"title": "Accidental damage protection for premium members on electronics over $100",
|
| 958 |
+
"category": "warranty",
|
| 959 |
+
"conditions": [
|
| 960 |
+
{
|
| 961 |
+
"field": "membership_tier",
|
| 962 |
+
"op": "eq",
|
| 963 |
+
"value": "premium"
|
| 964 |
+
},
|
| 965 |
+
{
|
| 966 |
+
"field": "cat",
|
| 967 |
+
"op": "eq",
|
| 968 |
+
"value": "electronics"
|
| 969 |
+
},
|
| 970 |
+
{
|
| 971 |
+
"field": "price",
|
| 972 |
+
"op": "gte",
|
| 973 |
+
"value": 100
|
| 974 |
+
}
|
| 975 |
+
],
|
| 976 |
+
"answer_type": "categorical",
|
| 977 |
+
"answer": "Included free for 12 months"
|
| 978 |
+
}
|
| 979 |
+
]
|
| 980 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=5.0.0
|
| 2 |
+
huggingface_hub>=0.26.0
|
| 3 |
+
sentence-transformers>=3.0.0
|
| 4 |
+
faiss-cpu>=1.7.0
|
| 5 |
+
datasets>=2.0.0
|
| 6 |
+
numpy>=1.24.0
|
| 7 |
+
pydantic>=2.0.0
|
| 8 |
+
torch>=2.0.0
|
| 9 |
+
transformers>=4.40.0
|