parse-bench 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. parse_bench/__init__.py +3 -0
  2. parse_bench/analysis/__init__.py +6 -0
  3. parse_bench/analysis/aggregation_report.py +582 -0
  4. parse_bench/analysis/cli.py +472 -0
  5. parse_bench/analysis/comparison.py +382 -0
  6. parse_bench/analysis/comparison_core.py +357 -0
  7. parse_bench/analysis/comparison_report.py +2066 -0
  8. parse_bench/analysis/detailed_report.py +2254 -0
  9. parse_bench/analysis/leaderboard_report.py +852 -0
  10. parse_bench/analysis/metric_definitions.py +771 -0
  11. parse_bench/cli.py +267 -0
  12. parse_bench/data/__init__.py +1 -0
  13. parse_bench/data/cli.py +118 -0
  14. parse_bench/data/download.py +127 -0
  15. parse_bench/evaluation/__init__.py +11 -0
  16. parse_bench/evaluation/cli.py +435 -0
  17. parse_bench/evaluation/evaluators/__init__.py +17 -0
  18. parse_bench/evaluation/evaluators/base.py +34 -0
  19. parse_bench/evaluation/evaluators/extract.py +429 -0
  20. parse_bench/evaluation/evaluators/layoutdet.py +1682 -0
  21. parse_bench/evaluation/evaluators/parse.py +1353 -0
  22. parse_bench/evaluation/evaluators/qa.py +199 -0
  23. parse_bench/evaluation/layout_adapters/__init__.py +21 -0
  24. parse_bench/evaluation/layout_adapters/adapters.py +3180 -0
  25. parse_bench/evaluation/layout_adapters/base.py +105 -0
  26. parse_bench/evaluation/layout_adapters/registry.py +109 -0
  27. parse_bench/evaluation/layout_label_mappers/__init__.py +22 -0
  28. parse_bench/evaluation/layout_label_mappers/base.py +66 -0
  29. parse_bench/evaluation/layout_label_mappers/mappers.py +332 -0
  30. parse_bench/evaluation/layout_label_mappers/projection.py +74 -0
  31. parse_bench/evaluation/layout_label_mappers/registry.py +119 -0
  32. parse_bench/evaluation/metric_aggregation.py +56 -0
  33. parse_bench/evaluation/metrics/__init__.py +5 -0
  34. parse_bench/evaluation/metrics/attribution/__init__.py +35 -0
  35. parse_bench/evaluation/metrics/attribution/constants.py +12 -0
  36. parse_bench/evaluation/metrics/attribution/core.py +1108 -0
  37. parse_bench/evaluation/metrics/attribution/evaluate.py +446 -0
  38. parse_bench/evaluation/metrics/attribution/geometry.py +161 -0
  39. parse_bench/evaluation/metrics/attribution/text_utils.py +233 -0
  40. parse_bench/evaluation/metrics/base.py +33 -0
  41. parse_bench/evaluation/metrics/downstream/__init__.py +0 -0
  42. parse_bench/evaluation/metrics/extract/__init__.py +29 -0
  43. parse_bench/evaluation/metrics/extract/json_subset_match.py +473 -0
  44. parse_bench/evaluation/metrics/extract/json_subset_match_metric.py +81 -0
  45. parse_bench/evaluation/metrics/extract/list_unwrap.py +340 -0
  46. parse_bench/evaluation/metrics/extract/rule_based_metric.py +90 -0
  47. parse_bench/evaluation/metrics/extract/test_rules.py +409 -0
  48. parse_bench/evaluation/metrics/extract/test_types.py +11 -0
  49. parse_bench/evaluation/metrics/field_grounding/__init__.py +21 -0
  50. parse_bench/evaluation/metrics/field_grounding/core.py +437 -0
  51. parse_bench/evaluation/metrics/field_grounding/extract_adapter.py +1224 -0
  52. parse_bench/evaluation/metrics/field_grounding/parse_adapter.py +697 -0
  53. parse_bench/evaluation/metrics/field_grounding/rule_filters.py +19 -0
  54. parse_bench/evaluation/metrics/field_grounding/value_compare.py +190 -0
  55. parse_bench/evaluation/metrics/layoutdet/__init__.py +17 -0
  56. parse_bench/evaluation/metrics/layoutdet/classification_utils.py +300 -0
  57. parse_bench/evaluation/metrics/layoutdet/iou.py +76 -0
  58. parse_bench/evaluation/metrics/parse/__init__.py +5 -0
  59. parse_bench/evaluation/metrics/parse/_vendor_grits_reference.py +531 -0
  60. parse_bench/evaluation/metrics/parse/cross_page_table_consistency.py +165 -0
  61. parse_bench/evaluation/metrics/parse/emphasis_spans.py +242 -0
  62. parse_bench/evaluation/metrics/parse/fast_tree_edit.py +282 -0
  63. parse_bench/evaluation/metrics/parse/grits_metric.py +1125 -0
  64. parse_bench/evaluation/metrics/parse/grits_reference_metric.py +142 -0
  65. parse_bench/evaluation/metrics/parse/header_accuracy_metric.py +1662 -0
  66. parse_bench/evaluation/metrics/parse/llm_normalization/__init__.py +51 -0
  67. parse_bench/evaluation/metrics/parse/llm_normalization/base.py +125 -0
  68. parse_bench/evaluation/metrics/parse/llm_normalization/config.py +44 -0
  69. parse_bench/evaluation/metrics/parse/llm_normalization/postprocess.py +322 -0
  70. parse_bench/evaluation/metrics/parse/llm_normalization/strategy_judge.py +541 -0
  71. parse_bench/evaluation/metrics/parse/mermaid_graph.py +682 -0
  72. parse_bench/evaluation/metrics/parse/rule_based_judge_metric.py +56 -0
  73. parse_bench/evaluation/metrics/parse/rule_based_metric.py +434 -0
  74. parse_bench/evaluation/metrics/parse/rules_bag.py +1161 -0
  75. parse_bench/evaluation/metrics/parse/rules_base.py +751 -0
  76. parse_bench/evaluation/metrics/parse/rules_chart.py +1556 -0
  77. parse_bench/evaluation/metrics/parse/rules_diagram.py +591 -0
  78. parse_bench/evaluation/metrics/parse/rules_form.py +2274 -0
  79. parse_bench/evaluation/metrics/parse/rules_formatting.py +1500 -0
  80. parse_bench/evaluation/metrics/parse/rules_heading.py +228 -0
  81. parse_bench/evaluation/metrics/parse/rules_list.py +226 -0
  82. parse_bench/evaluation/metrics/parse/rules_page_decoration.py +276 -0
  83. parse_bench/evaluation/metrics/parse/rules_table.py +1666 -0
  84. parse_bench/evaluation/metrics/parse/rules_text.py +340 -0
  85. parse_bench/evaluation/metrics/parse/rules_watermark.py +105 -0
  86. parse_bench/evaluation/metrics/parse/structural_consistency_metric.py +251 -0
  87. parse_bench/evaluation/metrics/parse/table_extraction.py +152 -0
  88. parse_bench/evaluation/metrics/parse/table_merging.py +195 -0
  89. parse_bench/evaluation/metrics/parse/table_pairing.py +87 -0
  90. parse_bench/evaluation/metrics/parse/table_parsing.py +955 -0
  91. parse_bench/evaluation/metrics/parse/table_record_match_metric.py +1453 -0
  92. parse_bench/evaluation/metrics/parse/table_splitting.py +301 -0
  93. parse_bench/evaluation/metrics/parse/table_title_stripping.py +530 -0
  94. parse_bench/evaluation/metrics/parse/teds_metric.py +600 -0
  95. parse_bench/evaluation/metrics/parse/test_rules.py +120 -0
  96. parse_bench/evaluation/metrics/parse/test_types.py +103 -0
  97. parse_bench/evaluation/metrics/parse/text_content_projection.py +175 -0
  98. parse_bench/evaluation/metrics/parse/text_similarity_metric.py +61 -0
  99. parse_bench/evaluation/metrics/parse/utils.py +885 -0
  100. parse_bench/evaluation/metrics/qa/__init__.py +5 -0
  101. parse_bench/evaluation/metrics/qa/answer_comparison.py +380 -0
  102. parse_bench/evaluation/qa/__init__.py +5 -0
  103. parse_bench/evaluation/qa/llm_service.py +335 -0
  104. parse_bench/evaluation/reports/__init__.py +8 -0
  105. parse_bench/evaluation/reports/csv.py +64 -0
  106. parse_bench/evaluation/reports/html.py +338 -0
  107. parse_bench/evaluation/reports/markdown.py +98 -0
  108. parse_bench/evaluation/reports/rule_csv.py +22 -0
  109. parse_bench/evaluation/runner.py +1864 -0
  110. parse_bench/evaluation/stats.py +104 -0
  111. parse_bench/extensions.py +72 -0
  112. parse_bench/inference/__init__.py +33 -0
  113. parse_bench/inference/chunkr_layout_extraction.py +160 -0
  114. parse_bench/inference/cli.py +484 -0
  115. parse_bench/inference/layout_extraction.py +422 -0
  116. parse_bench/inference/pipelines/__init__.py +59 -0
  117. parse_bench/inference/pipelines/extract.py +39 -0
  118. parse_bench/inference/pipelines/layout.py +142 -0
  119. parse_bench/inference/pipelines/parse.py +2603 -0
  120. parse_bench/inference/pipelines.py +0 -0
  121. parse_bench/inference/providers/__init__.py +28 -0
  122. parse_bench/inference/providers/base.py +196 -0
  123. parse_bench/inference/providers/cancellation.py +137 -0
  124. parse_bench/inference/providers/extract/__init__.py +22 -0
  125. parse_bench/inference/providers/extract/citations.py +549 -0
  126. parse_bench/inference/providers/extract/extend.py +851 -0
  127. parse_bench/inference/providers/extract/llamaextract_v2_api.py +583 -0
  128. parse_bench/inference/providers/layoutdet/__init__.py +25 -0
  129. parse_bench/inference/providers/layoutdet/adapters.py +946 -0
  130. parse_bench/inference/providers/layoutdet/base.py +203 -0
  131. parse_bench/inference/providers/layoutdet/chandra.py +449 -0
  132. parse_bench/inference/providers/layoutdet/docling.py +125 -0
  133. parse_bench/inference/providers/layoutdet/dots_ocr.py +606 -0
  134. parse_bench/inference/providers/layoutdet/layout_v3.py +137 -0
  135. parse_bench/inference/providers/layoutdet/layout_v3_byoc.py +204 -0
  136. parse_bench/inference/providers/layoutdet/paddle.py +117 -0
  137. parse_bench/inference/providers/layoutdet/qwen3vl.py +360 -0
  138. parse_bench/inference/providers/layoutdet/surya.py +250 -0
  139. parse_bench/inference/providers/layoutdet/yolo.py +109 -0
  140. parse_bench/inference/providers/parse/__init__.py +64 -0
  141. parse_bench/inference/providers/parse/_docling_common.py +233 -0
  142. parse_bench/inference/providers/parse/_layout_utils.py +611 -0
  143. parse_bench/inference/providers/parse/amazon_nova.py +515 -0
  144. parse_bench/inference/providers/parse/anthropic.py +882 -0
  145. parse_bench/inference/providers/parse/azure_document_intelligence.py +700 -0
  146. parse_bench/inference/providers/parse/chandra2.py +633 -0
  147. parse_bench/inference/providers/parse/chunkr.py +268 -0
  148. parse_bench/inference/providers/parse/databricks_ai_parse.py +724 -0
  149. parse_bench/inference/providers/parse/datalab.py +370 -0
  150. parse_bench/inference/providers/parse/deepseekocr2.py +382 -0
  151. parse_bench/inference/providers/parse/docling.py +281 -0
  152. parse_bench/inference/providers/parse/docling_serve.py +289 -0
  153. parse_bench/inference/providers/parse/dots_ocr.py +574 -0
  154. parse_bench/inference/providers/parse/extend_parse.py +710 -0
  155. parse_bench/inference/providers/parse/falconocr.py +436 -0
  156. parse_bench/inference/providers/parse/florin_parser_nano.py +559 -0
  157. parse_bench/inference/providers/parse/gemma4.py +472 -0
  158. parse_bench/inference/providers/parse/glm_zai.py +229 -0
  159. parse_bench/inference/providers/parse/google.py +1125 -0
  160. parse_bench/inference/providers/parse/google_agentic_vision.py +819 -0
  161. parse_bench/inference/providers/parse/google_docai.py +776 -0
  162. parse_bench/inference/providers/parse/google_docai_layout_normalization.py +573 -0
  163. parse_bench/inference/providers/parse/granite_vision.py +515 -0
  164. parse_bench/inference/providers/parse/infinity_parser2.py +704 -0
  165. parse_bench/inference/providers/parse/kdl_frontier_nano.py +3327 -0
  166. parse_bench/inference/providers/parse/landingai.py +452 -0
  167. parse_bench/inference/providers/parse/liteparse.py +350 -0
  168. parse_bench/inference/providers/parse/llamaparse.py +677 -0
  169. parse_bench/inference/providers/parse/llamaparse_v2_normalization.py +1013 -0
  170. parse_bench/inference/providers/parse/markitdown.py +138 -0
  171. parse_bench/inference/providers/parse/mineru25.py +405 -0
  172. parse_bench/inference/providers/parse/mineru2605pro.py +432 -0
  173. parse_bench/inference/providers/parse/mineru_diffusion.py +371 -0
  174. parse_bench/inference/providers/parse/mistral_ocr.py +546 -0
  175. parse_bench/inference/providers/parse/nemotron_omni.py +473 -0
  176. parse_bench/inference/providers/parse/oi_parser.py +222 -0
  177. parse_bench/inference/providers/parse/openai.py +740 -0
  178. parse_bench/inference/providers/parse/opendataloader.py +152 -0
  179. parse_bench/inference/providers/parse/paddleocr.py +624 -0
  180. parse_bench/inference/providers/parse/pdf_inspector.py +142 -0
  181. parse_bench/inference/providers/parse/pulse.py +785 -0
  182. parse_bench/inference/providers/parse/pymupdf.py +207 -0
  183. parse_bench/inference/providers/parse/pymupdf4llm.py +356 -0
  184. parse_bench/inference/providers/parse/pypdf.py +179 -0
  185. parse_bench/inference/providers/parse/qwen.py +678 -0
  186. parse_bench/inference/providers/parse/rakedoc_nano.py +70 -0
  187. parse_bench/inference/providers/parse/reducto.py +546 -0
  188. parse_bench/inference/providers/parse/surya2.py +372 -0
  189. parse_bench/inference/providers/parse/tesseract.py +301 -0
  190. parse_bench/inference/providers/parse/textract.py +694 -0
  191. parse_bench/inference/providers/parse/unlimitedocr.py +346 -0
  192. parse_bench/inference/providers/parse/unstructured.py +485 -0
  193. parse_bench/inference/providers/parse/warp_ingest.py +199 -0
  194. parse_bench/inference/providers/registry.py +49 -0
  195. parse_bench/inference/renormalize.py +170 -0
  196. parse_bench/inference/runner.py +2023 -0
  197. parse_bench/layout_label_mapping.py +424 -0
  198. parse_bench/layout_projection.py +179 -0
  199. parse_bench/pipeline/__init__.py +1 -0
  200. parse_bench/pipeline/cli.py +549 -0
  201. parse_bench/schemas/__init__.py +33 -0
  202. parse_bench/schemas/evaluation.py +93 -0
  203. parse_bench/schemas/extract_output.py +36 -0
  204. parse_bench/schemas/layout_detection_output.py +545 -0
  205. parse_bench/schemas/layout_ontology.py +315 -0
  206. parse_bench/schemas/metrics.py +69 -0
  207. parse_bench/schemas/parse_output.py +152 -0
  208. parse_bench/schemas/pipeline.py +22 -0
  209. parse_bench/schemas/pipeline_io.py +106 -0
  210. parse_bench/schemas/product.py +97 -0
  211. parse_bench/test_cases/__init__.py +25 -0
  212. parse_bench/test_cases/bbox_value_strict_comparator.py +880 -0
  213. parse_bench/test_cases/extract_field_paths.py +164 -0
  214. parse_bench/test_cases/layout_attribution_generation.py +287 -0
  215. parse_bench/test_cases/loader.py +652 -0
  216. parse_bench/test_cases/parse_rule_schemas.py +1071 -0
  217. parse_bench/test_cases/rule_filters.py +32 -0
  218. parse_bench/test_cases/rule_ids.py +107 -0
  219. parse_bench/test_cases/schema.py +427 -0
  220. parse_bench/utils/__init__.py +15 -0
  221. parse_bench/utils/gemini_layout_utils.py +670 -0
  222. parse_bench/utils/text_aggregation.py +100 -0
  223. parse_bench-1.0.0.dist-info/METADATA +476 -0
  224. parse_bench-1.0.0.dist-info/RECORD +227 -0
  225. parse_bench-1.0.0.dist-info/WHEEL +4 -0
  226. parse_bench-1.0.0.dist-info/entry_points.txt +2 -0
  227. parse_bench-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,515 @@
1
+ """Provider for Amazon Nova vision-based PARSE via the Bedrock Converse API.
2
+
3
+ Amazon Nova 2 models are served through Amazon Bedrock's ``converse`` API
4
+ (``bedrock-runtime``). Pages are rendered to images and sent as Converse
5
+ ``image`` content blocks, mirroring the OpenAI/Anthropic vision parse
6
+ providers so the resulting metrics are comparable.
7
+
8
+ Nova 2 Lite is not available for in-region inference in most Regions, so
9
+ pipelines address it through a cross-Region inference profile ID
10
+ (``us.``/``eu.``/``jp.`` for a geography, ``global.`` for worldwide routing).
11
+ See the model card:
12
+ https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-lite.html
13
+ """
14
+
15
+ import io
16
+ import os
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+ from typing import Any, NoReturn
20
+
21
+ from PIL import Image
22
+
23
+ from parse_bench.inference.providers.base import (
24
+ Provider,
25
+ ProviderConfigError,
26
+ ProviderPermanentError,
27
+ ProviderRateLimitError,
28
+ ProviderTransientError,
29
+ )
30
+ from parse_bench.inference.providers.parse._layout_utils import (
31
+ SYSTEM_PROMPT_LAYOUT,
32
+ USER_PROMPT_LAYOUT,
33
+ build_layout_pages,
34
+ close_open_ended_bands,
35
+ extract_layout_blocks_lenient,
36
+ items_to_markdown,
37
+ parse_layout_blocks,
38
+ )
39
+ from parse_bench.inference.providers.registry import register_provider
40
+ from parse_bench.schemas.parse_output import PageIR, ParseLayoutPageIR, ParseOutput
41
+ from parse_bench.schemas.pipeline import PipelineSpec
42
+ from parse_bench.schemas.pipeline_io import (
43
+ InferenceRequest,
44
+ InferenceResult,
45
+ RawInferenceResult,
46
+ )
47
+ from parse_bench.schemas.product import ProductType
48
+
49
+ # Cross-Region inference profile prefixes. Bedrock bills a geo profile
50
+ # (us./eu./jp.) at the source Region's standard on-demand rate and the global
51
+ # profile at its own (cheaper) cross-region-global rate, so pricing is keyed by
52
+ # the base model ID plus whether the request goes through `global.`.
53
+ _GEO_PROFILE_PREFIXES = ("us.", "eu.", "jp.", "apac.", "global.")
54
+
55
+ # Amazon Bedrock on-demand token pricing, USD per million tokens (input, output),
56
+ # us-east-1 "Standard" service tier. Values come from the AWS Price List API
57
+ # (offer code AmazonBedrock, us-east-1), usage types
58
+ # `USE1-Nova2.0Lite-{input,output}-tokens` and their `-cross-region-global`
59
+ # variants. Reasoning tokens are billed at the output rate.
60
+ # Source: https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrock/current/us-east-1/index.json
61
+ _NOVA_PRICING_PER_M: dict[str, tuple[float, float]] = {
62
+ # base-model-id: (input_per_M, output_per_M)
63
+ "amazon.nova-2-lite-v1:0": (0.33, 2.75),
64
+ }
65
+
66
+ # Same models routed through the `global.` inference profile.
67
+ _NOVA_GLOBAL_PRICING_PER_M: dict[str, tuple[float, float]] = {
68
+ "amazon.nova-2-lite-v1:0": (0.30, 2.50),
69
+ }
70
+
71
+ # Reasoning ("extended thinking") is disabled by default on Nova 2; when enabled
72
+ # an effort level is required.
73
+ _VALID_REASONING_EFFORTS = ("low", "medium", "high")
74
+
75
+ # Bedrock client error codes that the runner should retry.
76
+ _TRANSIENT_ERROR_CODES = frozenset(
77
+ {
78
+ "InternalServerException",
79
+ "ModelNotReadyException",
80
+ "ModelTimeoutException",
81
+ "ServiceUnavailableException",
82
+ }
83
+ )
84
+ _RATE_LIMIT_ERROR_CODES = frozenset(
85
+ {
86
+ "ThrottlingException",
87
+ "TooManyRequestsException",
88
+ "ServiceQuotaExceededException",
89
+ }
90
+ )
91
+
92
+
93
+ @register_provider("amazon_nova")
94
+ class AmazonNovaProvider(Provider):
95
+ """
96
+ Provider for Amazon Nova vision-based document parsing on Bedrock.
97
+
98
+ Renders PDF pages to images and sends each page through the Bedrock
99
+ Converse API with the shared layout prompt, so one pipeline produces both
100
+ the markdown and the per-element bounding boxes.
101
+ """
102
+
103
+ # Nova image-understanding limits: 8,000 x 8,000 px, 25 MB total request
104
+ # payload. Images are base64-encoded on the wire, so the raw byte budget is
105
+ # 3/4 of the payload cap.
106
+ # Source: https://docs.aws.amazon.com/nova/latest/nova2-userguide/using-multimodal-models.html
107
+ MAX_IMAGE_DIMENSION = 8000
108
+ MAX_IMAGE_SIZE_BYTES = int(24 * 1024 * 1024 * 3 / 4) # ~18 MB raw -> ~24 MB base64
109
+
110
+ def __init__(self, provider_name: str, base_config: dict[str, Any] | None = None):
111
+ """
112
+ Initialize the provider.
113
+
114
+ :param provider_name: Name of the provider
115
+ :param base_config: Optional configuration with:
116
+ - `model`: Bedrock model or inference profile ID
117
+ (default: "us.amazon.nova-2-lite-v1:0")
118
+ - `region`: AWS Region for the bedrock-runtime endpoint
119
+ (default: `$AWS_REGION`, else "us-east-1")
120
+ - `dpi`: DPI for PDF to image conversion (default: 150)
121
+ - `max_tokens`: Max tokens per response (default: 8192, model max 64K)
122
+ - `timeout`: Per-request read timeout in seconds (default: 300)
123
+ - `reasoning_effort`: "low", "medium" or "high" to enable extended
124
+ thinking. Unset (default) leaves reasoning disabled, which is the
125
+ Bedrock default for Nova 2.
126
+ - `temperature`: sampling temperature (default: 0 when reasoning is
127
+ disabled, omitted when it is enabled)
128
+ - `top_p`: nucleus sampling threshold (default: omitted)
129
+ """
130
+ super().__init__(provider_name, base_config)
131
+
132
+ self._model: str = self.base_config.get("model", "us.amazon.nova-2-lite-v1:0")
133
+ self._region: str = self.base_config.get("region") or os.environ.get("AWS_REGION") or "us-east-1"
134
+ self._dpi = self.base_config.get("dpi", 150)
135
+ self._max_tokens = self.base_config.get("max_tokens", 8192)
136
+ self._timeout = self.base_config.get("timeout", 300)
137
+ self._reasoning_effort = self.base_config.get("reasoning_effort", None)
138
+ self._top_p = self.base_config.get("top_p", None)
139
+ if self._reasoning_effort is not None and self._reasoning_effort not in _VALID_REASONING_EFFORTS:
140
+ raise ProviderConfigError(
141
+ f"Invalid reasoning_effort '{self._reasoning_effort}'. Must be one of {_VALID_REASONING_EFFORTS}."
142
+ )
143
+
144
+ # Nova 2 rejects sampling parameters when reasoning runs at high effort,
145
+ # so temperature defaults to 0 (deterministic parsing) only while
146
+ # reasoning is disabled.
147
+ # https://docs.aws.amazon.com/nova/latest/nova2-userguide/using-converse-api.html
148
+ default_temperature = 0 if self._reasoning_effort is None else None
149
+ self._temperature = self.base_config.get("temperature", default_temperature)
150
+ if self._reasoning_effort is not None and (self._temperature is not None or self._top_p is not None):
151
+ raise ProviderConfigError(
152
+ "Nova 2 rejects temperature/top_p when reasoning is enabled; "
153
+ "drop them from the pipeline config or unset reasoning_effort."
154
+ )
155
+
156
+ if not os.environ.get("AWS_ACCESS_KEY_ID") and not os.environ.get("AWS_BEARER_TOKEN_BEDROCK"):
157
+ raise ProviderConfigError(
158
+ "No AWS credentials found. Set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY "
159
+ "or AWS_BEARER_TOKEN_BEDROCK for Bedrock access."
160
+ )
161
+
162
+ try:
163
+ import boto3
164
+ from botocore.config import Config
165
+
166
+ self._client = boto3.client(
167
+ "bedrock-runtime",
168
+ region_name=self._region,
169
+ config=Config(
170
+ read_timeout=self._timeout,
171
+ connect_timeout=30,
172
+ retries={"max_attempts": 3, "mode": "standard"},
173
+ ),
174
+ )
175
+ except ImportError as e:
176
+ raise ProviderConfigError("boto3 package not installed. Run: pip install boto3") from e
177
+
178
+ def _base_model_id(self) -> str:
179
+ """Strip any cross-Region inference profile prefix from the model ID."""
180
+ for prefix in _GEO_PROFILE_PREFIXES:
181
+ if self._model.startswith(prefix):
182
+ return self._model[len(prefix) :]
183
+ return self._model
184
+
185
+ def _get_pricing(self) -> tuple[float, float]:
186
+ """Return (input_rate, output_rate) in USD per million tokens."""
187
+ table = _NOVA_GLOBAL_PRICING_PER_M if self._model.startswith("global.") else _NOVA_PRICING_PER_M
188
+ return table.get(self._base_model_id(), (0.0, 0.0))
189
+
190
+ def _raise_bedrock_error(self, e: Exception) -> NoReturn:
191
+ """Classify a Bedrock/botocore exception as transient, rate-limited or permanent."""
192
+ from botocore.exceptions import BotoCoreError, ClientError
193
+
194
+ if isinstance(e, ClientError):
195
+ code = e.response.get("Error", {}).get("Code", "")
196
+ if code in _RATE_LIMIT_ERROR_CODES:
197
+ raise ProviderRateLimitError(f"Bedrock rate limited ({code}): {e}") from e
198
+ if code in _TRANSIENT_ERROR_CODES:
199
+ raise ProviderTransientError(f"Transient Bedrock error ({code}): {e}") from e
200
+ raise ProviderPermanentError(f"Error calling Bedrock Converse API ({code}): {e}") from e
201
+
202
+ if isinstance(e, BotoCoreError):
203
+ # Connection/read timeouts and endpoint resolution blips.
204
+ raise ProviderTransientError(f"Transient error calling Bedrock Converse API: {e}") from e
205
+
206
+ raise ProviderPermanentError(f"Error calling Bedrock Converse API: {e}") from e
207
+
208
+ @staticmethod
209
+ def _extract_usage(response: dict[str, Any]) -> dict[str, int]:
210
+ """Extract token counts from a Converse response.
211
+
212
+ Nova bills reasoning tokens as output tokens and does not report them
213
+ separately, so ``thinking_tokens`` is always 0 — the spend is already
214
+ inside ``output_tokens``.
215
+ """
216
+ usage = response.get("usage") or {}
217
+ input_tok = int(usage.get("inputTokens", 0) or 0)
218
+ output_tok = int(usage.get("outputTokens", 0) or 0)
219
+ total_tok = int(usage.get("totalTokens", 0) or 0) or (input_tok + output_tok)
220
+ return {
221
+ "input_tokens": input_tok,
222
+ "output_tokens": output_tok,
223
+ "thinking_tokens": 0,
224
+ "total_tokens": total_tok,
225
+ }
226
+
227
+ @staticmethod
228
+ def _extract_text(response: dict[str, Any]) -> str:
229
+ """Concatenate the text blocks of a Converse response.
230
+
231
+ Reasoning blocks (``reasoningContent``) are skipped — Nova 2 returns
232
+ them as ``[REDACTED]`` and they are not part of the parsed document.
233
+ """
234
+ content = response.get("output", {}).get("message", {}).get("content", []) or []
235
+ return "".join(block["text"] for block in content if isinstance(block, dict) and "text" in block)
236
+
237
+ def _prepare_image_for_api(self, image: Image.Image) -> Image.Image:
238
+ """Resize an image that exceeds Nova's pixel limits."""
239
+ width, height = image.size
240
+ max_dim = max(width, height)
241
+
242
+ if max_dim <= self.MAX_IMAGE_DIMENSION:
243
+ return image
244
+
245
+ scale = self.MAX_IMAGE_DIMENSION / max_dim
246
+ return image.resize((int(width * scale), int(height * scale)), Image.Resampling.LANCZOS)
247
+
248
+ def _image_to_jpeg_bytes(self, image: Image.Image) -> bytes:
249
+ """Encode a PIL image as JPEG bytes within Nova's payload budget."""
250
+ image = self._prepare_image_for_api(image)
251
+
252
+ if image.mode in ("RGBA", "P"):
253
+ image = image.convert("RGB")
254
+
255
+ quality = 85
256
+ min_quality = 20
257
+
258
+ while quality >= min_quality:
259
+ buffer = io.BytesIO()
260
+ image.save(buffer, format="JPEG", quality=quality)
261
+ data = buffer.getvalue()
262
+ if len(data) <= self.MAX_IMAGE_SIZE_BYTES:
263
+ return data
264
+ quality -= 10
265
+
266
+ while True:
267
+ width, height = image.size
268
+ new_width, new_height = int(width * 0.8), int(height * 0.8)
269
+ if new_width < 100 or new_height < 100:
270
+ break
271
+
272
+ image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
273
+ buffer = io.BytesIO()
274
+ image.save(buffer, format="JPEG", quality=min_quality)
275
+ data = buffer.getvalue()
276
+ if len(data) <= self.MAX_IMAGE_SIZE_BYTES:
277
+ return data
278
+
279
+ buffer = io.BytesIO()
280
+ image.save(buffer, format="JPEG", quality=min_quality)
281
+ return buffer.getvalue()
282
+
283
+ def _pdf_to_images(self, pdf_path: str) -> list[Image.Image]:
284
+ """Convert PDF pages to images."""
285
+ try:
286
+ from pdf2image import convert_from_path
287
+ except ImportError as e:
288
+ raise ProviderConfigError("pdf2image package not installed. Run: pip install pdf2image") from e
289
+
290
+ try:
291
+ return convert_from_path(pdf_path, dpi=self._dpi)
292
+ except Exception as e:
293
+ raise ProviderPermanentError(f"Failed to convert PDF to images: {e}") from e
294
+
295
+ def _converse(self, image: Image.Image, system_prompt: str, user_prompt: str) -> tuple[str, dict[str, int], str]:
296
+ """Send one page image to Bedrock Converse and return (text, usage, stop_reason)."""
297
+ image_bytes = self._image_to_jpeg_bytes(image)
298
+
299
+ inference_config: dict[str, Any] = {"maxTokens": self._max_tokens}
300
+ if self._temperature is not None:
301
+ inference_config["temperature"] = self._temperature
302
+ if self._top_p is not None:
303
+ inference_config["topP"] = self._top_p
304
+
305
+ kwargs: dict[str, Any] = {
306
+ "modelId": self._model,
307
+ "system": [{"text": system_prompt}],
308
+ "messages": [
309
+ {
310
+ "role": "user",
311
+ "content": [
312
+ {"image": {"format": "jpeg", "source": {"bytes": image_bytes}}},
313
+ {"text": user_prompt},
314
+ ],
315
+ }
316
+ ],
317
+ "inferenceConfig": inference_config,
318
+ }
319
+ if self._reasoning_effort is not None:
320
+ kwargs["additionalModelRequestFields"] = {
321
+ "reasoningConfig": {"type": "enabled", "maxReasoningEffort": self._reasoning_effort}
322
+ }
323
+
324
+ try:
325
+ response = self._client.converse(**kwargs)
326
+ except Exception as e:
327
+ self._raise_bedrock_error(e)
328
+
329
+ stop_reason = str(response.get("stopReason", ""))
330
+ text = self._extract_text(response)
331
+
332
+ # Bedrock's built-in content filter returns HTTP 200 with a canned
333
+ # notice in the text block ("The generated text has been blocked by our
334
+ # content filters.") and zero output tokens. That notice is not page
335
+ # content, so it must never reach ParseOutput. Treated as transient so
336
+ # the runner retries — matching how the Gemini provider handles blocked
337
+ # responses — and a page that stays blocked surfaces as a failed doc
338
+ # rather than as an empty parse.
339
+ if stop_reason == "content_filtered":
340
+ raise ProviderTransientError(f"Bedrock content filter blocked the response (stopReason={stop_reason})")
341
+ if not text.strip():
342
+ raise ProviderTransientError(f"Bedrock Converse returned no text (stopReason={stop_reason or 'unknown'})")
343
+
344
+ return text, self._extract_usage(response), stop_reason
345
+
346
+ def _parse_image_with_layout(self, image: Image.Image) -> tuple[list[dict[str, Any]], str, dict[str, int], str]:
347
+ """Parse a page image to layout-annotated markdown blocks."""
348
+ text, usage, stop_reason = self._converse(image, SYSTEM_PROMPT_LAYOUT, USER_PROMPT_LAYOUT)
349
+ # Prefer the lenient reader (Nova uses <TABLE>/<p> wrappers and leaves
350
+ # them unclosed); fall back to the strict shared parser if it finds
351
+ # nothing, so this can never score worse than the default path.
352
+ items = extract_layout_blocks_lenient(text) or parse_layout_blocks(text)
353
+ return close_open_ended_bands(items), text, usage, stop_reason
354
+
355
+ def run_inference(self, pipeline: PipelineSpec, request: InferenceRequest) -> RawInferenceResult:
356
+ """
357
+ Run inference and return raw results.
358
+
359
+ :param pipeline: Pipeline specification
360
+ :param request: Inference request
361
+ :return: Raw inference result
362
+ """
363
+ if request.product_type != ProductType.PARSE:
364
+ raise ProviderPermanentError(
365
+ f"AmazonNovaProvider only supports PARSE product type, got {request.product_type}"
366
+ )
367
+
368
+ source_path = Path(request.source_file_path)
369
+ if not source_path.exists():
370
+ raise ProviderPermanentError(f"Source file not found: {source_path}")
371
+
372
+ supported_extensions = {".pdf", ".png", ".jpg", ".jpeg"}
373
+ if source_path.suffix.lower() not in supported_extensions:
374
+ raise ProviderPermanentError(
375
+ f"AmazonNovaProvider supports {supported_extensions}, got {source_path.suffix}"
376
+ )
377
+
378
+ started_at = datetime.now()
379
+
380
+ try:
381
+ page_usages: list[dict[str, int]] = []
382
+
383
+ if source_path.suffix.lower() == ".pdf":
384
+ images = self._pdf_to_images(str(source_path))
385
+ else:
386
+ images = [Image.open(source_path)]
387
+
388
+ pages: list[dict[str, Any]] = []
389
+ for page_index, image in enumerate(images):
390
+ items, raw_content, usage, stop_reason = self._parse_image_with_layout(image)
391
+ page_usages.append(usage)
392
+ pages.append(
393
+ {
394
+ "page_index": page_index,
395
+ "items": items,
396
+ "raw_content": raw_content,
397
+ "stop_reason": stop_reason,
398
+ "width": image.width,
399
+ "height": image.height,
400
+ }
401
+ )
402
+ num_pages = len(images)
403
+
404
+ completed_at = datetime.now()
405
+ latency_ms = int((completed_at - started_at).total_seconds() * 1000)
406
+
407
+ total_input = sum(u["input_tokens"] for u in page_usages)
408
+ total_output = sum(u["output_tokens"] for u in page_usages)
409
+ total_thinking = sum(u["thinking_tokens"] for u in page_usages)
410
+ total_all = sum(u["total_tokens"] for u in page_usages)
411
+
412
+ input_rate, output_rate = self._get_pricing()
413
+ cost = (total_input * input_rate + (total_output + total_thinking) * output_rate) / 1_000_000
414
+
415
+ config_info: dict[str, Any] = {
416
+ "dpi": self._dpi,
417
+ "max_tokens": self._max_tokens,
418
+ "region": self._region,
419
+ }
420
+ if self._reasoning_effort is not None:
421
+ config_info["reasoning_effort"] = self._reasoning_effort
422
+ if self._temperature is not None:
423
+ config_info["temperature"] = self._temperature
424
+ if self._top_p is not None:
425
+ config_info["top_p"] = self._top_p
426
+
427
+ raw_output = {
428
+ "pages": pages,
429
+ "num_pages": num_pages,
430
+ "model": self._model,
431
+ "config": config_info,
432
+ "input_tokens": total_input,
433
+ "output_tokens": total_output,
434
+ "thinking_tokens": total_thinking,
435
+ "total_tokens": total_all,
436
+ "cost_usd": cost,
437
+ "cost_per_page_usd": cost / num_pages if num_pages > 0 else 0.0,
438
+ "input_tokens_per_page": total_input / num_pages if num_pages > 0 else 0.0,
439
+ "output_tokens_per_page": total_output / num_pages if num_pages > 0 else 0.0,
440
+ }
441
+
442
+ return RawInferenceResult(
443
+ request=request,
444
+ pipeline=pipeline,
445
+ pipeline_name=pipeline.pipeline_name,
446
+ product_type=request.product_type,
447
+ raw_output=raw_output,
448
+ started_at=started_at,
449
+ completed_at=completed_at,
450
+ latency_in_ms=latency_ms,
451
+ )
452
+
453
+ except (ProviderPermanentError, ProviderRateLimitError, ProviderTransientError, ProviderConfigError):
454
+ raise
455
+ except Exception as e:
456
+ raise ProviderPermanentError(f"Unexpected error during inference: {e}") from e
457
+
458
+ def normalize(self, raw_result: RawInferenceResult) -> InferenceResult:
459
+ """
460
+ Normalize raw inference result to produce ParseOutput.
461
+
462
+ :param raw_result: Raw inference result from run_inference()
463
+ :return: Inference result with both raw and normalized outputs
464
+ """
465
+ if raw_result.product_type != ProductType.PARSE:
466
+ raise ProviderPermanentError(
467
+ f"AmazonNovaProvider only supports PARSE product type, got {raw_result.product_type}"
468
+ )
469
+
470
+ pages: list[PageIR] = []
471
+ page_markdowns: list[str] = []
472
+ layout_pages: list[ParseLayoutPageIR] = []
473
+
474
+ for page_data in raw_result.raw_output.get("pages", []):
475
+ page_index = page_data.get("page_index", 0)
476
+
477
+ items = page_data.get("items", [])
478
+ image_width = page_data.get("width", 0)
479
+ image_height = page_data.get("height", 0)
480
+ markdown = items_to_markdown(items)
481
+ layout_pages.extend(
482
+ build_layout_pages(
483
+ items,
484
+ image_width,
485
+ image_height,
486
+ markdown,
487
+ page_number=page_index + 1,
488
+ )
489
+ )
490
+
491
+ pages.append(PageIR(page_index=page_index, markdown=markdown))
492
+ page_markdowns.append(markdown)
493
+
494
+ pages.sort(key=lambda p: p.page_index)
495
+ full_markdown = "\n\n".join(page_markdowns)
496
+
497
+ output = ParseOutput(
498
+ task_type="parse",
499
+ example_id=raw_result.request.example_id,
500
+ pipeline_name=raw_result.pipeline_name,
501
+ pages=pages,
502
+ markdown=full_markdown,
503
+ layout_pages=layout_pages,
504
+ )
505
+
506
+ return InferenceResult(
507
+ request=raw_result.request,
508
+ pipeline_name=raw_result.pipeline_name,
509
+ product_type=raw_result.product_type,
510
+ raw_output=raw_result.raw_output,
511
+ output=output,
512
+ started_at=raw_result.started_at,
513
+ completed_at=raw_result.completed_at,
514
+ latency_in_ms=raw_result.latency_in_ms,
515
+ )