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)