Spaces:
Sleeping
Sleeping
Commit ·
862a5f6
1
Parent(s): 4e3e867
feat: add compare pipeline tab + has_context badge in UI
Browse files- api.py +13 -0
- src/rag.py +1 -0
- static/index.html +97 -6
api.py
CHANGED
|
@@ -31,6 +31,7 @@ class Source(BaseModel):
|
|
| 31 |
page: int
|
| 32 |
rrf_score: float
|
| 33 |
rerank_score: float
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
class QueryResponse(BaseModel):
|
|
@@ -107,6 +108,18 @@ async def stream(req: QueryRequest):
|
|
| 107 |
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
| 108 |
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
@app.get("/health")
|
| 111 |
def health():
|
| 112 |
return {"status": "ok"}
|
|
|
|
| 31 |
page: int
|
| 32 |
rrf_score: float
|
| 33 |
rerank_score: float
|
| 34 |
+
has_context: bool = False
|
| 35 |
|
| 36 |
|
| 37 |
class QueryResponse(BaseModel):
|
|
|
|
| 108 |
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
| 109 |
|
| 110 |
|
| 111 |
+
@app.post("/compare")
|
| 112 |
+
def compare(req: QueryRequest):
|
| 113 |
+
"""So sánh Text RAG vs ColPali retrieval trên cùng câu hỏi."""
|
| 114 |
+
from src.colpali_retriever import query as colpali_query
|
| 115 |
+
_, text_sources = retrieve(req.question, top_k=req.top_k)
|
| 116 |
+
try:
|
| 117 |
+
colpali_hits = colpali_query(req.question, top_k=req.top_k)
|
| 118 |
+
except Exception as e:
|
| 119 |
+
colpali_hits = []
|
| 120 |
+
return {"text_rag": text_sources, "colpali": colpali_hits}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
@app.get("/health")
|
| 124 |
def health():
|
| 125 |
return {"status": "ok"}
|
src/rag.py
CHANGED
|
@@ -48,6 +48,7 @@ def retrieve(question: str, top_k: int = 5):
|
|
| 48 |
"page": c["metadata"]["page"],
|
| 49 |
"rrf_score": round(c["score"], 3),
|
| 50 |
"rerank_score": round(c["rerank_score"], 3),
|
|
|
|
| 51 |
}
|
| 52 |
for c in contexts
|
| 53 |
]
|
|
|
|
| 48 |
"page": c["metadata"]["page"],
|
| 49 |
"rrf_score": round(c["score"], 3),
|
| 50 |
"rerank_score": round(c["rerank_score"], 3),
|
| 51 |
+
"has_context": c["metadata"].get("has_context", False),
|
| 52 |
}
|
| 53 |
for c in contexts
|
| 54 |
]
|
static/index.html
CHANGED
|
@@ -120,6 +120,33 @@
|
|
| 120 |
.cursor { animation: blink 0.8s step-end infinite; color: var(--primary); }
|
| 121 |
@keyframes blink { 50% { opacity: 0; } }
|
| 122 |
.uploaded-badge { display: inline-block; background: var(--green-bg); color: var(--green-text); border-radius: 4px; padding: 1px 5px; font-size: 0.68rem; font-weight: 600; margin-left: 4px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
/* Mobile */
|
| 125 |
@media (max-width: 680px) {
|
|
@@ -173,11 +200,41 @@
|
|
| 173 |
</div>
|
| 174 |
|
| 175 |
<div class="chat-area">
|
| 176 |
-
<div
|
| 177 |
-
<
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
</div>
|
|
@@ -193,6 +250,39 @@
|
|
| 193 |
</div>
|
| 194 |
|
| 195 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
// Theme
|
| 197 |
function toggleTheme() {
|
| 198 |
const html = document.documentElement;
|
|
@@ -280,9 +370,10 @@
|
|
| 280 |
const rrfPct = Math.min(s.rrf_score * 3000, 100).toFixed(0);
|
| 281 |
const rerankPct = Math.min(s.rerank_score * 150, 100).toFixed(0);
|
| 282 |
const badge = s.uploaded ? '<span class="uploaded-badge">uploaded</span>' : '';
|
|
|
|
| 283 |
return `<div class="source-item">
|
| 284 |
<div>
|
| 285 |
-
<div class="source-name">📄 ${s.source}${badge}</div>
|
| 286 |
<div class="source-page">Trang ${s.page}</div>
|
| 287 |
</div>
|
| 288 |
<div class="score-wrap">
|
|
|
|
| 120 |
.cursor { animation: blink 0.8s step-end infinite; color: var(--primary); }
|
| 121 |
@keyframes blink { 50% { opacity: 0; } }
|
| 122 |
.uploaded-badge { display: inline-block; background: var(--green-bg); color: var(--green-text); border-radius: 4px; padding: 1px 5px; font-size: 0.68rem; font-weight: 600; margin-left: 4px; }
|
| 123 |
+
.ctx-badge { display: inline-block; background: #fef3c7; color: #92400e; border-radius: 4px; padding: 1px 5px; font-size: 0.65rem; font-weight: 600; margin-left: 4px; }
|
| 124 |
+
[data-theme="dark"] .ctx-badge { background: #3a2a0a; color: #fbbf24; }
|
| 125 |
+
|
| 126 |
+
/* Tabs */
|
| 127 |
+
.tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid var(--border); }
|
| 128 |
+
.tab-btn { padding: 8px 16px; font-size: 0.85rem; font-weight: 600; border: none; background: none; color: var(--text2); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.15s; }
|
| 129 |
+
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
|
| 130 |
+
.tab-btn:hover:not(.active) { color: var(--text); }
|
| 131 |
+
.tab-panel { display: none; }
|
| 132 |
+
.tab-panel.active { display: block; }
|
| 133 |
+
|
| 134 |
+
/* Compare panel */
|
| 135 |
+
.compare-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
|
| 136 |
+
.compare-col h4 { font-size: 0.8rem; font-weight: 700; padding: 8px 12px; border-radius: 8px 8px 0 0; margin-bottom: 0; }
|
| 137 |
+
.col-text h4 { background: var(--primary-light); color: var(--primary); }
|
| 138 |
+
.col-colpali h4 { background: #fef3c7; color: #92400e; }
|
| 139 |
+
[data-theme="dark"] .col-colpali h4 { background: #3a2a0a; color: #fbbf24; }
|
| 140 |
+
.compare-result { background: var(--surface); border: 1px solid var(--border); border-radius: 0 0 8px 8px; padding: 10px; min-height: 80px; }
|
| 141 |
+
.compare-hit { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 0.78rem; }
|
| 142 |
+
.compare-hit:last-child { border-bottom: none; }
|
| 143 |
+
.compare-hit .hit-source { color: var(--text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 144 |
+
.compare-hit .hit-meta { color: var(--text3); font-size: 0.7rem; margin-top: 2px; }
|
| 145 |
+
.compare-empty { color: var(--text3); font-size: 0.76rem; text-align: center; padding: 16px 0; }
|
| 146 |
+
.compare-loading { color: var(--text3); font-size: 0.76rem; text-align: center; padding: 16px 0; }
|
| 147 |
+
@media (max-width: 680px) {
|
| 148 |
+
.compare-cols { grid-template-columns: 1fr; }
|
| 149 |
+
}
|
| 150 |
|
| 151 |
/* Mobile */
|
| 152 |
@media (max-width: 680px) {
|
|
|
|
| 200 |
</div>
|
| 201 |
|
| 202 |
<div class="chat-area">
|
| 203 |
+
<div class="tabs">
|
| 204 |
+
<button class="tab-btn active" onclick="switchTab('chat')">💬 Chat</button>
|
| 205 |
+
<button class="tab-btn" onclick="switchTab('compare')">⚖️ So sánh Pipeline</button>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div id="tab-chat" class="tab-panel active">
|
| 209 |
+
<div id="history">
|
| 210 |
+
<div class="empty-state">
|
| 211 |
+
<div class="icon">💬</div>
|
| 212 |
+
<h2>Hãy đặt câu hỏi về tài liệu</h2>
|
| 213 |
+
<p>Hóa đơn bán lẻ · Báo cáo tài chính · hoặc upload PDF của bạn</p>
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<div id="tab-compare" class="tab-panel">
|
| 219 |
+
<div class="card">
|
| 220 |
+
<h3>⚖️ So sánh Text RAG vs ColPali Visual Retrieval</h3>
|
| 221 |
+
<p style="font-size:0.78rem;color:var(--text2);margin-bottom:12px">Nhập câu hỏi và xem 2 pipeline truy xuất tài liệu như thế nào khác nhau.</p>
|
| 222 |
+
<div style="display:flex;gap:8px">
|
| 223 |
+
<input type="text" id="cmp-q" placeholder="Nhập câu hỏi để so sánh..." style="flex:1;border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:0.88rem;background:var(--surface2);color:var(--text);outline:none" onkeydown="if(event.key==='Enter')runCompare()">
|
| 224 |
+
<button onclick="runCompare()" style="padding:8px 16px;background:var(--primary);color:white;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:0.85rem">So sánh</button>
|
| 225 |
+
</div>
|
| 226 |
+
<div id="compare-result">
|
| 227 |
+
<div class="compare-cols">
|
| 228 |
+
<div class="compare-col col-text">
|
| 229 |
+
<h4>📝 Text RAG (BM25 + Vector + Rerank)</h4>
|
| 230 |
+
<div class="compare-result"><div class="compare-empty">Nhập câu hỏi để xem kết quả</div></div>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="compare-col col-colpali">
|
| 233 |
+
<h4>🖼️ ColPali Visual Retrieval (MaxSim)</h4>
|
| 234 |
+
<div class="compare-result"><div class="compare-empty">Nhập câu hỏi để xem kết quả</div></div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
</div>
|
| 239 |
</div>
|
| 240 |
</div>
|
|
|
|
| 250 |
</div>
|
| 251 |
|
| 252 |
<script>
|
| 253 |
+
// Tabs
|
| 254 |
+
function switchTab(name) {
|
| 255 |
+
document.querySelectorAll('.tab-btn').forEach((b, i) => b.classList.toggle('active', ['chat','compare'][i] === name));
|
| 256 |
+
document.getElementById('tab-chat').classList.toggle('active', name === 'chat');
|
| 257 |
+
document.getElementById('tab-compare').classList.toggle('active', name === 'compare');
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
// Compare
|
| 261 |
+
async function runCompare() {
|
| 262 |
+
const q = document.getElementById('cmp-q').value.trim();
|
| 263 |
+
if (!q) return;
|
| 264 |
+
const cols = document.querySelectorAll('#compare-result .compare-result');
|
| 265 |
+
cols.forEach(c => c.innerHTML = '<div class="compare-loading">Đang tìm kiếm...</div>');
|
| 266 |
+
try {
|
| 267 |
+
const res = await fetch('/compare', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({question: q, top_k: 5}) });
|
| 268 |
+
const data = await res.json();
|
| 269 |
+
// Text RAG
|
| 270 |
+
cols[0].innerHTML = data.text_rag.length ? data.text_rag.map((s,i) =>
|
| 271 |
+
`<div class="compare-hit">
|
| 272 |
+
<div class="hit-source">[${i+1}] 📄 ${s.source}${s.has_context ? '<span class="ctx-badge">✦ context</span>' : ''}</div>
|
| 273 |
+
<div class="hit-meta">Trang ${s.page} · RRF ${s.rrf_score} · Rerank ${s.rerank_score}</div>
|
| 274 |
+
</div>`).join('') : '<div class="compare-empty">Không có kết quả</div>';
|
| 275 |
+
// ColPali
|
| 276 |
+
cols[1].innerHTML = data.colpali.length ? data.colpali.map((s,i) =>
|
| 277 |
+
`<div class="compare-hit">
|
| 278 |
+
<div class="hit-source">[${i+1}] 🖼️ ${s.source || 'N/A'}</div>
|
| 279 |
+
<div class="hit-meta">Trang ${s.page || '?'} · Score ${s.score || 'N/A'}</div>
|
| 280 |
+
</div>`).join('') : '<div class="compare-empty">Không có kết quả (ColPali chạy local)</div>';
|
| 281 |
+
} catch(e) {
|
| 282 |
+
cols.forEach(c => c.innerHTML = `<div class="compare-empty">Lỗi: ${e.message}</div>`);
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
// Theme
|
| 287 |
function toggleTheme() {
|
| 288 |
const html = document.documentElement;
|
|
|
|
| 370 |
const rrfPct = Math.min(s.rrf_score * 3000, 100).toFixed(0);
|
| 371 |
const rerankPct = Math.min(s.rerank_score * 150, 100).toFixed(0);
|
| 372 |
const badge = s.uploaded ? '<span class="uploaded-badge">uploaded</span>' : '';
|
| 373 |
+
const ctxBadge = s.has_context ? '<span class="ctx-badge">✦ context</span>' : '';
|
| 374 |
return `<div class="source-item">
|
| 375 |
<div>
|
| 376 |
+
<div class="source-name">📄 ${s.source}${badge}${ctxBadge}</div>
|
| 377 |
<div class="source-page">Trang ${s.page}</div>
|
| 378 |
</div>
|
| 379 |
<div class="score-wrap">
|