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.
Files changed (131) hide show
  1. scinr/newton/README.md +655 -0
  2. scinr/newton/__init__.py +70 -0
  3. scinr/newton/annotation/__init__.py +0 -0
  4. scinr/newton/annotation/agent.py +463 -0
  5. scinr/newton/annotation/models.py +152 -0
  6. scinr/newton/annotation/neo4j_ops.py +1165 -0
  7. scinr/newton/annotation/nodes.py +322 -0
  8. scinr/newton/annotation/prompts.py +46 -0
  9. scinr/newton/annotation/prompts_claude.py +289 -0
  10. scinr/newton/annotation/prompts_generic.py +311 -0
  11. scinr/newton/annotation/prompts_gpt_reasoning.py +111 -0
  12. scinr/newton/annotation/state.py +26 -0
  13. scinr/newton/cli.py +506 -0
  14. scinr/newton/config.py +689 -0
  15. scinr/newton/converters/__init__.py +53 -0
  16. scinr/newton/converters/api_json.py +602 -0
  17. scinr/newton/converters/api_xml.py +714 -0
  18. scinr/newton/converters/base.py +269 -0
  19. scinr/newton/converters/config.py +60 -0
  20. scinr/newton/converters/csv.py +32 -0
  21. scinr/newton/converters/docx.py +340 -0
  22. scinr/newton/converters/html.py +276 -0
  23. scinr/newton/converters/main.py +672 -0
  24. scinr/newton/converters/pdf.py +210 -0
  25. scinr/newton/converters/pptx.py +312 -0
  26. scinr/newton/converters/registry.py +143 -0
  27. scinr/newton/converters/text.py +81 -0
  28. scinr/newton/converters/xlsx.py +32 -0
  29. scinr/newton/entity_extraction/__init__.py +8 -0
  30. scinr/newton/entity_extraction/agent.py +333 -0
  31. scinr/newton/entity_extraction/graph_mapper.py +972 -0
  32. scinr/newton/entity_extraction/model_resolver.py +136 -0
  33. scinr/newton/entity_extraction/neo4j_ops.py +191 -0
  34. scinr/newton/entity_extraction/nodes.py +336 -0
  35. scinr/newton/entity_extraction/prompts.py +205 -0
  36. scinr/newton/entity_extraction/prompts_claude.py +41 -0
  37. scinr/newton/entity_extraction/prompts_generic.py +50 -0
  38. scinr/newton/entity_extraction/prompts_gpt_reasoning.py +59 -0
  39. scinr/newton/entity_extraction/schema_composer.py +170 -0
  40. scinr/newton/entity_extraction/state.py +17 -0
  41. scinr/newton/exceptions.py +65 -0
  42. scinr/newton/extraction/__init__.py +0 -0
  43. scinr/newton/extraction/compact_extraction.py +231 -0
  44. scinr/newton/extraction/extraction.py +329 -0
  45. scinr/newton/ingest/__init__.py +0 -0
  46. scinr/newton/ingest/config.py +98 -0
  47. scinr/newton/ingest/loader.py +593 -0
  48. scinr/newton/ingest/nodes.py +647 -0
  49. scinr/newton/ingest/schema.py +249 -0
  50. scinr/newton/model-creation/AGENTS.md +791 -0
  51. scinr/newton/model-creation/README.md +1081 -0
  52. scinr/newton/model-creation/base.py +12 -0
  53. scinr/newton/model-creation/templates/README.md +47 -0
  54. scinr/newton/model-creation/templates/catalog.py +101 -0
  55. scinr/newton/model-creation/templates/models.py +376 -0
  56. scinr/newton/models/__init__.py +15 -0
  57. scinr/newton/models/base.py +12 -0
  58. scinr/newton/models/default/__init__.py +0 -0
  59. scinr/newton/models/default/catalog.py +13 -0
  60. scinr/newton/models/default/triple.py +26 -0
  61. scinr/newton/models/document_structure.py +283 -0
  62. scinr/newton/models/pharma_regulatory/__init__.py +0 -0
  63. scinr/newton/models/pharma_regulatory/baseModels.py +55 -0
  64. scinr/newton/models/pharma_regulatory/bpg/__init__.py +0 -0
  65. scinr/newton/models/pharma_regulatory/bpg/catalog.py +15 -0
  66. scinr/newton/models/pharma_regulatory/bpg/models.py +65 -0
  67. scinr/newton/models/pharma_regulatory/qa/__init__.py +0 -0
  68. scinr/newton/models/pharma_regulatory/qa/catalog.py +15 -0
  69. scinr/newton/models/pharma_regulatory/qa/models.py +111 -0
  70. scinr/newton/models/pharma_regulatory/structuralSignalModel.py +377 -0
  71. scinr/newton/models/pharma_regulatory/variation_guidelines/__init__.py +0 -0
  72. scinr/newton/models/pharma_regulatory/variation_guidelines/catalog.py +32 -0
  73. scinr/newton/models/pharma_regulatory/variation_guidelines/models.py +336 -0
  74. scinr/newton/pipeline.py +680 -0
  75. scinr/newton/prompts/__init__.py +0 -0
  76. scinr/newton/prompts/system_prompt.py +78 -0
  77. scinr/newton/prompts/system_prompt_claude.py +594 -0
  78. scinr/newton/prompts/system_prompt_generic.py +478 -0
  79. scinr/newton/prompts/system_prompt_gpt_reasoning.py +174 -0
  80. scinr/newton/results.py +104 -0
  81. scinr/newton/stages/__init__.py +26 -0
  82. scinr/newton/stages/annotation.py +128 -0
  83. scinr/newton/stages/entity_extraction.py +89 -0
  84. scinr/newton/stages/extraction.py +304 -0
  85. scinr/newton/stages/ingestion.py +240 -0
  86. scinr/newton/stages/preprocess.py +110 -0
  87. scinr/newton/stages/tabular.py +238 -0
  88. scinr/newton/storage/__init__.py +20 -0
  89. scinr/newton/storage/base.py +98 -0
  90. scinr/newton/storage/config.py +36 -0
  91. scinr/newton/storage/factory.py +83 -0
  92. scinr/newton/storage/models.py +79 -0
  93. scinr/newton/storage/mongodb/__init__.py +3 -0
  94. scinr/newton/storage/mongodb/client.py +134 -0
  95. scinr/newton/storage/mongodb/pages.py +115 -0
  96. scinr/newton/storage/mongodb/raw_files.py +96 -0
  97. scinr/newton/storage/null.py +39 -0
  98. scinr/newton/tabular/__init__.py +0 -0
  99. scinr/newton/tabular/agent.py +144 -0
  100. scinr/newton/tabular/graph.py +65 -0
  101. scinr/newton/tabular/models.py +64 -0
  102. scinr/newton/tabular/neo4j_ops.py +763 -0
  103. scinr/newton/tabular/nodes.py +471 -0
  104. scinr/newton/tabular/normalization/__init__.py +29 -0
  105. scinr/newton/tabular/normalization/detector.py +167 -0
  106. scinr/newton/tabular/normalization/engine.py +308 -0
  107. scinr/newton/tabular/normalization/hook.py +44 -0
  108. scinr/newton/tabular/normalization/models.py +32 -0
  109. scinr/newton/tabular/prompts.py +207 -0
  110. scinr/newton/tabular/prompts_claude.py +202 -0
  111. scinr/newton/tabular/prompts_generic.py +264 -0
  112. scinr/newton/tabular/prompts_gpt_reasoning.py +174 -0
  113. scinr/newton/tabular/reader.py +198 -0
  114. scinr/newton/tabular/state.py +47 -0
  115. scinr/newton/utils/__init__.py +15 -0
  116. scinr/newton/utils/bedrock_retry.py +7 -0
  117. scinr/newton/utils/document_resolver.py +64 -0
  118. scinr/newton/utils/file_archiver.py +111 -0
  119. scinr/newton/utils/llm_factory.py +10 -0
  120. scinr/newton/utils/llm_repair.py +229 -0
  121. scinr/newton/utils/llm_retry.py +124 -0
  122. scinr/newton/utils/logging_config.py +108 -0
  123. scinr/newton/utils/neo4j_concurrency.py +16 -0
  124. scinr/newton/utils/neo4j_retry.py +152 -0
  125. scinr/newton/utils/theme_registry.py +901 -0
  126. scinr/newton/utils/uid.py +71 -0
  127. scinr-0.2.0.dist-info/METADATA +455 -0
  128. scinr-0.2.0.dist-info/RECORD +131 -0
  129. scinr-0.2.0.dist-info/WHEEL +4 -0
  130. scinr-0.2.0.dist-info/entry_points.txt +2 -0
  131. 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).