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