hfchat / rag_engine.py
Phitographix's picture
Upload 14 files
bda6294 verified
Raw
History Blame Contribute Delete
6.51 kB
# -*- coding: utf-8 -*-
"""
Moteur RAG (Space edition) : recuperation + generation via l'API Hugging Face
(ou Ollama en repli local). Reponses fondees uniquement sur le contenu indexe
du site (public + non public selon les droits).
"""
import pickle
import functools
import requests
import config
import gdpr
@functools.lru_cache(maxsize=1)
def _get_embedder():
from sentence_transformers import SentenceTransformer
return SentenceTransformer(config.EMBEDDING_MODEL)
@functools.lru_cache(maxsize=1)
def _get_index_and_chunks():
import faiss
index = faiss.read_index(str(config.INDEX_FILE))
with open(config.CHUNKS_FILE, "rb") as f:
chunks = pickle.load(f)
return index, chunks
def reset_cache():
_get_index_and_chunks.cache_clear()
# --------------------------------------------------------------------------
# Recuperation / retrieval
# --------------------------------------------------------------------------
def retrieve(query, top_k=None):
top_k = top_k or config.TOP_K
index, chunks = _get_index_and_chunks()
embedder = _get_embedder()
q_vec = embedder.encode([query], normalize_embeddings=True,
convert_to_numpy=True).astype("float32")
scores, idx = index.search(q_vec, top_k)
results = []
for score, i in zip(scores[0], idx[0]):
if i < 0 or score < config.MIN_SCORE:
continue
c = dict(chunks[i])
c["score"] = float(score)
results.append(c)
return results
# --------------------------------------------------------------------------
# Prompt
# --------------------------------------------------------------------------
SYSTEM_FR = (
"Tu es l'assistant interne du site Vizyon Ayiti 360. "
"Tu reponds EXCLUSIVEMENT a partir du CONTEXTE fourni (extraits du site, "
"publics et internes). Interdiction d'utiliser des connaissances externes. "
"Si la reponse ne figure pas dans le contexte, dis-le clairement. Cite les "
"titres/URL des sources. Reponds dans la langue de la question."
)
SYSTEM_EN = (
"You are the internal assistant of the Vizyon Ayiti 360 website. "
"You answer EXCLUSIVELY from the provided CONTEXT (public and internal site "
"excerpts). Do not use external knowledge. If the answer is not in the "
"context, say so clearly. Cite source titles/URLs. Reply in the language of "
"the question."
)
SYSTEM_HT = (
"Ou se asistan enten sit Vizyon Ayiti 360 a. Ou reponn SELMAN ak KONTEKS yo "
"ba ou a (ekstre piblik ak enten nan sit la). Ou pa gen dwa itilize okenn "
"lot konesans deyo. Si repons lan pa nan konteks la, di sa kle. Site tit ak "
"adres (URL) sous yo. Reponn an kreyol ayisyen."
)
_SYSTEMS = {"fr": SYSTEM_FR, "en": SYSTEM_EN, "ht": SYSTEM_HT}
def build_prompt(query, passages, lang="fr"):
blocks = []
for i, p in enumerate(passages, 1):
tag = "" if p.get("status", "publish") == "publish" else " [INTERNE]"
blocks.append(f"[Source {i}]{tag} {p['title']} ({p['url']})\n{p['text']}")
context = "\n\n".join(blocks) if blocks else "(aucun extrait pertinent)"
system = _SYSTEMS.get(lang, SYSTEM_FR)
user = (
f"CONTEXTE:\n{context}\n\n"
f"QUESTION: {query}\n\n"
"REPONSE (fondee uniquement sur le contexte, avec citations) :"
)
return system, user
# --------------------------------------------------------------------------
# Generation
# --------------------------------------------------------------------------
def _generate_hf(system, user):
if not config.HF_API_TOKEN:
raise RuntimeError(
"Token HF manquant. Ajoutez le secret HF_API_TOKEN (ou HF_TOKEN) "
"dans les parametres du Space."
)
headers = {"Authorization": f"Bearer {config.HF_API_TOKEN}"}
url = "https://router.huggingface.co/v1/chat/completions"
payload = {
"model": config.HF_MODEL,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"temperature": config.TEMPERATURE,
"max_tokens": config.MAX_TOKENS,
}
r = requests.post(url, headers=headers, json=payload, timeout=180)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"].strip()
def _generate_ollama(system, user):
payload = {
"model": config.OLLAMA_MODEL,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"stream": False,
"options": {"temperature": config.TEMPERATURE,
"num_predict": config.MAX_TOKENS},
}
r = requests.post(f"{config.OLLAMA_HOST}/api/chat", json=payload, timeout=180)
r.raise_for_status()
return r.json()["message"]["content"].strip()
def generate(system, user):
if config.LLM_BACKEND == "ollama":
return _generate_ollama(system, user)
return _generate_hf(system, user)
# --------------------------------------------------------------------------
# Point d'entree / entry point
# --------------------------------------------------------------------------
def answer(query, lang="fr", extra_context=""):
query = gdpr.safe_for_processing(query)
passages = retrieve(query)
if extra_context:
passages = passages + [{
"title": "Document televerse (session)",
"url": "local://document",
"text": extra_context[:4000],
"score": 1.0,
"type": "Document",
"status": "publish",
}]
if not passages:
_no = {
"fr": ("Je n'ai trouve aucune information correspondante dans le "
"contenu indexe du site. Reformulez votre question."),
"en": ("I could not find matching information in the indexed site "
"content. Try rephrasing your question."),
"ht": ("Mwen pa jwenn okenn enfomasyon ki koresponn nan kontni sit "
"la. Eseye poze kesyon an yon lot jan."),
}
return _no.get(lang, _no["fr"]), []
system, user = build_prompt(query, passages, lang)
response = generate(system, user)
sources = [{"title": p["title"], "url": p["url"],
"score": p.get("score", 0),
"status": p.get("status", "publish")}
for p in passages if p["url"] != "local://document"]
gdpr.log_interaction(query, response, sources)
return response, sources