# 04 — Ultra × Outfit Join: Dùng `ai_matches` từ `ultra_descriptions` làm nguồn gợi ý phối đồ chính

## 📌 Idea
Bảng `pg__dashboard_canifa__ultra_descriptions` (3.856 rows, `backend/123.db`) đã có sẵn field
`ai_matches` — JSON **phân theo occasion** (`di_lam`, `di_choi`, `di_tiec`, `mac_nha`, `the_thao`,
`hang_ngay`), mỗi occasion có list **gợi ý sản phẩm kèm metadata**: `code, name, color, image, score, reason`.

Hiện tại `_enrich_with_outfit()` đang dùng bảng `ai_outfit_product_matches` (32K rows) — chỉ có
`anchor_code, match_code, match_role, score, ai_reason` — **thiếu `name, color, image` của match product**
và **không phân theo occasion** → Responder không biết gợi ý này phù hợp dịp nào.

Idea: **Switch sang `ultra_descriptions.ai_matches`** — giàu context hơn, đã phân occasion,
kèm sẵn `name + color + image + reason`. Chỉ cần join thêm StarRocks để lấy `price + url + stock`.

## 🗂 Codebase Scope
- **Root:** `d:\cnf\chatbot-canifa-feedback\backend`
- **Affected:**
  - `agent/tag_search_agent_v2/tag_search_tool.py` — refactor `_enrich_with_outfit()`
  - `backend/123.db` → `pg__dashboard_canifa__ultra_descriptions` (trường `ai_matches`)
  - StarRocks `test_db.magento_product_dimension_with_text_embedding` — join lấy price/url/stock
- **Languages:** Python 3.12+
- **Size:** S (refactor, không thêm flow mới)

## 📊 Current State Analysis

### Source 1 (hiện tại đang dùng): `ai_outfit_product_matches`
```
cols: id, rule_id, anchor_product_code, match_product_code,
      match_role, score, ai_reason
rows: 32.000
VẤN ĐỀ: không có name/color/image → phải fetch StarRocks 2 lần
        không biết match phù hợp occasion nào
```

### Source 2 (CHUYỂN SANG): `ultra_descriptions.ai_matches`
```json
{
  "di_lam":   {"bottom": [{"code":"5BP25W003", "name":"Quần scuba dài", "color":"Đen",
                            "image":"https://...", "score":75, "reason":"phối màu xám–đen..."}]},
  "di_choi":  {"bottom": [...], "top": [...], "accessories": [...]},
  "di_tiec":  {...},
  "mac_nha":  {...},
  "the_thao": {...},
  "hang_ngay":{...}
}
```
- **3.856 anchor products** đã có `ai_matches`
- Mỗi occasion có list gợi ý per role (`top`, `bottom`, `accessories`)
- Đã có `name, color, image, score, reason` → **chỉ thiếu `price` và `url`**
- `tags` field trên ultra_desc map thẳng với `occ:` tags từ `TagSearchInput`

### Mapping tags → occasion key
```python
TAGS_TO_OCCASION = {
    "occ:di_lam":   "di_lam",
    "occ:di_choi":  "di_choi",
    "occ:di_tiec":  "di_tiec",
    "occ:mac_nha":  "mac_nha",
    "occ:the_thao": "the_thao",
    "occ:hang_ngay": "hang_ngay",  # fallback default
}
```

## 🎯 Goal / Definition of Done
- `_enrich_with_outfit()` đọc `ultra_descriptions.ai_matches` thay vì `ai_outfit_product_matches`
- Chọn đúng occasion từ tags của user (VD: tags=['occ:di_choi'] → đọc `ai_matches["di_choi"]`)
- Fallback: nếu không có tag occasion → dùng `"hang_ngay"` làm default
- Fetch price + url từ StarRocks batch cho match codes
- Check stock cho match products (đã có `_enrich_with_stock()`)
- Output `ai_outfit_suggestions` kèm: `sku, name, color, image, price, url, role, ai_reason, score, in_stock`
- Không inject nếu `ai_matches` null hoặc occasion key không tồn tại

## 📋 Execution Plan

### Phase 1: Đọc `ai_matches` từ SQLite theo anchor codes + occasion (30 phút)
- [ ] Trong `_enrich_with_outfit()`, thay query `ai_outfit_product_matches` bằng:
  ```python
  # SQLite query lấy ai_matches theo anchor codes
  placeholders = ",".join(["?"] * len(anchor_codes))
  query = f"""
      SELECT magento_ref_code, ai_matches
      FROM pg__dashboard_canifa__ultra_descriptions
      WHERE base_ref_code IN ({placeholders})
        AND ai_matches IS NOT NULL
  """
  rows = cursor.execute(query, anchor_codes).fetchall()
  ```
- [ ] Parse JSON `ai_matches` → chọn đúng occasion key từ `req.tags`
- [ ] Build `match_codes` = union tất cả match codes trong occasion đó

### Phase 2: Resolve occasion từ tags (15 phút)
- [ ] Hàm `_resolve_occasion(tags: list[str]) -> str`:
  ```python
  def _resolve_occasion(tags: list[str]) -> str:
      for tag in (tags or []):
          occ = TAGS_TO_OCCASION.get(tag.lower())
          if occ:
              return occ
      return "hang_ngay"  # safe default
  ```
- [ ] Truyền `tags` từ `req.tags` vào `_enrich_with_outfit()`

### Phase 3: Fetch price + url từ StarRocks (đã có pattern) (20 phút)
- [ ] Reuse pattern từ code cũ — batch query StarRocks theo `match_codes`:
  ```python
  sql = f"SELECT {SELECT_COLUMNS} FROM {TABLE_NAME} WHERE magento_ref_code IN (...)"
  match_products = await db.execute_query_async(sql, params=tuple(match_codes))
  ```
- [ ] Map match_code → match_product dict → merge price/url vào

### Phase 4: Stock check + format output (20 phút)
- [ ] Gọi `_enrich_with_stock(match_products)` — đã có sẵn
- [ ] Build `ai_outfit_suggestions` list:
  ```python
  {
    "sku": match["code"],
    "name": match["name"],      # từ ai_matches (không cần StarRocks cho field này)
    "color": match["color"],
    "image": match["image"],
    "price": int(star_p["sale_price"]),   # từ StarRocks
    "url": star_p["product_web_url"],
    "role": role_key,          # "bottom" / "top" / "accessories"
    "ai_reason": match["reason"],
    "score": match["score"],
    "occasion": occasion,
    "in_stock": True,          # đã filter qua stock check
  }
  ```
- [ ] Sort by score DESC, limit 3 per anchor product

### Phase 5: Cleanup + test (15 phút)
- [ ] Xoá import `ai_outfit_product_matches` logic cũ (hoặc giữ fallback)
- [ ] Test với query "áo phông trắng đi chơi" → expect occasion=di_choi, suggestions có role bottom
- [ ] Test với query không có occasion tag → expect fallback hang_ngay

## ⚠️ Risks & Side Effects
- **`base_ref_code` vs `magento_ref_code`:** Bảng ultra_desc dùng `base_ref_code` (không có suffix màu),
  nhưng tag_search_tool dùng `magento_ref_code` (có suffix). Cần normalize:
  ```python
  # magento_ref_code "5BP25W003-SK010" → base = "5BP25W003"
  base = p.get("internal_ref_code") or p.get("magento_ref_code", "").split("-")[0]
  ```
- **`ai_matches` null:** ~20% records có thể chưa có ai_matches → fallback về `ai_outfit_product_matches` cũ
- **Occasion mismatch:** Tags `occ:di_bien`, `occ:du_lich` không có key trong ai_matches → map sang
  closest: `di_choi`
- **Performance:** SQLite read nhỏ (< 5ms) + StarRocks batch (đã parallel) → OK

## 🔧 Tools / Scripts Needed
- Không cần tool ngoài
- Pattern code đã có trong `_enrich_with_outfit()` hiện tại — refactor in-place

## ⏱ Effort Estimate
**S** — ~2 giờ. Refactor in-place, không thêm file mới, reuse pattern có sẵn.

| Size | Meaning |
|------|----------|
| S | < 2 hours, < 20 files |

---
_Captured: 2026-04-28 | Type: Codebase-Aware_

