Commit 5ac1c275 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat: improve stylist engine diversity with round-robin and add raw json description viewer

parent 62f1d65f
......@@ -74,7 +74,7 @@ def init_db() -> None:
reviewed_by TEXT,
reviewed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (feedback_id) REFERENCES feedbacks(id)
FOREIGN KEY (feedback_id) REFERENCES feedbacks(feedback_id)
)
"""
)
......@@ -133,10 +133,10 @@ def init_db() -> None:
cur.execute("SELECT COUNT(*) FROM event_daily_prompts")
if list(cur.fetchone().values())[0] == 0:
mock_events = [
('daily_default', 'Daily Standard', 'Daily', 'Mỗi ngày', 0, '- Tư vấn dựa trên yêu cầu cơ bản của khách hàng.\n- Tập trung vào tính tiện dụng và sự thoải mái.\n- Ưu tiên các sản phẩm mới nhất hoặc best seller nếu khách không có yêu cầu đặc biệt.'),
('womens_day', 'Quốc Tế Phụ Nữ 8/3', 'Campaign', '08/03/2026', 1, '- Ưu tiên gợi ý các sản phẩm váy đầm, đồ đôi, hoặc quà tặng cho phái nữ.\n- Gợi ý các combo quà tặng có giá trị cao.\n- Tone giọng: Nhẹ nhàng, tôn vinh phái đẹp, kèm lời chúc mừng 8/3.\n- Báo cho khách biết đang có chương trình giảm giá 30% cho đồ nữ.'),
('summer_sale', 'Đón Hè Sôi Động', 'Campaign', '01/05/2026 - 31/05/2026', 0, '- Tuyệt đối không gợi ý áo khoác dày, áo len.\n- Chỉ tập trung vào áo phông, đồ bơi, quần short, chất liệu thoáng mát (cotton, lanh).\n- Gợi ý đồ mặc đi biển, đi chơi ngày hè.'),
('black_friday', 'Black Friday Siêu Sale', 'Campaign', '27/11/2026', 0, '- NHẤN MẠNH vào giá sale cực sốc.\n- Up-sale mạnh tay, gợi ý mua theo combo để được giảm thêm.\n- Tạo cảm giác khan hiếm: \'Số lượng có hạn\', \'Sale lớn nhất năm\'.')
('daily_default', 'Daily Standard', 'Daily', 'Mỗi ngày', False, '- Tư vấn dựa trên yêu cầu cơ bản của khách hàng.\n- Tập trung vào tính tiện dụng và sự thoải mái.\n- Ưu tiên các sản phẩm mới nhất hoặc best seller nếu khách không có yêu cầu đặc biệt.'),
('womens_day', 'Quốc Tế Phụ Nữ 8/3', 'Campaign', '08/03/2026', True, '- Ưu tiên gợi ý các sản phẩm váy đầm, đồ đôi, hoặc quà tặng cho phái nữ.\n- Gợi ý các combo quà tặng có giá trị cao.\n- Tone giọng: Nhẹ nhàng, tôn vinh phái đẹp, kèm lời chúc mừng 8/3.\n- Báo cho khách biết đang có chương trình giảm giá 30% cho đồ nữ.'),
('summer_sale', 'Đón Hè Sôi Động', 'Campaign', '01/05/2026 - 31/05/2026', False, '- Tuyệt đối không gợi ý áo khoác dày, áo len.\n- Chỉ tập trung vào áo phông, đồ bơi, quần short, chất liệu thoáng mát (cotton, lanh).\n- Gợi ý đồ mặc đi biển, đi chơi ngày hè.'),
('black_friday', 'Black Friday Siêu Sale', 'Campaign', '27/11/2026', False, '- NHẤN MẠNH vào giá sale cực sốc.\n- Up-sale mạnh tay, gợi ý mua theo combo để được giảm thêm.\n- Tạo cảm giác khan hiếm: \'Số lượng có hạn\', \'Sale lớn nhất năm\'.')
]
cur.executemany(
"""
......@@ -148,7 +148,7 @@ def init_db() -> None:
conn.commit()
conn.close()
logger.info("Auto-Train DB initialized at %s", DB_PATH)
logger.info("Auto-Train DB initialized successfully.")
try:
......
......@@ -15,6 +15,7 @@ from config import CHECKPOINT_POSTGRES_URL
VN_TZ = timezone(timedelta(hours=7))
_pool: AsyncConnectionPool | None = None
logger = logging.getLogger(__name__)
def _ensure_table():
try:
......@@ -56,7 +57,6 @@ async def _get_pool() -> AsyncConnectionPool:
def _now() -> datetime:
return datetime.now(VN_TZ)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/dashboard", tags=["Experiment Log"])
......
......@@ -19,10 +19,10 @@ _BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# ═══════════════════════════════════════════════════════════════
# POSTGRES / STARROCKS TABLE NAMES
# ═══════════════════════════════════════════════════════════════
TABLE_OUTFIT_RULES = "pg__dashboard_canifa__chatbot_fashion_rules"
TABLE_FASHION_RULES = "pg__dashboard_canifa__chatbot_fashion_rules"
TABLE_OUTFIT_MATCHES = "pg__dashboard_canifa__ai_outfit_product_matches"
TABLE_PRODUCTS = "sr__test_db__magento_product_dimension_with_text_embedding"
TABLE_OUTFIT_RULES = "con_meo.chatbot_fashion_rules"
TABLE_FASHION_RULES = "con_meo.chatbot_fashion_rules"
TABLE_OUTFIT_MATCHES = "con_meo.ai_outfit_product_matches"
TABLE_PRODUCTS = "con_meo.magento_product_dimension_with_text_embedding"
# ═══════════════════════════════════════════════════════════════
# GENDER MAPPING (Vietnamese / English → DB key)
......
......@@ -12,6 +12,14 @@ class PooledConnectionWrapper:
self._conn = conn
self._closed = False
def cursor(self, *args, **kwargs):
if "cursor_factory" in kwargs:
factory = kwargs.pop("cursor_factory")
if getattr(factory, "__name__", None) in ("RealDictCursor", "DictCursor"):
from psycopg.rows import dict_row
kwargs["row_factory"] = dict_row
return self._conn.cursor(*args, **kwargs)
def __getattr__(self, name: str) -> Any:
return getattr(self._conn, name)
......
import sys
import os
import time
from pathlib import Path
# Add backend directory to Python path
sys.path.insert(0, str(Path(__file__).parent))
from worker.stylist_engine import StylistEngine
from common.db_pool import db_pool
def main():
if sys.platform.startswith('win'):
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
print("="*80)
print("🚀 STARTING STYLIST ENGINE BATCH V2 (COLOR-BASED & RULE-BASED)")
print("="*80)
start_time = time.time()
# Initialize engine
engine = StylistEngine()
# We will run the batch matching by color variants
# This automatically loads catalog with by_color=True, runs matches,
# and double-writes to both tables.
print("[INFO] Loading catalog and executing batch runs...")
catalog = engine._get_catalog(by_color=True)
total_products = len(catalog)
print(f"[INFO] Found {total_products} product color variants in catalog.")
if total_products == 0:
print("[ERROR] Catalog is empty. Exiting.")
return
# Run batch matching
done_count = engine.run_batch(by_color=True)
elapsed = time.time() - start_time
print("="*80)
print(f"🎉 BATCH COMPLETE!")
print(f" Successfully processed: {done_count}/{total_products} items.")
print(f" Total time: {elapsed:.2f} seconds.")
print("="*80)
if __name__ == "__main__":
main()
This diff is collapsed.
......@@ -156,6 +156,7 @@
<script>
// Chatbot Debug specific logic
const API_BASE = '/api/chatbot';
const NO_IMAGE_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="130" viewBox="0 0 100 130"><rect width="100" height="130" rx="6" fill="%23E2E0D8"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" font-weight="600" fill="%238C8A82">NO IMAGE</text></svg>`;
// State
let conversationHistory = [];
......@@ -290,7 +291,7 @@ function updateDebugPanel(data) {
const list = document.getElementById('productsList');
list.innerHTML = data.products.map(p => `
<div class="product-card">
<img src="${p.image || '/static/placeholder.png'}" alt="${p.name}" onerror="this.src='/static/placeholder.png'">
<img src="${p.image || NO_IMAGE_SVG}" alt="${p.name}" onerror="this.onerror=null; this.src=NO_IMAGE_SVG;">
<div class="product-info">
<div class="product-name">${p.name}</div>
<div class="product-sku">${p.sku}</div>
......
This diff is collapsed.
/* Modal Raw JSON style */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.modal-overlay.show {
opacity: 1;
pointer-events: auto;
}
.modal-container {
background: var(--s);
border: 1px solid var(--b);
border-radius: 12px;
width: 90%;
max-width: 650px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-2xl);
transform: scale(0.95);
transition: transform 0.25s ease;
}
.modal-overlay.show .modal-container {
transform: scale(1);
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid var(--b);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--t);
display: flex;
align-items: center;
gap: 8px;
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.modal-body pre {
background: #1e1e1e;
color: #9cdcfe;
padding: 16px;
border-radius: 8px;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
margin: 0;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.modal-footer {
padding: 12px 20px;
border-top: 1px solid var(--b);
display: flex;
justify-content: flex-end;
gap: 10px;
}
/**
* JSON Viewer Popup Module
* Handles displaying raw description JSON in a clean overlay modal.
*/
// Inject the modal HTML into body automatically on load
document.addEventListener("DOMContentLoaded", () => {
const modalHtml = `
<div class="modal-overlay" id="jsonModalOverlay" onclick="closeJsonModal()">
<div class="modal-container" onclick="event.stopPropagation()">
<div class="modal-header">
<h3><i data-lucide="code" style="color:var(--primary)"></i> Raw JSON mô tả sản phẩm</h3>
<button class="btn-icon" onclick="closeJsonModal()"><i data-lucide="x"></i></button>
</div>
<div class="modal-body">
<pre id="jsonPreCode">{}</pre>
</div>
<div class="modal-footer">
<button class="btn btn-outline btn-sm" onclick="copyJsonText()">
<i data-lucide="copy" style="width:14px;height:14px"></i> Copy JSON
</button>
<button class="btn btn-primary btn-sm" onclick="closeJsonModal()">Đóng</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
if (window.lucide) {
window.lucide.createIcons();
}
});
function showRawJson() {
if (!window.currentProduct) return;
const pre = document.getElementById('jsonPreCode');
let rawStr = window.currentProduct.description_data_for_production || '{}';
try {
const obj = JSON.parse(rawStr);
rawStr = JSON.stringify(obj, null, 2);
} catch (e) {
// Show raw text if parse fails
}
pre.textContent = rawStr;
document.getElementById('jsonModalOverlay').classList.add('show');
if (window.lucide) {
window.lucide.createIcons();
}
}
function closeJsonModal() {
document.getElementById('jsonModalOverlay').classList.remove('show');
}
function copyJsonText() {
const text = document.getElementById('jsonPreCode').textContent;
navigator.clipboard.writeText(text).then(() => {
if (typeof window.showToast === 'function') {
window.showToast("Đã copy Raw JSON vào clipboard!", "success");
} else {
alert("Đã copy Raw JSON vào clipboard!");
}
}).catch(err => {
if (typeof window.showToast === 'function') {
window.showToast("Không thể copy! Hãy bôi đen để copy thủ công.", "error");
} else {
alert("Không thể copy!");
}
});
}
......@@ -100,6 +100,7 @@
<script>
if (window.lucide) { lucide.createIcons(); }
const NO_IMAGE_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="130" viewBox="0 0 100 130"><rect width="100" height="130" rx="6" fill="%23E2E0D8"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" font-weight="600" fill="%238C8A82">NO IMAGE</text></svg>`;
let currentProductCode = null;
let currentDescriptionData = null;
let currentTags = [];
......@@ -121,7 +122,7 @@ async function fetchMissingTags() {
div.className = 'fm-item has-image';
div.onclick = () => loadProduct(p.code, p.product_name, div);
div.innerHTML = `
<img src="${p.product_image_url}" alt="" onerror="this.src='/static/common/img/placeholder.png'">
<img src="${p.product_image_url}" alt="" onerror="this.onerror=null; this.src=NO_IMAGE_SVG;">
<div class="fm-item-info">
<div class="fm-title">${p.code}</div>
<div class="fm-desc-sm">${p.product_name}</div>
......@@ -161,7 +162,7 @@ async function loadProduct(code, name, el) {
data.similar_items.forEach(s => {
simHtml += `
<div class="similar-item">
<img src="${s.product_image_url}" alt="" onerror="this.src='/static/common/img/placeholder.png'">
<img src="${s.product_image_url}" alt="" onerror="this.onerror=null; this.src=NO_IMAGE_SVG;">
<div style="flex:1; overflow:hidden;">
<div style="font-weight:600; font-size:12px;">${s.magento_ref_code}</div>
<div style="font-size:11px; color:var(--m); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${s.product_name}</div>
......
......@@ -86,10 +86,10 @@
"accessory": 4
},
"role_max_items": {
"top": 3,
"bottom": 3,
"outerwear": 2,
"accessory": 2,
"top": 6,
"bottom": 6,
"outerwear": 4,
"accessory": 4,
"underwear": 2
},
"_comment_product_line_to_role": "Ánh xạ tên product_line_vn → role trong outfit",
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment