ArthurSrz Claude commited on
Commit
690d9f0
·
1 Parent(s): ef8b156

feat: Update Gradio app with enhanced GraphRAG functionality

Browse files

- Add support for book upload (.txt and .zip files)
- Add external API connection capability for Borges integration
- Improve UI with two tabs: Search and Book Management
- Add Python 3.11 compatibility
- Update requirements for nano-graphrag support
- Add demo mode when GraphRAG is unavailable
- Enhanced documentation for deployment

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (4) hide show
  1. .DS_Store +0 -0
  2. README.md +65 -36
  3. app.py +254 -236
  4. requirements.txt +6 -3
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
README.md CHANGED
@@ -2,7 +2,7 @@
2
  title: Borges Graph
3
  emoji: 📚
4
  colorFrom: yellow
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 4.44.0
8
  app_file: app.py
@@ -11,56 +11,85 @@ 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
 
 
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
 
11
  short_description: GraphRAG Explorer for Borgesian Literature Analysis
12
  ---
13
 
14
+ # 📚 Borges Graph - GraphRAG Explorer
15
 
16
+ Une application Gradio interactive pour explorer vos données GraphRAG à travers l'intelligence artificielle. Inspirée par l'univers de Jorge Luis Borges et sa conception des bibliothèques infinies.
17
 
18
+ ## Fonctionnalités
19
 
20
+ - **🔍 Recherche intelligente**: Posez des questions en langage naturel sur vos livres
21
+ - **📊 Modes de recherche**: Local (focalisé) ou Global (vue d'ensemble)
22
+ - **📚 Gestion de livres**: Uploadez et traitez de nouveaux textes
23
+ - **🌐 API externe**: Connexion optionnelle à l'API Borges déployée
24
+ - **🎯 Interface intuitive**: Design élégant inspiré de l'esthétique borgésienne
25
 
26
+ ## 🚀 Installation et déploiement
27
 
28
+ ### Installation locale
 
 
 
 
29
 
30
+ ```bash
31
+ pip install -r requirements.txt
32
+ python app.py
 
33
  ```
34
 
35
+ ### Déploiement sur Hugging Face Spaces
36
+
37
+ 1. Forkez ou clonez ce repository
38
+ 2. Créez un nouvel Space sur [Hugging Face](https://huggingface.co/spaces)
39
+ 3. Uploadez les fichiers de ce dossier
40
+ 4. Configurez les variables d'environnement si nécessaire:
41
+ - `OPENAI_API_KEY`: Votre clé API OpenAI
42
+ - `BORGES_API_URL`: URL de votre API Borges (optionnel)
43
+ - `ENABLE_EXTERNAL_API`: "true" pour activer la connexion API externe
44
+
45
+ ## 📋 Utilisation
46
+
47
+ ### Mode local
48
+
49
+ 1. **Données existantes**: Si vous avez des dossiers avec des données GraphRAG (.graphml), ils seront automatiquement détectés
50
+ 2. **Nouveaux textes**: Uploadez un fichier .txt qui sera traité automatiquement
51
+ 3. **Données pré-traitées**: Uploadez un fichier .zip contenant des données GraphRAG existantes
52
+
53
+ ### Mode API externe
54
+
55
+ Activez l'option "Utiliser l'API Borges" pour interroger directement votre application déployée sur Vercel.
56
+
57
+ ## 🔧 Configuration
58
+
59
+ ### Variables d'environnement
60
+
61
+ - `OPENAI_API_KEY`: Requis pour le traitement GraphRAG local
62
+ - `BORGES_API_URL`: URL de l'API externe (défaut: https://borges-library.vercel.app/api/graphrag)
63
+ - `ENABLE_EXTERNAL_API`: Active l'option API externe dans l'interface
64
+
65
+ ### Formats de fichiers supportés
66
+
67
+ - **📄 .txt**: Texte brut qui sera traité par GraphRAG
68
+ - **📦 .zip**: Archive contenant des données GraphRAG pré-traitées
69
+
70
+ ## 🏗️ Architecture
71
+
72
+ L'application est construite avec:
73
 
74
+ - **Gradio**: Interface utilisateur interactive
75
+ - **nano-graphrag**: Moteur de traitement GraphRAG
76
+ - **NetworkX**: Manipulation des graphes
77
+ - **OpenAI API**: Modèles de langage pour l'analyse
78
 
79
+ ## 🎨 Interface
80
 
81
+ L'interface comprend deux onglets principaux:
 
 
 
82
 
83
+ 1. **🔍 Recherche**: Pour interroger vos données
84
+ 2. **📚 Gestion des livres**: Pour uploader et gérer vos textes
85
 
86
+ ## 🤝 Intégration avec l'écosystème Borges
87
 
88
+ Cette application Gradio est conçue pour fonctionner en synergie avec:
89
 
90
+ - **Borges Library Web**: Interface principale déployée sur Vercel
91
+ - **GraphRAG API**: API backend pour les requêtes GraphRAG
92
+ - **Neo4j**: Base de données graphe pour la persistance
 
93
 
94
  ## 🔗 Liens
95
 
app.py CHANGED
@@ -7,21 +7,21 @@ from pathlib import Path
7
  from typing import Dict, Any, List
8
  import tempfile
9
  import shutil
10
- from dotenv import load_dotenv
11
-
12
- # Load environment variables
13
- load_dotenv()
14
-
15
- # Check for OpenAI API key
16
- if not os.getenv("OPENAI_API_KEY"):
17
- print("⚠️ OPENAI_API_KEY not found in environment variables")
18
- print("⚠️ nano-graphrag requires OpenAI API key to function")
19
- else:
20
- print(" OpenAI API key found in environment")
21
-
22
- # Disable nano_graphrag to avoid Exit code 132 crashes
23
- NANO_GRAPHRAG_AVAILABLE = False
24
- print("ℹ️ Using direct JSON data mode instead of nano-graphrag")
25
 
26
  class BorgesGraphRAG:
27
  def __init__(self):
@@ -31,11 +31,9 @@ class BorgesGraphRAG:
31
  def load_book_data(self, book_folder: str):
32
  """Load GraphRAG data for a specific book"""
33
  if not NANO_GRAPHRAG_AVAILABLE:
34
- print(f"❌ nano-graphrag not available, cannot load {book_folder}")
35
  return False
36
 
37
  try:
38
- print(f"🔄 Loading GraphRAG instance for {book_folder}...")
39
  if book_folder not in self.instances:
40
  self.instances[book_folder] = GraphRAG(
41
  working_dir=book_folder,
@@ -44,24 +42,11 @@ class BorgesGraphRAG:
44
  best_model_max_async=3,
45
  cheap_model_max_async=3
46
  )
47
- print(f"✅ GraphRAG instance created for {book_folder}")
48
- else:
49
- print(f"♻️ Reusing existing GraphRAG instance for {book_folder}")
50
-
51
  self.current_book = book_folder
52
  return True
53
  except Exception as e:
54
- error_msg = str(e).lower()
55
- if 'matrix' in error_msg or 'graspologic' in error_msg:
56
- print(f"⚠️ Matrix/graspologic dependency issue for {book_folder}: {e}")
57
- print(f"⚠️ Falling back to demo mode due to advanced features unavailable")
58
- # Still set as current book but don't create instance
59
- self.current_book = book_folder
60
- return False # Will trigger demo mode
61
- else:
62
- print(f"❌ Error loading book data for {book_folder}: {e}")
63
- print(f"❌ Error type: {type(e).__name__}")
64
- return False
65
 
66
  def parse_context_csv(self, context_str: str):
67
  """Parse the CSV context returned by GraphRAG"""
@@ -99,148 +84,87 @@ class BorgesGraphRAG:
99
 
100
  return entities, relations
101
 
102
- async def query_book(self, query: str, mode: str = "local") -> Dict[str, Any]:
103
- """Query the current book with GraphRAG"""
104
- if not NANO_GRAPHRAG_AVAILABLE or not self.current_book:
105
- return self.get_demo_response(query)
106
-
107
- # Try GraphRAG first, fallback to reading raw data if it fails
108
  try:
109
- if self.current_book in self.instances:
110
- graph_instance = self.instances[self.current_book]
111
-
112
- # Get context with details
113
- context_param = QueryParam(mode=mode, only_need_context=True, top_k=20)
114
- context = await graph_instance.aquery(query, param=context_param)
115
-
116
- # Get actual answer
117
- answer_param = QueryParam(mode=mode, top_k=20)
118
- answer = await graph_instance.aquery(query, param=answer_param)
119
 
120
- # Parse context
121
- entities, relations = self.parse_context_csv(context)
 
 
 
122
 
 
 
 
123
  return {
124
- "success": True,
125
- "answer": answer,
126
- "searchPath": {
127
- "entities": [
128
- {**e, "order": i+1, "score": 1.0 - (i * 0.05)}
129
- for i, e in enumerate(entities[:15])
130
- ],
131
- "relations": [
132
- {**r, "traversalOrder": i+1}
133
- for i, r in enumerate(relations[:20])
134
- ],
135
- "communities": [
136
- {"id": "community_1", "content": "Cluster thématique principal", "relevance": 0.9}
137
- ]
138
- },
139
- "book_id": self.current_book,
140
- "mode": mode,
141
- "query": query
142
  }
143
- else:
144
- # Fallback: use raw data without full GraphRAG
145
- return await self.query_from_raw_data(query, mode)
146
 
147
  except Exception as e:
148
- print(f"❌ GraphRAG query failed: {e}")
149
- return await self.query_from_raw_data(query, mode)
 
 
 
150
 
151
- async def query_from_raw_data(self, query: str, mode: str) -> Dict[str, Any]:
152
- """Query using raw GraphRAG JSON data files"""
153
- if not self.current_book:
 
 
 
 
 
154
  return self.get_demo_response(query)
155
 
156
  try:
157
- import json
158
- import os
159
-
160
- # Try to load real data from JSON files
161
- book_dir = self.current_book
162
- entities_data = []
163
- relations_data = []
164
-
165
- # Load community reports if available
166
- community_file = os.path.join(book_dir, 'kv_store_community_reports.json')
167
- if os.path.exists(community_file):
168
- with open(community_file, 'r', encoding='utf-8') as f:
169
- community_data = json.load(f)
170
- print(f"📊 Loaded {len(community_data)} community reports")
171
-
172
- # Load text chunks for context
173
- chunks_file = os.path.join(book_dir, 'kv_store_text_chunks.json')
174
- chunks_content = ""
175
- if os.path.exists(chunks_file):
176
- with open(chunks_file, 'r', encoding='utf-8') as f:
177
- chunks_data = json.load(f)
178
- # Get first few chunks for context
179
- chunk_texts = [chunk.get('content', '') for chunk in list(chunks_data.values())[:3]]
180
- chunks_content = ' '.join(chunk_texts)[:500] + "..."
181
- print(f"📖 Loaded {len(chunks_data)} text chunks")
182
-
183
- # Use OpenAI to analyze the query with real book context
184
- from openai import OpenAI
185
- client = OpenAI()
186
-
187
- prompt = f"""Basé sur le livre "{self.current_book}" et ses données GraphRAG, réponds à la question: "{query}"
188
-
189
- Context du livre:
190
- {chunks_content}
191
-
192
- Fournis une réponse détaillée et littéraire comme un expert en analyse littéraire."""
193
-
194
- try:
195
- response = client.chat.completions.create(
196
- model="gpt-4o-mini",
197
- messages=[{"role": "user", "content": prompt}],
198
- max_tokens=400,
199
- temperature=0.7
200
- )
201
- answer = response.choices[0].message.content
202
- except Exception as openai_error:
203
- print(f"⚠️ OpenAI API failed: {openai_error}")
204
- answer = f"""D'après l'analyse du livre "{self.current_book}" via les données GraphRAG disponibles :
205
-
206
- Cette œuvre révèle une architecture narrative complexe où les thèmes principaux s'entrelacent à travers un réseau de personnages et de concepts. L'analyse des {len(chunks_data) if 'chunks_data' in locals() else 'nombreux'} fragments textuels montre une richesse thématique caractéristique de la littérature contemporaine.
207
-
208
- Les données GraphRAG permettent d'identifier les connexions profondes entre les éléments narratifs, révélant la structure sous-jacente de l'œuvre."""
209
-
210
- # Create realistic entities based on book data
211
- entities = [
212
- {"id": f"LIVRE_{self.current_book.upper()}", "type": "ŒUVRE", "description": f"L'œuvre principale {self.current_book}", "rank": 1, "order": 1, "score": 1.0},
213
- {"id": "ANALYSE_LITTÉRAIRE", "type": "CONCEPT", "description": "Analyse littéraire approfondie", "rank": 1, "order": 2, "score": 0.95},
214
- {"id": "STRUCTURE_NARRATIVE", "type": "CONCEPT", "description": "Structure narrative de l'œuvre", "rank": 1, "order": 3, "score": 0.90},
215
- {"id": "THÈMES_PRINCIPAUX", "type": "CONCEPT", "description": "Thèmes principaux identifiés", "rank": 1, "order": 4, "score": 0.85},
216
- {"id": "PERSONNAGES", "type": "ENTITY", "description": "Personnages de l'œuvre", "rank": 1, "order": 5, "score": 0.80}
217
- ]
218
 
219
- relations = [
220
- {"source": f"LIVRE_{self.current_book.upper()}", "target": "ANALYSE_LITTÉRAIRE", "description": "Œuvre analysée", "weight": 1, "rank": 1, "traversalOrder": 1},
221
- {"source": "ANALYSE_LITTÉRAIRE", "target": "STRUCTURE_NARRATIVE", "description": "Révèle la structure", "weight": 1, "rank": 1, "traversalOrder": 2},
222
- {"source": "STRUCTURE_NARRATIVE", "target": "THÈMES_PRINCIPAUX", "description": "Contient les thèmes", "weight": 1, "rank": 1, "traversalOrder": 3},
223
- {"source": "THÈMES_PRINCIPAUX", "target": "PERSONNAGES", "description": "Exprimés par les personnages", "weight": 1, "rank": 1, "traversalOrder": 4}
224
- ]
 
 
 
 
225
 
226
  return {
227
  "success": True,
228
  "answer": answer,
229
  "searchPath": {
230
- "entities": entities,
231
- "relations": relations,
 
 
 
 
 
 
232
  "communities": [
233
- {"id": "community_real", "content": f"Analyse de {self.current_book} (données réelles)", "relevance": 0.95}
234
  ]
235
  },
236
  "book_id": self.current_book,
237
- "mode": f"{mode}_real_data",
238
  "query": query
239
  }
240
 
241
  except Exception as e:
242
- print(f"❌ Raw data query failed: {e}")
243
- return self.get_demo_response(query)
 
 
 
244
 
245
  def get_demo_response(self, query: str) -> Dict[str, Any]:
246
  """Demo response when GraphRAG is not available"""
@@ -312,31 +236,25 @@ borges_rag = BorgesGraphRAG()
312
  # Check for available book data
313
  available_books = []
314
  for item in os.listdir('.'):
315
- if os.path.isdir(item) and not item.startswith('.') and not item.startswith('__'):
316
  graph_file = os.path.join(item, 'graph_chunk_entity_relation.graphml')
317
  if os.path.exists(graph_file):
318
  available_books.append(item)
319
- print(f"📚 Found book: {item}")
320
-
321
- print(f"📊 Total available books: {len(available_books)}")
322
- print(f"📋 Book list: {available_books}")
323
 
324
  if available_books:
325
  default_book = available_books[0]
326
- print(f"🎯 Loading default book: {default_book}")
327
  borges_rag.load_book_data(default_book)
328
  book_status = f"✅ Livre chargé: {default_book}"
329
  else:
330
- print("⚠️ No GraphRAG data found")
331
  book_status = "⚠️ Mode démo - Aucune donnée GraphRAG trouvée"
332
 
333
- async def process_query(query: str, mode: str) -> tuple:
334
  """Process a query and return formatted results"""
335
  if not query.strip():
336
  return "❌ Veuillez entrer une question", "{}", ""
337
 
338
  try:
339
- result = await borges_rag.query_book(query, mode.lower())
340
 
341
  if result.get("success"):
342
  # Format the answer
@@ -347,12 +265,16 @@ async def process_query(query: str, mode: str) -> tuple:
347
  entities_count = len(search_info["entities"])
348
  relations_count = len(search_info["relations"])
349
 
 
 
 
350
  # Create summary
351
  summary = f"""
352
  📊 **Analyse de la traversée du graphe:**
353
  • {entities_count} entités identifiées
354
  • {relations_count} relations explorées
355
  • Mode: {result.get('mode', 'demo')}
 
356
  • Livre: {result.get('book_id', 'demo')}
357
  """
358
 
@@ -362,58 +284,123 @@ async def process_query(query: str, mode: str) -> tuple:
362
  return answer, json_result, summary
363
  else:
364
  error_msg = result.get("error", "Erreur inconnue")
365
- return f"❌ Erreur: {error_msg}", "{}", ""
 
 
 
 
 
 
 
 
366
 
367
  except Exception as e:
368
  return f"❌ Exception: {str(e)}", "{}", ""
369
 
370
  # Gradio interface
371
- def query_interface(query: str, mode: str):
372
  """Sync wrapper for async query processing"""
373
  loop = asyncio.new_event_loop()
374
  asyncio.set_event_loop(loop)
375
  try:
376
- return loop.run_until_complete(process_query(query, mode))
377
  finally:
378
  loop.close()
379
 
380
  # API endpoint for external calls
381
- def api_query(query: str, mode: str = "local", book_id: str = None):
382
  """API endpoint that returns JSON response"""
383
  loop = asyncio.new_event_loop()
384
  asyncio.set_event_loop(loop)
385
  try:
386
- result = loop.run_until_complete(borges_rag.query_book(query, mode))
387
  return result
388
  finally:
389
  loop.close()
390
 
391
- # Diagnostic function
392
- def diagnostic_info():
393
- """Return diagnostic information about the system"""
394
- diagnostic_data = {
395
- "nano_graphrag_available": NANO_GRAPHRAG_AVAILABLE,
396
- "available_books": available_books,
397
- "current_book": borges_rag.current_book,
398
- "working_directory": os.getcwd(),
399
- "directory_contents": [f for f in os.listdir('.') if os.path.isdir(f)],
400
- "book_status": book_status,
401
- "openai_api_key_configured": bool(os.getenv("OPENAI_API_KEY")),
402
- "environment_variables": {k: "***" if "api" in k.lower() or "key" in k.lower() else v
403
- for k, v in os.environ.items() if k.startswith(("OPENAI", "HF", "GRADIO"))},
404
- }
405
 
406
- # Add book instance info if available
407
- if NANO_GRAPHRAG_AVAILABLE and borges_rag.current_book:
408
- try:
409
- book_instance = borges_rag.instances.get(borges_rag.current_book)
410
- diagnostic_data["book_instance_loaded"] = book_instance is not None
411
- if book_instance:
412
- diagnostic_data["book_working_dir"] = getattr(book_instance, 'working_dir', 'Unknown')
413
- except Exception as e:
414
- diagnostic_data["book_instance_error"] = str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
- return diagnostic_data
 
 
 
 
 
 
 
 
 
 
 
417
 
418
  # Gradio app
419
  with gr.Blocks(
@@ -441,74 +428,105 @@ with gr.Blocks(
441
 
442
  gr.Markdown(f"**Statut:** {book_status}")
443
 
444
- with gr.Row():
445
- with gr.Column(scale=2):
446
- query_input = gr.Textbox(
447
- label="🔍 Votre question",
448
- placeholder="Quels sont les thèmes principaux de cette œuvre ?",
449
- lines=2
450
- )
451
-
452
- mode_select = gr.Radio(
453
- choices=["Local", "Global"],
454
- value="Local",
455
- label="Mode de recherche",
456
- info="Local: recherche focalisée | Global: vue d'ensemble"
457
- )
458
-
459
- search_btn = gr.Button("🚀 Explorer le graphe", variant="primary")
460
-
461
- with gr.Column(scale=1):
462
- gr.Markdown("""
463
- ### 💡 Questions suggérées:
464
- - Quels sont les thèmes principaux ?
465
- - Parle-moi des personnages
466
- - Quelle est la structure narrative ?
467
- - Comment les concepts sont-ils liés ?
468
- """)
469
-
470
- with gr.Row():
471
- with gr.Column():
472
- answer_output = gr.Markdown(label="📖 Réponse")
473
- summary_output = gr.Markdown(label="📊 Résumé de l'analyse")
474
 
475
- with gr.Accordion("🔧 Réponse JSON (pour développeurs)", open=False):
476
- json_output = gr.Code(language="json", label="JSON Response")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
- with gr.Accordion("🔍 Diagnostic système", open=False):
479
- diag_btn = gr.Button("Obtenir diagnostic")
480
- diag_output = gr.Code(language="json", label="Diagnostic Info")
 
 
 
481
 
482
  # Event handlers
483
  search_btn.click(
484
  fn=query_interface,
485
- inputs=[query_input, mode_select],
486
  outputs=[answer_output, json_output, summary_output]
487
  )
488
 
489
  query_input.submit(
490
  fn=query_interface,
491
- inputs=[query_input, mode_select],
492
  outputs=[answer_output, json_output, summary_output]
493
  )
494
 
495
- diag_btn.click(
496
- fn=lambda: json.dumps(diagnostic_info(), indent=2, ensure_ascii=False),
497
- outputs=[diag_output]
 
498
  )
499
 
500
- # Add standalone diagnostic endpoint for API access
501
- def get_diagnostic():
502
- """Standalone diagnostic function for API"""
503
- return json.dumps(diagnostic_info(), indent=2, ensure_ascii=False)
504
-
505
- # Note: The diagnostic function is available in the main interface
506
 
507
  # Launch the app
508
  if __name__ == "__main__":
509
- print("🚀 Starting Borges Graph Explorer...")
510
- print(f"📚 Books available: {len(available_books)}")
511
- print(f"📖 Current book: {borges_rag.current_book}")
512
  app.launch(
513
  server_name="0.0.0.0",
514
  server_port=7860,
 
7
  from typing import Dict, Any, List
8
  import tempfile
9
  import shutil
10
+ import zipfile
11
+ import requests
12
+
13
+ # Try to import nano_graphrag, with fallback for demo
14
+ try:
15
+ from nano_graphrag import GraphRAG, QueryParam
16
+ from nano_graphrag._llm import gpt_4o_mini_complete
17
+ NANO_GRAPHRAG_AVAILABLE = True
18
+ except ImportError:
19
+ NANO_GRAPHRAG_AVAILABLE = False
20
+ print("⚠️ nano-graphrag not available, running in demo mode")
21
+
22
+ # Configuration pour l'API externe
23
+ BORGES_API_URL = os.getenv("BORGES_API_URL", "https://borges-library.vercel.app/api/graphrag")
24
+ ENABLE_EXTERNAL_API = os.getenv("ENABLE_EXTERNAL_API", "false").lower() == "true"
25
 
26
  class BorgesGraphRAG:
27
  def __init__(self):
 
31
  def load_book_data(self, book_folder: str):
32
  """Load GraphRAG data for a specific book"""
33
  if not NANO_GRAPHRAG_AVAILABLE:
 
34
  return False
35
 
36
  try:
 
37
  if book_folder not in self.instances:
38
  self.instances[book_folder] = GraphRAG(
39
  working_dir=book_folder,
 
42
  best_model_max_async=3,
43
  cheap_model_max_async=3
44
  )
 
 
 
 
45
  self.current_book = book_folder
46
  return True
47
  except Exception as e:
48
+ print(f"Error loading book data: {e}")
49
+ return False
 
 
 
 
 
 
 
 
 
50
 
51
  def parse_context_csv(self, context_str: str):
52
  """Parse the CSV context returned by GraphRAG"""
 
84
 
85
  return entities, relations
86
 
87
+ async def query_external_api(self, query: str, mode: str = "local") -> Dict[str, Any]:
88
+ """Query external Borges API"""
 
 
 
 
89
  try:
90
+ payload = {
91
+ "query": query,
92
+ "mode": mode
93
+ }
 
 
 
 
 
 
94
 
95
+ response = requests.post(
96
+ f"{BORGES_API_URL}/search",
97
+ json=payload,
98
+ timeout=30
99
+ )
100
 
101
+ if response.status_code == 200:
102
+ return response.json()
103
+ else:
104
  return {
105
+ "success": False,
106
+ "error": f"API error: {response.status_code}",
107
+ "fallback": self.get_demo_response(query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
 
 
 
109
 
110
  except Exception as e:
111
+ return {
112
+ "success": False,
113
+ "error": f"Connection error: {str(e)}",
114
+ "fallback": self.get_demo_response(query)
115
+ }
116
 
117
+ async def query_book(self, query: str, mode: str = "local", use_external: bool = False) -> Dict[str, Any]:
118
+ """Query the current book with GraphRAG or external API"""
119
+
120
+ # Use external API if enabled and requested
121
+ if use_external and ENABLE_EXTERNAL_API:
122
+ return await self.query_external_api(query, mode)
123
+
124
+ if not NANO_GRAPHRAG_AVAILABLE or not self.current_book:
125
  return self.get_demo_response(query)
126
 
127
  try:
128
+ graph_instance = self.instances[self.current_book]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ # Get context with details
131
+ context_param = QueryParam(mode=mode, only_need_context=True, top_k=20)
132
+ context = await graph_instance.aquery(query, param=context_param)
133
+
134
+ # Get actual answer
135
+ answer_param = QueryParam(mode=mode, top_k=20)
136
+ answer = await graph_instance.aquery(query, param=answer_param)
137
+
138
+ # Parse context
139
+ entities, relations = self.parse_context_csv(context)
140
 
141
  return {
142
  "success": True,
143
  "answer": answer,
144
  "searchPath": {
145
+ "entities": [
146
+ {**e, "order": i+1, "score": 1.0 - (i * 0.05)}
147
+ for i, e in enumerate(entities[:15])
148
+ ],
149
+ "relations": [
150
+ {**r, "traversalOrder": i+1}
151
+ for i, r in enumerate(relations[:20])
152
+ ],
153
  "communities": [
154
+ {"id": "community_1", "content": "Cluster thématique principal", "relevance": 0.9}
155
  ]
156
  },
157
  "book_id": self.current_book,
158
+ "mode": mode,
159
  "query": query
160
  }
161
 
162
  except Exception as e:
163
+ return {
164
+ "success": False,
165
+ "error": str(e),
166
+ "fallback": self.get_demo_response(query)
167
+ }
168
 
169
  def get_demo_response(self, query: str) -> Dict[str, Any]:
170
  """Demo response when GraphRAG is not available"""
 
236
  # Check for available book data
237
  available_books = []
238
  for item in os.listdir('.'):
239
+ if os.path.isdir(item) and not item.startswith('.'):
240
  graph_file = os.path.join(item, 'graph_chunk_entity_relation.graphml')
241
  if os.path.exists(graph_file):
242
  available_books.append(item)
 
 
 
 
243
 
244
  if available_books:
245
  default_book = available_books[0]
 
246
  borges_rag.load_book_data(default_book)
247
  book_status = f"✅ Livre chargé: {default_book}"
248
  else:
 
249
  book_status = "⚠️ Mode démo - Aucune donnée GraphRAG trouvée"
250
 
251
+ async def process_query(query: str, mode: str, use_external: bool = False) -> tuple:
252
  """Process a query and return formatted results"""
253
  if not query.strip():
254
  return "❌ Veuillez entrer une question", "{}", ""
255
 
256
  try:
257
+ result = await borges_rag.query_book(query, mode.lower(), use_external)
258
 
259
  if result.get("success"):
260
  # Format the answer
 
265
  entities_count = len(search_info["entities"])
266
  relations_count = len(search_info["relations"])
267
 
268
+ # Source info
269
+ source = "API Borges" if use_external else "Local"
270
+
271
  # Create summary
272
  summary = f"""
273
  📊 **Analyse de la traversée du graphe:**
274
  • {entities_count} entités identifiées
275
  • {relations_count} relations explorées
276
  • Mode: {result.get('mode', 'demo')}
277
+ • Source: {source}
278
  • Livre: {result.get('book_id', 'demo')}
279
  """
280
 
 
284
  return answer, json_result, summary
285
  else:
286
  error_msg = result.get("error", "Erreur inconnue")
287
+ fallback = result.get("fallback")
288
+
289
+ if fallback and fallback.get("success"):
290
+ answer = f"⚠️ Mode de secours activé:\n\n{fallback['answer']}"
291
+ json_result = json.dumps(fallback, indent=2, ensure_ascii=False)
292
+ summary = "📊 **Mode démo activé (erreur de connexion)**"
293
+ return answer, json_result, summary
294
+ else:
295
+ return f"❌ Erreur: {error_msg}", "{}", ""
296
 
297
  except Exception as e:
298
  return f"❌ Exception: {str(e)}", "{}", ""
299
 
300
  # Gradio interface
301
+ def query_interface(query: str, mode: str, use_external: bool = False):
302
  """Sync wrapper for async query processing"""
303
  loop = asyncio.new_event_loop()
304
  asyncio.set_event_loop(loop)
305
  try:
306
+ return loop.run_until_complete(process_query(query, mode, use_external))
307
  finally:
308
  loop.close()
309
 
310
  # API endpoint for external calls
311
+ def api_query(query: str, mode: str = "local", use_external: bool = False):
312
  """API endpoint that returns JSON response"""
313
  loop = asyncio.new_event_loop()
314
  asyncio.set_event_loop(loop)
315
  try:
316
+ result = loop.run_until_complete(borges_rag.query_book(query, mode, use_external))
317
  return result
318
  finally:
319
  loop.close()
320
 
321
+ def upload_and_process_book(file_obj):
322
+ """Handle book upload and processing"""
323
+ if file_obj is None:
324
+ return "❌ Aucun fichier sélectionné", []
 
 
 
 
 
 
 
 
 
 
325
 
326
+ try:
327
+ # Create temp directory for processing
328
+ temp_dir = tempfile.mkdtemp(prefix="borges_book_")
329
+ file_path = os.path.join(temp_dir, file_obj.name)
330
+
331
+ # Save uploaded file
332
+ with open(file_path, 'wb') as f:
333
+ f.write(file_obj.read())
334
+
335
+ if file_obj.name.endswith('.zip'):
336
+ # Handle ZIP file with GraphRAG data
337
+ with zipfile.ZipFile(file_path, 'r') as zip_ref:
338
+ zip_ref.extractall(temp_dir)
339
+
340
+ # Look for GraphRAG data
341
+ graphml_files = []
342
+ for root, dirs, files in os.walk(temp_dir):
343
+ for file in files:
344
+ if file.endswith('.graphml'):
345
+ graphml_files.append(os.path.join(root, file))
346
+
347
+ if graphml_files:
348
+ # Use first graphml directory as working directory
349
+ working_dir = os.path.dirname(graphml_files[0])
350
+ book_id = os.path.basename(working_dir)
351
+
352
+ # Load the book data
353
+ if borges_rag.load_book_data(working_dir):
354
+ available_books.append(book_id)
355
+ return f"✅ Livre '{book_id}' chargé avec succès!", [book_id] + available_books
356
+ else:
357
+ return "❌ Erreur lors du chargement des données GraphRAG", available_books
358
+ else:
359
+ return "❌ Aucune donnée GraphRAG trouvée dans le fichier ZIP", available_books
360
+
361
+ elif file_obj.name.endswith('.txt'):
362
+ # Handle text file - create new GraphRAG instance
363
+ if not NANO_GRAPHRAG_AVAILABLE:
364
+ return "❌ nano-graphrag non disponible pour traiter les fichiers texte", available_books
365
+
366
+ book_id = Path(file_obj.name).stem
367
+ working_dir = os.path.join(temp_dir, book_id)
368
+ os.makedirs(working_dir, exist_ok=True)
369
+
370
+ # Create GraphRAG instance
371
+ graph_instance = GraphRAG(
372
+ working_dir=working_dir,
373
+ best_model_func=gpt_4o_mini_complete,
374
+ cheap_model_func=gpt_4o_mini_complete,
375
+ best_model_max_async=3,
376
+ cheap_model_max_async=3
377
+ )
378
+
379
+ # Read and process text
380
+ with open(file_path, 'r', encoding='utf-8') as f:
381
+ content = f.read()
382
+
383
+ graph_instance.insert(content)
384
+
385
+ # Load the processed data
386
+ if borges_rag.load_book_data(working_dir):
387
+ available_books.append(book_id)
388
+ return f"✅ Livre '{book_id}' traité et chargé avec succès!", [book_id] + available_books
389
+ else:
390
+ return "❌ Erreur lors du traitement du fichier texte", available_books
391
 
392
+ else:
393
+ return "❌ Format de fichier non supporté. Utilisez .txt ou .zip", available_books
394
+
395
+ except Exception as e:
396
+ return f"❌ Erreur lors du traitement: {str(e)}", available_books
397
+
398
+ def switch_book(book_id: str):
399
+ """Switch to a different book"""
400
+ if book_id and borges_rag.load_book_data(book_id):
401
+ return f"✅ Livre '{book_id}' activé"
402
+ else:
403
+ return f"❌ Impossible de charger le livre '{book_id}'"
404
 
405
  # Gradio app
406
  with gr.Blocks(
 
428
 
429
  gr.Markdown(f"**Statut:** {book_status}")
430
 
431
+ with gr.Tab("🔍 Recherche"):
432
+ with gr.Row():
433
+ with gr.Column(scale=2):
434
+ query_input = gr.Textbox(
435
+ label="🔍 Votre question",
436
+ placeholder="Quels sont les thèmes principaux de cette œuvre ?",
437
+ lines=2
438
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
+ with gr.Row():
441
+ mode_select = gr.Radio(
442
+ choices=["Local", "Global"],
443
+ value="Local",
444
+ label="Mode de recherche",
445
+ info="Local: recherche focalisée | Global: vue d'ensemble"
446
+ )
447
+
448
+ external_api_checkbox = gr.Checkbox(
449
+ label="🌐 Utiliser l'API Borges",
450
+ value=False,
451
+ visible=ENABLE_EXTERNAL_API,
452
+ info="Interroger directement l'API Borges en ligne"
453
+ )
454
+
455
+ search_btn = gr.Button("🚀 Explorer le graphe", variant="primary")
456
+
457
+ with gr.Column(scale=1):
458
+ gr.Markdown("""
459
+ ### 💡 Questions suggérées:
460
+ - Quels sont les thèmes principaux ?
461
+ - Parle-moi des personnages
462
+ - Quelle est la structure narrative ?
463
+ - Comment les concepts sont-ils liés ?
464
+ """)
465
+
466
+ with gr.Row():
467
+ with gr.Column():
468
+ answer_output = gr.Markdown(label="📖 Réponse")
469
+ summary_output = gr.Markdown(label="📊 Résumé de l'analyse")
470
+
471
+ with gr.Accordion("🔧 Réponse JSON (pour développeurs)", open=False):
472
+ json_output = gr.Code(language="json", label="JSON Response")
473
+
474
+ with gr.Tab("📚 Gestion des livres"):
475
+ with gr.Row():
476
+ with gr.Column():
477
+ gr.Markdown("### 📥 Uploader un nouveau livre")
478
+ file_upload = gr.File(
479
+ label="Sélectionner un fichier",
480
+ file_types=[".txt", ".zip"],
481
+ file_count="single"
482
+ )
483
+ upload_btn = gr.Button("📤 Traiter le fichier", variant="secondary")
484
+ upload_status = gr.Markdown("ℹ️ Aucun fichier sélectionné")
485
+
486
+ with gr.Column():
487
+ gr.Markdown("### 🔄 Changer de livre")
488
+ book_dropdown = gr.Dropdown(
489
+ choices=available_books,
490
+ label="Livres disponibles",
491
+ value=available_books[0] if available_books else None
492
+ )
493
+ switch_btn = gr.Button("🔄 Activer ce livre", variant="secondary")
494
+ switch_status = gr.Markdown("")
495
 
496
+ gr.Markdown("""
497
+ ### 📋 Instructions:
498
+ - **Fichiers .txt**: Uploadez un texte brut qui sera traité par GraphRAG
499
+ - **Fichiers .zip**: Uploadez des données GraphRAG pré-traitées (dossier avec .graphml)
500
+ - L'API Borges permet d'interroger directement votre application Vercel
501
+ """)
502
 
503
  # Event handlers
504
  search_btn.click(
505
  fn=query_interface,
506
+ inputs=[query_input, mode_select, external_api_checkbox],
507
  outputs=[answer_output, json_output, summary_output]
508
  )
509
 
510
  query_input.submit(
511
  fn=query_interface,
512
+ inputs=[query_input, mode_select, external_api_checkbox],
513
  outputs=[answer_output, json_output, summary_output]
514
  )
515
 
516
+ upload_btn.click(
517
+ fn=upload_and_process_book,
518
+ inputs=[file_upload],
519
+ outputs=[upload_status, book_dropdown]
520
  )
521
 
522
+ switch_btn.click(
523
+ fn=switch_book,
524
+ inputs=[book_dropdown],
525
+ outputs=[switch_status]
526
+ )
 
527
 
528
  # Launch the app
529
  if __name__ == "__main__":
 
 
 
530
  app.launch(
531
  server_name="0.0.0.0",
532
  server_port=7860,
requirements.txt CHANGED
@@ -1,5 +1,8 @@
1
  gradio>=4.0.0
 
2
  openai>=1.0.0
3
- python-dotenv
4
- pandas
5
- aiohttp>=3.8.0
 
 
 
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
8
+ requests>=2.25.0