msi commited on
Commit
b4470c3
·
1 Parent(s): 6324c96

Add application file

Browse files
.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