FabIndy commited on
Commit
feddcd9
·
1 Parent(s): 247f65e

Stabilize RAG core: add SUMMARY_AI, speed up LIST, clean resources and config

Browse files
Files changed (4) hide show
  1. src/config.py +112 -34
  2. src/qa.py +52 -11
  3. src/rag_core.py +91 -11
  4. src/resources.py +74 -17
src/config.py CHANGED
@@ -1,55 +1,133 @@
1
  # src/config.py
 
 
2
  import os
3
  import re
4
  from pathlib import Path
5
 
6
- # Paths
7
- CHUNKS_PATH = Path("data/chunks_articles.jsonl")
8
- DB_DIR = Path("db/faiss_code_edu_by_article")
9
 
10
- # Embeddings
11
- EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
 
12
 
13
- # LLM (QA)
14
- LLM_MODEL_PATH = os.environ.get("LLM_MODEL_PATH", "models/model.gguf")
15
- LLM_N_CTX = int(os.environ.get("LLM_N_CTX", "1024"))
16
- LLM_N_THREADS = int(os.environ.get("LLM_N_THREADS", "10"))
17
- LLM_N_BATCH = int(os.environ.get("LLM_N_BATCH", "128"))
18
 
19
- QA_TOP_K_FINAL = int(os.environ.get("QA_TOP_K_FINAL", "2"))
20
- QA_DOC_MAX_CHARS = int(os.environ.get("QA_DOC_MAX_CHARS", "700"))
21
- QA_MAX_TOKENS = int(os.environ.get("QA_MAX_TOKENS", "160"))
22
- QA_TEMPERATURE = float(os.environ.get("QA_TEMPERATURE", "0.2"))
23
 
24
- ARTICLE_ID_RE = re.compile(
25
- r"\b(?:article\s+)?([LDR]\s?\d{1,4}(?:[.-]\d+){0,4})\b",
26
- flags=re.IGNORECASE,
27
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- EXPLAIN_TRIGGERS = [
30
- "synthèse", "synthese", "points clés", "points cles",
31
- "extraits", "extrait", "résumé extractif", "resume extractif",
32
- ]
33
  LIST_TRIGGERS = [
34
- "quels articles", "quelles dispositions", "articles parlent",
35
- "articles qui parlent", "articles sur", "donne les articles",
36
- "cite les articles", "références", "references",
 
 
 
 
 
 
37
  ]
 
38
  FULLTEXT_TRIGGERS = [
39
- "contenu exact", "texte exact", "texte intégral", "texte integral",
40
- "intégral", "integral", "cite intégralement", "cite integralement",
41
- "donne l'intégralité", "donne l'integralite", "recopie", "reproduis",
42
- "affiche l'article", "donne l'article", "donne moi l'article",
 
 
 
 
 
43
  ]
44
 
45
- REFUSAL = "Je ne peux pas répondre avec certitude à partir des articles fournis."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  SYNTHESIS_REFUSAL = (
48
- "Pour produire une synthèse extractive, indique un identifiant d’article (ex : D422-5). "
49
- "Sinon, commence par : \"Quels articles parlent de … ?\""
50
  )
51
 
52
  QA_WARNING = (
53
- "Mode QA (interprétation) : la réponse ci-dessous est rédigée par un modèle IA sur CPU. "
54
- "Elle peut être incomplète ou imprécise. Vérifie toujours sur le texte exact des articles."
55
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # src/config.py
2
+ from __future__ import annotations
3
+
4
  import os
5
  import re
6
  from pathlib import Path
7
 
 
 
 
8
 
9
+ # =========================
10
+ # Paths (HF / local)
11
+ # =========================
12
 
13
+ # Base directory = root of repo (…/hf-code-education)
14
+ BASE_DIR = Path(__file__).resolve().parents[1]
 
 
 
15
 
16
+ # Data
17
+ CHUNKS_PATH = str(Path(os.environ.get("CHUNKS_PATH", BASE_DIR / "data" / "chunks_articles.jsonl")))
 
 
18
 
19
+ # Vectorstore (FAISS)
20
+ DB_DIR = str(Path(os.environ.get("DB_DIR", BASE_DIR / "db" / "faiss_code_edu_by_article")))
21
+
22
+ # Embeddings model for FAISS queries (used in QA mode)
23
+ EMBED_MODEL = os.environ.get("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
24
+
25
+ # LLM (GGUF) path
26
+ LLM_MODEL_PATH = str(Path(os.environ.get("LLM_MODEL_PATH", BASE_DIR / "models" / "model.gguf")))
27
+
28
+
29
+ # =========================
30
+ # Article ID regex
31
+ # =========================
32
+
33
+ # Match typical French code article IDs: L111-1, R421-10, D521-5, etc.
34
+ ARTICLE_ID_RE = re.compile(r"\b([LDR]\s?\d{1,4}(?:-\d+){1,4})\b", re.IGNORECASE)
35
+
36
+
37
+ # =========================
38
+ # Triggers (routing)
39
+ # =========================
40
 
 
 
 
 
41
  LIST_TRIGGERS = [
42
+ "quels articles",
43
+ "quels sont les articles",
44
+ "articles sur",
45
+ "articles parlant",
46
+ "articles qui parlent",
47
+ "trouve des articles",
48
+ "trouver des articles",
49
+ "liste des articles",
50
+ "liste",
51
  ]
52
+
53
  FULLTEXT_TRIGGERS = [
54
+ "intégralité",
55
+ "integralite",
56
+ "texte officiel",
57
+ "texte intégral",
58
+ "texte integral",
59
+ "donne l’intégralité",
60
+ "donne l'integralite",
61
+ "donne le texte",
62
+ "affiche l'article",
63
  ]
64
 
65
+ EXPLAIN_TRIGGERS = [
66
+ "explique",
67
+ "expliquer",
68
+ "synthèse",
69
+ "synthese",
70
+ "points clés",
71
+ "points cles",
72
+ ]
73
+
74
+
75
+ # =========================
76
+ # Messages utilisateur
77
+ # =========================
78
+
79
+ REFUSAL = (
80
+ "Je ne peux pas répondre à cette demande telle quelle.\n"
81
+ "Indique un thème (mode LIST) ou un identifiant d’article (mode FULLTEXT / Résumé / Synthèse)."
82
+ )
83
 
84
  SYNTHESIS_REFUSAL = (
85
+ "Pour faire une synthèse, j’ai besoin d’un identifiant d’article (ex : D521-5)."
 
86
  )
87
 
88
  QA_WARNING = (
89
+ "Réponse IA : cette réponse peut contenir des erreurs. "
90
+ "Vérifie toujours sur le texte officiel et, en cas de doute, demande un avis juridique."
91
  )
92
+
93
+
94
+ # =========================
95
+ # QA settings (speed / safety)
96
+ # =========================
97
+
98
+ QA_TOP_K_FINAL = int(os.environ.get("QA_TOP_K_FINAL", "2"))
99
+ QA_DOC_MAX_CHARS = int(os.environ.get("QA_DOC_MAX_CHARS", "700"))
100
+ QA_MAX_TOKENS = int(os.environ.get("QA_MAX_TOKENS", "160"))
101
+ QA_TEMPERATURE = float(os.environ.get("QA_TEMPERATURE", "0.2"))
102
+
103
+
104
+ # =========================
105
+ # SUMMARY_AI settings (future move out of rag_core)
106
+ # =========================
107
+
108
+ SUMMARY_TRIGGERS = [
109
+ "résumé ia", "resume ia",
110
+ "résumé", "resume",
111
+ "résumer", "resumer",
112
+ "summary",
113
+ ]
114
+
115
+ SUMMARY_WARNING = (
116
+ "Résumé IA : reformulation automatique (peut contenir des erreurs ou omissions). "
117
+ "Vérifie toujours sur le texte officiel."
118
+ )
119
+
120
+ SUMMARY_DOC_MAX_CHARS = int(os.environ.get("SUMMARY_DOC_MAX_CHARS", "1200"))
121
+ SUMMARY_MAX_TOKENS = int(os.environ.get("SUMMARY_MAX_TOKENS", "180"))
122
+ SUMMARY_TEMPERATURE = float(os.environ.get("SUMMARY_TEMPERATURE", "0.2"))
123
+
124
+
125
+ # =========================
126
+ # Llama.cpp settings
127
+ # =========================
128
+
129
+ # Important : sur HF CPU, trop de threads peut parfois dégrader.
130
+ # Laisse configurable. Valeur par défaut prudente.
131
+ LLM_N_CTX = int(os.environ.get("LLM_N_CTX", "1024"))
132
+ LLM_N_THREADS = int(os.environ.get("LLM_N_THREADS", str(max(1, (os.cpu_count() or 2) - 1))))
133
+ LLM_N_BATCH = int(os.environ.get("LLM_N_BATCH", "128"))
src/qa.py CHANGED
@@ -4,10 +4,11 @@
4
  """
5
  qa.py — Mode QA (interprétatif, LLM CPU, plus lent)
6
 
7
- Origine :
8
- - build_qa_prompt_fast
9
- - _truncate
10
- - wrapper llm_generate_qa (dépend d'un objet Llama instancié ailleurs)
 
11
  """
12
 
13
  from __future__ import annotations
@@ -21,29 +22,40 @@ from typing import List
21
 
22
  @dataclass(frozen=True)
23
  class QAConfig:
 
24
  qa_top_k_final: int = int(os.environ.get("QA_TOP_K_FINAL", "2"))
25
  qa_doc_max_chars: int = int(os.environ.get("QA_DOC_MAX_CHARS", "700"))
26
  qa_max_tokens: int = int(os.environ.get("QA_MAX_TOKENS", "160"))
27
  qa_temperature: float = float(os.environ.get("QA_TEMPERATURE", "0.2"))
28
 
29
 
30
- # ==================== PROMPT UTILS ====================
31
 
32
  def truncate_text(s: str, n: int) -> str:
 
33
  if not s:
34
  return ""
35
  s = s.strip()
36
  return s if len(s) <= n else s[:n].rstrip() + "\n[...]\n"
37
 
38
 
 
 
39
  def build_qa_prompt_fast(question: str, context: str, sources: List[str]) -> str:
40
- src = ", ".join(sources)
 
 
 
 
 
 
41
  return f"""Tu es un assistant qui aide à comprendre le Code de l'éducation (France).
42
 
43
  CONTRAINTE :
44
- - Appuie-toi en priorité sur le CONTEXTE fourni.
45
- - Si l'information n'est pas dans le contexte, dis-le simplement.
46
- - Réponse courte, pratique, 6-10 phrases max.
 
47
 
48
  QUESTION :
49
  {question}
@@ -51,7 +63,31 @@ QUESTION :
51
  CONTEXTE :
52
  {context}
53
 
54
- Indique à la fin : "Sources (articles) : {src}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  """
56
 
57
 
@@ -59,9 +95,14 @@ Indique à la fin : "Sources (articles) : {src}"
59
 
60
  def llm_generate_qa(llm, prompt: str, cfg: QAConfig | None = None) -> str:
61
  """
62
- llm: instance llama_cpp.Llama créée ailleurs (ex: dans rag_core).
 
 
 
 
63
  """
64
  cfg = cfg or QAConfig()
 
65
  out = llm.create_chat_completion(
66
  messages=[{"role": "user", "content": prompt}],
67
  temperature=cfg.qa_temperature,
 
4
  """
5
  qa.py — Mode QA (interprétatif, LLM CPU, plus lent)
6
 
7
+ Objectif :
8
+ - Construire un prompt QA rapide et "prudent"
9
+ - Fournir un wrapper d'appel LLM (llama_cpp.Llama) instancié ailleurs
10
+ - Fournir une utilitaire de tronquage de contexte
11
+ - (ajout) Construire un prompt de Résumé IA (SUMMARY_AI), pour réutiliser le même moteur LLM
12
  """
13
 
14
  from __future__ import annotations
 
22
 
23
  @dataclass(frozen=True)
24
  class QAConfig:
25
+ # QA
26
  qa_top_k_final: int = int(os.environ.get("QA_TOP_K_FINAL", "2"))
27
  qa_doc_max_chars: int = int(os.environ.get("QA_DOC_MAX_CHARS", "700"))
28
  qa_max_tokens: int = int(os.environ.get("QA_MAX_TOKENS", "160"))
29
  qa_temperature: float = float(os.environ.get("QA_TEMPERATURE", "0.2"))
30
 
31
 
32
+ # ==================== TEXT UTILS ====================
33
 
34
  def truncate_text(s: str, n: int) -> str:
35
+ """Tronque une chaîne à n caractères, avec un marqueur explicite."""
36
  if not s:
37
  return ""
38
  s = s.strip()
39
  return s if len(s) <= n else s[:n].rstrip() + "\n[...]\n"
40
 
41
 
42
+ # ==================== PROMPTS ====================
43
+
44
  def build_qa_prompt_fast(question: str, context: str, sources: List[str]) -> str:
45
+ """
46
+ Prompt QA court :
47
+ - s'appuie sur le contexte fourni
48
+ - refuse clairement si l'info n'est pas présente
49
+ - impose une réponse brève
50
+ """
51
+ src = ", ".join(sources) if sources else "Aucune"
52
  return f"""Tu es un assistant qui aide à comprendre le Code de l'éducation (France).
53
 
54
  CONTRAINTE :
55
+ - Appuie-toi STRICTEMENT sur le CONTEXTE fourni.
56
+ - Si l'information n'est pas dans le contexte, dis-le clairement (sans inventer).
57
+ - Réponse courte, pratique, 6 à 10 phrases max.
58
+ - Ne cite pas de sources externes, uniquement les articles fournis.
59
 
60
  QUESTION :
61
  {question}
 
63
  CONTEXTE :
64
  {context}
65
 
66
+ Indique à la fin exactement : "Sources (articles) : {src}"
67
+ """
68
+
69
+
70
+ def build_summary_prompt(article_id: str, article_text: str) -> str:
71
+ """
72
+ Prompt Résumé IA (SUMMARY_AI).
73
+ - Résumé reformulé (contrairement à SYNTHESIS qui est extractif)
74
+ - Zéro invention : si ce n'est pas dans le texte, ne pas l'ajouter
75
+ - Format en puces, concis
76
+ """
77
+ return f"""Tu aides un professionnel à lire rapidement un article du Code de l'éducation (France).
78
+
79
+ TÂCHE : produire un résumé fidèle et utile de l'article, sans inventer.
80
+
81
+ RÈGLES :
82
+ - Ne mentionne QUE des informations présentes dans le texte.
83
+ - Pas d'ajout d'information extérieure.
84
+ - 4 à 8 puces maximum.
85
+ - Style neutre, factuel.
86
+ - Si le texte est très court, reformule simplement l'idée centrale en 2 à 4 puces.
87
+ - N'ajoute pas de conclusion, pas de conseils, pas d'interprétation.
88
+
89
+ ARTICLE {article_id} (texte officiel) :
90
+ {article_text}
91
  """
92
 
93
 
 
95
 
96
  def llm_generate_qa(llm, prompt: str, cfg: QAConfig | None = None) -> str:
97
  """
98
+ llm: instance llama_cpp.Llama créée ailleurs (ex: dans resources.py ou rag_core.py)
99
+
100
+ Remarque :
101
+ - On utilise create_chat_completion pour les modèles instruct/chat.
102
+ - Les paramètres doivent rester bas (température faible) pour limiter les dérives.
103
  """
104
  cfg = cfg or QAConfig()
105
+
106
  out = llm.create_chat_completion(
107
  messages=[{"role": "user", "content": prompt}],
108
  temperature=cfg.qa_temperature,
src/rag_core.py CHANGED
@@ -1,15 +1,14 @@
 
1
  # src/rag_core.py
2
  from __future__ import annotations
 
3
  from typing import Dict, Any, List
4
  import json
5
 
6
-
7
  from src import list as list_mode
8
  from src import fulltext as fulltext_mode
9
  from src import synthesis as synthesis_mode
10
  from src import qa as qa_mode
11
- from src import resources
12
-
13
 
14
  from src.config import (
15
  CHUNKS_PATH,
@@ -33,6 +32,50 @@ from src.utils import (
33
  from src.resources import get_vectorstore, get_llm
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  # ====================
38
  # CHARGEMENT CORPUS (UNE FOIS)
@@ -40,6 +83,7 @@ from src.resources import get_vectorstore, get_llm
40
 
41
  _ARTICLES: Dict[str, str] | None = None
42
 
 
43
  def get_all_articles() -> Dict[str, str]:
44
  global _ARTICLES
45
  if _ARTICLES is None:
@@ -105,6 +149,40 @@ def _qa_answer(question: str) -> Dict[str, Any]:
105
  }
106
 
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  # ====================
109
  # ROUTEUR
110
  # ====================
@@ -126,12 +204,16 @@ def answer_query(q: str) -> Dict[str, Any]:
126
  "articles": [article_id],
127
  }
128
 
129
- # LIST (LEXICAL-FIRST)
 
 
 
 
130
  if is_list_request(q):
131
  return list_mode.list_articles(
132
  q,
133
- articles=get_all_articles(), # CORPUS COMPLET
134
- vs=get_vectorstore(), # fallback uniquement
135
  normalize_article_id=normalize_article_id,
136
  list_triggers=LIST_TRIGGERS,
137
  cfg=list_mode.ListConfig(),
@@ -157,18 +239,16 @@ def answer_query(q: str) -> Dict[str, Any]:
157
  "articles": [article_id],
158
  }
159
 
160
-
161
- # LIST par défaut si requête courte (nominale)
162
  if len(q.split()) <= 5:
163
  return list_mode.list_articles(
164
  q,
165
  articles=get_all_articles(),
166
- vs=get_vectorstore(),
167
  normalize_article_id=normalize_article_id,
168
  list_triggers=LIST_TRIGGERS,
169
  cfg=list_mode.ListConfig(),
170
  )
171
 
172
- # QA explicite uniquement
173
  return _qa_answer(q)
174
-
 
1
+
2
  # src/rag_core.py
3
  from __future__ import annotations
4
+
5
  from typing import Dict, Any, List
6
  import json
7
 
 
8
  from src import list as list_mode
9
  from src import fulltext as fulltext_mode
10
  from src import synthesis as synthesis_mode
11
  from src import qa as qa_mode
 
 
12
 
13
  from src.config import (
14
  CHUNKS_PATH,
 
32
  from src.resources import get_vectorstore, get_llm
33
 
34
 
35
+ # ====================
36
+ # MODE SUMMARY_AI (temporaire dans rag_core)
37
+ # ====================
38
+
39
+ # Triggers locaux (on les déplacera ensuite dans config.py + utils.py)
40
+ SUMMARY_TRIGGERS = [
41
+ "résumé ia", "resume ia",
42
+ "résumé", "resume",
43
+ "résumer", "resumer",
44
+ "summary",
45
+ ]
46
+
47
+ SUMMARY_WARNING = (
48
+ "Résumé IA : reformulation automatique (peut contenir des erreurs ou omissions). "
49
+ "Vérifie toujours sur le texte officiel."
50
+ )
51
+
52
+ # Réglages simples (on les déplacera ensuite dans config.py)
53
+ SUMMARY_DOC_MAX_CHARS = 1200
54
+ SUMMARY_MAX_TOKENS = 180
55
+ SUMMARY_TEMPERATURE = 0.2
56
+
57
+
58
+ def is_summary_request(q: str) -> bool:
59
+ ql = (q or "").lower()
60
+ return any(t in ql for t in SUMMARY_TRIGGERS)
61
+
62
+
63
+ def build_summary_prompt(article_id: str, article_text: str) -> str:
64
+ # Prompt minimal, robuste, orienté "zéro invention"
65
+ return f"""Tu aides un professionnel à lire rapidement un article du Code de l'éducation (France).
66
+
67
+ TÂCHE : produire un résumé fidèle et utile, sans inventer.
68
+ RÈGLES :
69
+ - Ne cite QUE ce qui est présent dans le texte.
70
+ - Pas d’ajout d’information extérieure.
71
+ - 4 à 8 puces maximum.
72
+ - Style neutre, factuel.
73
+ - Si le texte est très court, reformule simplement l’idée centrale en 2 à 4 puces.
74
+
75
+ ARTICLE {article_id} (texte officiel) :
76
+ {article_text}
77
+ """
78
+
79
 
80
  # ====================
81
  # CHARGEMENT CORPUS (UNE FOIS)
 
83
 
84
  _ARTICLES: Dict[str, str] | None = None
85
 
86
+
87
  def get_all_articles() -> Dict[str, str]:
88
  global _ARTICLES
89
  if _ARTICLES is None:
 
149
  }
150
 
151
 
152
+ # ====================
153
+ # SUMMARY_AI
154
+ # ====================
155
+
156
+ def _summary_ai(article_id: str) -> Dict[str, Any]:
157
+ article_id = normalize_article_id(article_id)
158
+ text = load_article_text(article_id)
159
+
160
+ if not text:
161
+ return {
162
+ "mode": "SUMMARY_AI",
163
+ "answer": f"Article {article_id} introuvable.",
164
+ "articles": [],
165
+ }
166
+
167
+ short_text = qa_mode.truncate_text(text, SUMMARY_DOC_MAX_CHARS)
168
+ prompt = build_summary_prompt(article_id, short_text)
169
+
170
+ # On réutilise le moteur LLM existant sans toucher à qa.py
171
+ cfg = qa_mode.QAConfig(
172
+ qa_top_k_final=1, # non utilisé ici mais requis par la dataclass
173
+ qa_doc_max_chars=SUMMARY_DOC_MAX_CHARS,
174
+ qa_max_tokens=SUMMARY_MAX_TOKENS,
175
+ qa_temperature=SUMMARY_TEMPERATURE,
176
+ )
177
+
178
+ ans = qa_mode.llm_generate_qa(get_llm(), prompt, cfg=cfg).strip()
179
+ return {
180
+ "mode": "SUMMARY_AI",
181
+ "answer": f"{SUMMARY_WARNING}\n\n{ans}",
182
+ "articles": [article_id],
183
+ }
184
+
185
+
186
  # ====================
187
  # ROUTEUR
188
  # ====================
 
204
  "articles": [article_id],
205
  }
206
 
207
+ # SUMMARY_AI (doit passer AVANT les fallbacks LIST)
208
+ if article_id and is_summary_request(q):
209
+ return _summary_ai(article_id)
210
+
211
+ # LIST (LEXICAL-FIRST) — IMPORTANT : ne charge pas FAISS ici
212
  if is_list_request(q):
213
  return list_mode.list_articles(
214
  q,
215
+ articles=get_all_articles(),
216
+ vs=None, # <-- crucial pour HF: pas de chargement FAISS inutile
217
  normalize_article_id=normalize_article_id,
218
  list_triggers=LIST_TRIGGERS,
219
  cfg=list_mode.ListConfig(),
 
239
  "articles": [article_id],
240
  }
241
 
242
+ # LIST par défaut si requête courte (nominale) — ne charge pas FAISS ici non plus
 
243
  if len(q.split()) <= 5:
244
  return list_mode.list_articles(
245
  q,
246
  articles=get_all_articles(),
247
+ vs=None, # <-- crucial pour HF
248
  normalize_article_id=normalize_article_id,
249
  list_triggers=LIST_TRIGGERS,
250
  cfg=list_mode.ListConfig(),
251
  )
252
 
253
+ # QA
254
  return _qa_answer(q)
 
src/resources.py CHANGED
@@ -1,37 +1,94 @@
1
  # src/resources.py
2
  from __future__ import annotations
3
 
 
4
  from typing import Optional
 
5
  from langchain_community.vectorstores import FAISS
6
  from langchain_huggingface import HuggingFaceEmbeddings
7
  from llama_cpp import Llama
8
 
9
- from src.config import DB_DIR, EMBED_MODEL, LLM_MODEL_PATH, LLM_N_CTX, LLM_N_THREADS, LLM_N_BATCH
10
-
 
 
 
 
 
 
11
 
12
 
13
  _VS: Optional[FAISS] = None
14
  _LLM: Optional[Llama] = None
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  def get_vectorstore() -> FAISS:
 
 
 
 
17
  global _VS
18
- if _VS is None:
19
- embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
20
- _VS = FAISS.load_local(
21
- str(DB_DIR),
22
- embeddings,
23
- allow_dangerous_deserialization=True,
24
- )
 
 
 
 
 
 
25
  return _VS
26
 
 
 
 
 
 
 
 
 
 
 
 
27
  def get_llm() -> Llama:
 
 
 
 
28
  global _LLM
29
- if _LLM is None:
30
- _LLM = Llama(
31
- model_path=LLM_MODEL_PATH,
32
- n_ctx=LLM_N_CTX,
33
- n_threads=LLM_N_THREADS,
34
- n_batch=LLM_N_BATCH,
35
- verbose=False,
36
- )
 
 
 
 
 
37
  return _LLM
 
1
  # src/resources.py
2
  from __future__ import annotations
3
 
4
+ from pathlib import Path
5
  from typing import Optional
6
+
7
  from langchain_community.vectorstores import FAISS
8
  from langchain_huggingface import HuggingFaceEmbeddings
9
  from llama_cpp import Llama
10
 
11
+ from src.config import (
12
+ DB_DIR,
13
+ EMBED_MODEL,
14
+ LLM_MODEL_PATH,
15
+ LLM_N_CTX,
16
+ LLM_N_THREADS,
17
+ LLM_N_BATCH,
18
+ )
19
 
20
 
21
  _VS: Optional[FAISS] = None
22
  _LLM: Optional[Llama] = None
23
 
24
+
25
+ def _assert_vectorstore_files(db_dir: Path) -> None:
26
+ """Vérifie que le répertoire FAISS contient les fichiers nécessaires."""
27
+ if not db_dir.exists() or not db_dir.is_dir():
28
+ raise RuntimeError(
29
+ f"Vectorstore introuvable : {db_dir}\n"
30
+ "Attendu : un dossier contenant un index FAISS (ex: index.faiss, index.pkl)."
31
+ )
32
+
33
+ faiss_file = db_dir / "index.faiss"
34
+ pkl_file = db_dir / "index.pkl"
35
+
36
+ if not faiss_file.exists() or not pkl_file.exists():
37
+ raise RuntimeError(
38
+ f"Vectorstore incomplet dans {db_dir}\n"
39
+ f"Fichiers attendus : {faiss_file.name} et {pkl_file.name}"
40
+ )
41
+
42
+
43
  def get_vectorstore() -> FAISS:
44
+ """
45
+ Charge FAISS + embeddings UNE fois (lazy-loading).
46
+ IMPORTANT : coûteux (CPU + I/O). Ne l'appelle que si nécessaire (QA).
47
+ """
48
  global _VS
49
+ if _VS is not None:
50
+ return _VS
51
+
52
+ db_dir = Path(DB_DIR)
53
+ _assert_vectorstore_files(db_dir)
54
+
55
+ embeddings = HuggingFaceEmbeddings(model_name=EMBED_MODEL)
56
+
57
+ _VS = FAISS.load_local(
58
+ str(db_dir),
59
+ embeddings,
60
+ allow_dangerous_deserialization=True,
61
+ )
62
  return _VS
63
 
64
+
65
+ def _assert_llm_file(model_path: Path) -> None:
66
+ """Vérifie que le modèle GGUF est présent."""
67
+ if not model_path.exists() or not model_path.is_file():
68
+ raise RuntimeError(
69
+ f"Modèle GGUF introuvable : {model_path}\n"
70
+ "Assure-toi que app.py a bien téléchargé/copier le modèle dans models/ "
71
+ "ou que LLM_MODEL_PATH pointe vers un fichier GGUF valide."
72
+ )
73
+
74
+
75
  def get_llm() -> Llama:
76
+ """
77
+ Charge le modèle GGUF UNE fois (lazy-loading).
78
+ IMPORTANT : coûteux. Ne l'appelle que pour SUMMARY_AI et QA.
79
+ """
80
  global _LLM
81
+ if _LLM is not None:
82
+ return _LLM
83
+
84
+ model_path = Path(LLM_MODEL_PATH)
85
+ _assert_llm_file(model_path)
86
+
87
+ _LLM = Llama(
88
+ model_path=str(model_path),
89
+ n_ctx=int(LLM_N_CTX),
90
+ n_threads=int(LLM_N_THREADS),
91
+ n_batch=int(LLM_N_BATCH),
92
+ verbose=False, # garde HF plus propre
93
+ )
94
  return _LLM