Skip to content

fix(nodes): preserve financial scale headers in the llamaparse node#1594

Open
Ansh-Karnwal wants to merge 4 commits into
rocketride-org:developfrom
Ansh-Karnwal:fix/RR-1407-llamaparse-scale-header
Open

fix(nodes): preserve financial scale headers in the llamaparse node#1594
Ansh-Karnwal wants to merge 4 commits into
rocketride-org:developfrom
Ansh-Karnwal:fix/RR-1407-llamaparse-scale-header

Conversation

@Ansh-Karnwal

@Ansh-Karnwal Ansh-Karnwal commented Jul 15, 2026

Copy link
Copy Markdown

Problem

Fixes #1407. Financial statements declare magnitude in a caption above the table ("(In millions)", "$ in thousands"). When that caption gets separated from its table by downstream chunking or an LLM step, every figure reads as raw units and is silently 1,000x / 1,000,000x / 1,000,000,000x off. Nothing downstream catches it, because a wrong-by-1e6 number looks exactly like a correct one.

A repo-wide grep for scale / millions in nodes/ returns nothing today, so there is no scale handling anywhere in this node.

Approach

Make the loss non-silent. A new pure-Python module scale.py runs after the parser assembles its Markdown and:

  • Detects scale captions the parser already emitted and normalizes each to a unit, factor, and optional currency. Prose uses of the same words are rejected ("millions of users", "billions served", "hundreds of thousands of dollars", "$5 million in revenue").
  • Welds a marker to the table. When a caption is in scope for a table, a normalized line is injected directly above it, e.g. > Scale: amounts in millions (×1,000,000). A later chunker or LLM cannot separate the scale from its figures.
  • Flags a missing scale. When a table looks numeric and no caption is in scope, a warning line is injected above it and recorded structurally. A visible warning is far cheaper than a silent magnitude error.
  • Surfaces detected_scales and scale_warnings in parsing_metadata so downstream nodes get a structured signal, not just prose.

Scope is bounded by page/section breaks, so a caption on one page is not attached to an unrelated table on the next. Annotation is idempotent and wrapped so any failure falls back to the untouched text and never blocks parsing.

The |-row table-detection heuristic is factored into scale.py and reused by IInstance.extract_tables_from_text, so there is one implementation with no change to the table lane output.

Out of scope

Recovering a caption that LlamaParse dropped from the source entirely is not handled here. That needs an independent source-text extraction and belongs to the audit-grade datalab_parse node (this node's README already documents it as not audit-grade). A hard XBRL / prior-year magnitude cross-check is the natural follow-up.

Testing

nodes/test/test_llamaparse_scale.py (pure logic, no LlamaParse API or engine needed):

  • 18/18 labeled positive captions detected with the correct factor.
  • 0 false positives across 10 adversarial negatives.
  • Annotation, page-break scoping, idempotence, empty-input safety, and table-heuristic parity.

Locally: 12/12 tests pass, ruff check and ruff format --check clean on all touched files.

Docs

Updated the co-located node README with a "Scale-header preservation" section (detected captions, the injected marker and warning, the new parsing_metadata keys, and the out-of-scope note).

Summary by CodeRabbit

  • New Features
    • Preserve financial scale captions (e.g., “in millions/thousands”) alongside extracted Markdown tables.
    • Automatically inject idempotent “Scale” markers above applicable numeric tables, and add “scale?” warnings when finance-related tables lack in-scope scale captions.
    • Add parsing metadata for detected scales and per-table scale warning statuses.
  • Bug Fixes
    • Centralized Markdown table extraction to keep table detection consistent and avoid duplicate annotations.
  • Documentation
    • Document scale-header preservation behavior, observability fields, idempotency, non-blocking failure behavior, and the limitation when captions are irretrievably dropped upstream.
  • Tests
    • Add unit tests covering detection accuracy, currency extraction, annotation/warning logic, idempotence, and table parsing parity.

Financial statements declare magnitude in a caption above the table
("(In millions)", "$ in thousands"). When that caption is separated from
its table by downstream chunking or an LLM step, every figure reads as
raw units and is silently 1,000x / 1,000,000x / 1,000,000,000x off.
Nothing downstream catches it, since a wrong-by-1e6 number looks exactly
like a correct one.

This makes the loss non-silent. A new pure-Python module (scale.py) runs
after the parser assembles its Markdown and:

- detects scale captions the parser already emitted and normalizes each
  to a unit, factor, and optional currency. Prose uses of the same words
  ("millions of users", "billions served", "hundreds of thousands of
  dollars", "$5 million in revenue") are rejected.
- welds a normalized marker directly above each table whose caption is in
  scope ("> Scale: amounts in millions (x1,000,000)"), so a later chunker
  cannot split the scale away from its figures.
- flags any numeric table with no caption in scope with a visible warning
  line, and surfaces detected_scales and scale_warnings in
  parsing_metadata.

Scope is bounded by page/section breaks so a caption on one page is not
attached to an unrelated table on the next. Annotation is idempotent and
wrapped so any failure falls back to the untouched text and never blocks
parsing.

The table-detection heuristic is factored into scale.py and reused by
IInstance.extract_tables_from_text so there is one implementation, with
no change to the table lane output.

Recovering a caption the parser dropped from the source entirely is out
of scope; that needs source re-extraction and belongs to the audit-grade
datalab_parse node. Documented in the node README.

Adds test_llamaparse_scale.py: 18 labeled positive captions all detected
with the correct factor, 10 adversarial negatives with zero false
positives, plus annotation, scoping, idempotence, and table-parity cases.
@github-actions github-actions Bot added docs Documentation module:nodes Python pipeline nodes labels Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68b0c78b-889e-4d04-a750-c429de98633b

📥 Commits

Reviewing files that changed from the base of the PR and between 32a4ca9 and bc12aeb.

📒 Files selected for processing (1)
  • nodes/src/nodes/llamaparse/scale.py

📝 Walkthrough

Walkthrough

Changes

Scale-header preservation

Layer / File(s) Summary
Scale declaration detection
nodes/src/nodes/llamaparse/scale.py, nodes/test/test_llamaparse_scale.py
Adds normalized financial scale detection, currency extraction, Markdown table parsing, and detection tests.
Table scale annotation
nodes/src/nodes/llamaparse/scale.py, nodes/test/test_llamaparse_scale.py
Matches declarations to numeric tables, injects scale or missing-scale markers, records statuses, and tests scope, non-numeric tables, idempotence, and no-op behavior.
Parser and table-helper integration
nodes/src/nodes/llamaparse/parser.py, nodes/src/nodes/llamaparse/IInstance.py, nodes/src/nodes/llamaparse/README.md
Annotates parsed text in a guarded block, stores scale metadata, reuses shared table extraction, and documents the behavior and limitations.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Parser.parse
  participant scale.detect_scale_declarations
  participant scale.annotate_scale
  participant parsing_metadata
  Parser.parse->>scale.detect_scale_declarations: detect declarations in text_content
  Parser.parse->>scale.annotate_scale: annotate tables with declarations
  scale.annotate_scale-->>Parser.parse: annotated text and warning records
  Parser.parse->>parsing_metadata: store detected_scales and scale_warnings
Loading

Suggested reviewers: jmaionchi, rod-christensen, stepmikhaylov

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preserving financial scale headers in the LlamaParse node.
Linked Issues check ✅ Passed The changes implement scale-header detection and reinjection before downstream processing, matching issue #1407.
Out of Scope Changes check ✅ Passed The added docs, tests, parser update, and shared helper refactor all support the scale-header preservation objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nodes/src/nodes/llamaparse/scale.py`:
- Around line 366-370: Remove the unused table_spans parameter from the
annotate_scale function signature and update all callers to stop passing it,
while preserving the existing declarations and return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 83a48e34-0ccf-4f21-b2d0-82634d6d3a5a

📥 Commits

Reviewing files that changed from the base of the PR and between f41afb5 and e564f6a.

📒 Files selected for processing (5)
  • nodes/src/nodes/llamaparse/IInstance.py
  • nodes/src/nodes/llamaparse/README.md
  • nodes/src/nodes/llamaparse/parser.py
  • nodes/src/nodes/llamaparse/scale.py
  • nodes/test/test_llamaparse_scale.py

Comment thread nodes/src/nodes/llamaparse/scale.py
annotate_scale computes table blocks internally via iter_table_blocks;
the table_spans parameter was never read and no caller passed it. The
standalone find_markdown_table_spans helper is unchanged.
@Rod-Christensen

Copy link
Copy Markdown
Collaborator

Hi @Ansh-Karnwal — thanks for this PR, it's genuinely nice work. The problem statement is compelling and well-argued, and the engineering shows care: the pure-Python module with no engine dependency, the double failure isolation so parsing can never break, the adversarial negative corpus in the tests, and factoring the table heuristic into one shared implementation (I verified it's behavior-identical to the old one, and the table lane output stays byte-identical). The README section is a model for how node docs should read.

I ran the module against a range of documents and found a few things worth addressing before merge, roughly in order of importance:

1. The feature is unconditional, and non-financial documents pay for it. Any numeric table with no caption in scope gets the missing-scale warning — I ran a server-metrics report, an inventory list, and sports standings through it, and all three got "Scale not detected... Figures may be in thousands, millions, or billions" injected into the text lane. For RAG pipelines over ordinary documents that's misleading hedging welded into the content. Suggest either a config flag in services.json (default off), or a stronger gate before injecting the warning — e.g. require a currency signal or at least one detected caption elsewhere in the document.

2. Prose false positives weld a wrong scale. "The market opportunity, valued in billions, keeps growing" followed by an unrelated table gets > Scale: amounts in billions (×1,000,000,000) attached — the exact class of error the PR exists to prevent, now machine-authored. Gating the weld on _table_looks_numeric (it currently isn't consulted when a declaration is in scope) plus a tighter lead-in check would shrink this a lot.

3. parsing_metadata never reaches downstream nodes. The PR description says detected_scales/scale_warnings give downstream nodes a structured signal, but IInstance only debug-logs parsing_metadata in the close() path and never reads it in _process_document(). Either wire it onto a lane/tags or soften the claim in the description and README.

4. The marker detects itself. > Scale: amounts in millions (×1,000,000) matches the detector on a second pass ("in" before "millions"). Tables directly under a marker are protected, but in multi-table documents a previously unannotated table can pick up the marker-derived declaration on re-annotation, so it's not fully idempotent. Cheap fix: skip matches on lines starting with the marker prefix in detect_scale_declarations.

5. Smaller items: the warning marker's emoji should be plain text (project convention is no emoji in application output, and it already tripped a cp1252 UnicodeEncodeError in a default Windows console while I was testing); annotate_scale emits an already_annotated status that neither the README nor the docstring documents; and the from .scale import in IInstance.extract_tables_from_text can move to the top of the file (the lazy import in parser.py is justified by the try/except, this one isn't).

None of this diminishes the core idea — welding scale to tables so chunkers can't separate them is the right call, and the execution is close. Happy to discuss any of these.

Address maintainer review on the scale-header preservation:

- Gate the missing-scale warning on a financial signal. A numeric table
  with no caption is warned only when the document has a caption elsewhere
  or the table itself carries a currency marking. Plain numeric tables
  (inventory, server metrics, standings) are left untouched, so ordinary
  documents no longer get hedging welded into the text lane.
- Only weld a caption onto a table that looks numeric, so a caption over a
  prose or roster table is never mis-attached.
- Veto prose lead-ins ("valued in billions") so a sentence near a table no
  longer welds a wrong scale.
- Stop the injected markers from being read back as captions on a second
  pass, keeping re-annotation idempotent across multiple tables.
- Make the markers plain ASCII so they survive a cp1252 console.
- Move the shared-table import to module scope in IInstance and document
  the already_annotated status in the README and docstring.
@Ansh-Karnwal

Ansh-Karnwal commented Jul 15, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough read, @Rod-Christensen. All five are addressed in 32a4ca9.

1. Unconditional warning on non-financial docs. The missing-scale warning is now gated on a financial signal: a numeric table is only warned when the document has a caption detected elsewhere, or the table itself carries a currency marking ($, , a currency word, etc.). Plain numeric tables with no financial signal (I tested server metrics, inventory counts, and standings) get status not_financial and no injected text. Went with the gate over a config flag so the default stays useful on real filings without opt-in.

2. Prose false positives welding a wrong scale. Two changes. The weld is now gated on _table_looks_numeric even when a caption is in scope, so a caption over a roster or prose table is never attached. And detection vetoes a set of prose lead-in verbs before "in" (valued, worth, grew, reached, ...), so "valued in billions" is read as prose. The veto is a denylist of sentence verbs, so it costs no real captions ("Dollars in millions", "Amounts are in thousands" still detect). Added a test with your exact example.

3. parsing_metadata not reaching downstream. Softened the claim to match reality. The durable downstream signal is the welded in-band markers, which travel with the text and table lanes, that is the mechanism that actually protects the figures. parsing_metadata is now described as observability (debug-logged, not re-emitted onto a lane). Wiring it onto a lane or DocMetadata is a reasonable follow-up but felt out of scope for this fix.

4. Marker self-detection. detect_scale_declarations now skips any scale word sitting on one of our own marker lines, so a re-annotation pass cannot derive a scale from the marker text. Both markers contain scale words (the warning says "thousands, millions, or billions"), so both are excluded. Pinned with a test.

5. Smaller items. Markers are now plain ASCII (> [scale?] and (x1,000,000)); added a test that the output encodes to cp1252. Documented the already_annotated status in the README and the docstring. Moved the from .scale import in IInstance.extract_tables_from_text to module scope (the parser.py one stays lazy since it is inside the try/except).

Tests are at 17 (added 5 for the scenarios above), ruff clean. Happy to keep iterating.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nodes/src/nodes/llamaparse/scale.py`:
- Around line 399-400: Define a module-level compiled regex, such as
_CURRENCY_WORDS_RE, near _CURRENCY_WORDS using escaped alternation for all
currency terms, then update _table_has_currency to call its search method on
lowered table text instead of compiling and checking one pattern per word.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fc6ef5d3-6ec1-42bc-abcb-5b730051305a

📥 Commits

Reviewing files that changed from the base of the PR and between 042fc17 and 32a4ca9.

📒 Files selected for processing (4)
  • nodes/src/nodes/llamaparse/IInstance.py
  • nodes/src/nodes/llamaparse/README.md
  • nodes/src/nodes/llamaparse/scale.py
  • nodes/test/test_llamaparse_scale.py

Comment thread nodes/src/nodes/llamaparse/scale.py Outdated
Replace the per-word re.search loop in _table_has_currency with a single
module-level compiled alternation (_CURRENCY_WORDS_RE), matching the pattern
used elsewhere in the module.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation module:nodes Python pipeline nodes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LlamaParse can drop the scale header '(In millions)' → 1,000,000x error

2 participants