Spaces:
Sleeping
Sleeping
Add Borges Graph app with GraphRAG data
Browse files- README.md +61 -5
- a_rebours_huysmans/graph_chunk_entity_relation.graphml +0 -0
- a_rebours_huysmans/kv_store_community_reports.json +0 -0
- a_rebours_huysmans/kv_store_full_docs.json +0 -0
- a_rebours_huysmans/kv_store_llm_response_cache.json +0 -0
- a_rebours_huysmans/kv_store_text_chunks.json +0 -0
- app.py +347 -0
- requirements.txt +7 -0
README.md
CHANGED
|
@@ -1,13 +1,69 @@
|
|
| 1 |
---
|
| 2 |
title: Borges Graph
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: yellow
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|