Trabis commited on
Commit
3cf7a11
·
verified ·
1 Parent(s): e42d0b9
Files changed (1) hide show
  1. app.py +512 -267
app.py CHANGED
@@ -1,309 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  from langchain_mistralai.chat_models import ChatMistralAI
3
  from langchain.prompts import ChatPromptTemplate
4
  import os
5
  from pathlib import Path
6
- from typing import List, Dict, Optional
7
  import json
8
  import faiss
9
  import numpy as np
10
  from langchain.schema import Document
11
- from sentence_transformers import SentenceTransformer
12
  import pickle
13
  import re
 
 
 
 
 
 
 
14
 
15
- os.environ.get('HUGGINGFACE_TOKEN')
16
-
17
- class RAGLoader:
18
  def __init__(self,
19
  docs_folder: str = "./docs",
20
  splits_folder: str = "./splits",
21
- index_folder: str = "./index",):
22
- # model_name: str = "intfloat/multilingual-e5-large")
23
- """
24
- Initialise le RAG Loader
25
-
26
- Args:
27
- docs_folder: Dossier contenant les documents sources
28
- splits_folder: Dossier où seront stockés les morceaux de texte
29
- index_folder: Dossier où sera stocké l'index FAISS
30
- model_name: Nom du modèle SentenceTransformer à utiliser
31
- """
32
  self.docs_folder = Path(docs_folder)
33
  self.splits_folder = Path(splits_folder)
34
  self.index_folder = Path(index_folder)
35
- # self.model_name = model_name
36
-
37
- # Créer les dossiers s'ils n'existent pas
38
- self.splits_folder.mkdir(parents=True, exist_ok=True)
39
- self.index_folder.mkdir(parents=True, exist_ok=True)
40
-
41
- # Chemins des fichiers
42
  self.splits_path = self.splits_folder / "splits.json"
43
  self.index_path = self.index_folder / "faiss.index"
44
  self.documents_path = self.index_folder / "documents.pkl"
45
-
46
- # Initialiser le modèle
47
- # self.model = None
48
  self.index = None
49
  self.indexed_documents = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- def encode(self,payload):
52
- token = os.environ.get('HUGGINGFACE_TOKEN')
53
- API_URL = "https://api-inference.huggingface.co/models/intfloat/multilingual-e5-large"
54
- headers = {"Authorization": "Bearer {token}"}
55
- response = requests.post(API_URL, headers=headers, json=payload)
56
- return response.json()
57
-
58
- def load_and_split_texts(self) -> List[Document]:
59
- """
60
- Charge les textes du dossier docs, les découpe en morceaux et les sauvegarde
61
- dans un fichier JSON unique.
62
-
63
- Returns:
64
- Liste de Documents contenant les morceaux de texte et leurs métadonnées
65
- """
66
- documents = []
67
-
68
- # Vérifier d'abord si les splits existent déjà
69
  if self._splits_exist():
70
- print("Chargement des splits existants...")
71
  return self._load_existing_splits()
72
-
73
- print("Création de nouveaux splits...")
74
- # Parcourir tous les fichiers du dossier docs
 
75
  for file_path in self.docs_folder.glob("*.txt"):
76
- with open(file_path, 'r', encoding='utf-8') as file:
77
- text = file.read()
78
-
79
- # Découper le texte en phrases
80
- # chunks = [chunk.strip() for chunk in text.split('.') if chunk.strip()]
81
- chunks = [s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if s.strip()]
82
-
83
- # Créer un Document pour chaque morceau
84
- for i, chunk in enumerate(chunks):
85
- doc = Document(
86
- page_content=chunk,
87
- metadata={
88
- 'source': file_path.name,
89
- 'chunk_id': i,
90
- 'total_chunks': len(chunks)
91
- }
92
- )
93
- documents.append(doc)
94
-
95
- # Sauvegarder tous les splits dans un seul fichier JSON
96
  self._save_splits(documents)
97
-
98
- print(f"Nombre total de morceaux créés: {len(documents)}")
99
  return documents
100
-
101
- def _splits_exist(self) -> bool:
102
- """Vérifie si le fichier de splits existe"""
103
- return self.splits_path.exists()
104
-
105
- def _save_splits(self, documents: List[Document]):
106
- """Sauvegarde tous les documents découpés dans un seul fichier JSON"""
107
- splits_data = {
108
- 'splits': [
109
- {
110
- 'text': doc.page_content,
111
- 'metadata': doc.metadata
112
- }
113
- for doc in documents
 
 
114
  ]
115
- }
116
 
117
- with open(self.splits_path, 'w', encoding='utf-8') as f:
118
- json.dump(splits_data, f, ensure_ascii=False, indent=2)
119
-
120
- def _load_existing_splits(self) -> List[Document]:
121
- """Charge les splits depuis le fichier JSON unique"""
122
- with open(self.splits_path, 'r', encoding='utf-8') as f:
123
- splits_data = json.load(f)
124
-
125
- documents = [
126
- Document(
127
- page_content=split['text'],
128
- metadata=split['metadata']
129
- )
130
- for split in splits_data['splits']
131
- ]
132
-
133
- print(f"Nombre de splits chargés: {len(documents)}")
134
- return documents
135
-
136
- def load_index(self) -> bool:
137
- """
138
- Charge l'index FAISS et les documents associés s'ils existent
139
-
140
- Returns:
141
- bool: True si l'index a été chargé, False sinon
142
- """
143
- if not self._index_exists():
144
- print("Aucun index trouvé.")
145
- return False
146
-
147
- print("Chargement de l'index existant...")
148
- try:
149
- # Charger l'index FAISS
150
- self.index = faiss.read_index(str(self.index_path))
151
-
152
- # Charger les documents associés
153
- with open(self.documents_path, 'rb') as f:
154
- self.indexed_documents = pickle.load(f)
155
-
156
- print(f"Index chargé avec {self.index.ntotal} vecteurs")
157
- return True
158
-
159
- except Exception as e:
160
- print(f"Erreur lors du chargement de l'index: {e}")
161
  return False
162
-
163
- def create_index(self, documents: Optional[List[Document]] = None) -> bool:
164
- """
165
- Crée un nouvel index FAISS à partir des documents.
166
- Si aucun document n'est fourni, charge les documents depuis le fichier JSON.
167
-
168
- Args:
169
- documents: Liste optionnelle de Documents à indexer
170
-
171
- Returns:
172
- bool: True si l'index a été créé avec succès, False sinon
173
- """
174
- try:
175
- # # Initialiser le modèle si nécessaire
176
- # if self.model is None:
177
- # print("Chargement du modèle...")
178
- # self.model = SentenceTransformer(self.model_name)
179
-
180
- # Charger les documents si non fournis
181
- if documents is None:
182
- documents = self.load_and_split_texts()
183
-
184
- if not documents:
185
- print("Aucun document à indexer.")
186
- return False
187
-
188
- print("Création des embeddings...")
189
- texts = [doc.page_content for doc in documents]
190
- embeddings = self.encode(texts)
191
-
192
- # Initialiser l'index FAISS
193
- dimension = embeddings.shape[1]
194
- self.index = faiss.IndexFlatL2(dimension)
195
-
196
- # Ajouter les vecteurs à l'index
197
- self.index.add(np.array(embeddings).astype('float32'))
198
-
199
- # Sauvegarder l'index
200
- print("Sauvegarde de l'index...")
201
- faiss.write_index(self.index, str(self.index_path))
202
-
203
- # Sauvegarder les documents associés
204
- self.indexed_documents = documents
205
- with open(self.documents_path, 'wb') as f:
206
- pickle.dump(documents, f)
207
-
208
- print(f"Index créé avec succès : {self.index.ntotal} vecteurs")
209
- return True
210
-
211
- except Exception as e:
212
- print(f"Erreur lors de la création de l'index: {e}")
213
- return False
214
-
215
- def _index_exists(self) -> bool:
216
- """Vérifie si l'index et les documents associés existent"""
217
- return self.index_path.exists() and self.documents_path.exists()
218
 
219
  def get_retriever(self, k: int = 10):
220
- """
221
- Crée un retriever pour l'utilisation avec LangChain
222
-
223
- Args:
224
- k: Nombre de documents similaires à retourner
225
-
226
- Returns:
227
- Callable: Fonction de recherche compatible avec LangChain
228
- """
229
  if self.index is None:
230
  if not self.load_index():
231
  if not self.create_index():
232
- raise ValueError("Impossible de charger ou créer l'index")
233
 
234
- # if self.model is None:
235
- # self.model = SentenceTransformer(self.model_name)
 
 
 
236
 
237
- def retriever_function(query: str) -> List[Document]:
238
- # Créer l'embedding de la requête
239
- query_embedding = self.encode([query])[0]
240
-
241
- # Rechercher les documents similaires
242
  distances, indices = self.index.search(
243
  np.array([query_embedding]).astype('float32'),
244
  k
245
  )
246
-
247
- # Retourner les documents trouvés
248
- results = []
249
- for idx in indices[0]:
250
- if idx != -1: # FAISS retourne -1 pour les résultats invalides
251
- results.append(self.indexed_documents[idx])
252
-
 
 
253
  return results
254
-
255
  return retriever_function
256
 
257
- # Initialize the RAG system
258
- llm = ChatMistralAI(model="mistral-large-latest", mistral_api_key="QK0ZZpSxQbCEVgOLtI6FARQVmBYc6WGP")
259
- rag_loader = RAGLoader()
260
- retriever = rag_loader.get_retriever(k=10)
261
-
262
- prompt_template = ChatPromptTemplate.from_messages([
263
- ("system", """أنت مساعد مفيد يجيب على الأسئلة باللغة العربية باستخدام المعلومات المقدمة.
264
- استخدم المعلومات التالية للإجابة على السؤال:
265
 
266
- {context}
 
267
 
268
- إذا لم تكن المعلومات كافية للإجابة على السؤال بشكل كامل، قم بتوضيح ذلك.
269
- أجب بشكل موجز ودقيق."""),
270
- ("human", "{question}")
271
- ])
272
 
273
  def process_question(question: str) -> tuple[str, str]:
274
- """
275
- Process a question and return both the answer and the relevant context
276
- """
 
277
  relevant_docs = retriever(question)
278
  context = "\n".join([doc.page_content for doc in relevant_docs])
279
-
280
  prompt = prompt_template.format_messages(
281
  context=context,
282
  question=question
283
  )
284
-
285
  response = llm(prompt)
286
- return response.content, context
287
-
288
- def gradio_interface(question: str) -> tuple[str, str]:
289
- """
290
- Gradio interface function that returns both answer and context as a tuple
291
- """
292
- return process_question(question)
293
-
294
- # Custom CSS for right-aligned text in textboxes
295
- custom_css = """
296
- .rtl-text {
297
- text-align: right !important;
298
- direction: rtl !important;
299
- }
300
- .rtl-text textarea {
301
- text-align: right !important;
302
- direction: rtl !important;
303
- }
304
- """
305
-
306
- # Define the Gradio interface
307
  with gr.Blocks(css=custom_css) as iface:
308
  with gr.Column():
309
  input_text = gr.Textbox(
@@ -312,27 +551,33 @@ with gr.Blocks(css=custom_css) as iface:
312
  lines=2,
313
  elem_classes="rtl-text"
314
  )
315
-
316
- answer_box = gr.Textbox(
317
- label="الإجابة",
318
- lines=4,
319
- elem_classes="rtl-text"
320
- )
321
-
322
- context_box = gr.Textbox(
323
- label="السياق المستخدم",
324
- lines=8,
325
- elem_classes="rtl-text"
326
- )
327
-
328
  submit_btn = gr.Button("إرسال")
329
-
330
  submit_btn.click(
331
- fn=gradio_interface,
332
  inputs=input_text,
333
- outputs=[answer_box, context_box]
 
334
  )
335
 
336
- # Launch the interface
337
  if __name__ == "__main__":
338
- iface.launch(share=True)
 
 
 
 
 
 
1
+ # import gradio as gr
2
+ # from langchain_mistralai.chat_models import ChatMistralAI
3
+ # from langchain.prompts import ChatPromptTemplate
4
+ # import os
5
+ # from pathlib import Path
6
+ # from typing import List, Dict, Optional
7
+ # import json
8
+ # import faiss
9
+ # import numpy as np
10
+ # from langchain.schema import Document
11
+ # from sentence_transformers import SentenceTransformer
12
+ # import pickle
13
+ # import re
14
+
15
+ # os.environ.get('HUGGINGFACE_TOKEN')
16
+
17
+ # class RAGLoader:
18
+ # def __init__(self,
19
+ # docs_folder: str = "./docs",
20
+ # splits_folder: str = "./splits",
21
+ # index_folder: str = "./index",):
22
+ # # model_name: str = "intfloat/multilingual-e5-large")
23
+ # """
24
+ # Initialise le RAG Loader
25
+
26
+ # Args:
27
+ # docs_folder: Dossier contenant les documents sources
28
+ # splits_folder: Dossier où seront stockés les morceaux de texte
29
+ # index_folder: Dossier où sera stocké l'index FAISS
30
+ # model_name: Nom du modèle SentenceTransformer à utiliser
31
+ # """
32
+ # self.docs_folder = Path(docs_folder)
33
+ # self.splits_folder = Path(splits_folder)
34
+ # self.index_folder = Path(index_folder)
35
+ # # self.model_name = model_name
36
+
37
+ # # Créer les dossiers s'ils n'existent pas
38
+ # self.splits_folder.mkdir(parents=True, exist_ok=True)
39
+ # self.index_folder.mkdir(parents=True, exist_ok=True)
40
+
41
+ # # Chemins des fichiers
42
+ # self.splits_path = self.splits_folder / "splits.json"
43
+ # self.index_path = self.index_folder / "faiss.index"
44
+ # self.documents_path = self.index_folder / "documents.pkl"
45
+
46
+ # # Initialiser le modèle
47
+ # # self.model = None
48
+ # self.index = None
49
+ # self.indexed_documents = None
50
+
51
+ # def encode(self,payload):
52
+ # token = os.environ.get('HUGGINGFACE_TOKEN')
53
+ # API_URL = "https://api-inference.huggingface.co/models/intfloat/multilingual-e5-large"
54
+ # headers = {"Authorization": "Bearer {token}"}
55
+ # response = requests.post(API_URL, headers=headers, json=payload)
56
+ # return response.json()
57
+
58
+ # def load_and_split_texts(self) -> List[Document]:
59
+ # """
60
+ # Charge les textes du dossier docs, les découpe en morceaux et les sauvegarde
61
+ # dans un fichier JSON unique.
62
+
63
+ # Returns:
64
+ # Liste de Documents contenant les morceaux de texte et leurs métadonnées
65
+ # """
66
+ # documents = []
67
+
68
+ # # Vérifier d'abord si les splits existent déjà
69
+ # if self._splits_exist():
70
+ # print("Chargement des splits existants...")
71
+ # return self._load_existing_splits()
72
+
73
+ # print("Création de nouveaux splits...")
74
+ # # Parcourir tous les fichiers du dossier docs
75
+ # for file_path in self.docs_folder.glob("*.txt"):
76
+ # with open(file_path, 'r', encoding='utf-8') as file:
77
+ # text = file.read()
78
+
79
+ # # Découper le texte en phrases
80
+ # # chunks = [chunk.strip() for chunk in text.split('.') if chunk.strip()]
81
+ # chunks = [s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if s.strip()]
82
+
83
+ # # Créer un Document pour chaque morceau
84
+ # for i, chunk in enumerate(chunks):
85
+ # doc = Document(
86
+ # page_content=chunk,
87
+ # metadata={
88
+ # 'source': file_path.name,
89
+ # 'chunk_id': i,
90
+ # 'total_chunks': len(chunks)
91
+ # }
92
+ # )
93
+ # documents.append(doc)
94
+
95
+ # # Sauvegarder tous les splits dans un seul fichier JSON
96
+ # self._save_splits(documents)
97
+
98
+ # print(f"Nombre total de morceaux créés: {len(documents)}")
99
+ # return documents
100
+
101
+ # def _splits_exist(self) -> bool:
102
+ # """Vérifie si le fichier de splits existe"""
103
+ # return self.splits_path.exists()
104
+
105
+ # def _save_splits(self, documents: List[Document]):
106
+ # """Sauvegarde tous les documents découpés dans un seul fichier JSON"""
107
+ # splits_data = {
108
+ # 'splits': [
109
+ # {
110
+ # 'text': doc.page_content,
111
+ # 'metadata': doc.metadata
112
+ # }
113
+ # for doc in documents
114
+ # ]
115
+ # }
116
+
117
+ # with open(self.splits_path, 'w', encoding='utf-8') as f:
118
+ # json.dump(splits_data, f, ensure_ascii=False, indent=2)
119
+
120
+ # def _load_existing_splits(self) -> List[Document]:
121
+ # """Charge les splits depuis le fichier JSON unique"""
122
+ # with open(self.splits_path, 'r', encoding='utf-8') as f:
123
+ # splits_data = json.load(f)
124
+
125
+ # documents = [
126
+ # Document(
127
+ # page_content=split['text'],
128
+ # metadata=split['metadata']
129
+ # )
130
+ # for split in splits_data['splits']
131
+ # ]
132
+
133
+ # print(f"Nombre de splits chargés: {len(documents)}")
134
+ # return documents
135
+
136
+ # def load_index(self) -> bool:
137
+ # """
138
+ # Charge l'index FAISS et les documents associés s'ils existent
139
+
140
+ # Returns:
141
+ # bool: True si l'index a été chargé, False sinon
142
+ # """
143
+ # if not self._index_exists():
144
+ # print("Aucun index trouvé.")
145
+ # return False
146
+
147
+ # print("Chargement de l'index existant...")
148
+ # try:
149
+ # # Charger l'index FAISS
150
+ # self.index = faiss.read_index(str(self.index_path))
151
+
152
+ # # Charger les documents associés
153
+ # with open(self.documents_path, 'rb') as f:
154
+ # self.indexed_documents = pickle.load(f)
155
+
156
+ # print(f"Index chargé avec {self.index.ntotal} vecteurs")
157
+ # return True
158
+
159
+ # except Exception as e:
160
+ # print(f"Erreur lors du chargement de l'index: {e}")
161
+ # return False
162
+
163
+ # def create_index(self, documents: Optional[List[Document]] = None) -> bool:
164
+ # """
165
+ # Crée un nouvel index FAISS à partir des documents.
166
+ # Si aucun document n'est fourni, charge les documents depuis le fichier JSON.
167
+
168
+ # Args:
169
+ # documents: Liste optionnelle de Documents à indexer
170
+
171
+ # Returns:
172
+ # bool: True si l'index a été créé avec succès, False sinon
173
+ # """
174
+ # try:
175
+ # # # Initialiser le modèle si nécessaire
176
+ # # if self.model is None:
177
+ # # print("Chargement du modèle...")
178
+ # # self.model = SentenceTransformer(self.model_name)
179
+
180
+ # # Charger les documents si non fournis
181
+ # if documents is None:
182
+ # documents = self.load_and_split_texts()
183
+
184
+ # if not documents:
185
+ # print("Aucun document à indexer.")
186
+ # return False
187
+
188
+ # print("Création des embeddings...")
189
+ # texts = [doc.page_content for doc in documents]
190
+ # embeddings = self.encode(texts)
191
+
192
+ # # Initialiser l'index FAISS
193
+ # dimension = embeddings.shape[1]
194
+ # self.index = faiss.IndexFlatL2(dimension)
195
+
196
+ # # Ajouter les vecteurs à l'index
197
+ # self.index.add(np.array(embeddings).astype('float32'))
198
+
199
+ # # Sauvegarder l'index
200
+ # print("Sauvegarde de l'index...")
201
+ # faiss.write_index(self.index, str(self.index_path))
202
+
203
+ # # Sauvegarder les documents associés
204
+ # self.indexed_documents = documents
205
+ # with open(self.documents_path, 'wb') as f:
206
+ # pickle.dump(documents, f)
207
+
208
+ # print(f"Index créé avec succès : {self.index.ntotal} vecteurs")
209
+ # return True
210
+
211
+ # except Exception as e:
212
+ # print(f"Erreur lors de la création de l'index: {e}")
213
+ # return False
214
+
215
+ # def _index_exists(self) -> bool:
216
+ # """Vérifie si l'index et les documents associés existent"""
217
+ # return self.index_path.exists() and self.documents_path.exists()
218
+
219
+ # def get_retriever(self, k: int = 10):
220
+ # """
221
+ # Crée un retriever pour l'utilisation avec LangChain
222
+
223
+ # Args:
224
+ # k: Nombre de documents similaires à retourner
225
+
226
+ # Returns:
227
+ # Callable: Fonction de recherche compatible avec LangChain
228
+ # """
229
+ # if self.index is None:
230
+ # if not self.load_index():
231
+ # if not self.create_index():
232
+ # raise ValueError("Impossible de charger ou créer l'index")
233
+
234
+ # # if self.model is None:
235
+ # # self.model = SentenceTransformer(self.model_name)
236
+
237
+ # def retriever_function(query: str) -> List[Document]:
238
+ # # Créer l'embedding de la requête
239
+ # query_embedding = self.encode([query])[0]
240
+
241
+ # # Rechercher les documents similaires
242
+ # distances, indices = self.index.search(
243
+ # np.array([query_embedding]).astype('float32'),
244
+ # k
245
+ # )
246
+
247
+ # # Retourner les documents trouvés
248
+ # results = []
249
+ # for idx in indices[0]:
250
+ # if idx != -1: # FAISS retourne -1 pour les résultats invalides
251
+ # results.append(self.indexed_documents[idx])
252
+
253
+ # return results
254
+
255
+ # return retriever_function
256
+
257
+ # # Initialize the RAG system
258
+ # llm = ChatMistralAI(model="mistral-large-latest", mistral_api_key="QK0ZZpSxQbCEVgOLtI6FARQVmBYc6WGP")
259
+ # rag_loader = RAGLoader()
260
+ # retriever = rag_loader.get_retriever(k=10)
261
+
262
+ # prompt_template = ChatPromptTemplate.from_messages([
263
+ # ("system", """أنت مساعد مفيد يجيب على الأسئلة باللغة العربية باستخدام المعلومات المقدمة.
264
+ # استخدم المعلومات التالية للإجابة على السؤال:
265
+
266
+ # {context}
267
+
268
+ # إذا لم تكن المعلومات كافية للإجابة على السؤال بشكل كامل، قم بتوضيح ذلك.
269
+ # أجب بشكل موجز ودقيق."""),
270
+ # ("human", "{question}")
271
+ # ])
272
+
273
+ # def process_question(question: str) -> tuple[str, str]:
274
+ # """
275
+ # Process a question and return both the answer and the relevant context
276
+ # """
277
+ # relevant_docs = retriever(question)
278
+ # context = "\n".join([doc.page_content for doc in relevant_docs])
279
+
280
+ # prompt = prompt_template.format_messages(
281
+ # context=context,
282
+ # question=question
283
+ # )
284
+
285
+ # response = llm(prompt)
286
+ # return response.content, context
287
+
288
+ # def gradio_interface(question: str) -> tuple[str, str]:
289
+ # """
290
+ # Gradio interface function that returns both answer and context as a tuple
291
+ # """
292
+ # return process_question(question)
293
+
294
+ # # Custom CSS for right-aligned text in textboxes
295
+ # custom_css = """
296
+ # .rtl-text {
297
+ # text-align: right !important;
298
+ # direction: rtl !important;
299
+ # }
300
+ # .rtl-text textarea {
301
+ # text-align: right !important;
302
+ # direction: rtl !important;
303
+ # }
304
+ # """
305
+
306
+ # # Define the Gradio interface
307
+ # with gr.Blocks(css=custom_css) as iface:
308
+ # with gr.Column():
309
+ # input_text = gr.Textbox(
310
+ # label="السؤال",
311
+ # placeholder="اكتب سؤالك هنا...",
312
+ # lines=2,
313
+ # elem_classes="rtl-text"
314
+ # )
315
+
316
+ # answer_box = gr.Textbox(
317
+ # label="الإجابة",
318
+ # lines=4,
319
+ # elem_classes="rtl-text"
320
+ # )
321
+
322
+ # context_box = gr.Textbox(
323
+ # label="السياق المستخدم",
324
+ # lines=8,
325
+ # elem_classes="rtl-text"
326
+ # )
327
+
328
+ # submit_btn = gr.Button("إرسال")
329
+
330
+ # submit_btn.click(
331
+ # fn=gradio_interface,
332
+ # inputs=input_text,
333
+ # outputs=[answer_box, context_box]
334
+ # )
335
+
336
+ # # Launch the interface
337
+ # if __name__ == "__main__":
338
+ # iface.launch(share=True)
339
+
340
+
341
  import gradio as gr
342
  from langchain_mistralai.chat_models import ChatMistralAI
343
  from langchain.prompts import ChatPromptTemplate
344
  import os
345
  from pathlib import Path
 
346
  import json
347
  import faiss
348
  import numpy as np
349
  from langchain.schema import Document
 
350
  import pickle
351
  import re
352
+ import requests
353
+ from functools import lru_cache
354
+ import torch
355
+ from sentence_transformers import SentenceTransformer
356
+ import threading
357
+ from queue import Queue
358
+ import concurrent.futures
359
 
360
+ class OptimizedRAGLoader:
 
 
361
  def __init__(self,
362
  docs_folder: str = "./docs",
363
  splits_folder: str = "./splits",
364
+ index_folder: str = "./index"):
365
+
 
 
 
 
 
 
 
 
 
366
  self.docs_folder = Path(docs_folder)
367
  self.splits_folder = Path(splits_folder)
368
  self.index_folder = Path(index_folder)
369
+
370
+ # Create folders if they don't exist
371
+ for folder in [self.splits_folder, self.index_folder]:
372
+ folder.mkdir(parents=True, exist_ok=True)
373
+
374
+ # File paths
 
375
  self.splits_path = self.splits_folder / "splits.json"
376
  self.index_path = self.index_folder / "faiss.index"
377
  self.documents_path = self.index_folder / "documents.pkl"
378
+
379
+ # Initialize components
 
380
  self.index = None
381
  self.indexed_documents = None
382
+
383
+ # Initialize encoder model
384
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
385
+ self.encoder = SentenceTransformer("intfloat/multilingual-e5-large")
386
+ self.encoder.to(self.device)
387
+
388
+ # Initialize thread pool
389
+ self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
390
+
391
+ # Initialize response cache
392
+ self.response_cache = {}
393
+
394
+ @lru_cache(maxsize=1000)
395
+ def encode(self, text: str):
396
+ """Cached encoding function"""
397
+ with torch.no_grad():
398
+ embeddings = self.encoder.encode(
399
+ text,
400
+ convert_to_numpy=True,
401
+ normalize_embeddings=True
402
+ )
403
+ return embeddings
404
+
405
+ def batch_encode(self, texts: list):
406
+ """Batch encoding for multiple texts"""
407
+ with torch.no_grad():
408
+ embeddings = self.encoder.encode(
409
+ texts,
410
+ batch_size=32,
411
+ convert_to_numpy=True,
412
+ normalize_embeddings=True,
413
+ show_progress_bar=False
414
+ )
415
+ return embeddings
416
 
417
+ def load_and_split_texts(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  if self._splits_exist():
 
419
  return self._load_existing_splits()
420
+
421
+ documents = []
422
+ futures = []
423
+
424
  for file_path in self.docs_folder.glob("*.txt"):
425
+ future = self.executor.submit(self._process_file, file_path)
426
+ futures.append(future)
427
+
428
+ for future in concurrent.futures.as_completed(futures):
429
+ documents.extend(future.result())
430
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  self._save_splits(documents)
 
 
432
  return documents
433
+
434
+ def _process_file(self, file_path):
435
+ with open(file_path, 'r', encoding='utf-8') as file:
436
+ text = file.read()
437
+ chunks = [s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if s.strip()]
438
+
439
+ return [
440
+ Document(
441
+ page_content=chunk,
442
+ metadata={
443
+ 'source': file_path.name,
444
+ 'chunk_id': i,
445
+ 'total_chunks': len(chunks)
446
+ }
447
+ )
448
+ for i, chunk in enumerate(chunks)
449
  ]
 
450
 
451
+ def create_index(self, documents=None):
452
+ if documents is None:
453
+ documents = self.load_and_split_texts()
454
+
455
+ if not documents:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  return False
457
+
458
+ texts = [doc.page_content for doc in documents]
459
+ embeddings = self.batch_encode(texts)
460
+
461
+ dimension = embeddings.shape[1]
462
+ self.index = faiss.IndexFlatL2(dimension)
463
+
464
+ if torch.cuda.is_available():
465
+ # Use GPU for FAISS if available
466
+ res = faiss.StandardGpuResources()
467
+ self.index = faiss.index_cpu_to_gpu(res, 0, self.index)
468
+
469
+ self.index.add(np.array(embeddings).astype('float32'))
470
+ self.indexed_documents = documents
471
+
472
+ # Save index and documents
473
+ cpu_index = faiss.index_gpu_to_cpu(self.index) if torch.cuda.is_available() else self.index
474
+ faiss.write_index(cpu_index, str(self.index_path))
475
+
476
+ with open(self.documents_path, 'wb') as f:
477
+ pickle.dump(documents, f)
478
+
479
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
 
481
  def get_retriever(self, k: int = 10):
 
 
 
 
 
 
 
 
 
482
  if self.index is None:
483
  if not self.load_index():
484
  if not self.create_index():
485
+ raise ValueError("Unable to load or create index")
486
 
487
+ def retriever_function(query: str) -> list:
488
+ # Check cache first
489
+ cache_key = f"{query}_{k}"
490
+ if cache_key in self.response_cache:
491
+ return self.response_cache[cache_key]
492
 
493
+ query_embedding = self.encode(query)
494
+
 
 
 
495
  distances, indices = self.index.search(
496
  np.array([query_embedding]).astype('float32'),
497
  k
498
  )
499
+
500
+ results = [
501
+ self.indexed_documents[idx]
502
+ for idx in indices[0]
503
+ if idx != -1
504
+ ]
505
+
506
+ # Cache the results
507
+ self.response_cache[cache_key] = results
508
  return results
509
+
510
  return retriever_function
511
 
512
+ # Initialize components
513
+ llm = ChatMistralAI(
514
+ model="mistral-large-latest",
515
+ mistral_api_key="QK0ZZpSxQbCEVgOLtI6FARQVmBYc6WGP",
516
+ temperature=0.1 # Lower temperature for faster responses
517
+ )
 
 
518
 
519
+ rag_loader = OptimizedRAGLoader()
520
+ retriever = rag_loader.get_retriever(k=5) # Reduced k for faster retrieval
521
 
522
+ # Cache for processed questions
523
+ question_cache = {}
 
 
524
 
525
  def process_question(question: str) -> tuple[str, str]:
526
+ # Check cache first
527
+ if question in question_cache:
528
+ return question_cache[question]
529
+
530
  relevant_docs = retriever(question)
531
  context = "\n".join([doc.page_content for doc in relevant_docs])
532
+
533
  prompt = prompt_template.format_messages(
534
  context=context,
535
  question=question
536
  )
537
+
538
  response = llm(prompt)
539
+ result = (response.content, context)
540
+
541
+ # Cache the result
542
+ question_cache[question] = result
543
+ return result
544
+
545
+ # Gradio interface with queue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  with gr.Blocks(css=custom_css) as iface:
547
  with gr.Column():
548
  input_text = gr.Textbox(
 
551
  lines=2,
552
  elem_classes="rtl-text"
553
  )
554
+
555
+ with gr.Row():
556
+ answer_box = gr.Textbox(
557
+ label="الإجابة",
558
+ lines=4,
559
+ elem_classes="rtl-text"
560
+ )
561
+ context_box = gr.Textbox(
562
+ label="السياق المستخدم",
563
+ lines=8,
564
+ elem_classes="rtl-text"
565
+ )
566
+
567
  submit_btn = gr.Button("إرسال")
568
+
569
  submit_btn.click(
570
+ fn=process_question,
571
  inputs=input_text,
572
+ outputs=[answer_box, context_box],
573
+ api_name="predict"
574
  )
575
 
576
+ # Launch with optimized settings
577
  if __name__ == "__main__":
578
+ iface.queue(concurrency_count=3).launch(
579
+ share=True,
580
+ server_name="0.0.0.0",
581
+ server_port=7860,
582
+ enable_queue=True
583
+ )