scholaris-sdk 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. scholaris/__init__.py +48 -0
  2. scholaris/api.py +165 -0
  3. scholaris/auto_cite/__init__.py +113 -0
  4. scholaris/auto_cite/citation_engine.py +1695 -0
  5. scholaris/auto_cite/citation_export.py +349 -0
  6. scholaris/auto_cite/citation_index.py +1263 -0
  7. scholaris/auto_cite/document_formats.py +671 -0
  8. scholaris/auto_cite/formatters.py +347 -0
  9. scholaris/auto_cite/metadata_extractor.py +649 -0
  10. scholaris/auto_cite/models.py +430 -0
  11. scholaris/auto_cite/orchestrator.py +979 -0
  12. scholaris/auto_cite/page_aware_rag.py +548 -0
  13. scholaris/auto_cite/page_offset.py +826 -0
  14. scholaris/auto_cite/pdf_processor.py +344 -0
  15. scholaris/auto_cite/processed_pdf.py +815 -0
  16. scholaris/auto_cite/rag_engine.py +579 -0
  17. scholaris/auto_cite/validate_page_detection.py +384 -0
  18. scholaris/auto_cite/vision_ocr.py +768 -0
  19. scholaris/cli.py +648 -0
  20. scholaris/config.py +117 -0
  21. scholaris/converters/__init__.py +5 -0
  22. scholaris/converters/bibtex_parser.py +165 -0
  23. scholaris/converters/docx_converter.py +344 -0
  24. scholaris/converters/html_converter.py +236 -0
  25. scholaris/core/__init__.py +0 -0
  26. scholaris/core/citation.py +177 -0
  27. scholaris/core/models.py +195 -0
  28. scholaris/core/review.py +347 -0
  29. scholaris/exceptions.py +61 -0
  30. scholaris/providers/__init__.py +0 -0
  31. scholaris/providers/base.py +76 -0
  32. scholaris/providers/bibtex/__init__.py +0 -0
  33. scholaris/providers/bibtex/base.py +32 -0
  34. scholaris/providers/bibtex/llm_fallback.py +92 -0
  35. scholaris/providers/bibtex/pdf2bib.py +100 -0
  36. scholaris/providers/llm/__init__.py +25 -0
  37. scholaris/providers/llm/base.py +74 -0
  38. scholaris/providers/llm/deepseek.py +171 -0
  39. scholaris/providers/llm/gemini.py +267 -0
  40. scholaris/providers/llm/perplexity.py +179 -0
  41. scholaris/providers/search/__init__.py +0 -0
  42. scholaris/providers/search/base.py +52 -0
  43. scholaris/providers/search/pypaperbot.py +238 -0
  44. scholaris/scholaris.py +718 -0
  45. scholaris/utils/__init__.py +0 -0
  46. scholaris/utils/logging.py +44 -0
  47. scholaris/utils/rate_limiter.py +70 -0
  48. scholaris/v2/__init__.py +9 -0
  49. scholaris/v2/api.py +475 -0
  50. scholaris/v2/citas.py +58 -0
  51. scholaris/v2/spdf4.py +257 -0
  52. scholaris_sdk-2.0.0.dist-info/METADATA +168 -0
  53. scholaris_sdk-2.0.0.dist-info/RECORD +57 -0
  54. scholaris_sdk-2.0.0.dist-info/WHEEL +5 -0
  55. scholaris_sdk-2.0.0.dist-info/entry_points.txt +2 -0
  56. scholaris_sdk-2.0.0.dist-info/licenses/LICENSE +190 -0
  57. scholaris_sdk-2.0.0.dist-info/top_level.txt +1 -0
scholaris/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """Scholaris - Academic Research Automation Library.
2
+
3
+ A Python library for academic research automation including:
4
+ - Bibliography search and download
5
+ - BibTeX generation
6
+ - Literature review generation
7
+ - Academic writing assistance
8
+ """
9
+
10
+ import importlib
11
+ from typing import Any
12
+
13
+ # La v1 (procesado local con PyMuPDF y Gemini) se carga solo si se usa: así
14
+ # `scholaris.v2` (lector de SPDF 4.0 y cliente de la API) no necesita sus dependencias.
15
+ _PEREZOSOS = {
16
+ "Config": ".config",
17
+ "ScholarisError": ".exceptions", "ConfigurationError": ".exceptions", "SearchError": ".exceptions",
18
+ "DownloadError": ".exceptions", "BibTeXError": ".exceptions", "LLMError": ".exceptions",
19
+ "RateLimitError": ".exceptions", "ConversionError": ".exceptions", "ValidationError": ".exceptions",
20
+ "Paper": ".core.models", "Reference": ".core.models", "Section": ".core.models", "Review": ".core.models",
21
+ "Scholaris": ".scholaris",
22
+ }
23
+
24
+
25
+ def __getattr__(nombre: str) -> Any:
26
+ if nombre in _PEREZOSOS:
27
+ return getattr(importlib.import_module(_PEREZOSOS[nombre], __name__), nombre)
28
+ raise AttributeError(nombre)
29
+
30
+
31
+ __version__ = "2.0.0"
32
+ __all__ = [
33
+ "Scholaris",
34
+ "Config",
35
+ "Paper",
36
+ "Reference",
37
+ "Section",
38
+ "Review",
39
+ "ScholarisError",
40
+ "ConfigurationError",
41
+ "SearchError",
42
+ "DownloadError",
43
+ "BibTeXError",
44
+ "LLMError",
45
+ "RateLimitError",
46
+ "ConversionError",
47
+ "ValidationError",
48
+ ]
scholaris/api.py ADDED
@@ -0,0 +1,165 @@
1
+ """Scholaris en cinco verbos: subo cualquier cosa, busco, pregunto, cito.
2
+
3
+ Cliente fino de la API pública v1 (`/api/v1`). Crea una clave en Scholaris,
4
+ Ajustes → Claves de API, y:
5
+
6
+ from scholaris.api import Scholaris
7
+
8
+ s = Scholaris("sch_…") # o la variable SCHOLARIS_CLAVE
9
+ doc = s.subir("articulo.pdf") # también una URL: s.subir("https://…")
10
+ for p in s.buscar("atención escalada", k=3):
11
+ print(p["cita"], p["texto"][:80], p["enlace"])
12
+ r = s.preguntar("¿Qué es la atención multicabeza?")
13
+ print(r["respuesta"]) # Markdown con notas [^n] verificadas
14
+ print(s.citar("La atención sustituye a la recurrencia.")["texto"])
15
+ print(s.verificar("El Transformer prescinde de la recurrencia.")["veredicto"])
16
+
17
+ Contra la versión local: `Scholaris(base="http://localhost:8790")`.
18
+ Para todo lo demás (bibliotecas, SPDF, metadatos), `scholaris.v2.Scholaris`.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import mimetypes
24
+ import os
25
+ from typing import Any, Dict, Iterator, List, Optional, Sequence, Union
26
+
27
+ import requests
28
+
29
+ __all__ = ["Scholaris", "ErrorScholaris", "BASE"]
30
+
31
+ BASE = os.environ.get("SCHOLARIS_URL", "https://scholaris.joseluissaorin.com")
32
+ _MAX_CRUDO = 95 * 1024 * 1024
33
+ _MIMES = {".md": "text/markdown", ".epub": "application/epub+zip", ".m4a": "audio/mp4", ".opus": "audio/ogg", ".mkv": "video/x-matroska"}
34
+
35
+
36
+ class ErrorScholaris(Exception):
37
+ """Error de la API: `codigo` estable, `mensaje` en español y `message` en inglés."""
38
+
39
+ def __init__(self, estado: int, codigo: str, mensaje: str, message: str = "", detalles: Optional[dict] = None):
40
+ super().__init__(mensaje)
41
+ self.estado, self.codigo, self.mensaje, self.message, self.detalles = estado, codigo, mensaje, message, detalles or {}
42
+
43
+ def __str__(self) -> str:
44
+ return f"{self.mensaje} ({self.codigo}, {self.estado})"
45
+
46
+
47
+ class Scholaris:
48
+ def __init__(self, clave: Optional[str] = None, base: Optional[str] = None, *, sesion: Optional[requests.Session] = None, tiempo: float = 700):
49
+ self.clave = clave or os.environ.get("SCHOLARIS_CLAVE") or os.environ.get("SCHOLARIS_TOKEN")
50
+ self.base = (base or BASE).rstrip("/")
51
+ self.http = sesion or requests.Session()
52
+ self.tiempo = tiempo
53
+
54
+ # -- bajo nivel ---------------------------------------------------------
55
+
56
+ def pedir(self, metodo: str, ruta: str, *, json_: Any = None, datos: Any = None, params: Optional[dict] = None,
57
+ cabeceras: Optional[dict] = None, stream: bool = False) -> requests.Response:
58
+ h = {"x-scholaris-cliente": "python-v1", **(cabeceras or {})}
59
+ if self.clave:
60
+ h["authorization"] = f"Bearer {self.clave}"
61
+ p = {k: v for k, v in (params or {}).items() if v is not None}
62
+ r = self.http.request(metodo, f"{self.base}/api/v1{ruta}", json=json_, data=datos, params=p, headers=h, timeout=self.tiempo, stream=stream)
63
+ if r.status_code >= 400:
64
+ try:
65
+ e = r.json()["error"]
66
+ except Exception:
67
+ e = {"codigo": "interno", "mensaje": r.text[:300] or f"Error {r.status_code}"}
68
+ raise ErrorScholaris(r.status_code, e.get("codigo", "interno"), e.get("mensaje", ""), e.get("message", ""), e.get("detalles"))
69
+ return r
70
+
71
+ def _json(self, *a: Any, **k: Any) -> Any:
72
+ return self.pedir(*a, **k).json()
73
+
74
+ # -- documentos ---------------------------------------------------------
75
+
76
+ def subir(self, ruta_o_url: str, *, titulo: Optional[str] = None, autores: Optional[Sequence[str]] = None,
77
+ anio: Optional[int] = None, esperar: int = 300, idempotencia: Optional[str] = None) -> dict:
78
+ """Un fichero o una URL (web, PDF, YouTube, pódcast). Espera hasta `esperar` segundos;
79
+ si no ha terminado, el documento vuelve con `estado` y `progreso_url` (ver `documento(id, esperar=…)`)."""
80
+ params: Dict[str, Any] = {"esperar": esperar, "titulo": titulo, "anio": anio, "autores": "; ".join(autores) if autores else None}
81
+ cab = {"idempotency-key": idempotencia} if idempotencia else {}
82
+ if ruta_o_url.startswith(("http://", "https://")):
83
+ cuerpo = {"url": ruta_o_url, **{k: v for k, v in {"titulo": titulo, "autores": list(autores) if autores else None, "anio": anio}.items() if v is not None}}
84
+ return self._json("POST", "/documentos", json_=cuerpo, params={"esperar": esperar}, cabeceras=cab)
85
+ nombre = os.path.basename(ruta_o_url)
86
+ if os.path.getsize(ruta_o_url) > _MAX_CRUDO:
87
+ # Lo muy grande va por partes con el cliente de la v2, y se devuelve en la forma de la v1.
88
+ from .v2.api import Scholaris as V2
89
+ meta = {k: v for k, v in {"titulo": titulo, "anio": anio}.items() if v is not None}
90
+ d = V2(self.base, clave=self.clave).subir(ruta_o_url, metadatos=meta or None)
91
+ return self.documento(d["id"], esperar=esperar)
92
+ mime = _MIMES.get(os.path.splitext(nombre)[1].lower()) or mimetypes.guess_type(nombre)[0] or "application/octet-stream"
93
+ with open(ruta_o_url, "rb") as f:
94
+ return self._json("POST", "/documentos", datos=f, params={**params, "nombre": nombre}, cabeceras={"content-type": mime, **cab})
95
+
96
+ def documentos(self, q: Optional[str] = None, estado: Optional[str] = None) -> Iterator[dict]:
97
+ """Toda la biblioteca, página a página."""
98
+ cursor = None
99
+ while True:
100
+ p = self._json("GET", "/documentos", params={"q": q, "estado": estado, "cursor": cursor, "limite": 200})
101
+ yield from p["documentos"]
102
+ cursor = p.get("siguiente")
103
+ if not cursor:
104
+ return
105
+
106
+ def documento(self, id: str, esperar: int = 0) -> dict:
107
+ return self._json("GET", f"/documentos/{id}", params={"esperar": esperar or None})
108
+
109
+ def borrar(self, id: str) -> dict:
110
+ return self._json("DELETE", f"/documentos/{id}")
111
+
112
+ def texto(self, id: str, desde: Union[str, int, None] = None, hasta: Union[str, int, None] = None, *, markdown: bool = False) -> Union[dict, str]:
113
+ """Páginas por su folio impreso («23», «xiv») o física («[12]»); en audio y vídeo, tiempos («1:06:56»)."""
114
+ p = {"desde": desde, "hasta": hasta, "formato": "markdown" if markdown else None}
115
+ r = self.pedir("GET", f"/documentos/{id}/texto", params=p)
116
+ return r.text if markdown else r.json()
117
+
118
+ # -- buscar, preguntar, citar, verificar ---------------------------------
119
+
120
+ def buscar(self, q: str, k: int = 10, documentos: Optional[Sequence[str]] = None) -> List[dict]:
121
+ """Pasajes con `cita`, `localizador`, `ancla`, `enlace` y `texto` literal."""
122
+ return self._json("GET", "/buscar", params={"q": q, "k": k, "documento": ",".join(documentos) if documentos else None})["pasajes"]
123
+
124
+ def preguntar(self, pregunta: str, k: int = 8, documentos: Optional[Sequence[str]] = None, *, stream: bool = False) -> Any:
125
+ """Respuesta en Markdown con notas [^n] y sus `fuentes`. Con `stream=True`, un iterador de
126
+ (evento, datos): «pasajes», «texto» (delta), «fuente», «fin»."""
127
+ cuerpo = {"pregunta": pregunta, "k": k, **({"documentos": list(documentos)} if documentos else {})}
128
+ if not stream:
129
+ return self._json("POST", "/preguntar", json_=cuerpo)
130
+ return self._eventos(self.pedir("POST", "/preguntar", json_={**cuerpo, "stream": True}, stream=True))
131
+
132
+ @staticmethod
133
+ def _eventos(r: requests.Response) -> Iterator[tuple]:
134
+ evento, datos = "message", []
135
+ for linea in r.iter_lines(decode_unicode=True):
136
+ if linea is None:
137
+ continue
138
+ if linea == "":
139
+ if datos:
140
+ yield evento, json.loads("\n".join(datos))
141
+ evento, datos = "message", []
142
+ elif linea.startswith("event:"):
143
+ evento = linea[6:].strip()
144
+ elif linea.startswith("data:"):
145
+ datos.append(linea[5:].lstrip())
146
+
147
+ def citar(self, texto: str, estilo: str = "apa", *, idioma: str = "es-ES", esperar: int = 300) -> dict:
148
+ """El texto con citas verificadas insertadas, las `citas` y la `bibliografia`."""
149
+ return self._json("POST", "/citar", json_={"texto": texto, "estilo": estilo, "idioma": idioma}, params={"esperar": esperar})
150
+
151
+ def citar_docx(self, ruta: str, destino: str, estilo: str = "apa", *, esperar: int = 300) -> str:
152
+ """Un .docx citado, conservando su formato."""
153
+ with open(ruta, "rb") as f:
154
+ r = self.pedir("POST", "/citar", datos=f, params={"estilo": estilo, "esperar": esperar, "nombre": os.path.basename(ruta)},
155
+ cabeceras={"content-type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
156
+ "accept": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"})
157
+ if r.headers.get("content-type", "").startswith("application/json"):
158
+ raise ErrorScholaris(r.status_code, "conflicto", "La cita todavía no ha terminado: vuelve a pedirla con GET /citar/{id}.", detalles=r.json())
159
+ with open(destino, "wb") as f:
160
+ f.write(r.content)
161
+ return destino
162
+
163
+ def verificar(self, afirmacion: str, anio: Optional[int] = None) -> dict:
164
+ """`veredicto` (respaldada, parcial, sin_respaldo, contradicha), `probabilidad` y los pasajes."""
165
+ return self._json("POST", "/verificar", json_={"afirmacion": afirmacion, **({"anio": anio} if anio else {})})
@@ -0,0 +1,113 @@
1
+ """Auto-citation system for Scholaris.
2
+
3
+ This module provides automated in-text citation insertion for academic documents,
4
+ supporting both APA 7th edition and Chicago 17th edition citation styles.
5
+
6
+ Key Features:
7
+ - Vision OCR for scanned PDFs with verified page numbers
8
+ - Page-Aware RAG with grounded citation matching
9
+ - AI-powered metadata extraction when pdf2bib fails
10
+ - Shareable .spdf files for processed PDFs
11
+ - Bibliography folder auto-processing
12
+ - Support for bibliographies of 1-500+ PDFs
13
+ - Preview mode for citation validation before insertion
14
+
15
+ Main Components:
16
+ - ProcessedPDF: Shareable processed PDF format (.spdf)
17
+ - CitationIndex: Multi-source citation index with auto-processing
18
+ - CitationOrchestrator: Main controller with Vision OCR + Page-Aware RAG
19
+ - VisionOCRProcessor: Gemini Vision OCR for scanned PDFs
20
+ - PageAwareRAG: ChromaDB with verified page metadata
21
+ - MetadataExtractor: AI metadata extraction from PDF first pages
22
+ - GeminiCitationEngine: Citation matching with grounded RAG mode
23
+ """
24
+
25
+ from .models import (
26
+ PageOffsetResult,
27
+ PDFPage,
28
+ PageAwarePDF,
29
+ CitationStyle,
30
+ CitationRequest,
31
+ CitationResult,
32
+ OCRPage,
33
+ PageChunk,
34
+ RetrievedChunk,
35
+ )
36
+ from .page_offset import PageOffsetDetector
37
+ from .orchestrator import CitationOrchestrator
38
+ from .citation_engine import GeminiCitationEngine, GroundedCitation
39
+ from .citation_export import (
40
+ CitationExporter,
41
+ CitationExportResult,
42
+ CitationExportRow,
43
+ export_citations_to_csv,
44
+ )
45
+
46
+ # Shareable processed PDF format
47
+ from .processed_pdf import (
48
+ ProcessedPDF,
49
+ SPDFMetadata,
50
+ SPDFPage,
51
+ SPDFChunk,
52
+ SPDFPreview,
53
+ SUPPORTED_EXTENSIONS as SPDF_EXTENSIONS,
54
+ )
55
+
56
+ # Citation index for multi-source queries
57
+ from .citation_index import CitationIndex
58
+
59
+ # Optional components (require chromadb)
60
+ try:
61
+ from .vision_ocr import VisionOCRProcessor, process_pdf_vision
62
+ from .page_aware_rag import PageAwareRAG, GeminiEmbedder
63
+ from .metadata_extractor import MetadataExtractor, extract_pdf_metadata, batch_extract_metadata
64
+ VISION_RAG_AVAILABLE = True
65
+ except ImportError:
66
+ VISION_RAG_AVAILABLE = False
67
+ VisionOCRProcessor = None
68
+ PageAwareRAG = None
69
+ MetadataExtractor = None
70
+
71
+ __all__ = [
72
+ # Core models
73
+ "PageOffsetResult",
74
+ "PDFPage",
75
+ "PageAwarePDF",
76
+ "CitationStyle",
77
+ "CitationRequest",
78
+ "CitationResult",
79
+ "OCRPage",
80
+ "PageChunk",
81
+ "RetrievedChunk",
82
+ # Shareable processed PDF format (.spdf)
83
+ "ProcessedPDF",
84
+ "SPDFMetadata",
85
+ "SPDFPage",
86
+ "SPDFChunk",
87
+ "SPDFPreview",
88
+ "SPDF_EXTENSIONS",
89
+ # Citation index for multi-source queries
90
+ "CitationIndex",
91
+ # Main orchestrator
92
+ "CitationOrchestrator",
93
+ "PageOffsetDetector",
94
+ # Citation engine
95
+ "GeminiCitationEngine",
96
+ "GroundedCitation",
97
+ # Citation export
98
+ "CitationExporter",
99
+ "CitationExportResult",
100
+ "CitationExportRow",
101
+ "export_citations_to_csv",
102
+ # Vision OCR + RAG (optional)
103
+ "VisionOCRProcessor",
104
+ "PageAwareRAG",
105
+ "GeminiEmbedder",
106
+ "MetadataExtractor",
107
+ # Convenience functions
108
+ "process_pdf_vision",
109
+ "extract_pdf_metadata",
110
+ "batch_extract_metadata",
111
+ # Availability flag
112
+ "VISION_RAG_AVAILABLE",
113
+ ]