Spaces:
Sleeping
Sleeping
msi commited on
Commit ·
b4470c3
1
Parent(s): 6324c96
Add application file
Browse files- .gitignore +1 -0
- Dockerfile +31 -0
- faiss_index/index.faiss +0 -0
- faiss_index/index.pkl +3 -0
- main.py +169 -0
- prompt_engineering.py +111 -0
- requirements.txt +21 -0
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
# ── Variables d'environnement ─────────────────────────────
|
| 4 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 5 |
+
ENV PYTHONUNBUFFERED=1
|
| 6 |
+
|
| 7 |
+
# ── Dépendances système (FAISS + torch + build) ───────────
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
build-essential \
|
| 10 |
+
git \
|
| 11 |
+
curl \
|
| 12 |
+
libgomp1 \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# ── Dossier de travail ─────────────────────────────────────
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# ── Copier les fichiers du projet ──────────────────────────
|
| 19 |
+
COPY . /app
|
| 20 |
+
|
| 21 |
+
# ── Installer pip + dépendances Python ─────────────────────
|
| 22 |
+
RUN pip install --upgrade pip
|
| 23 |
+
|
| 24 |
+
# Si tu as requirements.txt
|
| 25 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 26 |
+
|
| 27 |
+
# ── Port utilisé par Hugging Face Spaces ───────────────────
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# ── Lancer l’API FastAPI ───────────────────────────────────
|
| 31 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
faiss_index/index.faiss
ADDED
|
Binary file (26.2 kB). View file
|
|
|
faiss_index/index.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:66a46a836820cb60788f167a000dde60b3eea93addc8ba94c56bd5c5c0163f9a
|
| 3 |
+
size 12061
|
main.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from contextlib import asynccontextmanager
|
| 5 |
+
from langchain_community.vectorstores import FAISS
|
| 6 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 7 |
+
from langchain_openai import ChatOpenAI
|
| 8 |
+
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
|
| 9 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 10 |
+
import os
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from prompt_engineering import build_prompt
|
| 13 |
+
|
| 14 |
+
# ── État global ───────────────────────────────────────────────────────────────
|
| 15 |
+
rag_chain = None
|
| 16 |
+
retriever = None
|
| 17 |
+
load_dotenv() # ← charge le fichier .env
|
| 18 |
+
|
| 19 |
+
# ── Helper format docs ────────────────────────────────────────────────────────
|
| 20 |
+
def format_docs(docs) -> str:
|
| 21 |
+
"""Convertit les documents récupérés en texte pour le prompt."""
|
| 22 |
+
return "\n\n".join(
|
| 23 |
+
f"[Source: {os.path.basename(doc.metadata.get('source', 'Inconnue'))}]\n{doc.page_content}"
|
| 24 |
+
for doc in docs
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def extract_sources(docs) -> list[str]:
|
| 28 |
+
"""Formate les sources depuis les métadonnées des documents."""
|
| 29 |
+
sources = []
|
| 30 |
+
seen = set()
|
| 31 |
+
for doc in docs:
|
| 32 |
+
source = doc.metadata.get("source", "Inconnue")
|
| 33 |
+
page = doc.metadata.get("page")
|
| 34 |
+
label = (
|
| 35 |
+
f"{os.path.basename(source)}, page {page + 1}"
|
| 36 |
+
if page is not None
|
| 37 |
+
else os.path.basename(source)
|
| 38 |
+
)
|
| 39 |
+
if label not in seen:
|
| 40 |
+
sources.append(label)
|
| 41 |
+
seen.add(label)
|
| 42 |
+
return sources
|
| 43 |
+
|
| 44 |
+
def get_confidence(docs_with_scores: list) -> str:
|
| 45 |
+
"""Calcule le niveau de confiance selon les scores FAISS (distance L2)."""
|
| 46 |
+
if not docs_with_scores:
|
| 47 |
+
return "low"
|
| 48 |
+
avg_score = sum(s for _, s in docs_with_scores) / len(docs_with_scores)
|
| 49 |
+
if avg_score < 0.4:
|
| 50 |
+
return "high"
|
| 51 |
+
elif avg_score < 0.8:
|
| 52 |
+
return "medium"
|
| 53 |
+
return "low"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ── Chargement au démarrage ───────────────────────────────────────────────────
|
| 57 |
+
@asynccontextmanager
|
| 58 |
+
async def lifespan(app: FastAPI):
|
| 59 |
+
global rag_chain, retriever
|
| 60 |
+
|
| 61 |
+
embedding = HuggingFaceEmbeddings(
|
| 62 |
+
model_name="sentence-transformers/all-MiniLM-L6-v2",
|
| 63 |
+
model_kwargs={"device": "cpu"},
|
| 64 |
+
encode_kwargs={"normalize_embeddings": True}
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
vectorstore = FAISS.load_local(
|
| 68 |
+
"faiss_index",
|
| 69 |
+
embeddings=embedding,
|
| 70 |
+
allow_dangerous_deserialization=True
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
llm = ChatOpenAI(
|
| 77 |
+
base_url="https://api.mistral.ai/v1",
|
| 78 |
+
api_key=os.getenv("MISTRAL_API_KEY"),
|
| 79 |
+
model_name="mistral-medium"
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
|
| 83 |
+
|
| 84 |
+
# ✅ FIX : retriever | format_docs (via RunnableLambda) pour convertir les
|
| 85 |
+
# documents en texte avant de les injecter dans le prompt
|
| 86 |
+
rag_chain = (
|
| 87 |
+
{
|
| 88 |
+
"context": retriever | RunnableLambda(format_docs),
|
| 89 |
+
"question": RunnablePassthrough()
|
| 90 |
+
}
|
| 91 |
+
| build_prompt()
|
| 92 |
+
| llm
|
| 93 |
+
| StrOutputParser()
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
print("✅ RAG chain chargée et prête.")
|
| 97 |
+
yield
|
| 98 |
+
print("🛑 Arrêt de l'API.")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# ── Application FastAPI ───────────────────────────────────────────────────────
|
| 102 |
+
app = FastAPI(
|
| 103 |
+
title="ShopVite RAG API",
|
| 104 |
+
version="1.0.0",
|
| 105 |
+
lifespan=lifespan
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ── Schémas ───────────────────────────────────────────────────────────────────
|
| 110 |
+
class AskRequest(BaseModel):
|
| 111 |
+
question: str
|
| 112 |
+
|
| 113 |
+
class AskResponse(BaseModel):
|
| 114 |
+
answer: str
|
| 115 |
+
sources: list[str]
|
| 116 |
+
confidence: str # "high" | "medium" | "low" | "out_of_context"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ── Routes ────────────────────────────────────────────────────────────────────
|
| 120 |
+
@app.get("/health")
|
| 121 |
+
def health():
|
| 122 |
+
if rag_chain is None:
|
| 123 |
+
raise HTTPException(status_code=503, detail="RAG chain non initialisée.")
|
| 124 |
+
return {
|
| 125 |
+
"status": "ok",
|
| 126 |
+
"model": "mistral-medium",
|
| 127 |
+
"vectorstore": "faiss_index"
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@app.post("/ask", response_model=AskResponse)
|
| 132 |
+
def ask(body: AskRequest):
|
| 133 |
+
question = body.question.strip()
|
| 134 |
+
|
| 135 |
+
if not question:
|
| 136 |
+
raise HTTPException(status_code=400, detail="La question ne peut pas être vide.")
|
| 137 |
+
if len(question) > 500:
|
| 138 |
+
raise HTTPException(status_code=400, detail="Question trop longue (max 500 caractères).")
|
| 139 |
+
|
| 140 |
+
# Récupérer les docs et leurs scores FAISS
|
| 141 |
+
docs_with_scores = retriever.vectorstore.similarity_search_with_score(question, k=3)
|
| 142 |
+
docs = [doc for doc, _ in docs_with_scores]
|
| 143 |
+
sources = extract_sources(docs)
|
| 144 |
+
|
| 145 |
+
# Générer la réponse via la chaîne RAG
|
| 146 |
+
try:
|
| 147 |
+
answer = rag_chain.invoke(question)
|
| 148 |
+
except Exception as e:
|
| 149 |
+
raise HTTPException(status_code=500, detail=f"Erreur LLM : {str(e)}")
|
| 150 |
+
|
| 151 |
+
# Détecter question hors contexte
|
| 152 |
+
if "HORS_CONTEXTE" in answer:
|
| 153 |
+
return AskResponse(
|
| 154 |
+
answer=(
|
| 155 |
+
"Je suis désolé, cette information ne figure pas dans mes documents. "
|
| 156 |
+
"Pour toute question spécifique, contactez notre support : "
|
| 157 |
+
"support@shopvite.fr | 01 23 45 67 89 (lun-ven, 9h-18h)."
|
| 158 |
+
),
|
| 159 |
+
sources=[],
|
| 160 |
+
confidence="out_of_context"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
confidence = get_confidence(docs_with_scores)
|
| 164 |
+
|
| 165 |
+
return AskResponse(
|
| 166 |
+
answer=answer,
|
| 167 |
+
sources=sources,
|
| 168 |
+
confidence=confidence
|
| 169 |
+
)
|
prompt_engineering.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_core.prompts import PromptTemplate
|
| 2 |
+
|
| 3 |
+
template = """
|
| 4 |
+
################################################################################
|
| 5 |
+
# IDENTITÉ
|
| 6 |
+
################################################################################
|
| 7 |
+
Tu es ShopBot, l'assistant virtuel officiel de ShopVite — spécialiste en
|
| 8 |
+
électronique grand public comme les smartphones, ordinateurs portables et accessoires. Tu incarnes la voix professionnelle et bienveillante
|
| 9 |
+
de ShopVite : précis, concis, toujours utile, jamais inventif.
|
| 10 |
+
|
| 11 |
+
Langue : français uniquement.
|
| 12 |
+
Ton : professionnel, chaleureux, direct. Jamais familier, jamais condescendant.
|
| 13 |
+
Taille : 3 à 6 phrases maximum par réponse.
|
| 14 |
+
|
| 15 |
+
################################################################################
|
| 16 |
+
# RÈGLES ABSOLUES
|
| 17 |
+
################################################################################
|
| 18 |
+
R1. Tu réponds UNIQUEMENT à partir du CONTEXTE fourni ci-dessous.
|
| 19 |
+
R2. Si l'information n'est pas dans le contexte applique le REFUS POLI.
|
| 20 |
+
R3. Chaque réponse doit citer la source entre crochets : [Source : nom_fichier].
|
| 21 |
+
R4. Tu n'inventes jamais de chiffre, de délai, de politique ou de procédure.
|
| 22 |
+
|
| 23 |
+
################################################################################
|
| 24 |
+
# MÉTHODE DE RAISONNEMENT (Chain-of-Thought — interne, non affiché)
|
| 25 |
+
################################################################################
|
| 26 |
+
Avant de rédiger ta réponse, raisonne silencieusement en 3 étapes :
|
| 27 |
+
|
| 28 |
+
ETAPE 1 — PERTINENCE
|
| 29 |
+
La question porte-t-elle sur les produits, commandes, livraisons,
|
| 30 |
+
retours, garanties ou données personnelles de ShopVite ?
|
| 31 |
+
Si NON, passe directement au REFUS POLI.
|
| 32 |
+
|
| 33 |
+
ETAPE 2 — EXTRACTION
|
| 34 |
+
Quels passages du contexte répondent précisément à la question ?
|
| 35 |
+
Identifie la source (nom de fichier) de chaque passage retenu.
|
| 36 |
+
|
| 37 |
+
ETAPE 3 — RÉDACTION
|
| 38 |
+
Formule une réponse courte, claire, en français.
|
| 39 |
+
Termine par la citation de source : [Source : nom_fichier].
|
| 40 |
+
|
| 41 |
+
Ce raisonnement est INTERNE : n'affiche pas les étapes dans ta réponse.
|
| 42 |
+
|
| 43 |
+
################################################################################
|
| 44 |
+
# FEW-SHOT EXAMPLES
|
| 45 |
+
################################################################################
|
| 46 |
+
|
| 47 |
+
--- EXEMPLE 1 : question dans le scope ---
|
| 48 |
+
Question : Quel est le délai de rétractation ?
|
| 49 |
+
Réponse : Conformément à nos conditions générales, vous disposez de 30 jours
|
| 50 |
+
à compter de la réception de votre commande pour exercer votre droit
|
| 51 |
+
de rétractation, sans justification requise.
|
| 52 |
+
[Source : conditions_generales.txt]
|
| 53 |
+
|
| 54 |
+
--- EXEMPLE 2 : question dans le scope avec plusieurs sources ---
|
| 55 |
+
Question : Comment retourner un produit défectueux ?
|
| 56 |
+
Réponse : Pour retourner un produit défectueux, contactez notre service client
|
| 57 |
+
sous 48 h avec votre numéro de commande et une photo du défaut.
|
| 58 |
+
Un bon de retour prépayé vous sera envoyé par e-mail sous 24 h.
|
| 59 |
+
Les remboursements sont effectués sous 5 à 7 jours ouvrés.
|
| 60 |
+
[Source : politique_retours.pdf, section 3] [Source : faq_sav.txt]
|
| 61 |
+
|
| 62 |
+
--- EXEMPLE 3 : question hors scope ---
|
| 63 |
+
Question : Pouvez-vous me recommander une recette de cuisine ?
|
| 64 |
+
Réponse : Je suis spécialisé dans l'assistance aux clients ShopVite et je ne
|
| 65 |
+
suis pas en mesure de répondre à cette question.
|
| 66 |
+
Pour toute question relative à vos commandes, produits ou livraisons,
|
| 67 |
+
je reste à votre disposition.
|
| 68 |
+
Pour d'autres besoins, contactez notre support : support@shopvite.fr.
|
| 69 |
+
|
| 70 |
+
--- EXEMPLE 4 : information absente du contexte ---
|
| 71 |
+
Question : Livrez-vous en Martinique ?
|
| 72 |
+
Réponse : Je n'ai pas trouvé d'information sur les livraisons en Martinique
|
| 73 |
+
dans mes documents actuels.
|
| 74 |
+
Contactez notre service client a support@shopvite.fr pour une
|
| 75 |
+
réponse précise.
|
| 76 |
+
|
| 77 |
+
################################################################################
|
| 78 |
+
# REFUS POLI
|
| 79 |
+
################################################################################
|
| 80 |
+
Si la question est hors scope ou absente du contexte, répondre exactement :
|
| 81 |
+
|
| 82 |
+
Je suis ShopBot, assistant dédié aux questions ShopVite (commandes, produits,
|
| 83 |
+
livraisons, retours, garanties). Je ne suis pas en mesure de répondre a cette
|
| 84 |
+
question.
|
| 85 |
+
|
| 86 |
+
Pour toute assistance, notre équipe est disponible :
|
| 87 |
+
- Email : support@shopvite.fr
|
| 88 |
+
- Horaires : Lun-Ven, 9h-18h
|
| 89 |
+
|
| 90 |
+
HORS_CONTEXTE
|
| 91 |
+
|
| 92 |
+
################################################################################
|
| 93 |
+
# CONTEXTE (documents récupérés)
|
| 94 |
+
################################################################################
|
| 95 |
+
{context}
|
| 96 |
+
|
| 97 |
+
################################################################################
|
| 98 |
+
# QUESTION CLIENT
|
| 99 |
+
################################################################################
|
| 100 |
+
{question}
|
| 101 |
+
|
| 102 |
+
################################################################################
|
| 103 |
+
# RÉPONSE DE SHOPBOT
|
| 104 |
+
################################################################################
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def build_prompt() -> PromptTemplate:
|
| 108 |
+
return PromptTemplate(
|
| 109 |
+
template=template,
|
| 110 |
+
input_variables=["context", "question"]
|
| 111 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.30.6
|
| 3 |
+
pydantic==2.8.2
|
| 4 |
+
|
| 5 |
+
python-dotenv==1.0.1
|
| 6 |
+
|
| 7 |
+
langchain==0.2.14
|
| 8 |
+
langchain-core==0.2.35
|
| 9 |
+
langchain-community==0.2.12
|
| 10 |
+
langchain-huggingface==0.0.3
|
| 11 |
+
langchain-openai==0.1.23
|
| 12 |
+
|
| 13 |
+
faiss-cpu==1.8.0.post1
|
| 14 |
+
|
| 15 |
+
sentence-transformers==3.0.1
|
| 16 |
+
huggingface-hub==0.24.6
|
| 17 |
+
transformers==4.44.2
|
| 18 |
+
torch==2.3.1
|
| 19 |
+
|
| 20 |
+
openai==1.40.6
|
| 21 |
+
tiktoken==0.7.0
|