letxinet / backend /tools /pdf_tools.py
C2MV's picture
Initial upload for Build Small Hackathon
68fb5e2 verified
Raw
History Blame Contribute Delete
4.46 kB
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]