Spaces:
Paused
Paused
| # -*- 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 | |
| def _get_embedder(): | |
| from sentence_transformers import SentenceTransformer | |
| return SentenceTransformer(config.EMBEDDING_MODEL) | |
| 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 | |