from backend.providers.base import fetch_json import fitz # PyMuPDF import httpx import os import tempfile from langchain_text_splitters import RecursiveCharacterTextSplitter async def resolve_pdf(identifier: str) -> dict: """Resolve PDF URL from DOI or identifier.""" steps = [] # Extract DOI doi = None if identifier.startswith("10."): doi = identifier elif "doi.org" in identifier or "10." in identifier: import re match = re.search(r'(10\.\d{4,}/[^\s]+)', identifier) if match: doi = match.group(1) if doi: steps.append(f"DOI detectado: {doi}") # Try Unpaywall data = await fetch_json(f"https://api.unpaywall.org/v2/{doi}?email=test@example.com") if "error" not in data and data.get("best_oa_location"): url = data["best_oa_location"].get("url_for_pdf") or data["best_oa_location"].get("url") if url: steps.append("Unpaywall resolvió") return {"pdfUrl": url, "resolvedFrom": "Unpaywall", "doi": doi, "steps": steps} # Try Semantic Scholar data = await fetch_json(f"https://api.semanticscholar.org/graph/v1/paper/DOI:{doi}?fields=openAccessPdf") if "error" not in data and data.get("openAccessPdf"): steps.append("Semantic Scholar resolvió") return {"pdfUrl": data["openAccessPdf"]["url"], "resolvedFrom": "Semantic Scholar", "doi": doi, "steps": steps} # Try DOI.org landing page steps.append("DOI.org como fallback") return {"pdfUrl": f"https://doi.org/{doi}", "resolvedFrom": "DOI.org", "doi": doi, "steps": steps} return {"error": "No se pudo resolver el identificador", "steps": steps} async def download_pdf(url: str) -> dict: """Descarga un PDF desde una URL y lo guarda en un archivo temporal.""" try: async with httpx.AsyncClient(follow_redirects=True, verify=False) as client: response = await client.get(url, timeout=30.0) response.raise_for_status() # Verificar si realmente es un PDF content_type = response.headers.get("Content-Type", "") if "pdf" not in content_type.lower() and not url.lower().endswith(".pdf"): # Algunos repositorios devuelven HTML (landing page) en lugar del PDF directo. # Como heurística simple, si el contenido empieza con %PDF, lo procesamos. if not response.content.startswith(b"%PDF"): return {"error": f"La URL no retornó un PDF válido (Content-Type: {content_type})"} tmp_fd, tmp_path = tempfile.mkstemp(suffix=".pdf") with os.fdopen(tmp_fd, "wb") as f: f.write(response.content) return {"success": True, "path": tmp_path, "size": len(response.content)} except Exception as e: return {"error": f"Error descargando PDF: {str(e)}"} async def read_pdf(file_path: str) -> dict: """Extrae texto de un archivo PDF usando PyMuPDF.""" try: # Abrir el documento doc = fitz.open(file_path) text_pages = [] full_text = "" for i, page in enumerate(doc): page_text = page.get_text() text_pages.append(page_text) full_text += f"\n--- Página {i+1} ---\n{page_text}" doc.close() # Eliminar el archivo temporal si es necesario if file_path.startswith(tempfile.gettempdir()): try: os.remove(file_path) except Exception: pass return { "success": True, "text": full_text, "pages": len(text_pages), "preview": full_text[:1000] } except Exception as e: return {"error": f"Error leyendo PDF: {str(e)}"} def chunk_text(text: str, chunk_size: int = 1500, chunk_overlap: int = 200) -> list: """Divide texto en fragmentos (chunks) usando LangChain.""" try: splitter = RecursiveCharacterTextSplitter( chunk_size=chunk_size, chunk_overlap=chunk_overlap, separators=["\\n\\n", "\\n", ". ", " ", ""] ) chunks = splitter.split_text(text) return chunks except Exception as e: print(f"Error chunking text: {e}") return [text]