ArthurSrz commited on
Commit
8a86da4
·
1 Parent(s): 0a69846

Add Borges Graph app with GraphRAG data

Browse files
README.md CHANGED
@@ -1,13 +1,69 @@
1
  ---
2
  title: Borges Graph
3
- emoji: 🌍
4
  colorFrom: yellow
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
- short_description: Hosting of Borges graphRAG mechanism
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Borges Graph
3
+ emoji: 📚
4
  colorFrom: yellow
5
+ colorTo: orange
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: GraphRAG Explorer for Borgesian Literature Analysis
12
  ---
13
 
14
+ # Borges Graph - GraphRAG Explorer
15
+
16
+ Une interface intelligente pour explorer la littérature avec GraphRAG. Basé sur nano-graphrag, cette application permet de poser des questions en langage naturel sur des œuvres littéraires et visualise le processus de recherche dans le graphe de connaissances.
17
+
18
+ ## 🌟 Fonctionnalités
19
+
20
+ - **Recherche sémantique** : Posez vos questions en français
21
+ - **Analyse GraphRAG** : Utilise nano-graphrag pour explorer les connexions
22
+ - **Interface Gradio** : Interface web intuitive
23
+ - **API intégrée** : Endpoint pour intégrations externes
24
+ - **Mode démo** : Fonctionne même sans données GraphRAG
25
+
26
+ ## 🚀 Utilisation
27
+
28
+ ### Interface Web
29
+ 1. Tapez votre question dans le champ de recherche
30
+ 2. Choisissez le mode (Local ou Global)
31
+ 3. Cliquez sur "Explorer le graphe"
32
+ 4. Découvrez la réponse et l'analyse du parcours
33
+
34
+ ### API
35
+ L'application expose automatiquement une API Gradio accessible via :
36
+ ```
37
+ POST /api/predict
38
+ ```
39
+
40
+ ## 📖 Questions d'exemple
41
+
42
+ - "Quels sont les thèmes principaux de cette œuvre ?"
43
+ - "Parle-moi des personnages"
44
+ - "Comment les concepts sont-ils interconnectés ?"
45
+ - "Quelle est la structure narrative ?"
46
+
47
+ ## 🛠 Architecture
48
+
49
+ - **nano-graphrag** : Moteur de recherche GraphRAG
50
+ - **Gradio** : Interface utilisateur et API
51
+ - **OpenAI** : Modèles de langage pour l'analyse
52
+ - **NetworkX** : Gestion des graphes de connaissances
53
+
54
+ ## 📊 Données
55
+
56
+ Cette application peut travailler avec des données GraphRAG pré-générées. Les fichiers de données doivent être organisés dans des dossiers contenant `graph_chunk_entity_relation.graphml`.
57
+
58
+ ## 🎯 Intégration
59
+
60
+ Cette API peut être intégrée dans d'autres applications, notamment :
61
+ - Applications web Vercel/Next.js
62
+ - Interfaces de visualisation de graphes
63
+ - Outils d'analyse littéraire
64
+
65
+ ## 🔗 Liens
66
+
67
+ - [nano-graphrag](https://github.com/gusye1234/nano-graphrag)
68
+ - [Gradio](https://gradio.app)
69
+ - [Hugging Face Spaces](https://huggingface.co/spaces)
a_rebours_huysmans/graph_chunk_entity_relation.graphml ADDED
The diff for this file is too large to render. See raw diff
 
a_rebours_huysmans/kv_store_community_reports.json ADDED
The diff for this file is too large to render. See raw diff
 
a_rebours_huysmans/kv_store_full_docs.json ADDED
The diff for this file is too large to render. See raw diff
 
a_rebours_huysmans/kv_store_llm_response_cache.json ADDED
The diff for this file is too large to render. See raw diff
 
a_rebours_huysmans/kv_store_text_chunks.json ADDED
The diff for this file is too large to render. See raw diff
 
app.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import re
4
+ import os
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Dict, Any, List
8
+ import tempfile
9
+ import shutil
10
+
11
+ # Try to import nano_graphrag, with fallback for demo
12
+ try:
13
+ from nano_graphrag import GraphRAG, QueryParam
14
+ from nano_graphrag._llm import gpt_4o_mini_complete
15
+ NANO_GRAPHRAG_AVAILABLE = True
16
+ except ImportError:
17
+ NANO_GRAPHRAG_AVAILABLE = False
18
+ print("⚠️ nano-graphrag not available, running in demo mode")
19
+
20
+ class BorgesGraphRAG:
21
+ def __init__(self):
22
+ self.instances = {}
23
+ self.current_book = None
24
+
25
+ def load_book_data(self, book_folder: str):
26
+ """Load GraphRAG data for a specific book"""
27
+ if not NANO_GRAPHRAG_AVAILABLE:
28
+ return False
29
+
30
+ try:
31
+ if book_folder not in self.instances:
32
+ self.instances[book_folder] = GraphRAG(
33
+ working_dir=book_folder,
34
+ best_model_func=gpt_4o_mini_complete,
35
+ cheap_model_func=gpt_4o_mini_complete,
36
+ best_model_max_async=3,
37
+ cheap_model_max_async=3
38
+ )
39
+ self.current_book = book_folder
40
+ return True
41
+ except Exception as e:
42
+ print(f"Error loading book data: {e}")
43
+ return False
44
+
45
+ def parse_context_csv(self, context_str: str):
46
+ """Parse the CSV context returned by GraphRAG"""
47
+ entities = []
48
+ relations = []
49
+
50
+ # Parse entities section
51
+ entities_match = re.search(r'-----Entities-----\n```csv\n(.*?)\n```', context_str, re.DOTALL)
52
+ if entities_match:
53
+ lines = entities_match.group(1).strip().split('\n')
54
+ for line in lines[1:]: # Skip header
55
+ parts = line.split(',')
56
+ if len(parts) >= 5:
57
+ entities.append({
58
+ 'id': parts[1].strip(),
59
+ 'type': parts[2].strip(),
60
+ 'description': ','.join(parts[3:-1]).strip(),
61
+ 'rank': float(parts[-1]) if parts[-1].strip() else 0
62
+ })
63
+
64
+ # Parse relationships section
65
+ relations_match = re.search(r'-----Relationships-----\n```csv\n(.*?)\n```', context_str, re.DOTALL)
66
+ if relations_match:
67
+ lines = relations_match.group(1).strip().split('\n')
68
+ for line in lines[1:]: # Skip header
69
+ parts = line.split(',')
70
+ if len(parts) >= 6:
71
+ relations.append({
72
+ 'source': parts[1].strip(),
73
+ 'target': parts[2].strip(),
74
+ 'description': ','.join(parts[3:-2]).strip(),
75
+ 'weight': float(parts[-2]) if parts[-2].strip() else 1,
76
+ 'rank': float(parts[-1]) if parts[-1].strip() else 0
77
+ })
78
+
79
+ return entities, relations
80
+
81
+ async def query_book(self, query: str, mode: str = "local") -> Dict[str, Any]:
82
+ """Query the current book with GraphRAG"""
83
+ if not NANO_GRAPHRAG_AVAILABLE or not self.current_book:
84
+ return self.get_demo_response(query)
85
+
86
+ try:
87
+ graph_instance = self.instances[self.current_book]
88
+
89
+ # Get context with details
90
+ context_param = QueryParam(mode=mode, only_need_context=True, top_k=20)
91
+ context = await graph_instance.aquery(query, param=context_param)
92
+
93
+ # Get actual answer
94
+ answer_param = QueryParam(mode=mode, top_k=20)
95
+ answer = await graph_instance.aquery(query, param=answer_param)
96
+
97
+ # Parse context
98
+ entities, relations = self.parse_context_csv(context)
99
+
100
+ return {
101
+ "success": True,
102
+ "answer": answer,
103
+ "searchPath": {
104
+ "entities": [
105
+ {**e, "order": i+1, "score": 1.0 - (i * 0.05)}
106
+ for i, e in enumerate(entities[:15])
107
+ ],
108
+ "relations": [
109
+ {**r, "traversalOrder": i+1}
110
+ for i, r in enumerate(relations[:20])
111
+ ],
112
+ "communities": [
113
+ {"id": "community_1", "content": "Cluster thématique principal", "relevance": 0.9}
114
+ ]
115
+ },
116
+ "book_id": self.current_book,
117
+ "mode": mode,
118
+ "query": query
119
+ }
120
+
121
+ except Exception as e:
122
+ return {
123
+ "success": False,
124
+ "error": str(e),
125
+ "fallback": self.get_demo_response(query)
126
+ }
127
+
128
+ def get_demo_response(self, query: str) -> Dict[str, Any]:
129
+ """Demo response when GraphRAG is not available"""
130
+ query_lower = query.lower()
131
+
132
+ if "thème" in query_lower or "theme" in query_lower:
133
+ answer = """Les thèmes principaux de cette œuvre borgésienne incluent:
134
+
135
+ **1. Le Labyrinthe de la Connaissance**
136
+ La bibliothèque infinie comme métaphore de l'univers et de la quête du savoir.
137
+
138
+ **2. L'Identité et le Double**
139
+ L'exploration de la nature fragmentée de l'identité humaine.
140
+
141
+ **3. Le Temps Cyclique**
142
+ La répétition éternelle et la nature circulaire de l'existence.
143
+
144
+ **4. La Réalité et la Fiction**
145
+ Les frontières floues entre le réel et l'imaginaire."""
146
+
147
+ entities = ["LABYRINTHE", "BIBLIOTHÈQUE", "IDENTITÉ", "TEMPS", "RÉALITÉ"]
148
+ relations = [
149
+ {"source": "LABYRINTHE", "target": "BIBLIOTHÈQUE"},
150
+ {"source": "IDENTITÉ", "target": "RÉALITÉ"},
151
+ {"source": "TEMPS", "target": "LABYRINTHE"}
152
+ ]
153
+ else:
154
+ answer = f"Analyse de votre question: '{query}'\n\nD'après l'exploration du graphe de connaissances, cette question touche aux concepts fondamentaux de l'univers borgésien. Les connexions révèlent une architecture narrative complexe où chaque élément participe d'un réseau de significations multiples."
155
+ entities = ["QUESTION", "CONNAISSANCE", "RÉSEAU", "ANALYSE"]
156
+ relations = [{"source": "QUESTION", "target": "CONNAISSANCE"}]
157
+
158
+ return {
159
+ "success": True,
160
+ "answer": answer,
161
+ "searchPath": {
162
+ "entities": [
163
+ {
164
+ "id": entity,
165
+ "type": "CONCEPT",
166
+ "description": f"{entity} - Concept clé de l'œuvre",
167
+ "rank": 1,
168
+ "order": i+1,
169
+ "score": 0.9 - (i * 0.1)
170
+ }
171
+ for i, entity in enumerate(entities)
172
+ ],
173
+ "relations": [
174
+ {
175
+ **rel,
176
+ "description": f"Relation entre {rel['source']} et {rel['target']}",
177
+ "weight": 1,
178
+ "rank": 1,
179
+ "traversalOrder": i+1
180
+ }
181
+ for i, rel in enumerate(relations)
182
+ ],
183
+ "communities": [
184
+ {"id": "demo_community", "content": "Cluster thématique (mode démo)", "relevance": 0.8}
185
+ ]
186
+ },
187
+ "book_id": "demo_book",
188
+ "mode": "demo",
189
+ "query": query
190
+ }
191
+
192
+ # Initialize GraphRAG instance
193
+ borges_rag = BorgesGraphRAG()
194
+
195
+ # Check for available book data
196
+ available_books = []
197
+ for item in os.listdir('.'):
198
+ if os.path.isdir(item) and not item.startswith('.'):
199
+ graph_file = os.path.join(item, 'graph_chunk_entity_relation.graphml')
200
+ if os.path.exists(graph_file):
201
+ available_books.append(item)
202
+
203
+ if available_books:
204
+ default_book = available_books[0]
205
+ borges_rag.load_book_data(default_book)
206
+ book_status = f"✅ Livre chargé: {default_book}"
207
+ else:
208
+ book_status = "⚠️ Mode démo - Aucune donnée GraphRAG trouvée"
209
+
210
+ async def process_query(query: str, mode: str) -> tuple:
211
+ """Process a query and return formatted results"""
212
+ if not query.strip():
213
+ return "❌ Veuillez entrer une question", "{}", ""
214
+
215
+ try:
216
+ result = await borges_rag.query_book(query, mode.lower())
217
+
218
+ if result.get("success"):
219
+ # Format the answer
220
+ answer = result["answer"]
221
+
222
+ # Format search path info
223
+ search_info = result["searchPath"]
224
+ entities_count = len(search_info["entities"])
225
+ relations_count = len(search_info["relations"])
226
+
227
+ # Create summary
228
+ summary = f"""
229
+ 📊 **Analyse de la traversée du graphe:**
230
+ • {entities_count} entités identifiées
231
+ • {relations_count} relations explorées
232
+ • Mode: {result.get('mode', 'demo')}
233
+ • Livre: {result.get('book_id', 'demo')}
234
+ """
235
+
236
+ # JSON for API
237
+ json_result = json.dumps(result, indent=2, ensure_ascii=False)
238
+
239
+ return answer, json_result, summary
240
+ else:
241
+ error_msg = result.get("error", "Erreur inconnue")
242
+ return f"❌ Erreur: {error_msg}", "{}", ""
243
+
244
+ except Exception as e:
245
+ return f"❌ Exception: {str(e)}", "{}", ""
246
+
247
+ # Gradio interface
248
+ def query_interface(query: str, mode: str):
249
+ """Sync wrapper for async query processing"""
250
+ loop = asyncio.new_event_loop()
251
+ asyncio.set_event_loop(loop)
252
+ try:
253
+ return loop.run_until_complete(process_query(query, mode))
254
+ finally:
255
+ loop.close()
256
+
257
+ # API endpoint for external calls
258
+ def api_query(query: str, mode: str = "local", book_id: str = None):
259
+ """API endpoint that returns JSON response"""
260
+ loop = asyncio.new_event_loop()
261
+ asyncio.set_event_loop(loop)
262
+ try:
263
+ result = loop.run_until_complete(borges_rag.query_book(query, mode))
264
+ return result
265
+ finally:
266
+ loop.close()
267
+
268
+ # Gradio app
269
+ with gr.Blocks(
270
+ title="Borges Graph - GraphRAG Explorer",
271
+ theme=gr.themes.Soft(primary_hue="amber"),
272
+ css="""
273
+ .gradio-container {
274
+ font-family: 'Georgia', serif;
275
+ background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
276
+ color: #d4af37;
277
+ }
278
+ .gr-button-primary {
279
+ background: linear-gradient(135deg, #d4af37 0%, #b8941f 100%);
280
+ border: none;
281
+ }
282
+ """
283
+ ) as app:
284
+
285
+ gr.Markdown("""
286
+ # 📚 Borges Graph - GraphRAG Explorer
287
+
288
+ Explorez la bibliothèque infinie avec l'intelligence artificielle. Posez vos questions en langage naturel et découvrez les connexions secrètes dans l'univers borgésien.
289
+
290
+ """)
291
+
292
+ gr.Markdown(f"**Statut:** {book_status}")
293
+
294
+ with gr.Row():
295
+ with gr.Column(scale=2):
296
+ query_input = gr.Textbox(
297
+ label="🔍 Votre question",
298
+ placeholder="Quels sont les thèmes principaux de cette œuvre ?",
299
+ lines=2
300
+ )
301
+
302
+ mode_select = gr.Radio(
303
+ choices=["Local", "Global"],
304
+ value="Local",
305
+ label="Mode de recherche",
306
+ info="Local: recherche focalisée | Global: vue d'ensemble"
307
+ )
308
+
309
+ search_btn = gr.Button("🚀 Explorer le graphe", variant="primary")
310
+
311
+ with gr.Column(scale=1):
312
+ gr.Markdown("""
313
+ ### 💡 Questions suggérées:
314
+ - Quels sont les thèmes principaux ?
315
+ - Parle-moi des personnages
316
+ - Quelle est la structure narrative ?
317
+ - Comment les concepts sont-ils liés ?
318
+ """)
319
+
320
+ with gr.Row():
321
+ with gr.Column():
322
+ answer_output = gr.Markdown(label="📖 Réponse")
323
+ summary_output = gr.Markdown(label="📊 Résumé de l'analyse")
324
+
325
+ with gr.Accordion("🔧 Réponse JSON (pour développeurs)", open=False):
326
+ json_output = gr.Code(language="json", label="JSON Response")
327
+
328
+ # Event handlers
329
+ search_btn.click(
330
+ fn=query_interface,
331
+ inputs=[query_input, mode_select],
332
+ outputs=[answer_output, json_output, summary_output]
333
+ )
334
+
335
+ query_input.submit(
336
+ fn=query_interface,
337
+ inputs=[query_input, mode_select],
338
+ outputs=[answer_output, json_output, summary_output]
339
+ )
340
+
341
+ # Launch the app
342
+ if __name__ == "__main__":
343
+ app.launch(
344
+ server_name="0.0.0.0",
345
+ server_port=7860,
346
+ share=False
347
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ nano-graphrag
3
+ openai>=1.0.0
4
+ networkx>=3.0
5
+ numpy>=1.21.0
6
+ tiktoken>=0.4.0
7
+ aiohttp>=3.8.0