adrienbrdne commited on
Commit
fbf2452
·
verified ·
1 Parent(s): 8ac0b12

Update api.py

Browse files
Files changed (1) hide show
  1. api.py +246 -255
api.py CHANGED
@@ -1,255 +1,246 @@
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 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)
 
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 logging module
8
+
9
+ # --- Logging Configuration ---
10
+ # Basic logger configuration to display INFO messages and above.
11
+ # The format includes timestamp, log level, and message.
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
15
+ handlers=[
16
+ logging.StreamHandler() # Display logs in the console (stderr by default)
17
+ # You could add a logging.FileHandler("app.log") here to write to a file
18
+ ]
19
+ )
20
+ logger = logging.getLogger(__name__) # Create a logger instance for this module
21
+
22
+ # --- Environment Variable Configuration ---
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 of essential configurations
29
+ if not NEO4J_URI or not NEO4J_USER or not NEO4J_PASSWORD:
30
+ logger.critical("CRITICAL ERROR: NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD environment variables must be set.")
31
+ # In a real application, you might want to exit or prevent FastAPI from starting.
32
+ # For now, we let the application try and fail at runtime if they are missing.
33
+
34
+ # Initialize FastAPI application
35
+ app = FastAPI(
36
+ title="Arxiv to Neo4j Importer",
37
+ description="API to fetch research paper data from Arxiv, summarize it with Gemini, and add it to Neo4j.",
38
+ version="1.0.0"
39
+ )
40
+
41
+ # --- Gemini API Client Initialization ---
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") # Specified model
47
+ logger.info("Gemini API client initialized successfully.")
48
+ except Exception as e:
49
+ logger.warning(f"WARNING: Failed to initialize Gemini API client: {e}. Summary generation will be affected.")
50
+ else:
51
+ logger.warning("WARNING: GEMINI_API_KEY environment variable not set. Summary generation will be disabled.")
52
+
53
+ # --- Utility Functions (Adapted from your script) ---
54
+
55
+ def get_content(number: str, node_type: str) -> str:
56
+ """Fetches raw HTML content from Arxiv or other 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"Unknown node type: {node_type} for number {number}")
65
+ return ""
66
+
67
+ try:
68
+ response = requests.get(url, timeout=10) # Added a timeout
69
+ response.raise_for_status() # Raises HTTPError for bad responses (4XX or 5XX)
70
+ return response.content.decode('utf-8', errors='replace').replace("\n", "")
71
+ except requests.exceptions.RequestException as e:
72
+ logger.error(f"Request error for {node_type} number: {number} at URL {url}: {e}")
73
+ return ""
74
+ except Exception as e:
75
+ logger.error(f"An unexpected error occurred in get_content for {number}: {e}")
76
+ return ""
77
+
78
+ def extract_research_paper_arxiv(rp_number: str, node_type: str) -> dict:
79
+ """Extracts information from an Arxiv research paper and generates a summary."""
80
+ raw_content = get_content(rp_number, node_type)
81
+
82
+ rp_data = {
83
+ "document": f"Arxiv {rp_number}", # ID for the paper
84
+ "arxiv_id": rp_number,
85
+ "title": "Error fetching content or content not found",
86
+ "abstract": "Error fetching content or content not found",
87
+ "summary": "Summary not generated" # Default summary
88
+ }
89
+
90
+ if not raw_content:
91
+ logger.warning(f"No content fetched for Arxiv ID: {rp_number}")
92
+ return rp_data # Returns default error data
93
+
94
+ try:
95
+ soup = BeautifulSoup(raw_content, 'html.parser')
96
+
97
+ # Extract Title
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 if the span descriptor is not there but h1.title exists
106
+ rp_data["title"] = title_tag.get_text(separator=" ", strip=True).replace("Title:", "").strip()
107
+
108
+
109
+ # Extract 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'): # Check if "abstract" (case-insensitive) is at the beginning
114
+ # Find the first occurrence of ':' after "abstract" or just remove "abstract" prefix
115
+ prefix_end = abstract_text.lower().find('abstract') + len('abstract')
116
+ if prefix_end < len(abstract_text) and abstract_text[prefix_end] == ':':
117
+ prefix_end += 1 # Include the colon in removal
118
+ abstract_text = abstract_text[prefix_end:].strip()
119
+
120
+ rp_data["abstract"] = abstract_text
121
+
122
+ # Mark if title or abstract are still not found
123
+ if rp_data["title"] == "Error fetching content or content not found" and not title_tag:
124
+ rp_data["title"] = "Title not found on page"
125
+ if rp_data["abstract"] == "Error fetching content or content not found" and not abstract_tag:
126
+ rp_data["abstract"] = "Abstract not found on page"
127
+
128
+ # Generate summary with Gemini API if available and abstract exists
129
+ if gemini_model and rp_data["abstract"] and \
130
+ not rp_data["abstract"].startswith("Error fetching content") and \
131
+ not rp_data["abstract"].startswith("Abstract not found"):
132
+ # English prompt for Gemini
133
+ prompt = f"""You are a 3GPP standardization expert. Summarize the key information in the provided document in simple technical English relevant to identifying potential Key Issues.
134
+ Focus on challenges, gaps, or novel aspects.
135
+ Here is the document: <document>{rp_data['abstract']}<document>"""
136
+
137
+ try:
138
+ response = gemini_model.generate_content(prompt)
139
+ rp_data["summary"] = response.text
140
+ logger.info(f"Summary generated for Arxiv ID: {rp_number}")
141
+ except Exception as e:
142
+ logger.error(f"Error generating summary with Gemini for Arxiv ID {rp_number}: {e}")
143
+ rp_data["summary"] = "Error generating summary (API failure)"
144
+ elif not gemini_model:
145
+ rp_data["summary"] = "Summary not generated (Gemini API client not available)"
146
+ else:
147
+ rp_data["summary"] = "Summary not generated (Abstract unavailable or problematic)"
148
+
149
+ except Exception as e:
150
+ logger.error(f"Error parsing content for Arxiv ID {rp_number}: {e}")
151
+
152
+ return rp_data
153
+
154
+ def add_nodes_to_neo4j(driver, data_list: list, node_label: str):
155
+ """Adds a list of nodes to Neo4j in a single transaction."""
156
+ if not data_list:
157
+ logger.warning("No data provided to add_nodes_to_neo4j.")
158
+ return 0
159
+
160
+ query = (
161
+ f"UNWIND $data as properties "
162
+ f"MERGE (n:{node_label} {{arxiv_id: properties.arxiv_id}}) " # Use MERGE for idempotency
163
+ f"ON CREATE SET n = properties "
164
+ f"ON MATCH SET n += properties" # Update properties if the node already exists
165
+ )
166
+
167
+ try:
168
+ with driver.session(database="neo4j") as session: # Specify database if not default
169
+ result = session.execute_write(lambda tx: tx.run(query, data=data_list).consume())
170
+ nodes_created = result.counters.nodes_created
171
+
172
+ if nodes_created > 0:
173
+ logger.info(f"{nodes_created} new {node_label} node(s) added successfully.")
174
+
175
+ summary = result.summary
176
+ logger.info(f"MERGE operation for {node_label}: {summary.counters.nodes_created} created, {summary.counters.properties_set} properties affected.")
177
+
178
+ return nodes_created # Return the number of nodes actually created
179
+ except Exception as e:
180
+ logger.error(f"Neo4j Error - Failed to add/update {node_label} nodes: {e}")
181
+ raise HTTPException(status_code=500, detail=f"Neo4j database error: {e}")
182
+
183
+
184
+ # --- FastAPI Endpoint ---
185
+
186
+ @app.post("/add_research_paper/{arxiv_id}", status_code=201) # 201 Created for successful creation
187
+ async def add_single_research_paper(arxiv_id: str):
188
+ """
189
+ Fetches a research paper from Arxiv by its ID, extracts information,
190
+ generates a summary, and adds/updates it as a 'ResearchPaper' node in Neo4j.
191
+ """
192
+ node_type = "ResearchPaper"
193
+ logger.info(f"Processing request for Arxiv ID: {arxiv_id}")
194
+
195
+ if not NEO4J_URI or not NEO4J_USER or not NEO4J_PASSWORD:
196
+ logger.error("Neo4j database connection details are not configured on the server.")
197
+ raise HTTPException(status_code=500, detail="Neo4j database connection details are not configured on the server.")
198
+
199
+ # Step 1: Extract paper data
200
+ paper_data = extract_research_paper_arxiv(arxiv_id, node_type)
201
+
202
+ if paper_data["title"].startswith("Error fetching content") or paper_data["title"] == "Title not found on page":
203
+ logger.warning(f"Could not fetch or parse content for Arxiv ID {arxiv_id}. Title: {paper_data['title']}")
204
+ raise HTTPException(status_code=404, detail=f"Could not fetch or parse content for Arxiv ID {arxiv_id}. Title: {paper_data['title']}")
205
+
206
+ # Step 2: Add/Update in Neo4j
207
+ driver_instance = None # Initialize for the finally block
208
+ try:
209
+ auth_token = basic_auth(NEO4J_USER, NEO4J_PASSWORD)
210
+ driver_instance = GraphDatabase.driver(NEO4J_URI, auth=auth_token)
211
+ driver_instance.verify_connectivity()
212
+ logger.info("Successfully connected to Neo4j.")
213
+
214
+ nodes_created_count = add_nodes_to_neo4j(driver_instance, [paper_data], node_type)
215
+
216
+ if nodes_created_count > 0 :
217
+ message = f"Research paper {arxiv_id} was successfully added to Neo4j."
218
+ status_code_response = 201 # Created
219
+ else:
220
+ # If MERGE found an existing node and updated it, nodes_created_count will be 0.
221
+ # This is considered a success (idempotency).
222
+ message = f"Research paper {arxiv_id} was processed (potentially updated if it already existed)."
223
+ status_code_response = 200 # OK (because no new creation, but operation successful)
224
+
225
+ logger.info(message)
226
+ # Note: FastAPI uses the status_code from the decorator or HTTPException.
227
+ # This custom status_code_response is for the JSON body if needed, but the actual HTTP response status
228
+ # will be 201 (from decorator) unless an HTTPException overrides it or we change the decorator based on logic.
229
+ # For simplicity here, we'll return it in the body and let the decorator's 201 stand if no error.
230
+ # A more advanced setup might change the response status dynamically.
231
+ return {
232
+ "message": message,
233
+ "data": paper_data,
234
+ "response_status_info": status_code_response
235
+ }
236
+
237
+ except HTTPException as e: # Re-raise HTTPExceptions
238
+ logger.error(f"HTTPException during Neo4j operation for {arxiv_id}: {e.detail}")
239
+ raise e
240
+ except Exception as e:
241
+ logger.error(f"An unexpected error occurred during Neo4j operation for {arxiv_id}: {e}", exc_info=True)
242
+ raise HTTPException(status_code=500, detail=f"An unexpected server error occurred: {e}")
243
+ finally:
244
+ if driver_instance:
245
+ driver_instance.close()
246
+ logger.info("Neo4j connection closed.")