Spaces:
Sleeping
Sleeping
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
- 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'
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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(
|
|
|
|
|
|
|
| 93 |
elif "pubmed_" in pid:
|
| 94 |
-
svc = PubMedService(
|
|
|
|
|
|
|
| 95 |
elif "openalex_" in pid:
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
elif "arxiv_" in pid:
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
else:
|
| 100 |
-
# Fallback
|
| 101 |
-
svc = ArXivService(
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
if remote_data:
|
| 104 |
p_info = remote_data[0]
|
| 105 |
-
|
|
|
|
| 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
|
| 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
|
| 128 |
dtype = "custom"
|
| 129 |
if paper.remote_id:
|
| 130 |
-
if "pubmed_" in paper.remote_id:
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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()
|
| 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 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 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()
|