GitHub Actions commited on
Commit ·
527676c
1
Parent(s): 49e7344
Deploy from GitHub (a639361)
Browse files- .gitattributes +1 -1
- Dockerfile +4 -2
- README.md +3 -1
- config.py +10 -0
- pyproject.toml +1 -1
- rag/embeddings.py +10 -2
- rag/retrieve.py +4 -11
- static/index.html +1 -1
- static/styles.css +19 -11
- templates/results.html +26 -24
.gitattributes
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
|
|
| 1 |
data/*.json filter=lfs diff=lfs merge=lfs -text
|
| 2 |
data/*.db filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
data/*.faiss filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
data/*.faiss filter=lfs diff=lfs merge=lfs -text
|
| 2 |
data/*.json filter=lfs diff=lfs merge=lfs -text
|
| 3 |
data/*.db filter=lfs diff=lfs merge=lfs -text
|
|
|
Dockerfile
CHANGED
|
@@ -10,10 +10,12 @@ RUN uv sync --frozen --no-dev
|
|
| 10 |
COPY config.py ./
|
| 11 |
COPY rag/ rag/
|
| 12 |
|
| 13 |
-
# Pre-
|
| 14 |
RUN uv run python -c "\
|
| 15 |
from rag.embeddings import load_embedding_model, load_cross_encoder; \
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
|
| 18 |
COPY app.py ./
|
| 19 |
COPY templates/ templates/
|
|
|
|
| 10 |
COPY config.py ./
|
| 11 |
COPY rag/ rag/
|
| 12 |
|
| 13 |
+
# Pre-download and load quantized ONNX models at build time (cached in layer)
|
| 14 |
RUN uv run python -c "\
|
| 15 |
from rag.embeddings import load_embedding_model, load_cross_encoder; \
|
| 16 |
+
print('Loading embedding model...'); load_embedding_model(); \
|
| 17 |
+
print('Loading cross-encoder...'); load_cross_encoder(); \
|
| 18 |
+
print('Models cached.')"
|
| 19 |
|
| 20 |
COPY app.py ./
|
| 21 |
COPY templates/ templates/
|
README.md
CHANGED
|
@@ -9,7 +9,7 @@ pinned: false
|
|
| 9 |
preload_from_hub:
|
| 10 |
- sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
| 11 |
- cross-encoder/mmarco-mMiniLMv2-L12-H384-v1
|
| 12 |
-
startup_duration_timeout:
|
| 13 |
---
|
| 14 |
|
| 15 |
<!-- HuggingFace Spaces frontmatter above -- do not remove -->
|
|
@@ -79,6 +79,7 @@ The entire system runs locally with no external API calls, no paid dependencies,
|
|
| 79 |
- **Fast** -- sub-2s response times on CPU
|
| 80 |
- **35,000+ verses** -- complete French Bible (AELF translation)
|
| 81 |
- **PWA-ready** -- offline support via service worker, installable on mobile
|
|
|
|
| 82 |
- **Self-contained** -- no external APIs, runs entirely on your machine
|
| 83 |
|
| 84 |
## Live Demo
|
|
@@ -112,6 +113,7 @@ Open [http://localhost:8000](http://localhost:8000) in your browser.
|
|
| 112 |
| GET | `/health` | Health check (200 `ok` or 503 `loading`) |
|
| 113 |
| GET | `/robots.txt` | Robots.txt for crawlers |
|
| 114 |
| GET | `/sitemap.xml` | XML sitemap for crawlers |
|
|
|
|
| 115 |
|
| 116 |
## Architecture
|
| 117 |
|
|
|
|
| 9 |
preload_from_hub:
|
| 10 |
- sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
|
| 11 |
- cross-encoder/mmarco-mMiniLMv2-L12-H384-v1
|
| 12 |
+
startup_duration_timeout: 10m
|
| 13 |
---
|
| 14 |
|
| 15 |
<!-- HuggingFace Spaces frontmatter above -- do not remove -->
|
|
|
|
| 79 |
- **Fast** -- sub-2s response times on CPU
|
| 80 |
- **35,000+ verses** -- complete French Bible (AELF translation)
|
| 81 |
- **PWA-ready** -- offline support via service worker, installable on mobile
|
| 82 |
+
- **Per-verse feedback** -- thumbs up/down on results, synced to HuggingFace Dataset
|
| 83 |
- **Self-contained** -- no external APIs, runs entirely on your machine
|
| 84 |
|
| 85 |
## Live Demo
|
|
|
|
| 113 |
| GET | `/health` | Health check (200 `ok` or 503 `loading`) |
|
| 114 |
| GET | `/robots.txt` | Robots.txt for crawlers |
|
| 115 |
| GET | `/sitemap.xml` | XML sitemap for crawlers |
|
| 116 |
+
| POST | `/feedback` | Per-verse feedback (fire-and-forget, returns 204) |
|
| 117 |
|
| 118 |
## Architecture
|
| 119 |
|
config.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""Central configuration for the RAG Bible pipeline."""
|
| 2 |
|
| 3 |
import os
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
# Paths
|
|
@@ -16,6 +17,15 @@ EMBEDDING_DIMENSION: int = 384
|
|
| 16 |
# Cross-encoder model
|
| 17 |
CROSS_ENCODER_MODEL: str = "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# Retrieval parameters
|
| 20 |
FAISS_TOP_K: int = 20
|
| 21 |
RERANK_TOP_K: int = 5
|
|
|
|
| 1 |
"""Central configuration for the RAG Bible pipeline."""
|
| 2 |
|
| 3 |
import os
|
| 4 |
+
import platform
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
# Paths
|
|
|
|
| 17 |
# Cross-encoder model
|
| 18 |
CROSS_ENCODER_MODEL: str = "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
|
| 19 |
|
| 20 |
+
# ONNX quantized model file (architecture-specific)
|
| 21 |
+
_machine = platform.machine()
|
| 22 |
+
_onnx_map: dict[str, str] = {
|
| 23 |
+
"x86_64": "onnx/model_qint8_avx512.onnx",
|
| 24 |
+
"AMD64": "onnx/model_qint8_avx512.onnx",
|
| 25 |
+
"arm64": "onnx/model_qint8_arm64.onnx",
|
| 26 |
+
}
|
| 27 |
+
ONNX_FILE_NAME: str = _onnx_map.get(_machine, "onnx/model.onnx")
|
| 28 |
+
|
| 29 |
# Retrieval parameters
|
| 30 |
FAISS_TOP_K: int = 20
|
| 31 |
RERANK_TOP_K: int = 5
|
pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "rag-bible"
|
| 3 |
-
version = "1.1.
|
| 4 |
description = "French Bible RAG system with FAISS + cross-encoder reranking"
|
| 5 |
requires-python = ">=3.12"
|
| 6 |
dependencies = [
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "rag-bible"
|
| 3 |
+
version = "1.1.1"
|
| 4 |
description = "French Bible RAG system with FAISS + cross-encoder reranking"
|
| 5 |
requires-python = ">=3.12"
|
| 6 |
dependencies = [
|
rag/embeddings.py
CHANGED
|
@@ -20,7 +20,11 @@ def load_embedding_model(model_name: str | None = None) -> SentenceTransformer:
|
|
| 20 |
Loaded embedding model.
|
| 21 |
"""
|
| 22 |
name = model_name or config.EMBEDDING_MODEL
|
| 23 |
-
return SentenceTransformer(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
def encode_texts(
|
|
@@ -71,5 +75,9 @@ def load_cross_encoder(model_name: str | None = None) -> CrossEncoder:
|
|
| 71 |
Loaded cross-encoder model.
|
| 72 |
"""
|
| 73 |
name = model_name or config.CROSS_ENCODER_MODEL
|
| 74 |
-
model: CrossEncoder = CrossEncoder(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
return model
|
|
|
|
| 20 |
Loaded embedding model.
|
| 21 |
"""
|
| 22 |
name = model_name or config.EMBEDDING_MODEL
|
| 23 |
+
return SentenceTransformer(
|
| 24 |
+
name,
|
| 25 |
+
backend="onnx",
|
| 26 |
+
model_kwargs={"file_name": config.ONNX_FILE_NAME},
|
| 27 |
+
)
|
| 28 |
|
| 29 |
|
| 30 |
def encode_texts(
|
|
|
|
| 75 |
Loaded cross-encoder model.
|
| 76 |
"""
|
| 77 |
name = model_name or config.CROSS_ENCODER_MODEL
|
| 78 |
+
model: CrossEncoder = CrossEncoder(
|
| 79 |
+
name,
|
| 80 |
+
backend="onnx",
|
| 81 |
+
model_kwargs={"file_name": config.ONNX_FILE_NAME},
|
| 82 |
+
)
|
| 83 |
return model
|
rag/retrieve.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
"""Two-stage retrieval: FAISS vector search + cross-encoder reranking."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 5 |
from pathlib import Path
|
| 6 |
from typing import Any
|
| 7 |
|
|
@@ -50,16 +49,10 @@ def load_pipeline(
|
|
| 50 |
idx_path = index_path or config.INDEX_PATH
|
| 51 |
map_path = mapping_path or config.MAPPING_PATH
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
f_cross = pool.submit(load_cross_encoder)
|
| 58 |
-
|
| 59 |
-
index = f_index.result()
|
| 60 |
-
mapping = f_mapping.result()
|
| 61 |
-
embed_model = f_embed.result()
|
| 62 |
-
cross_encoder = f_cross.result()
|
| 63 |
|
| 64 |
return index, mapping, embed_model, cross_encoder
|
| 65 |
|
|
|
|
| 1 |
"""Two-stage retrieval: FAISS vector search + cross-encoder reranking."""
|
| 2 |
|
| 3 |
import json
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import Any
|
| 6 |
|
|
|
|
| 49 |
idx_path = index_path or config.INDEX_PATH
|
| 50 |
map_path = mapping_path or config.MAPPING_PATH
|
| 51 |
|
| 52 |
+
index = faiss.read_index(str(idx_path))
|
| 53 |
+
mapping = _load_mapping(map_path)
|
| 54 |
+
embed_model = load_embedding_model()
|
| 55 |
+
cross_encoder = load_cross_encoder()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
return index, mapping, embed_model, cross_encoder
|
| 58 |
|
static/index.html
CHANGED
|
@@ -65,7 +65,7 @@
|
|
| 65 |
<h2 class="sidebar-title">Historique</h2>
|
| 66 |
<div class="history-list"></div>
|
| 67 |
<p class="history-empty">Vos recherches apparaîtront ici</p>
|
| 68 |
-
<p class="sidebar-version">v1.1.
|
| 69 |
</aside>
|
| 70 |
|
| 71 |
<div class="hero-section">
|
|
|
|
| 65 |
<h2 class="sidebar-title">Historique</h2>
|
| 66 |
<div class="history-list"></div>
|
| 67 |
<p class="history-empty">Vos recherches apparaîtront ici</p>
|
| 68 |
+
<p class="sidebar-version">v1.1.1</p>
|
| 69 |
</aside>
|
| 70 |
|
| 71 |
<div class="hero-section">
|
static/styles.css
CHANGED
|
@@ -564,43 +564,51 @@ h1 {
|
|
| 564 |
color: var(--color-text-card);
|
| 565 |
}
|
| 566 |
|
| 567 |
-
/* ---
|
| 568 |
|
| 569 |
-
.card-
|
| 570 |
display: flex;
|
| 571 |
-
|
| 572 |
gap: var(--space-xs);
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
| 576 |
}
|
| 577 |
|
| 578 |
.feedback-btn {
|
| 579 |
display: flex;
|
| 580 |
align-items: center;
|
| 581 |
justify-content: center;
|
| 582 |
-
width:
|
| 583 |
-
height:
|
| 584 |
padding: 0;
|
| 585 |
border: none;
|
| 586 |
border-radius: var(--radius-sm);
|
| 587 |
background: transparent;
|
| 588 |
color: var(--color-text-muted);
|
| 589 |
cursor: pointer;
|
|
|
|
| 590 |
transition: color var(--transition-quick),
|
| 591 |
-
background-color var(--transition-quick)
|
|
|
|
| 592 |
}
|
| 593 |
|
| 594 |
.feedback-btn:hover {
|
| 595 |
background: var(--color-highlight);
|
|
|
|
| 596 |
}
|
| 597 |
|
| 598 |
.feedback-up[aria-pressed="true"] {
|
| 599 |
color: var(--color-score-high);
|
|
|
|
| 600 |
}
|
| 601 |
|
| 602 |
.feedback-down[aria-pressed="true"] {
|
| 603 |
color: var(--color-feedback-error);
|
|
|
|
| 604 |
}
|
| 605 |
|
| 606 |
/* --- Status Messages --- */
|
|
@@ -1018,8 +1026,8 @@ textarea:focus-visible {
|
|
| 1018 |
}
|
| 1019 |
|
| 1020 |
.feedback-btn {
|
| 1021 |
-
width:
|
| 1022 |
-
height:
|
| 1023 |
}
|
| 1024 |
|
| 1025 |
.example-query {
|
|
|
|
| 564 |
color: var(--color-text-card);
|
| 565 |
}
|
| 566 |
|
| 567 |
+
/* --- Header Actions / Feedback --- */
|
| 568 |
|
| 569 |
+
.card-header-actions {
|
| 570 |
display: flex;
|
| 571 |
+
align-items: center;
|
| 572 |
gap: var(--space-xs);
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.feedback-group {
|
| 576 |
+
display: flex;
|
| 577 |
+
align-items: center;
|
| 578 |
+
gap: 2px;
|
| 579 |
}
|
| 580 |
|
| 581 |
.feedback-btn {
|
| 582 |
display: flex;
|
| 583 |
align-items: center;
|
| 584 |
justify-content: center;
|
| 585 |
+
width: 28px;
|
| 586 |
+
height: 28px;
|
| 587 |
padding: 0;
|
| 588 |
border: none;
|
| 589 |
border-radius: var(--radius-sm);
|
| 590 |
background: transparent;
|
| 591 |
color: var(--color-text-muted);
|
| 592 |
cursor: pointer;
|
| 593 |
+
opacity: 0.5;
|
| 594 |
transition: color var(--transition-quick),
|
| 595 |
+
background-color var(--transition-quick),
|
| 596 |
+
opacity var(--transition-quick);
|
| 597 |
}
|
| 598 |
|
| 599 |
.feedback-btn:hover {
|
| 600 |
background: var(--color-highlight);
|
| 601 |
+
opacity: 1;
|
| 602 |
}
|
| 603 |
|
| 604 |
.feedback-up[aria-pressed="true"] {
|
| 605 |
color: var(--color-score-high);
|
| 606 |
+
opacity: 1;
|
| 607 |
}
|
| 608 |
|
| 609 |
.feedback-down[aria-pressed="true"] {
|
| 610 |
color: var(--color-feedback-error);
|
| 611 |
+
opacity: 1;
|
| 612 |
}
|
| 613 |
|
| 614 |
/* --- Status Messages --- */
|
|
|
|
| 1026 |
}
|
| 1027 |
|
| 1028 |
.feedback-btn {
|
| 1029 |
+
width: 32px;
|
| 1030 |
+
height: 32px;
|
| 1031 |
}
|
| 1032 |
|
| 1033 |
.example-query {
|
templates/results.html
CHANGED
|
@@ -19,32 +19,34 @@
|
|
| 19 |
data-score="{{ r.score }}">
|
| 20 |
<div class="card-header">
|
| 21 |
<span><strong>{{ r.book_title }}</strong> — {{ r.chapter }}{% if r.verse %}:{{ r.verse }}{% endif %}</span>
|
| 22 |
-
<
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
</div>
|
| 29 |
{% include "context_verses.html" %}
|
| 30 |
-
<div class="card-footer">
|
| 31 |
-
<button type="button" class="feedback-btn feedback-up"
|
| 32 |
-
data-feedback="up" aria-label="Pertinent" aria-pressed="false">
|
| 33 |
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 34 |
-
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 35 |
-
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/>
|
| 36 |
-
<path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
|
| 37 |
-
</svg>
|
| 38 |
-
</button>
|
| 39 |
-
<button type="button" class="feedback-btn feedback-down"
|
| 40 |
-
data-feedback="down" aria-label="Non pertinent" aria-pressed="false">
|
| 41 |
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 42 |
-
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 43 |
-
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/>
|
| 44 |
-
<path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/>
|
| 45 |
-
</svg>
|
| 46 |
-
</button>
|
| 47 |
-
</div>
|
| 48 |
</div>
|
| 49 |
</div>
|
| 50 |
{% endfor %}
|
|
|
|
| 19 |
data-score="{{ r.score }}">
|
| 20 |
<div class="card-header">
|
| 21 |
<span><strong>{{ r.book_title }}</strong> — {{ r.chapter }}{% if r.verse %}:{{ r.verse }}{% endif %}</span>
|
| 22 |
+
<div class="card-header-actions">
|
| 23 |
+
<span class="score-badge
|
| 24 |
+
{%- if r.pct >= 80 %} score-high
|
| 25 |
+
{%- elif r.pct >= 50 %} score-mid
|
| 26 |
+
{%- elif r.pct >= 30 %} score-low
|
| 27 |
+
{%- else %} score-minimal
|
| 28 |
+
{%- endif %}">{{ r.label }} ({{ r.pct }}%)</span>
|
| 29 |
+
<div class="feedback-group">
|
| 30 |
+
<button type="button" class="feedback-btn feedback-up"
|
| 31 |
+
data-feedback="up" aria-label="Pertinent" aria-pressed="false">
|
| 32 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 33 |
+
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 34 |
+
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/>
|
| 35 |
+
<path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
|
| 36 |
+
</svg>
|
| 37 |
+
</button>
|
| 38 |
+
<button type="button" class="feedback-btn feedback-down"
|
| 39 |
+
data-feedback="down" aria-label="Non pertinent" aria-pressed="false">
|
| 40 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
| 41 |
+
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
| 42 |
+
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/>
|
| 43 |
+
<path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/>
|
| 44 |
+
</svg>
|
| 45 |
+
</button>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
</div>
|
| 49 |
{% include "context_verses.html" %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
</div>
|
| 51 |
</div>
|
| 52 |
{% endfor %}
|