Dama12 commited on
Commit
3badc4e
·
1 Parent(s): c68925d

fix: Correct ArXiv/OpenAlex paper fetching in analysis_service

Browse files

- ArXiv: strip 'arxiv_' prefix correctly, attempt full PDF download
with PDF parser for richer content, fallback to abstract
- OpenAlex: use direct Work ID REST endpoint (/works/W...) instead of
search-by-ID (which returned irrelevant results)
- All sources: prefer full content over abstract for K2 analysis
- Fix missing remote_id DB lookup before triggering remote fetch
- Add proper logging for each step

Files changed (1) hide show
  1. app/services/analysis_service.py +108 -42
app/services/analysis_service.py CHANGED
@@ -20,7 +20,7 @@ class AnalysisService:
20
  project_id: UUID,
21
  model_used: str
22
  ):
23
- """Crée une nouvelle run d' d'analyse"""
24
  analysis = AnalysisRun(
25
  project_id=project_id,
26
  model_used=model_used,
@@ -43,7 +43,7 @@ class AnalysisService:
43
  analysis.status = status
44
  analysis.completed_at = datetime.utcnow()
45
  if result:
46
- analysis.result_data = result # SQLAlchemy handles JSON serialization
47
  self.analysis_repo.db.add(analysis)
48
  self.analysis_repo.db.commit()
49
  self.analysis_repo.db.refresh(analysis)
@@ -61,82 +61,150 @@ class AnalysisService:
61
  from app.services.doaj_service import DOAJService
62
  from app.services.pubmed_service import PubMedService
63
  from app.services.openalex_service import OpenAlexService
64
-
 
65
  db = self.analysis_repo.db
66
  try:
67
- # 1. Fetch papers from DB or Remote
68
  paper_repo = PaperRepository(db)
69
  analysis_run = self.analysis_repo.get_by_id(UUID(analysis_id))
70
  project_id = analysis_run.project_id
71
-
72
  docs = []
73
  for pid in request.paper_ids:
74
- # Try UUID search first
75
  paper = None
 
 
76
  try:
77
  paper_uuid = UUID(pid)
78
  paper = paper_repo.get_by_id(paper_uuid)
79
  except (ValueError, AttributeError):
80
- # Try Remote ID search in DB
 
 
 
81
  paper = db.query(ResearchPaper).filter(
82
  ResearchPaper.remote_id == pid,
83
  ResearchPaper.project_id == project_id
84
  ).first()
85
-
 
86
  if not paper:
87
- # FETCH FROM REMOTE AND SAVE
88
- logger.info(f"Paper {pid} not in DB. Fetching metadata on-demand...")
89
  remote_data = None
90
  try:
91
  if "doaj_" in pid:
92
- svc = DOAJService(); remote_data = svc.fetch_papers(pid.replace("doaj_", ""), 1)
 
 
93
  elif "pubmed_" in pid:
94
- svc = PubMedService(); remote_data = svc.fetch_papers(pid.replace("pubmed_", ""), 1)
 
 
95
  elif "openalex_" in pid:
96
- svc = OpenAlexService(); remote_data = svc.fetch_papers(pid.replace("openalex_", ""), 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  elif "arxiv_" in pid:
98
- svc = ArXivService(); remote_data = svc.fetch_papers(pid.replace("arxiv_", ""), 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  else:
100
- # Fallback to ArXiv if no prefix (for old saved papers without prefix)
101
- svc = ArXivService(); remote_data = svc.fetch_papers(pid, 1)
102
-
 
 
 
 
 
 
 
 
 
 
 
 
103
  if remote_data:
104
  p_info = remote_data[0]
105
- snippet = p_info.get("content", p_info.get("summary", ""))[:10000]
 
106
  paper = ResearchPaper(
107
  project_id=project_id,
108
  remote_id=pid,
109
  title=p_info.get("title", "Unknown"),
110
  authors=", ".join(p_info.get("authors", [])),
111
  summary=snippet,
112
- publication_year=datetime.now().year # Fallback
113
  )
114
  db.add(paper)
115
  db.commit()
116
  db.refresh(paper)
117
- logger.info(f"Saved remote paper {pid} to project {project_id}")
118
  else:
119
- logger.warning(f"No remote data found for {pid}")
 
120
  except Exception as fetch_err:
121
  logger.error(f"Failed to fetch/save remote paper {pid}: {fetch_err}")
122
 
 
123
  if paper:
124
- # Convert authors string to list
125
  author_list = [a.strip() for a in paper.authors.split(",")] if paper.authors else ["Unknown"]
126
-
127
- # Determine document type based on remote_id prefix
128
  dtype = "custom"
129
  if paper.remote_id:
130
- if "pubmed_" in paper.remote_id: dtype = "pubmed"
131
- elif "arxiv_" in paper.remote_id or "." in paper.remote_id: dtype = "arxiv"
132
- else: dtype = "arxiv" # default for most research platforms
 
 
 
 
 
133
 
134
  docs.append(ScientificDocument(
135
  id=str(paper.id),
136
  title=paper.title,
137
  authors=author_list,
138
  abstract=paper.summary or "",
139
- content=(paper.summary or "")[:10000],
140
  document_type=dtype,
141
  url=paper.pdf_path or ""
142
  ))
@@ -145,6 +213,8 @@ class AnalysisService:
145
  logger.error(f"No documents found for analysis {analysis_id}")
146
  raise ValueError("Zero documents identified for analysis. Aborting.")
147
 
 
 
148
  # 2. Prepare K2 Request
149
  k2_req = K2AnalysisRequest(
150
  documents=docs,
@@ -164,35 +234,31 @@ class AnalysisService:
164
  analysis_id=UUID(analysis_id),
165
  step_number=1,
166
  reasoning=result.reasoning_summary or "Analysis complete",
167
- source_chunks=result.reasoning_trace # JSON field
168
  )
169
  db.add(trace)
170
-
171
- # 5. Complete Analysis
172
- # result is a Pydantic model, convert to dict for storage
173
  self.complete_analysis(UUID(analysis_id), result=result.model_dump())
174
  db.commit()
 
175
 
176
  except Exception as e:
177
- db.rollback() # CLEAN TRANSACTION
178
  logger.error(f"FATAL ERROR in process_analysis for {analysis_id}: {str(e)}")
179
  import traceback
180
  logger.error(traceback.format_exc())
181
-
182
  try:
183
  error_data = {
184
  "status": "FAILED",
185
  "reasoning_summary": f"Erreur technique lors de l'analyse : {str(e)}\n\nTrace: {traceback.format_exc()[:500]}...",
186
  "confidence_overall": 0
187
  }
188
- # Use a safe UUID conversion
189
- try:
190
- target_uuid = UUID(analysis_id) if isinstance(analysis_id, str) else analysis_id
191
- self.complete_analysis(target_uuid, status="FAILED", result=error_data)
192
- db.commit()
193
- logger.info(f"Marked analysis {analysis_id} as FAILED in DB")
194
- except Exception as uuid_err:
195
- logger.error(f"Could not convert {analysis_id} to UUID or save failure: {uuid_err}")
196
  except Exception as final_err:
197
  logger.error(f"Failed to even mark analysis as FAILED: {final_err}")
198
  db.rollback()
 
20
  project_id: UUID,
21
  model_used: str
22
  ):
23
+ """Crée une nouvelle run d'analyse"""
24
  analysis = AnalysisRun(
25
  project_id=project_id,
26
  model_used=model_used,
 
43
  analysis.status = status
44
  analysis.completed_at = datetime.utcnow()
45
  if result:
46
+ analysis.result_data = result # SQLAlchemy handles JSON serialization
47
  self.analysis_repo.db.add(analysis)
48
  self.analysis_repo.db.commit()
49
  self.analysis_repo.db.refresh(analysis)
 
61
  from app.services.doaj_service import DOAJService
62
  from app.services.pubmed_service import PubMedService
63
  from app.services.openalex_service import OpenAlexService
64
+
65
+ UPLOAD_DIR = "./uploaded_files"
66
  db = self.analysis_repo.db
67
  try:
68
+ # 1. Fetch papers from DB or fetch on-demand from remote sources
69
  paper_repo = PaperRepository(db)
70
  analysis_run = self.analysis_repo.get_by_id(UUID(analysis_id))
71
  project_id = analysis_run.project_id
72
+
73
  docs = []
74
  for pid in request.paper_ids:
 
75
  paper = None
76
+
77
+ # 1a. Try UUID lookup in DB (for uploaded PDFs)
78
  try:
79
  paper_uuid = UUID(pid)
80
  paper = paper_repo.get_by_id(paper_uuid)
81
  except (ValueError, AttributeError):
82
+ pass
83
+
84
+ # 1b. Try remote_id lookup in DB (for previously fetched discovery papers)
85
+ if paper is None:
86
  paper = db.query(ResearchPaper).filter(
87
  ResearchPaper.remote_id == pid,
88
  ResearchPaper.project_id == project_id
89
  ).first()
90
+
91
+ # 1c. Not in DB → fetch from remote source on-demand
92
  if not paper:
93
+ logger.info(f"Paper {pid} not in DB. Fetching from remote source...")
 
94
  remote_data = None
95
  try:
96
  if "doaj_" in pid:
97
+ svc = DOAJService(download_dir=UPLOAD_DIR)
98
+ remote_data = svc.fetch_papers(pid.replace("doaj_", ""), 1)
99
+
100
  elif "pubmed_" in pid:
101
+ svc = PubMedService(download_dir=UPLOAD_DIR)
102
+ remote_data = svc.fetch_papers(pid.replace("pubmed_", ""), 1)
103
+
104
  elif "openalex_" in pid:
105
+ # Use OpenAlex direct Work ID endpoint (e.g. W2987984220)
106
+ svc = OpenAlexService(download_dir=UPLOAD_DIR)
107
+ openalex_bare_id = pid.replace("openalex_", "")
108
+ import httpx
109
+ try:
110
+ with httpx.Client(timeout=15.0) as client:
111
+ r = client.get(
112
+ f"https://api.openalex.org/works/{openalex_bare_id}",
113
+ params={
114
+ "mailto": svc.email,
115
+ "select": (
116
+ "id,title,authorships,abstract_inverted_index,"
117
+ "open_access,topics,publication_date,doi,"
118
+ "cited_by_count,primary_location"
119
+ )
120
+ }
121
+ )
122
+ r.raise_for_status()
123
+ parsed = svc._parse_item(r.json())
124
+ remote_data = [parsed] if parsed else []
125
+ logger.info(f"OpenAlex direct ID lookup succeeded for {openalex_bare_id}")
126
+ except Exception as oa_err:
127
+ logger.warning(f"OpenAlex direct ID lookup failed for {openalex_bare_id}: {oa_err}. Falling back to search.")
128
+ remote_data = svc.fetch_papers(openalex_bare_id, 1)
129
+
130
  elif "arxiv_" in pid:
131
+ # ArXiv: strip prefix to get proper ArXiv ID, try to download full PDF
132
+ svc = ArXivService(download_dir=UPLOAD_DIR)
133
+ arxiv_id = pid.replace("arxiv_", "")
134
+ meta_list = svc.fetch_papers(arxiv_id, 1)
135
+ if meta_list:
136
+ try:
137
+ from app.rag.pdf_parser import PDFParser
138
+ pdf_path = svc.download_paper(arxiv_id)
139
+ if pdf_path:
140
+ full_text = PDFParser.extract_text(pdf_path)
141
+ if full_text:
142
+ meta_list[0]["content"] = full_text
143
+ logger.info(f"ArXiv PDF downloaded & parsed for {arxiv_id}: {len(full_text)} chars")
144
+ except Exception as pdf_err:
145
+ logger.warning(f"ArXiv PDF download failed for {arxiv_id}, using abstract only: {pdf_err}")
146
+ remote_data = meta_list
147
+
148
  else:
149
+ # Fallback: treat as ArXiv search query
150
+ svc = ArXivService(download_dir=UPLOAD_DIR)
151
+ meta_list = svc.fetch_papers(pid, 1)
152
+ if meta_list:
153
+ try:
154
+ from app.rag.pdf_parser import PDFParser
155
+ pdf_path = svc.download_paper(pid)
156
+ if pdf_path:
157
+ full_text = PDFParser.extract_text(pdf_path)
158
+ if full_text:
159
+ meta_list[0]["content"] = full_text
160
+ except Exception:
161
+ pass
162
+ remote_data = meta_list
163
+
164
  if remote_data:
165
  p_info = remote_data[0]
166
+ # Prefer full PDF content over abstract for richer K2 analysis
167
+ snippet = (p_info.get("content") or p_info.get("summary") or "")[:10000]
168
  paper = ResearchPaper(
169
  project_id=project_id,
170
  remote_id=pid,
171
  title=p_info.get("title", "Unknown"),
172
  authors=", ".join(p_info.get("authors", [])),
173
  summary=snippet,
174
+ publication_year=datetime.now().year # Fallback
175
  )
176
  db.add(paper)
177
  db.commit()
178
  db.refresh(paper)
179
+ logger.info(f"Saved remote paper {pid} to project {project_id} ({len(snippet)} chars of content)")
180
  else:
181
+ logger.warning(f"No remote data found for {pid} — skipping.")
182
+
183
  except Exception as fetch_err:
184
  logger.error(f"Failed to fetch/save remote paper {pid}: {fetch_err}")
185
 
186
+ # 1d. Build ScientificDocument from DB record
187
  if paper:
 
188
  author_list = [a.strip() for a in paper.authors.split(",")] if paper.authors else ["Unknown"]
189
+
190
+ # Determine document type
191
  dtype = "custom"
192
  if paper.remote_id:
193
+ if "pubmed_" in paper.remote_id:
194
+ dtype = "pubmed"
195
+ elif "arxiv_" in paper.remote_id or "." in paper.remote_id:
196
+ dtype = "arxiv"
197
+ elif "openalex_" in paper.remote_id:
198
+ dtype = "openalex"
199
+ else:
200
+ dtype = "arxiv" # sensible default
201
 
202
  docs.append(ScientificDocument(
203
  id=str(paper.id),
204
  title=paper.title,
205
  authors=author_list,
206
  abstract=paper.summary or "",
207
+ content=(paper.summary or "")[:10000],
208
  document_type=dtype,
209
  url=paper.pdf_path or ""
210
  ))
 
213
  logger.error(f"No documents found for analysis {analysis_id}")
214
  raise ValueError("Zero documents identified for analysis. Aborting.")
215
 
216
+ logger.info(f"Starting K2 analysis {analysis_id} with {len(docs)} document(s)")
217
+
218
  # 2. Prepare K2 Request
219
  k2_req = K2AnalysisRequest(
220
  documents=docs,
 
234
  analysis_id=UUID(analysis_id),
235
  step_number=1,
236
  reasoning=result.reasoning_summary or "Analysis complete",
237
+ source_chunks=result.reasoning_trace # JSON field
238
  )
239
  db.add(trace)
240
+
241
+ # 5. Complete Analysis (convert Pydantic model to dict for storage)
 
242
  self.complete_analysis(UUID(analysis_id), result=result.model_dump())
243
  db.commit()
244
+ logger.info(f"Analysis {analysis_id} completed successfully")
245
 
246
  except Exception as e:
247
+ db.rollback() # CLEAN TRANSACTION
248
  logger.error(f"FATAL ERROR in process_analysis for {analysis_id}: {str(e)}")
249
  import traceback
250
  logger.error(traceback.format_exc())
251
+
252
  try:
253
  error_data = {
254
  "status": "FAILED",
255
  "reasoning_summary": f"Erreur technique lors de l'analyse : {str(e)}\n\nTrace: {traceback.format_exc()[:500]}...",
256
  "confidence_overall": 0
257
  }
258
+ target_uuid = UUID(analysis_id) if isinstance(analysis_id, str) else analysis_id
259
+ self.complete_analysis(target_uuid, status="FAILED", result=error_data)
260
+ db.commit()
261
+ logger.info(f"Marked analysis {analysis_id} as FAILED in DB")
 
 
 
 
262
  except Exception as final_err:
263
  logger.error(f"Failed to even mark analysis as FAILED: {final_err}")
264
  db.rollback()