Skip to content

UN-2771 [FEAT] Report text extraction time in API deployment metrics#2032

Open
athul-rs wants to merge 6 commits into
mainfrom
UN-2771-extraction-time-metric
Open

UN-2771 [FEAT] Report text extraction time in API deployment metrics#2032
athul-rs wants to merge 6 commits into
mainfrom
UN-2771-extraction-time-metric

Conversation

@athul-rs

@athul-rs athul-rs commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What

  • API deployment responses with include_metrics=True now report the text extraction (LLMWhisperer/X2Text) duration as metrics._pipeline.extraction["time_taken(s)"], alongside the existing per-output indexing time.
"metrics": { "_pipeline": { "extraction": { "time_taken(s)": 1.23 } } }

The metric lives under a reserved _pipeline namespace rather than a bare top-level extraction key: sibling keys in the metrics dict are user-defined output/prompt names, so a prompt named "extraction" would otherwise collide with it.

Why

UN-2771 — metrics only included indexing time; the LLMWhisperer call (often the dominant cost for large documents) was invisible, making it hard to attribute slow executions.

How

Reworked per review (@chandrasekharan-zipstack): the original version added timing to tools/structure (deprecated Docker path); that's reverted. The change now lives in the live celery flow: LegacyExecutor._handle_structure_pipeline times the extract step (time.monotonic()) and _finalize_pipeline_result merges {"extraction": {"time_taken(s)": ...}} into result metrics via the existing _merge_pipeline_metrics, exactly mirroring how _index_pipeline_output records indexing time. The Prompt Studio IDE flow (_handle_ide_index) is untouched.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. Additive metrics key only, under a reserved _pipeline namespace that cannot collide with user-defined output names; existing indexing metrics and response shape unchanged. Paths that skip extraction (smart-table) leave the dict empty — nothing added, same as today.
  • The indexing path was moved from wall-clock datetime.now() to time.monotonic(). Same units and semantics (elapsed seconds as a float), but immune to NTP/system-clock adjustments; it cannot regress a caller.

Database Migrations

  • None

Env Config

  • None

Relevant Docs

  • N/A

Related Issues or PRs

  • Jira: UN-2771

Notes on Testing

  • 5 new unit tests in workers/tests/test_phase5d.py::TestExtractionMetrics: metric recorded, name-collision guard (a prompt named "extraction"), index+extraction merge, skip-extraction records nothing, extract-failure records nothing.
  • Worker suite: 723 passed, 6 failed — the same 6 failures present on main (Postgres auth + pre-existing mock assertions), none in the touched paths.
  • ruff check/format clean (pre-commit hooks pass).
  • Manual: run an API deployment (structure-tool workflow routed via the worker pipeline) with include_metrics=True → response metrics contain _pipeline.extraction.time_taken(s); smart-table path unchanged.

Screenshots

N/A

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

The structure tool timed indexing but not the text extraction
(LLMWhisperer/X2Text) call, so API responses with include_metrics=True
reported indexing time only. Time dynamic_extraction the same way and
merge it into the result metrics as extraction.time_taken(s).
Bump structure tool to 0.0.102.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 68fe70de-eb01-47dc-b3df-dd9991722c9a

📥 Commits

Reviewing files that changed from the base of the PR and between 81d01ed and 214d1a8.

📒 Files selected for processing (3)
  • workers/executor/executors/constants.py
  • workers/executor/executors/legacy_executor.py
  • workers/tests/test_structure_pipeline.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • workers/executor/executors/legacy_executor.py

Summary by CodeRabbit

  • New Features

    • Added extraction timing metrics to structured pipeline results.
    • Extraction and indexing metrics are now combined in a consistent format.
    • Extraction metrics are reported under a dedicated file-level namespace without conflicting with output fields.
  • Bug Fixes

    • Prevented extraction timing from being reported when extraction is skipped or fails.
    • Improved duration measurement consistency for extraction and indexing operations.

Walkthrough

The structure pipeline now reports extraction and indexing durations using shared metric keys. Extraction timing is stored under a reserved file-level namespace, merged with indexing metrics, and covered by tests for success, skipping, failure, single-pass execution, and metric-name collisions.

Changes

Pipeline timing metrics

Layer / File(s) Summary
Extraction metrics and result merging
workers/executor/executors/constants.py, workers/executor/executors/legacy_executor.py
Adds shared metric keys, records successful extraction duration, and merges extraction metrics with indexing metrics in the finalized pipeline result.
Index timing and pipeline coverage
workers/executor/executors/legacy_executor.py, workers/tests/test_structure_pipeline.py
Uses monotonic timing for indexing and adds coverage for extraction metric behavior across pipeline control-flow cases.

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

Sequence Diagram(s)

sequenceDiagram
  participant StructurePipeline
  participant Extract
  participant IndexPipelineOutput
  participant FinalizePipelineResult
  StructurePipeline->>Extract: execute extraction
  Extract-->>StructurePipeline: return extracted output
  StructurePipeline->>IndexPipelineOutput: index pipeline output
  IndexPipelineOutput-->>StructurePipeline: return indexing metrics
  StructurePipeline->>FinalizePipelineResult: pass extraction and indexing metrics
  FinalizePipelineResult-->>StructurePipeline: write merged metrics
Loading

Suggested reviewers: jaseemjaskp, hari-kuriakose

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: reporting text extraction time in API deployment metrics.
Description check ✅ Passed The description follows the template and covers the required context; only Dependencies Versions is omitted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch UN-2771-extraction-time-metric

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.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds text-extraction (LLMWhisperer/X2Text) duration reporting to the Celery worker pipeline metrics. When include_metrics=True, API deployment responses now include a _file.text_extraction.time_taken(s) key alongside the existing per-output indexing times. Indexing timing is also migrated from datetime.now() to time.monotonic().

  • legacy_executor.py: _handle_structure_pipeline brackets the extract step with time.monotonic() and stores the result in extraction_metrics; _finalize_pipeline_result merges it with index_metrics before writing to structured_output[\"metrics\"].
  • constants.py: Adds FILE = \"_file\", TEXT_EXTRACTION = \"text_extraction\", and TIME_TAKEN = \"time_taken(s)\" to PSKeys to avoid raw string literals and clarify namespace intent.
  • test_structure_pipeline.py: Six new tests in TestExtractionMetrics cover normal recording, namespace collision avoidance, index+extraction merge, skip-extraction, single-pass, and extract-failure paths.

Confidence Score: 5/5

Purely additive metrics change with no effect on existing response shape or code paths; safe to merge.

The change only writes a new _file.text_extraction key when extraction actually runs; all existing callers are unaffected. The time.monotonic() migration is a drop-in replacement. Six targeted unit tests cover every relevant code path.

No files require special attention; all changes are isolated to the extraction timing path.

Important Files Changed

Filename Overview
workers/executor/executors/constants.py Adds FILE ("_file"), TEXT_EXTRACTION ("text_extraction"), and TIME_TAKEN ("time_taken(s)") constants to PSKeys for the new extraction metric namespace.
workers/executor/executors/legacy_executor.py Times the extract step with time.monotonic(), stores result in extraction_metrics under the reserved _file namespace, passes it to _finalize_pipeline_result, and migrates indexing timing from datetime to time.monotonic(). Logic is additive and correct.
workers/tests/test_structure_pipeline.py Adds TestExtractionMetrics with 6 targeted tests covering normal recording, collision avoidance, index+extraction merge, skip-extraction, single-pass, and extract-failure paths.

Sequence Diagram

sequenceDiagram
    participant C as Caller
    participant LSP as _handle_structure_pipeline
    participant EX as _handle_extract
    participant IDX as _run_pipeline_index
    participant ANS as _run_pipeline_answer_step
    participant FIN as _finalize_pipeline_result

    C->>LSP: ExecutionContext (pipeline_options)
    LSP->>EX: extract_ctx
    Note over LSP: extraction_start = time.monotonic()
    EX-->>LSP: ExecutionResult(extracted_text)
    Note over LSP: extraction_metrics = {_file: {text_extraction: {time_taken(s): elapsed}}}
    LSP->>IDX: index per output
    Note over IDX: indexing_start = time.monotonic()
    IDX-->>LSP: "index_metrics = {output_name: {indexing: {time_taken(s): elapsed}}}"
    LSP->>ANS: answer_params
    ANS-->>LSP: structured_output (with existing metrics)
    LSP->>FIN: index_metrics + extraction_metrics
    Note over FIN: new_metrics = merge(index_metrics, extraction_metrics)
    FIN-->>LSP: structured_output mutated
    LSP-->>C: ExecutionResult(structured_output)
Loading

Reviews (5): Last reviewed commit: "UN-2771 Rename metric namespace to _file..." | Re-trigger Greptile

@chandrasekharan-zipstack chandrasekharan-zipstack 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.

@athul-rs I like your intent here however I don't think the tool related code or logic is being used anymore. This is deprecated and in fact we'll be removing this code soon. Can you check if this is handled in the equivalent flow involving the celery tasks and ensure this concern is addressed?
cc: @harini-venkataraman

Per review, the structure tool's Docker path is deprecated — the live
flow is the celery-based LegacyExecutor structure pipeline. Time the
extract step there and merge {'extraction': {'time_taken(s)': ...}}
into the result metrics alongside the existing per-output indexing
timing. Structure tool changes reverted (no tool version bump needed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@athul-rs

Copy link
Copy Markdown
Contributor Author

@chandrasekharan-zipstack you were right — reworked. The tools/structure change (and the tool version bump) is reverted; extraction timing now lives in the celery flow: LegacyExecutor._handle_structure_pipeline times the extract step and merges extraction.time_taken(s) into result metrics through _finalize_pipeline_result/_merge_pipeline_metrics, mirroring the existing indexing timing in _index_pipeline_output. Confirmed the worker path is what cloud runs (workflows using the structure tool image are routed to it via _is_structure_tool_workflow). PR is now a 12-line change to legacy_executor.py only.

@sonarqubecloud

Copy link
Copy Markdown

@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 `@workers/executor/executors/legacy_executor.py`:
- Around line 645-647: The current extraction_metrics dict uses a top-level key
"extraction" which can collide with user-defined output/prompt names when merged
later; update this to use a reserved pipeline namespace (e.g., set
extraction_metrics = {"_pipeline": {"extraction": {"time_taken(s)": ...}}}) and
then merge into the main metrics map so pipeline-level metrics live under
metrics["_pipeline"]; adjust the merge logic where metrics and
extraction_metrics are combined (the existing merge around the metrics variable
at lines ~804-811) to preserve the "_pipeline" namespace, or alternatively add a
pre-merge check to disallow user outputs named "extraction" if you prefer the
blocking approach. Ensure you update references to extraction_metrics and the
merge operation accordingly.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5ab6512-472a-43ec-a802-245f014d585e

📥 Commits

Reviewing files that changed from the base of the PR and between ab98fc5 and ec778bc.

📒 Files selected for processing (1)
  • workers/executor/executors/legacy_executor.py

Comment thread workers/executor/executors/legacy_executor.py

@jaseemjaskp jaseemjaskp 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.

Automated review (PR Review Toolkit) of the extraction-time metric.

Net assessment: the merge logic is correct, empty-dict-safe, and introduces no silent failures — error propagation (LegacyExecutorError re-raise) is byte-for-byte unchanged. No blocking bugs. The substantive open item is the top-level-key collision already flagged by @coderabbitai on L647; the inline notes below are distinct consistency/maintainability/test points.

Biggest gap — test coverage (not inline-able; test_phase5d.py isn't in this diff): the PR adds zero tests for the new behavior. _finalize_pipeline_result / _merge_pipeline_metrics are exercised by test_phase5d.py (test_index_metrics_merged L261, test_merge_* L915-927) but none assert on extraction_metrics. Recommend adding before merge:

  • test_extraction_metrics_recorded — normal run: result.data["metrics"]["extraction"]["time_taken(s)"] is a float >= 0 and coexists with per-prompt keys.
  • test_extract_failure_records_no_extraction_metric — metric is computed after the _failure early-return, so a failed extract must record no timing (pin via a _finalize_pipeline_result spy / assert_not_called).
  • test_index_and_extraction_metrics_merged — both families merge (top-level extraction + per-output indexing).
  • test_skip_extraction_records_no_extraction_metricskip_extraction ⇒ no "extraction" key.

Comment thread workers/executor/executors/legacy_executor.py Outdated
Comment thread workers/executor/executors/legacy_executor.py
Comment thread workers/executor/executors/legacy_executor.py Outdated
… align clocks

- Nest extraction timing under a reserved "_pipeline" namespace
  (PSKeys.PIPELINE) so it cannot collide with a user-defined output/prompt
  named "extraction" at the top level of the metrics dict.
- Use PSKeys.EXTRACTION instead of the hardcoded literal, and capture the
  duration in a named local for parity with the indexing path.
- Promote the duplicated "time_taken(s)" literal to PSKeys.TIME_TAKEN.
- Align the indexing path onto time.monotonic(): it measured a duration with
  wall-clock datetime.now(), which is wrong across NTP/system-clock
  adjustments and left the two producers of time_taken(s) on different
  clocks. Drops the now-unused local datetime import.
- Document the extraction_metrics shape on _finalize_pipeline_result and
  type it dict[str, dict] | None.

Tests: 5 new cases in test_phase5d.py — metric recorded, name-collision
guard, index+extraction merge, skip-extraction, and extract-failure (no
timing recorded). Worker suite: 723 passed, same 6 pre-existing failures
as main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@athul-rs

Copy link
Copy Markdown
Contributor Author

Thanks @jaseemjaskp — all four inline threads plus the test-coverage gap are addressed in 81d01ed. Summary of what changed:

Design (the substantive one): the metric is no longer a bare top-level extraction key. It is nested under a reserved _pipeline namespace, so the response shape is now:

"metrics": { "_pipeline": { "extraction": { "time_taken(s)": 1.23 } } }

Sibling keys in that dict are user-defined output/prompt names, so the old shape would have collided with a prompt named "extraction". PR description updated to match.

Tests — all four you listed, plus one for the collision (test_phase5d.py::TestExtractionMetrics):

  • test_extraction_metrics_recorded — normal run, time_taken(s) is a float >= 0.
  • test_extraction_metric_does_not_collide_with_output_name — a prompt named "extraction" and the pipeline metric coexist untouched.
  • test_index_and_extraction_metrics_merged — per-output indexing + top-level _pipeline.extraction both survive the merge.
  • test_skip_extraction_records_no_extraction_metric — smart-table path adds no _pipeline key.
  • test_extract_failure_records_no_extraction_metric — timing is taken after the failure early-return, pinned with a _finalize_pipeline_result spy and assert_not_called.

Also fixed: PSKeys.EXTRACTION / PSKeys.PIPELINE instead of literals; "time_taken(s)" promoted to PSKeys.TIME_TAKEN; and the indexing path moved off wall-clock datetime.now() onto time.monotonic() so both producers use the same (correct) clock — I did this here rather than deferring it, since the PR was what introduced the inconsistency.

Verification: worker suite is 723 passed, 6 failed — the same 6 failures present on main (Postgres auth + pre-existing mock assertions), none in the touched paths. Branch is up to date with main.

@chandrasekharan-zipstack chandrasekharan-zipstack 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.

Agreed follow-ups from a design discussion on the metric shape. Two mechanical renames for clarity; plus a heads-up: I filed UN-3714 for the per-profile X2Text extractor being collapsed to the default profile's extractor in the worker pipeline (out of scope for this PR).

  1. _pipeline_file — the bucket holds file-level (whole-document) metrics, and "file" is already the API response key for the source file (workflow_manager/workflow_v2/dto.py:45), so _file is the intuitive/consistent name.
  2. extractiontext_extraction — avoids confusion with extraction_llm (the extraction-purpose LLM call) which sits beside it in the same metrics dict; the collision is visible in single-pass responses (extraction_llm and _pipeline.extraction coexist).

Single-pass needs no special-casing: _file.text_extraction is merged in by the shared _finalize_pipeline_result, so single-pass metrics stay flat and just gain the _file sibling — same convention as multi-prompt.

Comment thread workers/executor/executors/constants.py Outdated
Comment thread workers/executor/executors/legacy_executor.py Outdated
Comment thread workers/tests/test_phase5d.py Outdated
athul-rs and others added 2 commits July 22, 2026 10:06
Review follow-up: the reserved bucket holds file-level (whole-document)
metrics, so _file matches the existing file key of the API response
better than _pipeline. The metric itself becomes text_extraction to stay
distinct from extraction_llm, the extraction-purpose LLM call that sits
beside it in the same metrics dict.

Adds a single-pass test pinning cross-mode consistency: single pass skips
indexing but shares _finalize_pipeline_result, so its flat metrics simply
gain the same _file sibling the multi-prompt path produces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 20.6
e2e-coowners e2e 1 0 0 0 1.4
e2e-etl e2e 1 0 0 0 8.4
e2e-login e2e 2 0 0 0 1.1
e2e-prompt-studio e2e 1 0 0 0 4.6
e2e-smoke e2e 2 0 0 0 1.0
e2e-workflow e2e 1 0 0 0 16.2
integration-backend integration 124 0 0 27 68.0
integration-connectors integration 1 0 0 7 8.1
unit-backend unit 160 0 0 0 15.0
unit-connectors unit 63 0 0 0 6.7
unit-core unit 27 0 0 0 0.8
unit-platform-service unit 15 0 0 0 1.7
unit-rig unit 76 0 0 0 3.1
unit-sdk1 unit 490 0 0 0 21.4
unit-workers unit 729 0 0 0 36.8
TOTAL 1696 0 0 34 214.8

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants