ALBERT Clement commited on
Commit
c4bf3b5
·
0 Parent(s):

Initial RAG

Browse files
.gitignore ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==========================
2
+ # Python
3
+ # ==========================
4
+
5
+ __pycache__/
6
+ *.py[cod]
7
+
8
+ # ==========================
9
+ # Jupyter
10
+ # ==========================
11
+
12
+ .ipynb_checkpoints/
13
+
14
+ # ==========================
15
+ # Environnements virtuels
16
+ # ==========================
17
+
18
+ venv/
19
+ .venv/
20
+ env/
21
+
22
+ # ==========================
23
+ # Variables d'environnement
24
+ # ==========================
25
+
26
+ .env
27
+
28
+ # ==========================
29
+ # Système
30
+ # ==========================
31
+
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # ==========================
36
+ # Notes personnelles
37
+ # ==========================
38
+ config.png
39
+ help.txt
40
+ requirements_full.txt
41
+
42
+ # ==========================
43
+ # Code a ne pas push
44
+ # ==========================
45
+ data/raw
46
+ src/main.py
47
+ src/chat.py
48
+ src/llm/prompt2.py
49
+ src/loaders
README.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RAG Assistant
2
+
3
+ Assistant conversationnel basé sur une architecture **Retrieval Augmented Generation (RAG)**.
4
+
5
+ Cette application permet d'interroger une base documentaire pré-indexée en combinant une recherche vectorielle, un reranking des documents et un modèle de langage.
6
+
7
+ ---
8
+
9
+ # Modèles utilisés
10
+
11
+ | Composant | Modèle |
12
+ |-----------|---------|
13
+ | **LLM** | TinyLlama/TinyLlama-1.1B-Chat-v1.0 |
14
+ | **Embeddings** | sentence-transformers/all-MiniLM-L6-v2 |
15
+ | **Reranker** | cross-encoder/ms-marco-MiniLM-L-6-v2 |
16
+ | **Recherche vectorielle** | FAISS |
17
+ | **Interface** | Gradio |
18
+
19
+ ---
20
+
21
+ # Fonctionnement
22
+
23
+ Le pipeline RAG suit les étapes suivantes :
24
+
25
+ 1. La question de l'utilisateur est convertie en embedding grâce au modèle **sentence-transformers/all-MiniLM-L6-v2**.
26
+ 2. Une recherche de similarité est effectuée dans la base vectorielle **FAISS** afin de récupérer les passages les plus proches.
27
+ 3. Les documents retrouvés sont ensuite réordonnés (**reranking**) grâce au modèle **cross-encoder/ms-marco-MiniLM-L-6-v2** afin de conserver les passages les plus pertinents.
28
+ 4. Les passages sélectionnés sont injectés dans le prompt du modèle **TinyLlama/TinyLlama-1.1B-Chat-v1.0**.
29
+ 5. Le modèle génère une réponse contextualisée.
30
+
31
+ Architecture :
32
+
33
+ ```text
34
+ Question utilisateur
35
+
36
+
37
+ Embedding (all-MiniLM-L6-v2)
38
+
39
+
40
+ Recherche vectorielle
41
+ (FAISS)
42
+
43
+
44
+ Top-k documents
45
+
46
+
47
+ Reranking (CrossEncoder)
48
+ cross-encoder/ms-marco-MiniLM-L-6-v2
49
+
50
+
51
+ Contexte optimisé
52
+
53
+
54
+ TinyLlama-1.1B-Chat-v1.0
55
+
56
+
57
+ Réponse
58
+ ```
59
+
60
+ ## Objectif du projet
61
+
62
+ Ce projet a été développé afin d'explorer les différentes étapes de la construction d'un système RAG complet :
63
+
64
+ - chargement et préparation des documents ;
65
+ - création d'une base vectorielle avec FAISS ;
66
+ - recherche sémantique par embeddings ;
67
+ - amélioration de la pertinence grâce à un CrossEncoder (reranking) ;
68
+ - génération de réponses avec un modèle de langage Hugging Face ;
69
+ - interface utilisateur développée avec Gradio.
app.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from src.vectorstore.faiss_index import load_index
4
+ from src.vectorstore.metadata_store import load_metadata
5
+
6
+ from src.retrieval.retriever import retrieve
7
+ from src.retrieval.reranker import rerank
8
+
9
+ from src.llm.generator import generate_answer
10
+ from src.llm.postprocessing import clean_answer
11
+
12
+ from src.data_processing.txt_embeddings import compute_embeddings
13
+
14
+ # Load once
15
+ from pathlib import Path
16
+
17
+ BASE_DIR = Path(__file__).parent
18
+
19
+ INDEX_PATH = BASE_DIR / "data/vectorstore/faiss.index"
20
+ METADATA_PATH = BASE_DIR / "data/metadata/metadata.json"
21
+
22
+ index = load_index(INDEX_PATH)
23
+ metadata = load_metadata(METADATA_PATH)
24
+
25
+
26
+ def chat_fn(message, history):
27
+
28
+ # 1. Embedding de la question
29
+ query_emb = compute_embeddings([message])
30
+
31
+ # 2. Retrieval FAISS
32
+ docs = retrieve(query_emb, index, metadata, top_k=10)
33
+
34
+ # 3. Reranking (IMPORTANT: retourne (doc, score))
35
+ docs = rerank(message, docs, top_k=10)
36
+
37
+ # 4. Construction du contexte LLM
38
+ context = "\n\n".join([doc["text"] for doc, score in docs])
39
+
40
+ # 5. Prompt
41
+ prompt = f"""
42
+ Context:
43
+ {context}
44
+
45
+ Question:
46
+ {message}
47
+
48
+ Answer:
49
+ """
50
+
51
+ # 6. Génération réponse LLM
52
+ answer = generate_answer(prompt)
53
+
54
+ # 6.1. Postprocessing
55
+ answer = clean_answer(answer)
56
+
57
+ # 7. Debug chunks avec scores
58
+ debug = "\n\n".join(
59
+ [
60
+ f"📄 Chunk {i+1} (Score : {score:.3f})\n{doc['text']}"
61
+ for i, (doc, score) in enumerate(docs)
62
+ ]
63
+ )
64
+
65
+ # 8. Output final
66
+ final_output = f"{answer}\n\n---\n\n🔍 Retrieved chunks:\n{debug}"
67
+
68
+ return final_output
69
+
70
+ ###########################################################################
71
+ # Interface Gradio type ChatGPT
72
+
73
+ demo = gr.ChatInterface(
74
+ fn=chat_fn,
75
+ title="💬 RAG Assistant",
76
+ description="Chat avec ton système RAG local"
77
+ )
78
+
79
+ if __name__ == "__main__":
80
+ demo.launch()
data/metadata/metadata.json ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "text": "Le contrat de M. A se termine le 12 août 2025. Le contrat de M. B se termine le 12 août 2025. Le contrat de M. C se termine le 12 août 2025.",
4
+ "filename": "doc1.txt",
5
+ "chunk_id": 0,
6
+ "length": 140,
7
+ "source": "doc1.txt"
8
+ },
9
+ {
10
+ "text": "Le contrat de M. C se termine le 12 août 2025. Le contrat de M. D se termine le 12 août 2025. Le contrat de M. E se termine le 12 août 2025.",
11
+ "filename": "doc1.txt",
12
+ "chunk_id": 1,
13
+ "length": 140,
14
+ "source": "doc1.txt"
15
+ },
16
+ {
17
+ "text": "Le contrat de M. E se termine le 12 août 2025. Le contrat de M. F se termine le 12 août 2025. Le contrat de M. G se termine le 12 août 2025.",
18
+ "filename": "doc1.txt",
19
+ "chunk_id": 2,
20
+ "length": 140,
21
+ "source": "doc1.txt"
22
+ },
23
+ {
24
+ "text": "Le contrat de M. G se termine le 12 août 2025. Le contrat de M. H se termine le 12 août 2025. Le contrat de M. I se termine le 12 août 2025.",
25
+ "filename": "doc1.txt",
26
+ "chunk_id": 3,
27
+ "length": 140,
28
+ "source": "doc1.txt"
29
+ },
30
+ {
31
+ "text": "Le contrat de M. I se termine le 12 août 2025. Le contrat de M. J se termine le 12 août 2025. Le contrat de M. K se termine le 12 août 2025.",
32
+ "filename": "doc1.txt",
33
+ "chunk_id": 4,
34
+ "length": 140,
35
+ "source": "doc1.txt"
36
+ },
37
+ {
38
+ "text": "Le contrat de M. K se termine le 12 août 2025. Le contrat de M. L se termine le 12 août 2025. Le contrat de M. M se termine le 12 août 2025.",
39
+ "filename": "doc1.txt",
40
+ "chunk_id": 5,
41
+ "length": 140,
42
+ "source": "doc1.txt"
43
+ },
44
+ {
45
+ "text": "Le contrat de M. M se termine le 12 août 2025. Le contrat de M. N se termine le 12 août 2025. Le contrat de M. O se termine le 12 août 2025.",
46
+ "filename": "doc1.txt",
47
+ "chunk_id": 6,
48
+ "length": 140,
49
+ "source": "doc1.txt"
50
+ },
51
+ {
52
+ "text": "Le contrat de M. O se termine le 12 août 2025. Le contrat de M. P se termine le 12 août 2025. Le contrat de M. Q se termine le 12 août 2025.",
53
+ "filename": "doc1.txt",
54
+ "chunk_id": 7,
55
+ "length": 140,
56
+ "source": "doc1.txt"
57
+ },
58
+ {
59
+ "text": "Le contrat de M. Q se termine le 12 août 2025. Le contrat de M. R se termine le 12 août 2025. Le contrat de M. S se termine le 12 août 2025.",
60
+ "filename": "doc1.txt",
61
+ "chunk_id": 8,
62
+ "length": 140,
63
+ "source": "doc1.txt"
64
+ },
65
+ {
66
+ "text": "Le contrat de M. S se termine le 12 août 2025. Le contrat de M. T se termine le 12 août 2025. Le contrat de M. U se termine le 12 août 2025.",
67
+ "filename": "doc1.txt",
68
+ "chunk_id": 9,
69
+ "length": 140,
70
+ "source": "doc1.txt"
71
+ },
72
+ {
73
+ "text": "Le contrat de M. U se termine le 12 août 2025. Le contrat de M. V se termine le 12 août 2025. Le contrat de M. W se termine le 12 août 2025.",
74
+ "filename": "doc1.txt",
75
+ "chunk_id": 10,
76
+ "length": 140,
77
+ "source": "doc1.txt"
78
+ },
79
+ {
80
+ "text": "Le contrat de M. W se termine le 12 août 2025. Le contrat de M. X se termine le 28 janvier 2024. Le contrat de M. Y se termine le 12 août 2025.",
81
+ "filename": "doc1.txt",
82
+ "chunk_id": 11,
83
+ "length": 143,
84
+ "source": "doc1.txt"
85
+ },
86
+ {
87
+ "text": "Le contrat de M. Y se termine le 12 août 2025. Le contrat de M. Z se termine le 12 août 2025.",
88
+ "filename": "doc1.txt",
89
+ "chunk_id": 12,
90
+ "length": 93,
91
+ "source": "doc1.txt"
92
+ },
93
+ {
94
+ "text": "Les CDD doivent être renouvelés par écrit. Les CDI doivent être renouvelés par écrit. Les CDD doivent être renouvelés par écrit pour X nombre de jours.",
95
+ "filename": "doc2.txt",
96
+ "chunk_id": 0,
97
+ "length": 151,
98
+ "source": "doc2.txt"
99
+ },
100
+ {
101
+ "text": "Les CDD doivent être renouvelés par écrit pour X nombre de jours. Les CDI et CDI doivent être renouvelés par écrit pour 100 jours de travaille supplémentaires. La captiale de la France est Paris, c'est un très beau pays.",
102
+ "filename": "doc2.txt",
103
+ "chunk_id": 1,
104
+ "length": 220,
105
+ "source": "doc2.txt"
106
+ },
107
+ {
108
+ "text": "La captiale de la France est Paris, c'est un très beau pays. Situé au centre de l'Europe, ses voisins sont l'Espagne, la Belgique, le Portugal.",
109
+ "filename": "doc2.txt",
110
+ "chunk_id": 2,
111
+ "length": 143,
112
+ "source": "doc2.txt"
113
+ },
114
+ {
115
+ "text": "La période d'essai est de 10 mois maximum. correctif janvier 2026 : La période d'essai est de 3 mois maximum. correctif février 2026 : La période d'essai est de 2 mois maximum.",
116
+ "filename": "doc3.txt",
117
+ "chunk_id": 0,
118
+ "length": 176,
119
+ "source": "doc3.txt"
120
+ },
121
+ {
122
+ "text": "correctif février 2026 : La période d'essai est de 2 mois maximum.",
123
+ "filename": "doc3.txt",
124
+ "chunk_id": 1,
125
+ "length": 66,
126
+ "source": "doc3.txt"
127
+ },
128
+ {
129
+ "text": "Le contrat de MMe. Y se termine le 12 août 2025. Le contrat de MMe.",
130
+ "filename": "doc4.txt",
131
+ "chunk_id": 0,
132
+ "length": 67,
133
+ "source": "doc4.txt"
134
+ },
135
+ {
136
+ "text": "Le contrat de MMe. Xtra se termine le 10 mai 1996.",
137
+ "filename": "doc4.txt",
138
+ "chunk_id": 1,
139
+ "length": 50,
140
+ "source": "doc4.txt"
141
+ }
142
+ ]
data/vectorstore/faiss.index ADDED
Binary file (30.8 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.12.1
2
+ transformers==5.12.1
3
+ sentence-transformers==5.6.0
4
+ numpy==2.4.6
5
+ nltk==3.9.4
6
+ python-docx==1.2.0
7
+ PyMuPDF==1.28.0
8
+ pandas==2.3.3
9
+ faiss-cpu==1.14.3
10
+ accelerate==1.14.0
11
+ safetensors==0.8.0
12
+ gradio==6.19.0
src/data_processing/txt_chunker.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Jul 3 15:41:48 2026
4
+
5
+ @author: ALBERT
6
+ """
7
+
8
+ from nltk.tokenize import sent_tokenize
9
+ import re
10
+
11
+
12
+ def clean_text(text: str) -> str:
13
+ """Nettoyage basique du texte"""
14
+
15
+ text = text.strip()
16
+
17
+ # supprime répétitions type "aaaaaa"
18
+ if re.search(r"(.)\1{8,}", text):
19
+ return ""
20
+
21
+ # supprime bruit évident
22
+ if len(text) < 30:
23
+ return ""
24
+
25
+ return text
26
+
27
+
28
+ def chunk_text(text, max_sentences=3, overlap=1):
29
+
30
+ sentences = sent_tokenize(text)
31
+
32
+ # nettoyage sentences
33
+ sentences = [s.strip() for s in sentences if len(s.strip()) > 0]
34
+
35
+ chunks = []
36
+
37
+ step = max_sentences - overlap
38
+
39
+ for i in range(0, len(sentences), step):
40
+
41
+ chunk_sentences = sentences[i:i + max_sentences]
42
+ chunk = " ".join(chunk_sentences)
43
+
44
+ chunk = clean_text(chunk)
45
+
46
+ if chunk: # garde uniquement chunks valides
47
+ chunks.append(chunk)
48
+
49
+ return chunks
50
+
51
+ def create_chunks_metadata(documents):
52
+
53
+ all_chunks = []
54
+ metadata = []
55
+
56
+ for doc in documents:
57
+
58
+ chunks = chunk_text(doc["text"])
59
+
60
+ for i, chunk in enumerate(chunks):
61
+
62
+ all_chunks.append(chunk)
63
+
64
+ metadata.append({
65
+ "text": chunk,
66
+ "filename": doc.get("filename", "unknown"),
67
+ "chunk_id": i,
68
+ "length": len(chunk),
69
+ "source": doc.get("filename", "unknown")
70
+ })
71
+
72
+ return all_chunks, metadata
src/data_processing/txt_embeddings.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Jul 3 15:48:54 2026
4
+
5
+ @author: ALBERT
6
+ """
7
+
8
+ from sentence_transformers import SentenceTransformer
9
+ import numpy as np
10
+
11
+ model = SentenceTransformer("all-MiniLM-L6-v2")
12
+
13
+ def compute_embeddings(chunks):
14
+ embeddings = model.encode(chunks)
15
+ return np.array(embeddings).astype("float32")
src/llm/generator.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/llm/generator.py
2
+
3
+ import torch
4
+ from .model import model, tokenizer
5
+
6
+
7
+ def generate_answer(prompt, max_new_tokens=50, temperature=0.2):
8
+
9
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
10
+
11
+ with torch.no_grad():
12
+ output = model.generate(
13
+ **inputs,
14
+ max_new_tokens=max_new_tokens,
15
+ temperature=temperature,
16
+ do_sample=False,
17
+ top_p=0.9,
18
+ repetition_penalty=1.2,
19
+ eos_token_id=tokenizer.eos_token_id,
20
+ pad_token_id=tokenizer.eos_token_id
21
+ )
22
+
23
+ response = tokenizer.decode(output[0], skip_special_tokens=True)
24
+
25
+ # On enlève le prompt pour ne garder que la réponse
26
+ return response[len(prompt):].strip()
src/llm/model.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from transformers import AutoTokenizer, AutoModelForCausalLM
3
+ import torch
4
+
5
+ MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
6
+
7
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
8
+
9
+ model = AutoModelForCausalLM.from_pretrained(
10
+ MODEL_NAME,
11
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
12
+ device_map="auto"
13
+ )
14
+
15
+ model.eval()
src/llm/postprocessing.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ STOP_SEQUENCES = [
4
+ "Question:",
5
+ "Context:",
6
+ "Answer:",
7
+ "User:",
8
+ "Assistant:"
9
+ ]
10
+
11
+ def clean_answer(text: str) -> str:
12
+ """
13
+ Nettoyage de base de la réponse LLM
14
+ """
15
+
16
+ if not text:
17
+ return ""
18
+
19
+ # 1. strip global
20
+ text = text.strip()
21
+
22
+ # 2. couper si le modèle recommence un dialogue
23
+ for stop in STOP_SEQUENCES:
24
+ if stop in text:
25
+ text = text.split(stop)[0]
26
+
27
+ # 3. enlever répétitions de whitespace
28
+ text = re.sub(r"\s+", " ", text)
29
+
30
+ # 4. enlever phrases incomplètes finales (très simple)
31
+ text = re.sub(r"\b\w{1,2}$", "", text).strip()
32
+
33
+ return text
src/retrieval/query.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from data_processing.txt_embeddings import compute_embeddings
2
+
3
+
4
+ def create_query_embedding(question):
5
+ """
6
+ Transforme une question utilisateur en embedding.
7
+ """
8
+
9
+ return compute_embeddings([question])
src/retrieval/reranker.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import CrossEncoder
2
+
3
+ model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
4
+
5
+
6
+ def rerank(query, documents, top_k=3):
7
+
8
+ pairs = [(query, doc["text"]) for doc in documents]
9
+
10
+ scores = model.predict(pairs)
11
+
12
+ ranked = sorted(
13
+ zip(documents, scores),
14
+ key=lambda x: x[1],
15
+ reverse=True
16
+ )
17
+
18
+ return ranked[:top_k]
src/retrieval/retriever.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def retrieve(query_embedding, index, metadata, top_k=3):
2
+
3
+ distances, indices = index.search(query_embedding, top_k)
4
+
5
+ results = []
6
+
7
+ for distance, idx in zip(distances[0], indices[0]):
8
+
9
+ results.append({
10
+ "text": metadata[idx]["text"],
11
+ "source": metadata[idx]["filename"],
12
+ "distance": float(distance)
13
+ })
14
+
15
+ return results
src/vectorstore/faiss_index.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Jul 3 15:59:06 2026
4
+
5
+ @author: ALBERT
6
+ """
7
+
8
+ import faiss
9
+
10
+ def build_index(embeddings, index_path):
11
+ dimension = embeddings.shape[1]
12
+
13
+ index = faiss.IndexFlatL2(dimension)
14
+ index.add(embeddings)
15
+
16
+ faiss.write_index(index, index_path)
17
+
18
+ print("Index FAISS sauvegardé.")
19
+
20
+
21
+ def load_index(index_path):
22
+ print("Chargement de l'index FAISS...")
23
+ return faiss.read_index(index_path)
src/vectorstore/metadata_store.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ def save_metadata(metadata, metadata_path):
4
+ with open(metadata_path, "w", encoding="utf-8") as f:
5
+ json.dump(metadata, f, ensure_ascii=False, indent=4)
6
+
7
+ print("Métadonnées sauvegardées.")
8
+
9
+
10
+ def load_metadata(metadata_path):
11
+ with open(metadata_path, "r", encoding="utf-8") as f:
12
+ metadata = json.load(f)
13
+
14
+ print("Métadonnées chargées.")
15
+
16
+ return metadata