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>

Files changed (5) hide show
  1. README.md +58 -5
  2. app.py +798 -0
  3. catalog.jsonl +0 -0
  4. policies.json +980 -0
  5. requirements.txt +9 -0
README.md CHANGED
@@ -1,13 +1,66 @@
1
  ---
2
- title: EcomRLVE Gym
3
- emoji: 🏃
4
- colorFrom: blue
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.11.0
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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} &middot; {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'} &middot;
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 &amp; 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