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.
- scholaris/__init__.py +48 -0
- scholaris/api.py +165 -0
- scholaris/auto_cite/__init__.py +113 -0
- scholaris/auto_cite/citation_engine.py +1695 -0
- scholaris/auto_cite/citation_export.py +349 -0
- scholaris/auto_cite/citation_index.py +1263 -0
- scholaris/auto_cite/document_formats.py +671 -0
- scholaris/auto_cite/formatters.py +347 -0
- scholaris/auto_cite/metadata_extractor.py +649 -0
- scholaris/auto_cite/models.py +430 -0
- scholaris/auto_cite/orchestrator.py +979 -0
- scholaris/auto_cite/page_aware_rag.py +548 -0
- scholaris/auto_cite/page_offset.py +826 -0
- scholaris/auto_cite/pdf_processor.py +344 -0
- scholaris/auto_cite/processed_pdf.py +815 -0
- scholaris/auto_cite/rag_engine.py +579 -0
- scholaris/auto_cite/validate_page_detection.py +384 -0
- scholaris/auto_cite/vision_ocr.py +768 -0
- scholaris/cli.py +648 -0
- scholaris/config.py +117 -0
- scholaris/converters/__init__.py +5 -0
- scholaris/converters/bibtex_parser.py +165 -0
- scholaris/converters/docx_converter.py +344 -0
- scholaris/converters/html_converter.py +236 -0
- scholaris/core/__init__.py +0 -0
- scholaris/core/citation.py +177 -0
- scholaris/core/models.py +195 -0
- scholaris/core/review.py +347 -0
- scholaris/exceptions.py +61 -0
- scholaris/providers/__init__.py +0 -0
- scholaris/providers/base.py +76 -0
- scholaris/providers/bibtex/__init__.py +0 -0
- scholaris/providers/bibtex/base.py +32 -0
- scholaris/providers/bibtex/llm_fallback.py +92 -0
- scholaris/providers/bibtex/pdf2bib.py +100 -0
- scholaris/providers/llm/__init__.py +25 -0
- scholaris/providers/llm/base.py +74 -0
- scholaris/providers/llm/deepseek.py +171 -0
- scholaris/providers/llm/gemini.py +267 -0
- scholaris/providers/llm/perplexity.py +179 -0
- scholaris/providers/search/__init__.py +0 -0
- scholaris/providers/search/base.py +52 -0
- scholaris/providers/search/pypaperbot.py +238 -0
- scholaris/scholaris.py +718 -0
- scholaris/utils/__init__.py +0 -0
- scholaris/utils/logging.py +44 -0
- scholaris/utils/rate_limiter.py +70 -0
- scholaris/v2/__init__.py +9 -0
- scholaris/v2/api.py +475 -0
- scholaris/v2/citas.py +58 -0
- scholaris/v2/spdf4.py +257 -0
- scholaris_sdk-2.0.0.dist-info/METADATA +168 -0
- scholaris_sdk-2.0.0.dist-info/RECORD +57 -0
- scholaris_sdk-2.0.0.dist-info/WHEEL +5 -0
- scholaris_sdk-2.0.0.dist-info/entry_points.txt +2 -0
- scholaris_sdk-2.0.0.dist-info/licenses/LICENSE +190 -0
- 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
|
+
]
|