| from backend.providers.base import fetch_json |
| import fitz |
| 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 = [] |
| |
| |
| 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}") |
| |
| |
| 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} |
| |
| |
| 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} |
| |
| |
| 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() |
| |
| |
| content_type = response.headers.get("Content-Type", "") |
| if "pdf" not in content_type.lower() and not url.lower().endswith(".pdf"): |
| |
| |
| 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: |
| |
| 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() |
| |
| |
| 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] |
|
|