adrienbrdne commited on
Commit
8e68e82
·
verified ·
1 Parent(s): b3ae5fb

Upload api.py

Browse files
Files changed (1) hide show
  1. api.py +255 -0
api.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ from fastapi import FastAPI, HTTPException
5
+ from neo4j import GraphDatabase, basic_auth
6
+ import google.generativeai as genai
7
+ import logging # Import du module logging
8
+
9
+ # --- Configuration du Logging ---
10
+ # Configuration de base du logger pour afficher les messages INFO et supérieurs.
11
+ # Le format inclut le timestamp, le niveau du log, et le message.
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
15
+ handlers=[
16
+ logging.StreamHandler() # Affichage des logs dans la console (stderr par défaut)
17
+ # Vous pourriez ajouter ici un logging.FileHandler("app.log") pour écrire dans un fichier
18
+ ]
19
+ )
20
+ logger = logging.getLogger(__name__) # Création d'une instance de logger pour ce module
21
+
22
+ # --- Configuration des variables d'environnement ---
23
+ NEO4J_URI = os.getenv("NEO4J_URI")
24
+ NEO4J_USER = os.getenv("NEO4J_USER")
25
+ NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
26
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
27
+
28
+ # Validation des configurations essentielles
29
+ if not NEO4J_URI or not NEO4J_USER or not NEO4J_PASSWORD:
30
+ logger.critical("ERREUR CRITIQUE: Les variables d'environnement NEO4J_URI, NEO4J_USER, et NEO4J_PASSWORD doivent être définies.")
31
+ # Dans une application réelle, vous pourriez vouloir quitter ou empêcher FastAPI de démarrer.
32
+ # Pour l'instant, nous laissons l'application essayer et échouer lors de l'exécution si elles manquent.
33
+
34
+ # Initialisation de l'application FastAPI
35
+ app = FastAPI(
36
+ title="Arxiv to Neo4j Importer",
37
+ description="API pour récupérer les données d'articles de recherche depuis Arxiv, les résumer avec Gemini, et les ajouter à Neo4j.",
38
+ version="1.0.0"
39
+ )
40
+
41
+ # --- Initialisation du client API Gemini ---
42
+ gemini_model = None
43
+ if GEMINI_API_KEY:
44
+ try:
45
+ genai.configure(api_key=GEMINI_API_KEY)
46
+ gemini_model = genai.GenerativeModel(model_name="gemini-2.5-flash-preview-05-20") # Modèle spécifié
47
+ logger.info("Client API Gemini initialisé avec succès.")
48
+ except Exception as e:
49
+ logger.warning(f"AVERTISSEMENT: Échec de l'initialisation du client API Gemini: {e}. La génération de résumés sera affectée.")
50
+ else:
51
+ logger.warning("AVERTISSEMENT: La variable d'environnement GEMINI_API_KEY n'est pas définie. La génération de résumés sera désactivée.")
52
+
53
+ # --- Fonctions Utilitaires (Adaptées de votre script) ---
54
+
55
+ def get_content(number: str, node_type: str) -> str:
56
+ """Récupère le contenu HTML brut depuis Arxiv ou d'autres sources."""
57
+ redirect_links = {
58
+ "Patent": f"https://patents.google.com/patent/{number}/en",
59
+ "ResearchPaper": f"https://arxiv.org/abs/{number}"
60
+ }
61
+
62
+ url = redirect_links.get(node_type)
63
+ if not url:
64
+ logger.warning(f"Type de noeud inconnu: {node_type} pour le numéro {number}")
65
+ return ""
66
+
67
+ try:
68
+ response = requests.get(url, timeout=10) # Ajout d'un timeout
69
+ response.raise_for_status() # Lève une HTTPError pour les mauvaises réponses (4XX ou 5XX)
70
+ return response.content.decode('utf-8', errors='replace').replace("\n", "")
71
+ except requests.exceptions.RequestException as e:
72
+ logger.error(f"Erreur de requête pour {node_type} numéro: {number} à l'URL {url}: {e}")
73
+ return ""
74
+ except Exception as e:
75
+ logger.error(f"Une erreur inattendue est survenue dans get_content pour {number}: {e}")
76
+ return ""
77
+
78
+ def extract_research_paper_arxiv(rp_number: str, node_type: str) -> dict:
79
+ """Extrait les informations d'un article de recherche Arxiv et génère un résumé."""
80
+ raw_content = get_content(rp_number, node_type)
81
+
82
+ rp_data = {
83
+ "document": f"Arxiv {rp_number}", # ID pour l'article
84
+ "arxiv_id": rp_number,
85
+ "title": "Erreur lors de la récupération du contenu ou contenu non trouvé",
86
+ "abstract": "Erreur lors de la récupération du contenu ou contenu non trouvé",
87
+ "summary": "Résumé non généré" # Résumé par défaut
88
+ }
89
+
90
+ if not raw_content:
91
+ logger.warning(f"Aucun contenu récupéré pour l'ID Arxiv: {rp_number}")
92
+ return rp_data # Retourne les données d'erreur par défaut
93
+
94
+ try:
95
+ soup = BeautifulSoup(raw_content, 'html.parser')
96
+
97
+ # Extraction du Titre
98
+ title_tag = soup.find('h1', class_='title')
99
+ if title_tag and title_tag.find('span', class_='descriptor'):
100
+ title_text_candidate = title_tag.find('span', class_='descriptor').next_sibling
101
+ if title_text_candidate and isinstance(title_text_candidate, str):
102
+ rp_data["title"] = title_text_candidate.strip()
103
+ else:
104
+ rp_data["title"] = title_tag.get_text(separator=" ", strip=True).replace("Title:", "").strip()
105
+ elif title_tag : # Fallback si le span descriptor n'est pas là mais h1.title existe
106
+ rp_data["title"] = title_tag.get_text(separator=" ", strip=True).replace("Title:", "").strip()
107
+
108
+
109
+ # Extraction de l'Abstract
110
+ abstract_tag = soup.find('blockquote', class_='abstract')
111
+ if abstract_tag:
112
+ abstract_text = abstract_tag.get_text(strip=True)
113
+ if abstract_text.lower().startswith('abstract'):
114
+ abstract_text = abstract_text[len('abstract'):].strip()
115
+ rp_data["abstract"] = abstract_text
116
+
117
+ # Marquer si le titre ou l'abstract ne sont toujours pas trouvés
118
+ if rp_data["title"] == "Erreur lors de la récupération du contenu ou contenu non trouvé" and not title_tag:
119
+ rp_data["title"] = "Titre non trouvé sur la page"
120
+ if rp_data["abstract"] == "Erreur lors de la récupération du contenu ou contenu non trouvé" and not abstract_tag:
121
+ rp_data["abstract"] = "Abstract non trouvé sur la page"
122
+
123
+ # Génération du résumé avec l'API Gemini si disponible et si l'abstract existe
124
+ if gemini_model and rp_data["abstract"] and \
125
+ not rp_data["abstract"].startswith("Erreur lors de la récupération du contenu") and \
126
+ not rp_data["abstract"].startswith("Abstract non trouvé"):
127
+ prompt = f"""Vous êtes un expert en standardisation 3GPP. Résumez les informations clés du document fourni en anglais technique simple, pertinent pour identifier les problèmes clés potentiels.
128
+ Concentrez-vous sur les défis, les lacunes ou les aspects nouveaux.
129
+ Voici le document: <document>{rp_data['abstract']}<document>"""
130
+
131
+ try:
132
+ response = gemini_model.generate_content(prompt)
133
+ rp_data["summary"] = response.text
134
+ logger.info(f"Résumé généré pour l'ID Arxiv: {rp_number}")
135
+ except Exception as e:
136
+ logger.error(f"Erreur lors de la génération du résumé avec Gemini pour l'ID Arxiv {rp_number}: {e}")
137
+ rp_data["summary"] = "Erreur lors de la génération du résumé (échec API)"
138
+ elif not gemini_model:
139
+ rp_data["summary"] = "Résumé non généré (client API Gemini non disponible)"
140
+ else:
141
+ rp_data["summary"] = "Résumé non généré (Abstract indisponible ou problématique)"
142
+
143
+ except Exception as e:
144
+ logger.error(f"Erreur lors de l'analyse du contenu pour l'ID Arxiv {rp_number}: {e}")
145
+
146
+ return rp_data
147
+
148
+ def add_nodes_to_neo4j(driver, data_list: list, node_label: str):
149
+ """Ajoute une liste de noeuds à Neo4j dans une seule transaction."""
150
+ if not data_list:
151
+ logger.warning("Aucune donnée fournie à add_nodes_to_neo4j.")
152
+ return 0
153
+
154
+ query = (
155
+ f"UNWIND $data as properties "
156
+ f"MERGE (n:{node_label} {{arxiv_id: properties.arxiv_id}}) " # Utilise MERGE pour l'idempotence
157
+ f"ON CREATE SET n = properties "
158
+ f"ON MATCH SET n += properties" # Met à jour les propriétés si le noeud existe déjà
159
+ )
160
+
161
+ try:
162
+ with driver.session(database="neo4j") as session: # Spécifier la base de données si non défaut
163
+ result = session.execute_write(lambda tx: tx.run(query, data=data_list).consume())
164
+ nodes_created = result.counters.nodes_created
165
+ nodes_updated = result.counters.properties_set - (nodes_created * len(data_list[0])) if data_list and nodes_created >=0 else result.counters.properties_set # Estimation
166
+
167
+ if nodes_created > 0:
168
+ logger.info(f"{nodes_created} nouveau(x) noeud(s) {node_label} ajouté(s) avec succès.")
169
+ # properties_set compte toutes les propriétés définies, y compris sur les noeuds créés.
170
+ # Pour les noeuds mis à jour, il faut une logique plus fine si on veut un compte exact des noeuds *juste* mis à jour.
171
+ # Le plus simple est de regarder si des propriétés ont été mises à jour au-delà de la création.
172
+ # Note: result.counters.properties_set compte le nombre total de propriétés définies ou mises à jour.
173
+ # Si un noeud est créé, toutes ses propriétés sont "set". Si un noeud est matché, les propriétés sont "set" via ON MATCH.
174
+ # Un compte plus précis des "noeuds mis à jour (non créés)" est plus complexe avec UNWIND et MERGE.
175
+ # On peut se contenter de savoir combien de noeuds ont été affectés au total.
176
+ summary = result.summary
177
+ affected_nodes = summary.counters.nodes_created + summary.counters.nodes_deleted # ou autre logique selon la requête
178
+ logger.info(f"Opération MERGE pour {node_label}: {summary.counters.nodes_created} créé(s), {summary.counters.properties_set} propriétés affectées.")
179
+
180
+ return nodes_created # Retourne le nombre de noeuds effectivement créés
181
+ except Exception as e:
182
+ logger.error(f"Erreur Neo4j - Échec de l'ajout/mise à jour des noeuds {node_label}: {e}")
183
+ raise HTTPException(status_code=500, detail=f"Erreur base de données Neo4j: {e}")
184
+
185
+
186
+ # --- Endpoint FastAPI ---
187
+
188
+ @app.post("/add_research_paper/{arxiv_id}", status_code=201) # 201 Created pour la création réussie
189
+ async def add_single_research_paper(arxiv_id: str):
190
+ """
191
+ Récupère un article de recherche d'Arxiv par son ID, extrait les informations,
192
+ génère un résumé, et l'ajoute/met à jour comme un noeud 'ResearchPaper' dans Neo4j.
193
+ """
194
+ node_type = "ResearchPaper"
195
+ logger.info(f"Traitement de la requête pour l'ID Arxiv: {arxiv_id}")
196
+
197
+ if not NEO4J_URI or not NEO4J_USER or not NEO4J_PASSWORD:
198
+ logger.error("Les détails de connexion à la base de données Neo4j ne sont pas configurés sur le serveur.")
199
+ raise HTTPException(status_code=500, detail="Les détails de connexion à la base de données Neo4j ne sont pas configurés sur le serveur.")
200
+
201
+ # Étape 1: Extraire les données de l'article
202
+ paper_data = extract_research_paper_arxiv(arxiv_id, node_type)
203
+
204
+ if paper_data["title"].startswith("Erreur lors de la récupération du contenu") or paper_data["title"] == "Titre non trouvé sur la page":
205
+ logger.warning(f"Impossible de récupérer ou d'analyser le contenu pour l'ID Arxiv {arxiv_id}. Titre: {paper_data['title']}")
206
+ raise HTTPException(status_code=404, detail=f"Impossible de récupérer ou d'analyser le contenu pour l'ID Arxiv {arxiv_id}. Titre: {paper_data['title']}")
207
+
208
+ # Étape 2: Ajouter/Mettre à jour dans Neo4j
209
+ driver = None # Initialisation pour le bloc finally
210
+ try:
211
+ auth_token = basic_auth(NEO4J_USER, NEO4J_PASSWORD)
212
+ driver = GraphDatabase.driver(NEO4J_URI, auth=auth_token)
213
+ driver.verify_connectivity()
214
+ logger.info("Connecté avec succès à Neo4j.")
215
+
216
+ nodes_created_count = add_nodes_to_neo4j(driver, [paper_data], node_type)
217
+
218
+ if nodes_created_count > 0 :
219
+ message = f"L'article de recherche {arxiv_id} a été ajouté avec succès à Neo4j."
220
+ status_code = 201 # Created
221
+ else:
222
+ # Si MERGE a trouvé un noeud existant et l'a mis à jour, nodes_created_count sera 0.
223
+ # On considère cela comme un succès (idempotence).
224
+ message = f"L'article de recherche {arxiv_id} a été traité (potentiellement mis à jour s'il existait déjà)."
225
+ status_code = 200 # OK (car pas de nouvelle création, mais opération réussie)
226
+
227
+ logger.info(message)
228
+ return {
229
+ "message": message,
230
+ "data": paper_data,
231
+ "status_code_override": status_code # Pour information, FastAPI utilisera le status_code de l'endpoint ou celui de l'HTTPException
232
+ }
233
+
234
+ except HTTPException as e: # Re-lever les HTTPExceptions
235
+ logger.error(f"HTTPException lors de l'opération Neo4j pour {arxiv_id}: {e.detail}")
236
+ raise e
237
+ except Exception as e:
238
+ logger.error(f"Une erreur inattendue est survenue lors de l'opération Neo4j pour {arxiv_id}: {e}", exc_info=True)
239
+ raise HTTPException(status_code=500, detail=f"Une erreur serveur inattendue est survenue: {e}")
240
+ finally:
241
+ if driver:
242
+ driver.close()
243
+ logger.info("Connexion Neo4j fermée.")
244
+
245
+
246
+ # --- Pour exécuter cette application (exemple avec uvicorn) ---
247
+ # 1. Sauvegardez ce code sous main.py
248
+ # 2. Définissez les variables d'environnement: NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD, GEMINI_API_KEY
249
+ # 3. Installez les dépendances: pip install fastapi uvicorn requests beautifulsoup4 neo4j google-generativeai python-dotenv
250
+ # (python-dotenv est utile pour charger les fichiers .env localement)
251
+ # 4. Exécutez avec Uvicorn: uvicorn main:app --reload
252
+ #
253
+ # Exemple d'utilisation avec curl après avoir démarré le serveur:
254
+ # curl -X POST http://127.0.0.1:8000/add_research_paper/2305.12345
255
+ # (Remplacez 2305.12345 par un ID Arxiv valide)