scinr 0.2.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.
- scinr/newton/README.md +655 -0
- scinr/newton/__init__.py +70 -0
- scinr/newton/annotation/__init__.py +0 -0
- scinr/newton/annotation/agent.py +463 -0
- scinr/newton/annotation/models.py +152 -0
- scinr/newton/annotation/neo4j_ops.py +1165 -0
- scinr/newton/annotation/nodes.py +322 -0
- scinr/newton/annotation/prompts.py +46 -0
- scinr/newton/annotation/prompts_claude.py +289 -0
- scinr/newton/annotation/prompts_generic.py +311 -0
- scinr/newton/annotation/prompts_gpt_reasoning.py +111 -0
- scinr/newton/annotation/state.py +26 -0
- scinr/newton/cli.py +506 -0
- scinr/newton/config.py +689 -0
- scinr/newton/converters/__init__.py +53 -0
- scinr/newton/converters/api_json.py +602 -0
- scinr/newton/converters/api_xml.py +714 -0
- scinr/newton/converters/base.py +269 -0
- scinr/newton/converters/config.py +60 -0
- scinr/newton/converters/csv.py +32 -0
- scinr/newton/converters/docx.py +340 -0
- scinr/newton/converters/html.py +276 -0
- scinr/newton/converters/main.py +672 -0
- scinr/newton/converters/pdf.py +210 -0
- scinr/newton/converters/pptx.py +312 -0
- scinr/newton/converters/registry.py +143 -0
- scinr/newton/converters/text.py +81 -0
- scinr/newton/converters/xlsx.py +32 -0
- scinr/newton/entity_extraction/__init__.py +8 -0
- scinr/newton/entity_extraction/agent.py +333 -0
- scinr/newton/entity_extraction/graph_mapper.py +972 -0
- scinr/newton/entity_extraction/model_resolver.py +136 -0
- scinr/newton/entity_extraction/neo4j_ops.py +191 -0
- scinr/newton/entity_extraction/nodes.py +336 -0
- scinr/newton/entity_extraction/prompts.py +205 -0
- scinr/newton/entity_extraction/prompts_claude.py +41 -0
- scinr/newton/entity_extraction/prompts_generic.py +50 -0
- scinr/newton/entity_extraction/prompts_gpt_reasoning.py +59 -0
- scinr/newton/entity_extraction/schema_composer.py +170 -0
- scinr/newton/entity_extraction/state.py +17 -0
- scinr/newton/exceptions.py +65 -0
- scinr/newton/extraction/__init__.py +0 -0
- scinr/newton/extraction/compact_extraction.py +231 -0
- scinr/newton/extraction/extraction.py +329 -0
- scinr/newton/ingest/__init__.py +0 -0
- scinr/newton/ingest/config.py +98 -0
- scinr/newton/ingest/loader.py +593 -0
- scinr/newton/ingest/nodes.py +647 -0
- scinr/newton/ingest/schema.py +249 -0
- scinr/newton/model-creation/AGENTS.md +791 -0
- scinr/newton/model-creation/README.md +1081 -0
- scinr/newton/model-creation/base.py +12 -0
- scinr/newton/model-creation/templates/README.md +47 -0
- scinr/newton/model-creation/templates/catalog.py +101 -0
- scinr/newton/model-creation/templates/models.py +376 -0
- scinr/newton/models/__init__.py +15 -0
- scinr/newton/models/base.py +12 -0
- scinr/newton/models/default/__init__.py +0 -0
- scinr/newton/models/default/catalog.py +13 -0
- scinr/newton/models/default/triple.py +26 -0
- scinr/newton/models/document_structure.py +283 -0
- scinr/newton/models/pharma_regulatory/__init__.py +0 -0
- scinr/newton/models/pharma_regulatory/baseModels.py +55 -0
- scinr/newton/models/pharma_regulatory/bpg/__init__.py +0 -0
- scinr/newton/models/pharma_regulatory/bpg/catalog.py +15 -0
- scinr/newton/models/pharma_regulatory/bpg/models.py +65 -0
- scinr/newton/models/pharma_regulatory/qa/__init__.py +0 -0
- scinr/newton/models/pharma_regulatory/qa/catalog.py +15 -0
- scinr/newton/models/pharma_regulatory/qa/models.py +111 -0
- scinr/newton/models/pharma_regulatory/structuralSignalModel.py +377 -0
- scinr/newton/models/pharma_regulatory/variation_guidelines/__init__.py +0 -0
- scinr/newton/models/pharma_regulatory/variation_guidelines/catalog.py +32 -0
- scinr/newton/models/pharma_regulatory/variation_guidelines/models.py +336 -0
- scinr/newton/pipeline.py +680 -0
- scinr/newton/prompts/__init__.py +0 -0
- scinr/newton/prompts/system_prompt.py +78 -0
- scinr/newton/prompts/system_prompt_claude.py +594 -0
- scinr/newton/prompts/system_prompt_generic.py +478 -0
- scinr/newton/prompts/system_prompt_gpt_reasoning.py +174 -0
- scinr/newton/results.py +104 -0
- scinr/newton/stages/__init__.py +26 -0
- scinr/newton/stages/annotation.py +128 -0
- scinr/newton/stages/entity_extraction.py +89 -0
- scinr/newton/stages/extraction.py +304 -0
- scinr/newton/stages/ingestion.py +240 -0
- scinr/newton/stages/preprocess.py +110 -0
- scinr/newton/stages/tabular.py +238 -0
- scinr/newton/storage/__init__.py +20 -0
- scinr/newton/storage/base.py +98 -0
- scinr/newton/storage/config.py +36 -0
- scinr/newton/storage/factory.py +83 -0
- scinr/newton/storage/models.py +79 -0
- scinr/newton/storage/mongodb/__init__.py +3 -0
- scinr/newton/storage/mongodb/client.py +134 -0
- scinr/newton/storage/mongodb/pages.py +115 -0
- scinr/newton/storage/mongodb/raw_files.py +96 -0
- scinr/newton/storage/null.py +39 -0
- scinr/newton/tabular/__init__.py +0 -0
- scinr/newton/tabular/agent.py +144 -0
- scinr/newton/tabular/graph.py +65 -0
- scinr/newton/tabular/models.py +64 -0
- scinr/newton/tabular/neo4j_ops.py +763 -0
- scinr/newton/tabular/nodes.py +471 -0
- scinr/newton/tabular/normalization/__init__.py +29 -0
- scinr/newton/tabular/normalization/detector.py +167 -0
- scinr/newton/tabular/normalization/engine.py +308 -0
- scinr/newton/tabular/normalization/hook.py +44 -0
- scinr/newton/tabular/normalization/models.py +32 -0
- scinr/newton/tabular/prompts.py +207 -0
- scinr/newton/tabular/prompts_claude.py +202 -0
- scinr/newton/tabular/prompts_generic.py +264 -0
- scinr/newton/tabular/prompts_gpt_reasoning.py +174 -0
- scinr/newton/tabular/reader.py +198 -0
- scinr/newton/tabular/state.py +47 -0
- scinr/newton/utils/__init__.py +15 -0
- scinr/newton/utils/bedrock_retry.py +7 -0
- scinr/newton/utils/document_resolver.py +64 -0
- scinr/newton/utils/file_archiver.py +111 -0
- scinr/newton/utils/llm_factory.py +10 -0
- scinr/newton/utils/llm_repair.py +229 -0
- scinr/newton/utils/llm_retry.py +124 -0
- scinr/newton/utils/logging_config.py +108 -0
- scinr/newton/utils/neo4j_concurrency.py +16 -0
- scinr/newton/utils/neo4j_retry.py +152 -0
- scinr/newton/utils/theme_registry.py +901 -0
- scinr/newton/utils/uid.py +71 -0
- scinr-0.2.0.dist-info/METADATA +455 -0
- scinr-0.2.0.dist-info/RECORD +131 -0
- scinr-0.2.0.dist-info/WHEEL +4 -0
- scinr-0.2.0.dist-info/entry_points.txt +2 -0
- scinr-0.2.0.dist-info/licenses/LICENSE +201 -0
scinr/newton/README.md
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
# scinr.newton — Technical Reference
|
|
2
|
+
|
|
3
|
+
> Part of the [scinr](../../README.md) library. `scinr.newton` is the document ingestion module. See root README for installation, quick start, and platform overview.
|
|
4
|
+
|
|
5
|
+
> **Requires Python 3.11+** and a running Neo4j 5.x instance.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Public API](#public-api)
|
|
10
|
+
2. [Pipeline Stages — Detailed Reference](#pipeline-stages--detailed-reference)
|
|
11
|
+
3. [Neo4j Schema](#neo4j-schema)
|
|
12
|
+
4. [Storage Layer](#storage-layer)
|
|
13
|
+
5. [Extending the Pipeline](#extending-the-pipeline)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Public API
|
|
18
|
+
|
|
19
|
+
All symbols below are importable directly from `scinr.newton`:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from scinr.newton import (
|
|
23
|
+
configure, get_config, get_available_themes, ThemePath,
|
|
24
|
+
run_pipeline,
|
|
25
|
+
run_preprocess, run_extraction, run_ingestion,
|
|
26
|
+
run_annotation, run_entity_extraction, run_tabular_pipeline,
|
|
27
|
+
DocumentResult, StageResult, PipelineResult,
|
|
28
|
+
ScinrError, ConfigurationError, PreconditionError,
|
|
29
|
+
ExtractionError, IngestionError, ModelError, StorageError, ConversionError,
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### `configure()`
|
|
36
|
+
|
|
37
|
+
**Module:** `scinr.newton.config`
|
|
38
|
+
|
|
39
|
+
Configure the library before using any pipeline function. Must be called once at startup. Parameter resolution order: explicit argument → environment variable → hard-coded default.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from langchain_openai import ChatOpenAI
|
|
43
|
+
from scinr.newton import configure
|
|
44
|
+
|
|
45
|
+
configure(
|
|
46
|
+
llm=ChatOpenAI(model="gpt-4o"),
|
|
47
|
+
neo4j_uri="bolt://localhost:7687",
|
|
48
|
+
neo4j_user="neo4j",
|
|
49
|
+
neo4j_password="secret",
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Parameter Table
|
|
54
|
+
|
|
55
|
+
| Parameter | Type | Env Var | Default | Description |
|
|
56
|
+
|---|---|---|---|---|
|
|
57
|
+
| `llm` | `BaseChatModel` | `MODEL_ID` | — | LangChain chat model for all LLM calls. If `None`, falls back to `ChatBedrockConverse` using `MODEL_ID`. Required. |
|
|
58
|
+
| `repair_llm` | `BaseChatModel` | — | falls back to `llm` | Separate model for JSON repair loop. Recommended: a smaller/cheaper model. |
|
|
59
|
+
| `neo4j_uri` | `str` | `NEO4J_URI` | `bolt://localhost:7687` | Neo4j Bolt connection URI. |
|
|
60
|
+
| `neo4j_user` | `str` | `NEO4J_USER` or `NEO4J_AUTH` | — | Neo4j username. Required. Also parsed from `NEO4J_AUTH=user/password`. |
|
|
61
|
+
| `neo4j_password` | `str` | `NEO4J_PASSWORD` or `NEO4J_AUTH` | — | Neo4j password. Required. Also parsed from `NEO4J_AUTH=user/password`. |
|
|
62
|
+
| `enabled_base_themes` | `list[ThemePath \| str] \| None` | — | `None` (all) | Whitelist of built-in theme paths to activate. `None` activates all. |
|
|
63
|
+
| `enabled_user_themes` | `list[str] \| None` | — | `None` (all) | Whitelist of user-defined theme paths to activate. `None` activates all. |
|
|
64
|
+
| `extra_models_paths` | `list[str \| Path] \| None` | `SCINR_EXTRA_MODELS_PATHS` | `[]` | Filesystem directories scanned for additional user themes. Colon-separated in env var. |
|
|
65
|
+
| `storage_backend` | `Literal["none", "mongodb", "custom"]` | `STORAGE_BACKEND` | `"none"` | Storage backend. `"none"` silently skips all storage calls. |
|
|
66
|
+
| `mongodb_uri` | `str` | `MONGODB_URI` | `mongodb://localhost:27017` | MongoDB connection URI. |
|
|
67
|
+
| `mongodb_database` | `str` | `MONGODB_DATABASE` | `"scinr"` | MongoDB database name. |
|
|
68
|
+
| `mongodb_raw_files_collection` | `str` | `MONGODB_RAW_FILES_COLLECTION` | `"raw_files"` | Collection for raw file metadata. |
|
|
69
|
+
| `mongodb_pages_collection` | `str` | `MONGODB_PAGES_COLLECTION` | `"converted_pages"` | Collection for converted page content. |
|
|
70
|
+
| `mongodb_gridfs_bucket` | `str` | `MONGODB_GRIDFS_BUCKET` | `"raw_binaries"` | GridFS bucket for raw binary files. |
|
|
71
|
+
| `custom_storage` | `tuple \| None` | — | `None` | `(RawFileRepository, PageRepository)` when `storage_backend="custom"`. |
|
|
72
|
+
| `extra_converters` | `dict[str, type] \| None` | — | `{}` | Maps file extensions to `BaseConverter` subclasses, overriding built-in converters. |
|
|
73
|
+
| `mistral_api_key` | `str \| None` | `MISTRAL_API_KEY` | `None` | Mistral API key for PDF OCR conversion. |
|
|
74
|
+
| `prompt_caching_enabled` | `bool \| None` | `PROMPT_CACHING_ENABLED` | `True` | Enable Bedrock Converse prompt caching (~90% token cost reduction on repeated calls). |
|
|
75
|
+
| `extraction_batch_size` | `int \| None` | `EXTRACTION_BATCH_SIZE` | `1` | Pages processed per extraction chunk (sliding window step). |
|
|
76
|
+
| `llm_concurrency` | `int \| None` | `LLM_CONCURRENCY` | `4` | Max concurrent LLM calls (asyncio semaphore size). |
|
|
77
|
+
| `neo4j_concurrency` | `int \| None` | `NEO4J_CONCURRENCY` | `10` | Max concurrent Neo4j session writes during annotation and entity extraction. |
|
|
78
|
+
| `log_level` | `str` | — | `"INFO"` | Logging level string. |
|
|
79
|
+
|
|
80
|
+
**Returns:** `ScinrConfig` — the populated configuration object (also stored as module-level singleton).
|
|
81
|
+
|
|
82
|
+
**Raises:** `ConfigurationError` — if `llm` is not set and `MODEL_ID` is absent, if Neo4j credentials are missing, or if `storage_backend` is invalid.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### `run_pipeline()`
|
|
87
|
+
|
|
88
|
+
**Module:** `scinr.newton.pipeline`
|
|
89
|
+
|
|
90
|
+
Async function. Orchestrates the full scinr.newton pipeline end-to-end, chaining Stages 0–4 in sequence. Passes data between stages in memory when intermediate directory parameters are omitted. Tabular files (`.csv`, `.xlsx`, `.xls`) found in `input_raw` are automatically routed to the tabular pipeline.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import asyncio
|
|
94
|
+
from scinr.newton import configure, run_pipeline
|
|
95
|
+
|
|
96
|
+
configure(llm=my_llm, neo4j_user="neo4j", neo4j_password="secret")
|
|
97
|
+
result = asyncio.run(run_pipeline(input_raw="files/"))
|
|
98
|
+
print(result.success, result.total_duration_seconds)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Parameter Table
|
|
102
|
+
|
|
103
|
+
| Parameter | Type | Default | Description |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| `input_raw` | `str \| None` | `None` | Folder of raw source files for Stage 0. Required when `stages` includes `"preprocess"`. Mutually exclusive with `extraction_input_dir` and `ingestion_input_dir`. |
|
|
106
|
+
| `converter_output_dir` | `str \| None` | `None` | Where Stage 0 writes intermediate JSON to disk. When `None`, Stage 0 returns documents in memory only. |
|
|
107
|
+
| `extraction_input_dir` | `str \| None` | `None` | Where Stage 1 reads JSON input from disk, skipping Stage 0. Mutually exclusive with `input_raw`. |
|
|
108
|
+
| `extraction_output_dir` | `str \| None` | `None` | Where Stage 1 writes `extract-*.json` output to disk. `None` keeps output in memory. |
|
|
109
|
+
| `ingestion_input_dir` | `str \| None` | `None` | Where Stage 2 reads `extract-*.json` from disk, skipping Stages 0 and 1. Mutually exclusive with `input_raw` and `extraction_input_dir`. |
|
|
110
|
+
| `stages` | `list[str] \| None` | `None` (all 5) | Ordered list of stages to run. Valid values: `"preprocess"`, `"extraction"`, `"ingestion"`, `"annotation"`, `"entity_extraction"`, `"tabular"`. `None` runs the full pipeline. `"tabular"` cannot be combined with other stages. |
|
|
111
|
+
| `document_names` | `list[str] \| None` | `None` | Explicit list of Neo4j `document_name` values for Stage 3/4 when running without Stage 2. Mutually exclusive with `document_names_dir`. |
|
|
112
|
+
| `document_names_dir` | `str \| None` | `None` | Folder containing `extract-*.json` files whose `document_name` fields are used for Stage 3/4. Mutually exclusive with `document_names`. |
|
|
113
|
+
| `manual` | `bool` | `False` | If `True`, Stage 3 assigns `model_class` to all qualifying nodes without LLM. Requires `model_class`. |
|
|
114
|
+
| `model_class` | `str \| None` | `None` | CamelCase model class name for manual annotation. Required when `manual=True`. |
|
|
115
|
+
| `only_unannotated` | `bool` | `False` | Stage 3 only: skip nodes already having a `:HAS_MODEL_DECISION` relationship. Useful for resuming. |
|
|
116
|
+
| `only_unextracted` | `bool` | `False` | Stage 4 only: skip nodes already having a `:HAS_EXTRACTION` relationship. Useful for resuming. |
|
|
117
|
+
| `context_instructions` | `str \| None` | `None` | Free-text context injected into Stage 0 (converter) and Stage 3 (annotation agent). |
|
|
118
|
+
| `update_mode` | `bool` | `False` | If `True`, Stage 2 replaces the latest document version without creating a new one. Only one source file allowed. Mutually exclusive with `replaces`. |
|
|
119
|
+
| `replaces` | `str \| None` | `None` | `document_name` of an existing document superseded by newly ingested content. Verified in Neo4j before stages run. Mutually exclusive with `update_mode`. |
|
|
120
|
+
| `parallel_docs` | `int` | `1` | Maximum documents processed concurrently across all stages, including tabular. Must be ≥ 1. |
|
|
121
|
+
| `on_partial_failure` | `Literal["abort", "continue", "warn"]` | `"abort"` | Behaviour when a stage returns `success=False`. `"abort"`: stop immediately. `"continue"`: proceed. `"warn"`: log warning and proceed. |
|
|
122
|
+
| `tabular_extensions` | `set[str] \| None` | `None` | File extensions treated as tabular. Defaults to `{'.csv', '.xlsx', '.xls'}`. |
|
|
123
|
+
| `tabular_delimiter` | `str \| None` | `None` | CSV field delimiter forwarded to the tabular agent. Uses agent default when `None`. |
|
|
124
|
+
|
|
125
|
+
**Returns:** `PipelineResult`
|
|
126
|
+
|
|
127
|
+
**Raises:** `ValueError` — on invalid parameter combinations. `FileNotFoundError` — if a required directory does not exist.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### Stage Functions
|
|
132
|
+
|
|
133
|
+
All stage functions are async and importable from `scinr.newton`:
|
|
134
|
+
|
|
135
|
+
#### `run_preprocess(input_raw, output_dir=None, context_instructions=None)`
|
|
136
|
+
|
|
137
|
+
**Stage 0.** Converts raw source files in `input_raw` to intermediate JSON using the registered converters. When `output_dir` is `None`, intermediate documents are returned in memory only. Returns `tuple[StageResult, list[IntermediateDocument]]`.
|
|
138
|
+
|
|
139
|
+
#### `run_extraction(input_folder=None, output_folder=None, intermediate_documents=None, parallel_docs=1)`
|
|
140
|
+
|
|
141
|
+
**Stage 1.** Reads intermediate JSON (from `input_folder` on disk or from `intermediate_documents` in memory) and runs LLM extraction to produce `Document` objects. Exactly one of `input_folder` or `intermediate_documents` must be provided. Returns `tuple[StageResult, list[Document]]`.
|
|
142
|
+
|
|
143
|
+
#### `run_ingestion(output_folder=None, files=None, documents=None, update_mode=False)`
|
|
144
|
+
|
|
145
|
+
**Stage 2.** Loads extracted documents into Neo4j. Accepts documents from `output_folder` (scanned for `extract-*.json`), an explicit `files` list, or in-memory `documents`. Returns `StageResult`.
|
|
146
|
+
|
|
147
|
+
#### `run_annotation(document_name, manual=False, model_class=None, parallel_docs=1, only_unannotated=False, context_instructions_override=None)`
|
|
148
|
+
|
|
149
|
+
**Stage 3.** Runs the LangGraph annotation agent over every `(:StructureNode)` in the named document. In manual mode, assigns `model_class` without LLM. Returns `StageResult`.
|
|
150
|
+
|
|
151
|
+
#### `run_entity_extraction(document_name, parallel_docs=1, only_unextracted=False)`
|
|
152
|
+
|
|
153
|
+
**Stage 4.** Runs the LangGraph entity extraction agent over annotated `(:StructureNode)` nodes in the named document. Returns `StageResult`.
|
|
154
|
+
|
|
155
|
+
#### `run_tabular_pipeline(input_raw, update_mode=False, parallel_docs=1, tabular_extensions=None, tabular_delimiter=None)`
|
|
156
|
+
|
|
157
|
+
**Tabular bypass.** Ingests CSV/XLSX/XLS files directly into Neo4j, bypassing Stages 0–4. Returns `StageResult`.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### Result Types
|
|
162
|
+
|
|
163
|
+
**Module:** `scinr.newton.results`
|
|
164
|
+
|
|
165
|
+
#### `DocumentResult`
|
|
166
|
+
|
|
167
|
+
Result of processing a single document through a pipeline stage.
|
|
168
|
+
|
|
169
|
+
| Field | Type | Description |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `document_name` | `str` | Neo4j `document_name` (or filename stem) of the processed document. |
|
|
172
|
+
| `nodes_processed` | `int` | Successfully processed nodes. For Stages 0–2: `1` on success, `0` on failure. For Stages 3–4: number of `StructureNode`s processed. |
|
|
173
|
+
| `nodes_failed` | `int` | Number of nodes that failed processing. |
|
|
174
|
+
| `errors` | `list[str]` | Error messages for this document. Empty on full success. |
|
|
175
|
+
|
|
176
|
+
#### `StageResult`
|
|
177
|
+
|
|
178
|
+
Aggregated result of running a single pipeline stage.
|
|
179
|
+
|
|
180
|
+
| Field | Type | Description |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `stage` | `str` | Stage identifier: `"preprocess"`, `"extraction"`, `"ingestion"`, `"annotation"`, `"entity_extraction"`, or `"tabular"`. |
|
|
183
|
+
| `success` | `bool` | `True` if `total_failed == 0` and no global errors occurred. |
|
|
184
|
+
| `documents` | `list[DocumentResult]` | Per-document results, one per file or document processed. |
|
|
185
|
+
| `total_processed` | `int` | Sum of `nodes_processed` across all `DocumentResult` entries. |
|
|
186
|
+
| `total_failed` | `int` | Sum of `nodes_failed` across all `DocumentResult` entries. |
|
|
187
|
+
| `duration_seconds` | `float` | Wall-clock time in seconds for the entire stage. |
|
|
188
|
+
| `errors` | `list[str]` | Global stage-level errors not attributable to a specific document. |
|
|
189
|
+
|
|
190
|
+
#### `PipelineResult`
|
|
191
|
+
|
|
192
|
+
Aggregated result of a full `run_pipeline()` invocation.
|
|
193
|
+
|
|
194
|
+
| Field | Type | Description |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `success` | `bool` | `True` only if every executed stage succeeded. |
|
|
197
|
+
| `total_duration_seconds` | `float` | Total wall-clock time for the entire pipeline run. |
|
|
198
|
+
| `stages_executed` | `list[str]` | Ordered list of stages actually run (skipped stages excluded). |
|
|
199
|
+
| `preprocess` | `StageResult \| None` | Stage 0 result, or `None` if not executed. |
|
|
200
|
+
| `extraction` | `StageResult \| None` | Stage 1 result, or `None` if not executed. |
|
|
201
|
+
| `ingestion` | `StageResult \| None` | Stage 2 result, or `None` if not executed. |
|
|
202
|
+
| `annotation` | `StageResult \| None` | Stage 3 result, or `None` if not executed. |
|
|
203
|
+
| `entity_extraction` | `StageResult \| None` | Stage 4 result, or `None` if not executed. |
|
|
204
|
+
| `tabular` | `StageResult \| None` | Tabular pipeline result, or `None` if not executed. |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### Exceptions
|
|
209
|
+
|
|
210
|
+
**Module:** `scinr.newton.exceptions`
|
|
211
|
+
|
|
212
|
+
All exceptions inherit from `ScinrError`, so the entire family can be caught with a single `except ScinrError` clause.
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
ScinrError (base)
|
|
216
|
+
├── ConfigurationError — library misconfigured (LLM missing, bad credentials, invalid storage backend)
|
|
217
|
+
├── PreconditionError — pipeline called out of order (e.g. entity extraction before annotation)
|
|
218
|
+
├── ExtractionError — LLM extraction failed after all retries exhausted
|
|
219
|
+
├── IngestionError — Neo4j write failed (version conflict, schema constraint violation)
|
|
220
|
+
├── ModelError — Pydantic model cannot be resolved or is invalid (bad catalog.py)
|
|
221
|
+
├── StorageError — MongoDB unavailable or misconfigured
|
|
222
|
+
└── ConversionError — file converter failed to process a source file
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Pipeline Stages — Detailed Reference
|
|
228
|
+
|
|
229
|
+
### Stage 0 — Preprocess
|
|
230
|
+
|
|
231
|
+
**Directory:** `src/scinr/newton/converters/`
|
|
232
|
+
**Input:** Raw source files
|
|
233
|
+
**Output:** `data/json/{filename}.json`
|
|
234
|
+
|
|
235
|
+
Normalises every supported file format into a uniform intermediate JSON envelope consumed by Stage 1:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"pages": [
|
|
240
|
+
{ "index": 0, "markdown": "# Title\n\nContent of page 1…", "page_id": "…" },
|
|
241
|
+
{ "index": 1, "markdown": "Content of page 2…", "page_id": "…" }
|
|
242
|
+
],
|
|
243
|
+
"folder_path": "optional/subfolder",
|
|
244
|
+
"raw_file_id": "…"
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
| Format | Library / API |
|
|
249
|
+
|---|---|
|
|
250
|
+
| PDF | Mistral OCR API (returns markdown with tables + images) |
|
|
251
|
+
| DOCX | python-docx |
|
|
252
|
+
| XLSX / XLS | openpyxl + pandas |
|
|
253
|
+
| PPTX | python-pptx |
|
|
254
|
+
| CSV | pandas |
|
|
255
|
+
| HTML | BeautifulSoup4 |
|
|
256
|
+
| TXT / MD | plain read |
|
|
257
|
+
| JSON API | httpx + jsonpath-ng |
|
|
258
|
+
| XML / SOAP API | lxml + XPath |
|
|
259
|
+
|
|
260
|
+
Converters are registered in `src/scinr/newton/converters/registry.py` (keyed by file extension), so the pipeline auto-selects the right converter for each file. When a storage backend is configured, Stage 0 also stores the raw binary and converted pages in MongoDB.
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# CLI
|
|
264
|
+
newton --stage preprocess --input-raw files/ --input data/json/
|
|
265
|
+
|
|
266
|
+
# Library API
|
|
267
|
+
result, docs = asyncio.run(run_preprocess("files/", output_dir="data/json/"))
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
See each converter's docstring in `src/scinr/newton/converters/` for converter details and how to add new formats.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### Stage 1 — Extract
|
|
275
|
+
|
|
276
|
+
**Directory:** `src/scinr/newton/extraction/`
|
|
277
|
+
**Input:** `data/json/{filename}.json`
|
|
278
|
+
**Output:** `data/output/extract-{filename}.json`
|
|
279
|
+
**LLM:** Any LangChain `BaseChatModel` — temperature `0.0`, max tokens configurable
|
|
280
|
+
|
|
281
|
+
Reads the paged JSON and processes pages through a **sliding window** (default 2 pages per chunk). For each window:
|
|
282
|
+
|
|
283
|
+
1. Sends page content plus the active document hierarchy as context to the LLM.
|
|
284
|
+
2. Receives back a `DocumentStructure` Pydantic tree (sections, subsections, tables, info units, verbatim quotes).
|
|
285
|
+
3. Merges the chunk result into the growing `Document` object.
|
|
286
|
+
4. **Crash-safe write**: writes the intermediate JSON to disk after every successfully processed chunk.
|
|
287
|
+
|
|
288
|
+
A **2-phase extraction + repair loop** handles malformed LLM output: if Pydantic validation fails, a dedicated repair model retries up to 3 times with escalating temperatures (`0.0 → 0.3 → 0.6`). The repair logic is shared across all LLM stages via `src/scinr/newton/utils/llm_repair.py`.
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
# CLI
|
|
292
|
+
newton --stage extract --input data/json/ --output data/output/ --parallel-docs 4
|
|
293
|
+
|
|
294
|
+
# Library API
|
|
295
|
+
result, docs = asyncio.run(run_extraction(input_folder="data/json/", output_folder="data/output/", parallel_docs=4))
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### Stage 2 — Ingest
|
|
301
|
+
|
|
302
|
+
**Directory:** `src/scinr/newton/ingest/`
|
|
303
|
+
**Input:** `data/output/extract-{filename}.json`
|
|
304
|
+
**Target:** Neo4j database
|
|
305
|
+
|
|
306
|
+
Reads the extracted `Document` JSON and writes it to Neo4j using **`MERGE` statements throughout** — every write is idempotent, so re-running the stage on the same document is always safe. All writes for a single document are wrapped in one transaction.
|
|
307
|
+
|
|
308
|
+
Key behaviours:
|
|
309
|
+
|
|
310
|
+
- **Deterministic UIDs** — `InfoUnit` nodes receive a SHA-256[:16] hash derived from their content, so identical text fragments always map to the same node across runs.
|
|
311
|
+
- **Folder hierarchy** — when a `folder_path` is present, ancestor `(:Document)` nodes are created and connected via `[:IS_COMPOSED_OF]` relationships, mirroring the source directory tree inside the graph.
|
|
312
|
+
- **Versioning** — each ingest increments the `version` counter and sets `latest=true` on the new node while all previous versions become `latest=false`.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# CLI — ingest from output folder
|
|
316
|
+
newton --stage ingest --output data/output/
|
|
317
|
+
|
|
318
|
+
# CLI — update in-place (no new version created)
|
|
319
|
+
newton --stage ingest --output data/output/ --update
|
|
320
|
+
|
|
321
|
+
# CLI — link as successor of another document
|
|
322
|
+
newton --stage all --input-raw files/new/ --input data/json/ --output data/output/ --replaces "OldDocumentName"
|
|
323
|
+
|
|
324
|
+
# Library API
|
|
325
|
+
result = asyncio.run(run_ingestion(output_folder="data/output/"))
|
|
326
|
+
result = asyncio.run(run_ingestion(output_folder="data/output/", update_mode=True))
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### Stage 3 — Annotate
|
|
332
|
+
|
|
333
|
+
**Directory:** `src/scinr/newton/annotation/`
|
|
334
|
+
**Requires:** Stage 2 completed for the target document
|
|
335
|
+
|
|
336
|
+
Runs a **LangGraph `StateGraph` agent** over every `(:StructureNode)` in the graph for the given document and assigns it an extraction model. The agent loop:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
load_nodes → [check_done] → prepare_node → classify_theme
|
|
340
|
+
↑ ↓
|
|
341
|
+
write_decision ←──────────────── decide_model
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
For each node the agent reads the theme assigned during Stage 1, then makes a structured LLM call producing an `AnnotationDecision` (primary model, complementary models, supplementary fields, and rationale). Results are written as `(:StructureNode)-[:HAS_MODEL_DECISION]->(:ModelDecision)` nodes.
|
|
345
|
+
|
|
346
|
+
The **ThemeRegistry** auto-discovers all `src/scinr/newton/models/*/catalog.py` files at startup and presents their `THEME_DESCRIPTION` and `SELECTABLE_MODELS` to the LLM — no code changes needed when a new domain is added.
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
# CLI — annotate all nodes
|
|
350
|
+
newton --stage annotate --document "MyDocument"
|
|
351
|
+
|
|
352
|
+
# CLI — resume (skip already-annotated nodes)
|
|
353
|
+
newton --stage annotate --document "MyDocument" --only-unannotated
|
|
354
|
+
|
|
355
|
+
# CLI — manual override (assign a fixed model without LLM)
|
|
356
|
+
newton --stage annotate --document "MyDocument" --manual --model "Triple"
|
|
357
|
+
|
|
358
|
+
# Library API
|
|
359
|
+
result = asyncio.run(run_annotation("MyDocument"))
|
|
360
|
+
result = asyncio.run(run_annotation("MyDocument", only_unannotated=True))
|
|
361
|
+
result = asyncio.run(run_annotation("MyDocument", manual=True, model_class="Triple"))
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### Stage 4 — Entity Extract
|
|
367
|
+
|
|
368
|
+
**Directory:** `src/scinr/newton/entity_extraction/`
|
|
369
|
+
**Requires:** Stage 3 completed for the target document
|
|
370
|
+
|
|
371
|
+
Runs a second **LangGraph `StateGraph` agent** that reads the model decisions from Stage 3 and extracts typed entities from each structural node. The agent loop:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
load_targets → [check_done] → prepare_target → compose_schema
|
|
375
|
+
↑ ↓
|
|
376
|
+
mark_extracted ← write_entities ← extract_entities
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The **`compose_schema`** step dynamically constructs a composite Pydantic model at runtime by merging the node's primary model, any complementary models, and supplementary fields declared in the `ModelDecision`. This composite schema is used as the structured output target for the extraction LLM call.
|
|
380
|
+
|
|
381
|
+
- Fields annotated with `json_schema_extra={"entity_label": "X"}` are written as **global `(:LabeledEntity)` singletons** keyed by `(label, normalized_value)`, enabling cross-document deduplication.
|
|
382
|
+
- Fields with `field_relationships` metadata generate typed Neo4j relationships between entity nodes.
|
|
383
|
+
- Nodes without a model decision fall back to the `Triple` (RDF) model.
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
# CLI — extract entities for all annotated nodes
|
|
387
|
+
newton --stage entity_extract --document "MyDocument"
|
|
388
|
+
|
|
389
|
+
# CLI — resume (skip nodes that already have an ExtractionResult)
|
|
390
|
+
newton --stage entity_extract --document "MyDocument" --only-unextracted --parallel-docs 4
|
|
391
|
+
|
|
392
|
+
# Library API
|
|
393
|
+
result = asyncio.run(run_entity_extraction("MyDocument"))
|
|
394
|
+
result = asyncio.run(run_entity_extraction("MyDocument", only_unextracted=True, parallel_docs=4))
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
### Tabular Bypass
|
|
400
|
+
|
|
401
|
+
**Directory:** `src/scinr/newton/tabular/`
|
|
402
|
+
**Input:** CSV / XLSX files
|
|
403
|
+
**Target:** Neo4j database
|
|
404
|
+
**LLM:** 3 calls per sheet (classify theme → decide model → map columns)
|
|
405
|
+
|
|
406
|
+
Ingests tabular files directly into Neo4j, bypassing Stages 0–4. For each sheet:
|
|
407
|
+
|
|
408
|
+
1. Reads headers and a 5-row preview.
|
|
409
|
+
2. Selects an extraction model via LLM.
|
|
410
|
+
3. Maps sheet columns to model fields via LLM.
|
|
411
|
+
4. Writes `(:Document)-[:HAS_STRUCTURE]->(:StructureNode:Table)-[:HAS_CHILD]->(:StructureNode:Row)` subgraph.
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# CLI — ingest all CSV/XLSX files in a folder
|
|
415
|
+
newton --stage tabular --input-raw files/data/
|
|
416
|
+
|
|
417
|
+
# CLI — update mode (wipe and re-ingest)
|
|
418
|
+
newton --stage tabular --input-raw files/data/ --update
|
|
419
|
+
|
|
420
|
+
# Library API
|
|
421
|
+
result = asyncio.run(run_tabular_pipeline("files/data/"))
|
|
422
|
+
result = asyncio.run(run_tabular_pipeline("files/data/", update_mode=True))
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The tabular pipeline is also automatically invoked by `--stage all` (and `run_pipeline()` with `input_raw`) when CSV/XLSX/XLS files are present in the input directory.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
### CLI Reference
|
|
430
|
+
|
|
431
|
+
The CLI entry point is `scinr.newton.cli:main_sync`, registered as `newton` via `pyproject.toml`:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
newton --stage <STAGE> [options]
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
| `--stage` choice | Equivalent `run_pipeline()` stages | Description |
|
|
438
|
+
|---|---|---|
|
|
439
|
+
| `all` | `["preprocess", "extraction", "ingestion", "annotation", "entity_extraction"]` | Full pipeline |
|
|
440
|
+
| `preprocess` | `["preprocess"]` | Stage 0 only |
|
|
441
|
+
| `extract` | `["extraction"]` | Stage 1 only |
|
|
442
|
+
| `ingest` | `["ingestion"]` | Stage 2 only |
|
|
443
|
+
| `annotate` | `["annotation"]` | Stage 3 only |
|
|
444
|
+
| `entity_extract` | `["entity_extraction"]` | Stage 4 only |
|
|
445
|
+
| `tabular` | `["tabular"]` | Tabular bypass only |
|
|
446
|
+
|
|
447
|
+
Key CLI flags:
|
|
448
|
+
|
|
449
|
+
| Flag | Type | Default | Description |
|
|
450
|
+
|---|---|---|---|
|
|
451
|
+
| `--input` | `DIR` | `data/json/` | Input folder for Stage 1 (intermediate JSON files) |
|
|
452
|
+
| `--input-raw` | `DIR` | — | Raw source files folder for Stage 0 / tabular |
|
|
453
|
+
| `--output` | `DIR` | `data/output/` | Output folder for Stage 1/2 |
|
|
454
|
+
| `--document` | `NAME` | — | Document name for Stage 3/4. Required with `--stage annotate` and `--stage entity_extract` |
|
|
455
|
+
| `--update` | flag | off | Update mode: re-ingest into the latest version without creating a new one |
|
|
456
|
+
| `--replaces` | `DOC_NAME` | — | Name of existing document being superseded |
|
|
457
|
+
| `--parallel-docs` | `N` | `1` | Concurrent documents across stages |
|
|
458
|
+
| `--only-unannotated` | flag | off | Stage 3 only: skip already-annotated nodes |
|
|
459
|
+
| `--only-unextracted` | flag | off | Stage 4 only: skip already-extracted nodes |
|
|
460
|
+
| `--manual` | flag | off | Stage 3 only: assign fixed model without LLM. Requires `--model` |
|
|
461
|
+
| `--model` | `CLASS_NAME` | — | CamelCase model class name for `--manual` annotation |
|
|
462
|
+
| `--context` | `TEXT` | — | Free-text context instructions passed to Stage 0 and Stage 3 LLMs |
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Neo4j Schema
|
|
467
|
+
|
|
468
|
+
### Node Labels
|
|
469
|
+
|
|
470
|
+
| Label | Key Properties | Description |
|
|
471
|
+
|---|---|---|
|
|
472
|
+
| `:Document` | `name`, `path`, `version`, `latest` | Document root. `latest=true` on the current version. |
|
|
473
|
+
| `:StructureNode` + role label | `node_id`, `title`, `role`, `theme` | One node per structural element; also labeled `:Section`, `:Subsection`, `:Table`, `:Appendix`, `:FieldGroup`, or `:FreeformBlock`. |
|
|
474
|
+
| `:InfoUnit` | `uid` (SHA-256[:16]), `title`, `description` | Semantic concept unit extracted from a node's body text. |
|
|
475
|
+
| `:ModelDecision` | `node_id` | Stage 3 annotation result for a StructureNode. |
|
|
476
|
+
| `:CatalogModel` | `name` | Registered Pydantic extraction model. |
|
|
477
|
+
| `:Theme` | `name`, `path` | Extraction domain. |
|
|
478
|
+
| `:ExtractionResult` | `node_id` | Stage 4 entity extraction result for a StructureNode. |
|
|
479
|
+
| `:LabeledEntity` | `label`, `normalized_value` | Global entity singleton — shared across documents. |
|
|
480
|
+
| `:Entity` | `normalized_value` | Triple-fallback entity (RDF subject / object). |
|
|
481
|
+
|
|
482
|
+
### Key Relationships
|
|
483
|
+
|
|
484
|
+
| Relationship | From → To | Description |
|
|
485
|
+
|---|---|---|
|
|
486
|
+
| `HAS_STRUCTURE` | `:Document` → `:StructureNode` | Document root to top-level structural nodes. |
|
|
487
|
+
| `HAS_CHILD` | `:StructureNode` → `:StructureNode` | Parent → child in the document tree. |
|
|
488
|
+
| `HAS_INFO_UNIT` | `:StructureNode` → `:InfoUnit` | Node to its semantic information units. |
|
|
489
|
+
| `IS_COMPOSED_OF` | `:Document` → `:Document` | Folder hierarchy (parent folder → child document). |
|
|
490
|
+
| `HAS_NEWER_VERSION` | `:Document` → `:Document` | Version chain: old version → new version. |
|
|
491
|
+
| `HAS_MODEL_DECISION` | `:StructureNode` → `:ModelDecision` | Stage 3 annotation result. |
|
|
492
|
+
| `MATCHED_MODEL` | `:ModelDecision` → `:CatalogModel` | Primary model selected for a node. |
|
|
493
|
+
| `HAS_EXTRACTION` | `:StructureNode` → `:ExtractionResult` | Stage 4 result link. |
|
|
494
|
+
| `REFERENCES` | `:ExtractionResult` → `:LabeledEntity` | Extraction → global entity singleton. |
|
|
495
|
+
| `<REL_TYPE>` | `:LabeledEntity` → `:LabeledEntity` | Field relationship (defined in model metadata). |
|
|
496
|
+
|
|
497
|
+
### Cypher Examples
|
|
498
|
+
|
|
499
|
+
```cypher
|
|
500
|
+
-- Find all latest documents
|
|
501
|
+
MATCH (d:Document {latest: true})
|
|
502
|
+
RETURN d.name, d.path, d.version
|
|
503
|
+
ORDER BY d.name;
|
|
504
|
+
|
|
505
|
+
-- Traverse document structure
|
|
506
|
+
MATCH (d:Document {name: "MyDocument", latest: true})
|
|
507
|
+
-[:HAS_STRUCTURE]->
|
|
508
|
+
(s:StructureNode)
|
|
509
|
+
-[:HAS_INFO_UNIT]->
|
|
510
|
+
(u:InfoUnit)
|
|
511
|
+
RETURN s.title, u.title, u.description
|
|
512
|
+
ORDER BY s.appearance_order;
|
|
513
|
+
|
|
514
|
+
-- Find all global entities of a given label
|
|
515
|
+
MATCH (e:LabeledEntity {label: "Substance"})
|
|
516
|
+
RETURN e.normalized_value
|
|
517
|
+
ORDER BY e.normalized_value;
|
|
518
|
+
|
|
519
|
+
-- Follow a document version chain
|
|
520
|
+
MATCH path = (old:Document)-[:HAS_NEWER_VERSION*]->(latest:Document {latest: true})
|
|
521
|
+
WHERE old.name = "MyDocument"
|
|
522
|
+
RETURN [n IN nodes(path) | {name: n.name, version: n.version, latest: n.latest}];
|
|
523
|
+
|
|
524
|
+
-- Find all nodes annotated with a specific model
|
|
525
|
+
MATCH (s:StructureNode)-[:HAS_MODEL_DECISION]->(md:ModelDecision)
|
|
526
|
+
-[:MATCHED_MODEL]->(m:CatalogModel {name: "Triple"})
|
|
527
|
+
RETURN s.node_id, s.title, s.theme;
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Document Versioning
|
|
531
|
+
|
|
532
|
+
Every ingest run auto-increments the `version` counter and marks only the newest node as `latest=true`.
|
|
533
|
+
|
|
534
|
+
| Scenario | CLI flag / API param | Behaviour |
|
|
535
|
+
|---|---|---|
|
|
536
|
+
| First ingest | *(none)* | Creates version 1 with `latest=true`. |
|
|
537
|
+
| Re-ingest (correction) | `--update` / `update_mode=True` | Wipes and re-ingests into the existing latest version; no new version node created. |
|
|
538
|
+
| New version | *(none, run again)* | Creates version N+1, links via `HAS_NEWER_VERSION`, sets `latest=true`. |
|
|
539
|
+
| Document supersedes another | `--replaces <name>` / `replaces="name"` | Links the new document as the successor of the named existing document. The old document's `latest=True` version becomes `latest=False`. |
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## Storage Layer
|
|
544
|
+
|
|
545
|
+
The storage layer is **fully optional**. When `STORAGE_BACKEND` is not set (or set to `"none"`), the pipeline runs without MongoDB and all storage calls are silently skipped.
|
|
546
|
+
|
|
547
|
+
When enabled (`STORAGE_BACKEND=mongodb`), the storage layer persists:
|
|
548
|
+
|
|
549
|
+
| Collection / Bucket | Contents |
|
|
550
|
+
|---|---|
|
|
551
|
+
| `raw_files` (MongoDB) | Metadata for each ingested raw file (filename, content type, folder path, upload timestamp). |
|
|
552
|
+
| `converted_pages` (MongoDB) | Converted page content (markdown) + source page metadata per page. |
|
|
553
|
+
| `raw_binaries` (GridFS) | Binary content of raw source files (PDF bytes, DOCX bytes, etc.). |
|
|
554
|
+
|
|
555
|
+
The `raw_file_id` and `page_id` fields stored in Neo4j nodes allow cross-referencing back to the original binary and page content in MongoDB.
|
|
556
|
+
|
|
557
|
+
### Enable / Disable Storage
|
|
558
|
+
|
|
559
|
+
```dotenv
|
|
560
|
+
# Enable MongoDB storage (with authentication)
|
|
561
|
+
STORAGE_BACKEND=mongodb
|
|
562
|
+
MONGODB_URI=mongodb://your_user:your_password@localhost:27017/your_db?authSource=your_db
|
|
563
|
+
MONGODB_DATABASE=your_db
|
|
564
|
+
|
|
565
|
+
# Disable storage (omit STORAGE_BACKEND or leave blank)
|
|
566
|
+
# STORAGE_BACKEND=
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
> **`MONGODB_URI` must include credentials** when connecting to an authenticated MongoDB instance. The `authSource` query parameter must match the database where the user was created (same value as `MONGODB_DATABASE`).
|
|
570
|
+
>
|
|
571
|
+
> MongoDB creates the database automatically on the first write — no prior `CREATE DATABASE` step is needed.
|
|
572
|
+
|
|
573
|
+
### Custom Storage Backend
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
from scinr.newton import configure
|
|
577
|
+
|
|
578
|
+
class MyRawFileRepo:
|
|
579
|
+
def save(self, filename, content_type, folder_path, binary): ...
|
|
580
|
+
|
|
581
|
+
class MyPageRepo:
|
|
582
|
+
def save(self, raw_file_id, pages): ...
|
|
583
|
+
|
|
584
|
+
configure(
|
|
585
|
+
llm=my_llm,
|
|
586
|
+
neo4j_user="neo4j",
|
|
587
|
+
neo4j_password="secret",
|
|
588
|
+
storage_backend="custom",
|
|
589
|
+
custom_storage=(MyRawFileRepo(), MyPageRepo()),
|
|
590
|
+
)
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The storage backend abstraction lives in `src/scinr/newton/storage/base.py` and `src/scinr/newton/storage/factory.py`. Additional backends (e.g. PostgreSQL, S3) can be added by implementing the base interface.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Extending the Pipeline
|
|
598
|
+
|
|
599
|
+
### Adding a New File Format Converter
|
|
600
|
+
|
|
601
|
+
1. Create `src/scinr/newton/converters/<format>.py` and subclass `BaseConverter` from `src/scinr/newton/converters/base.py`.
|
|
602
|
+
2. Set `supported_extensions: list[str]` on the class.
|
|
603
|
+
3. Implement `async convert(file_path: Path) -> dict` returning the paged JSON envelope:
|
|
604
|
+
```python
|
|
605
|
+
{
|
|
606
|
+
"pages": [{"index": 0, "markdown": "…"}],
|
|
607
|
+
"folder_path": None, # or a relative subfolder string
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
4. Register the converter in `src/scinr/newton/converters/registry.py` by adding it to the `CONVERTERS` dict keyed by extension.
|
|
611
|
+
|
|
612
|
+
Alternatively, pass `extra_converters` to `configure()` to override converters at runtime without modifying the package:
|
|
613
|
+
|
|
614
|
+
```python
|
|
615
|
+
configure(
|
|
616
|
+
llm=my_llm,
|
|
617
|
+
neo4j_user="neo4j",
|
|
618
|
+
neo4j_password="secret",
|
|
619
|
+
extra_converters={".rtf": MyRtfConverter},
|
|
620
|
+
)
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
See each converter's docstring in `src/scinr/newton/converters/` for the full `BaseConverter` interface and working examples.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
### Adding a New Extraction Domain
|
|
628
|
+
|
|
629
|
+
1. Create `src/scinr/newton/models/<your_theme>/` with `__init__.py` and `catalog.py`.
|
|
630
|
+
2. Export `THEME_DESCRIPTION: str` and `SELECTABLE_MODELS: list[type[ExtractionModel]]` from `catalog.py`:
|
|
631
|
+
```python
|
|
632
|
+
# src/scinr/newton/models/my_domain/catalog.py
|
|
633
|
+
|
|
634
|
+
THEME_DESCRIPTION: str = (
|
|
635
|
+
"One-line description used by the Stage 3 classification LLM to pick this theme."
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
SELECTABLE_MODELS: list[type[ExtractionModel]] = [MyModelA, MyModelB]
|
|
639
|
+
```
|
|
640
|
+
3. Write Pydantic model classes that inherit from `ExtractionModel`.
|
|
641
|
+
4. No other changes required — `ThemeRegistry` (`src/scinr/newton/utils/theme_registry.py`) picks up the new domain automatically on next startup.
|
|
642
|
+
|
|
643
|
+
For user-defined themes outside the package, pass the directory path to `configure()`:
|
|
644
|
+
|
|
645
|
+
```python
|
|
646
|
+
configure(
|
|
647
|
+
llm=my_llm,
|
|
648
|
+
neo4j_user="neo4j",
|
|
649
|
+
neo4j_password="secret",
|
|
650
|
+
extra_models_paths=["/path/to/my/themes/"],
|
|
651
|
+
enabled_user_themes=["my_domain"], # None activates all
|
|
652
|
+
)
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
See [model-creation/README.md](model-creation/README.md) for the full developer guide including worked examples, entity label conventions, nested model patterns, `field_relationships` syntax, and `instance_relationships` syntax. For AI agent instructions on creating models, see [model-creation/AGENTS.md](model-creation/AGENTS.md).
|