Skip to content

Add lightweight reviewer app and review endpoints - #1907

Open
chezhia wants to merge 3 commits into
mainfrom
feature/lightweight-reviewer
Open

Add lightweight reviewer app and review endpoints#1907
chezhia wants to merge 3 commits into
mainfrom
feature/lightweight-reviewer

Conversation

@chezhia

@chezhia chezhia commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Adds a lightweight MONAILabel reviewer workflow for existing segmentations without loading AI tasks.

What changed

  • adds a review-focused sample app under sample-apps/reviewer with the required MONAILabel app entrypoint
  • adds reviewer aggregate endpoints under /review and mounts them in the main FastAPI app
  • aligns the reviewer client and smoke test with the implemented server routes
  • fixes reviewer image downloads in the 3D Slicer plugin by using direct REST fetches and normalizing server URLs at the UI and REST layers
  • teaches local datastore reconciliation to recognize reviewer label files named .seg.nrrd
  • documents the reviewer app in the top-level README and refreshes the sample reviewer README to match the implemented workflow

Validation

  • python -m unittest tests.unit.datastore.test_local
  • python -m py_compile monailabel/app.py monailabel/datastore/local.py monailabel/endpoints/datastore_review.py sample-apps/reviewer/app.py sample-apps/reviewer/client.py sample-apps/reviewer/main.py sample-apps/reviewer/lib/config.py plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py test_reviewer_server.py

Notes

  • untracked scratch notes and draft cache-helper files were intentionally left out of this PR because they are not wired into the current reviewer workflow.

Summary by CodeRabbit

  • New Features
    • Added authenticated /review APIs for listing review cases, viewing per-image label versions, and generating review reports (json|csv|html) with filtering.
    • Introduced a lightweight Reviewer sample app and client for review/approval workflow and report generation.
  • Bug Fixes
    • Improved .seg.nrrd label-to-image ID matching and standardized review timestamp (last_reviewed) behavior.
    • Hardened reviewer plugin server URL handling, DICOM loading, and temporary file path safety.
  • Documentation
    • Expanded README with Reviewer App onboarding and added dedicated reviewer sample app documentation.
  • Tests / Quality
    • Added unit tests for review endpoints, reviewer app/client behavior, datastore logic, and date-range handling, plus a manual smoke test.
  • Chores
    • Updated tooling and pinned dependencies for highdicom by Python version.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

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
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Adds a review-only sample app, datastore-backed review APIs, report generation, Slicer image-loading integration, .seg.nrrd handling, review metadata timestamps, documentation, and validation.

Changes

Reviewer workflow

Layer / File(s) Summary
Review API and datastore backend
monailabel/endpoints/datastore_review.py, monailabel/datastore/local.py, monailabel/app.py, tests/unit/datastore/*, tests/unit/endpoints/*
Adds filtered review cases, label versions, JSON/CSV/HTML reports, RBAC-protected routing, .seg.nrrd persistence, last_reviewed handling, and backend tests.
Review-only app and HTTP client
sample-apps/reviewer/app.py, sample-apps/reviewer/main.py, sample-apps/reviewer/client.py, sample-apps/reviewer/lib/config.py, tests/unit/sample_apps/*
Adds reviewer configuration, a no-AI application entry point, lazy configuration, HTTP operations for cases, labels, versions, and reports, and related tests.
Slicer image-loading integration
plugins/slicer/MONAILabelReviewer/...
Normalizes server URLs, retrieves image bytes through the REST client, and validates temporary paths before loading volumes.
Validation and documentation
README.md, sample-apps/reviewer/README.md, scripts/manual_reviewer_smoke_test.py, tests/unit/datastore/test_convert.py
Documents the reviewer workflow, adds a manual server smoke test, and hardens test cleanup.
Repository maintenance
.pre-commit-config.yaml, monailabel/_version.py, requirements.txt
Updates the pyupgrade hook, changes version-command output decoding, and applies Python-version-specific highdicom pins.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LightweightReviewClient
  participant ReviewAPI
  participant LocalDatastore
  LightweightReviewClient->>ReviewAPI: Request review cases or reports
  ReviewAPI->>LocalDatastore: Read image and label metadata
  LocalDatastore-->>ReviewAPI: Return review data
  ReviewAPI-->>LightweightReviewClient: Return cases, versions, or reports
  LightweightReviewClient->>ReviewAPI: Submit label metadata or label
  ReviewAPI->>LocalDatastore: Persist review data
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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: adding a lightweight reviewer app plus review endpoints.
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 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/lightweight-reviewer

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

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 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 `@monailabel/datastore/local.py`:
- Around line 228-232: Update save_label to derive the label identifier and
extension through _to_label_id instead of _to_id, preserving the .seg.nrrd
suffix when re-saving labels discovered by _add_non_existing_labels. Keep the
existing save behavior unchanged for other extensions.

In `@monailabel/endpoints/datastore_review.py`:
- Around line 156-172: Update _render_csv to sanitize free-text CSV values
before writer.writerow serializes them, especially comment and reviewer, by
prefixing values beginning with =, +, -, or @ so spreadsheet applications treat
them as text. Apply the same protection consistently to other user-controlled
fields while preserving normal values and CSV formatting.
- Around line 88-101: Ensure label updates populate last_reviewed so date
filtering can match reviewed cases. Update LocalDatastore.update_label_info to
preserve an existing timestamp or stamp the server-side update time, with a
documented fallback to the label’s existing ts when appropriate. Ensure
_reviews_case exposes that populated value through
label_info.get("last_reviewed") without changing unrelated case fields.

In `@plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py`:
- Around line 1729-1731: Constrain the server-derived image_name in the download
flow around getPathToStore before writing response.content, ensuring the
resolved destination remains within self.temp_dir.name; reject or safely
normalize traversal and absolute-path components, while preserving valid
filenames and the existing file-write behavior.

In `@sample-apps/reviewer/app.py`:
- Around line 76-84: Update the mode assignment in the configuration-loading
flow to use the existing configured mode as the fallback when
MONAI_LABEL_REVIEW_MODE is unset. Preserve an explicitly provided environment
override while ensuring conf_dict’s mode, such as “standalone”, is not replaced
by “review”.

In `@sample-apps/reviewer/client.py`:
- Around line 171-174: Update the label download method around the response
handling to return response.content instead of calling response.json(), since
/datastore/label returns binary NIfTI/NRRD data. Change the method’s return type
annotation to reflect bytes while preserving the existing successful-response
logging and return flow.
- Around line 254-259: Apply self.timeout to both write requests: add the
configured timeout to the label-info PUT and the label-upload PUT in
sample-apps/reviewer/client.py at lines 254-259 and 308-314, respectively. No
other request behavior should change.
- Line 416: Update create_client() and the LightweightReviewClient constructor
flow so the default None server_url is replaced with the appropriate usable
default before __init__ calls server_url.rstrip("/"). Preserve explicit
server_url values while ensuring calling create_client() without arguments
succeeds.

In `@sample-apps/reviewer/README.md`:
- Around line 209-211: Update the README configuration example under “Mode” to
use the environment variable read by the app, MONAI_LABEL_REVIEW_MODE, instead
of the unused MONAI_LABEL_REVIEW_ENABLED.
- Around line 181-184: Update the documented PUT /datastore/label/info request
in the README to submit the JSON payload using the server-supported info form
field instead of approvals, while preserving the existing payload contents.

In `@test_reviewer_server.py`:
- Around line 1-16: Prevent pytest from auto-discovering the manual smoke-test
functions in test_reviewer_server.py by relocating the script under scripts/ and
renaming it without a test_ prefix, such as manual_reviewer_smoke_test.py.
Preserve the existing functions test_list_images, test_download_image,
test_download_label, test_update_labelinfo, and test_generate_report as manual
checks rather than converting them into CI tests.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 28bdeb2d-21cb-44fa-8481-f781e6f709dd

📥 Commits

Reviewing files that changed from the base of the PR and between 6ed8f8c and 99030ca.

📒 Files selected for processing (15)
  • README.md
  • monailabel/app.py
  • monailabel/datastore/local.py
  • monailabel/endpoints/datastore_review.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py
  • sample-apps/reviewer/README.md
  • sample-apps/reviewer/app.py
  • sample-apps/reviewer/client.py
  • sample-apps/reviewer/lib/__init__.py
  • sample-apps/reviewer/lib/config.py
  • sample-apps/reviewer/main.py
  • test_reviewer_server.py
  • tests/unit/datastore/test_local.py

Comment thread monailabel/datastore/local.py
Comment thread monailabel/endpoints/datastore_review.py
Comment thread monailabel/endpoints/datastore_review.py
Comment thread plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
Comment thread sample-apps/reviewer/app.py Outdated
Comment thread sample-apps/reviewer/client.py
Comment thread sample-apps/reviewer/client.py Outdated
Comment thread sample-apps/reviewer/README.md Outdated
Comment thread sample-apps/reviewer/README.md
Comment thread test_reviewer_server.py
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #1908

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 7 file(s) based on 11 unresolved review comments.

Files modified:

  • test_reviewer_server.py
  • monailabel/datastore/local.py
  • monailabel/endpoints/datastore_review.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • sample-apps/reviewer/README.md
  • sample-apps/reviewer/app.py
  • sample-apps/reviewer/client.py

Commit: 704977c9647b454408888ebe78e93412d9c92bce

The changes have been pushed to the feature/lightweight-reviewer branch.

Time taken: 6m 48s

coderabbitai Bot added a commit that referenced this pull request Jul 27, 2026
Docstrings generation was requested by @chezhia.

* #1907 (comment)

The following files were modified:

* `monailabel/datastore/local.py`
* `monailabel/endpoints/datastore_review.py`
* `plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py`
* `plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py`
* `plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py`
* `sample-apps/reviewer/app.py`
* `sample-apps/reviewer/client.py`
* `sample-apps/reviewer/lib/config.py`

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@monailabel/datastore/local.py`:
- Around line 561-573: Update the last_reviewed handling around the existing
metadata fields so explicit incoming info["last_reviewed"] values remain
unchanged, while updates containing review metadata such as status, level,
comment, reviewer, or reviewer_name receive datetime.now().isoformat() instead
of reusing the prior timestamp. Retain the existing fallback behavior for
updates without review metadata.

In `@tests/unit/sample_apps/test_reviewer_app.py`:
- Around line 29-30: Update the patch.dict call surrounding
app._load_review_config in the test to use clear=True instead of clear=False,
ensuring inherited environment variables such as MONAI_LABEL_REVIEW_MODE are
removed while preserving the test’s provided inputs.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bbf738aa-f472-4c88-b23e-2ee84839586d

📥 Commits

Reviewing files that changed from the base of the PR and between 99030ca and a799578.

📒 Files selected for processing (9)
  • monailabel/datastore/local.py
  • monailabel/endpoints/datastore_review.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • sample-apps/reviewer/README.md
  • sample-apps/reviewer/app.py
  • sample-apps/reviewer/client.py
  • tests/unit/datastore/test_local.py
  • tests/unit/sample_apps/test_reviewer_app.py
  • tests/unit/sample_apps/test_reviewer_client.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • sample-apps/reviewer/README.md
  • sample-apps/reviewer/app.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • monailabel/endpoints/datastore_review.py

Comment thread monailabel/datastore/local.py Outdated
Comment thread tests/unit/sample_apps/test_reviewer_app.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@scripts/manual_reviewer_smoke_test.py`:
- Around line 32-38: Remove the unnecessary f-string prefixes from the literal
arguments in the command construction around the app, studies, port, and conf
options, including the corresponding block near the additionally referenced
lines. Preserve f-strings only for values that interpolate variables such as
TEST_DATA_DIR or PORT.
- Around line 47-71: Update the server startup flow around the subprocess.Popen
readiness loop to use nonblocking merged stdout/stderr handling, check
proc.poll() and fail immediately when the child exits, and enforce an unused
test port before startup and readiness polling. Validate that the readiness
response belongs to the spawned process rather than accepting an existing
service on port 8079, preventing review metadata from being sent to another
server.
- Around line 157-161: Update the download success checks in the image and label
download paths to require both HTTP status 200 and non-empty response content
before returning success. Apply this consistently to the checks around the image
download and the additional locations at the indicated response-handling
branches, preserving the existing success logging for valid non-empty downloads.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

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 Plus

Run ID: 5a89550a-de23-49aa-b85c-e59ba858c2ce

📥 Commits

Reviewing files that changed from the base of the PR and between a799578 and 09e93a3.

📒 Files selected for processing (3)
  • sample-apps/reviewer/README.md
  • scripts/manual_reviewer_smoke_test.py
  • tests/unit/sample_apps/test_reviewer_app.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/sample_apps/test_reviewer_app.py
  • sample-apps/reviewer/README.md

Comment thread scripts/manual_reviewer_smoke_test.py Outdated
Comment thread scripts/manual_reviewer_smoke_test.py Outdated
Comment thread scripts/manual_reviewer_smoke_test.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
scripts/manual_reviewer_smoke_test.py (3)

32-38: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove extraneous f-string prefixes (still unresolved).

Ruff F541 still fires on f"--app", f"--studies", f"--port", f"--conf" (L32,34,36,38) and f"✅ Successfully listed images", f"\n Sample images:" (L105,110) — none contain placeholders.

Also applies to: 105-110

🤖 Prompt for 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.

In `@scripts/manual_reviewer_smoke_test.py` around lines 32 - 38, Remove the
unnecessary f-string prefixes from the constant CLI argument strings and fixed
output messages in the smoke test, including the arguments near the command
construction and the success/listing messages in the relevant output section;
preserve their exact string contents.

Source: Linters/SAST tools


47-73: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Server readiness check can still block indefinitely and validate the wrong server (still unresolved).

proc.stdout.readline() can block, the except: pass swallows all errors silently, and wait_for_server_ready never calls proc.poll() nor confirms the responding server on port 8079 is the spawned child (a port collision could make this script mutate a different server's review metadata).

Also applies to: 76-88

🤖 Prompt for 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.

In `@scripts/manual_reviewer_smoke_test.py` around lines 47 - 73, Update
wait_for_server_ready to use non-blocking readiness checks with a hard timeout,
report subprocess errors instead of swallowing them, and call proc.poll() to
detect early process exit. Confirm the spawned server itself is serving on port
8079 before returning, preventing an existing process on that port from being
treated as ready.

Source: Linters/SAST tools


157-161: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Empty download bodies still pass as success (still unresolved).

download_response.status_code == 200 alone is treated as success for image/label downloads; an empty body would silently pass, masking a broken datastore response.

Also applies to: 198-212

🤖 Prompt for 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.

In `@scripts/manual_reviewer_smoke_test.py` around lines 157 - 161, Update the
download success checks around the image and label response handlers to require
both HTTP 200 and a non-empty response body before returning success. Apply the
same validation to the additional block referenced by the comment, preserving
the existing logging and failure behavior for empty downloads.
🧹 Nitpick comments (3)
tests/unit/datastore/test_local.py (1)

99-120: 📐 Maintainability & Code Quality | 🔵 Trivial

Test relies on get_label_info returning a mutable internal reference.

The legacy-ts-fallback scenario is set up by mutating the dict returned from get_label_info in place, rather than going through a public write path. This is a fragile assumption about the return value's identity; if get_label_info is later changed to return a defensive copy, this test would stop exercising the fallback behavior without failing loudly.

Consider setting up the legacy state through save_label/update_label_info (or another public API) instead of mutating the returned dict directly.

🤖 Prompt for 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.

In `@tests/unit/datastore/test_local.py` around lines 99 - 120, Update
test_update_label_info_falls_back_to_existing_ts_for_legacy_reviews so the
legacy ts and approved status are persisted through LocalDatastore’s public
APIs, such as save_label or update_label_info, rather than mutating the
dictionary returned by get_label_info. Keep the test focused on verifying that
update_label_info preserves the existing timestamp as last_reviewed for legacy
reviews.
monailabel/endpoints/datastore_review.py (2)

104-133: 🚀 Performance & Scalability | 🔵 Trivial

/review/cases and /review/report do a full datastore scan on every request.

_list_review_cases iterates every image and issues get_image_info/get_label_info/get_labels_by_image_id per image, with no caching, on every call. For large datastores this makes both /review/cases (likely polled by the Slicer UI) and /review/report expensive per-request I/O operations. Consider caching aggregated case metadata and invalidating on label updates if this is expected to scale beyond small review sets.

🤖 Prompt for 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.

In `@monailabel/endpoints/datastore_review.py` around lines 104 - 133, Update
_list_review_cases to reuse a cache of aggregated review-case metadata instead
of scanning the datastore and fetching per-image details on every request. Add
cache invalidation tied to label updates so /review/cases and /review/report
observe updated statuses, reviewers, dates, and tags while avoiding repeated
full datastore I/O.

80-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Confirm get_label_info lookup key is correct for non-Local datastores.

_review_case passes image_id as the label_id argument to datastore.get_label_info(image_id, tag), while api_review_versions correctly resolves label_id from get_labels_by_image_id() first. Per the Datastore interface, get_label_info(label_id, label_tag) expects the label id, which only coincides with image_id for LocalDatastore. For DSA/XNAT-backed reviewers this could silently return {} for every case.

For consistency (and to be safe if this app is ever pointed at a non-local datastore), consider resolving label_id = labels.get(tag) before the lookup, mirroring api_review_versions.

Also applies to: 234-258

🤖 Prompt for 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.

In `@monailabel/endpoints/datastore_review.py` around lines 80 - 101, Update
_review_case to resolve label_id from labels.get(tag) before calling
_safe_label_info, and pass that label ID to the label-info lookup instead of
image_id. Preserve the existing fallback behavior for missing labels and keep
the remaining status and response construction unchanged.
🤖 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 `@monailabel/endpoints/datastore_review.py`:
- Around line 261-287: Update api_review_report so fmt=csv and fmt=html return
raw rendered content with the appropriate text media type instead of JSON
envelopes; preserve the existing JSON response structure for fmt=json and the
invalid-format error behavior.

In `@sample-apps/reviewer/app.py`:
- Line 62: Update the logger.info call in the Reviewer App initialization to use
a regular string literal instead of an f-string, since it contains no
interpolation placeholders.

In `@sample-apps/reviewer/client.py`:
- Around line 234-265: Update update_labelinfo’s review_info construction so
reviewer_name and workflow_id are only included when they have values, matching
the conditional handling of level, comment, and reviewer_email. Preserve the
existing payload and update behavior while preventing absent arguments from
overwriting stored label information with null.
- Around line 290-310: Update the multipart files payload in the request flow
around label_file and files so the uploaded label uses label_file.name, or
another explicit filename preserving the source file’s extension, instead of
image_id. Keep the existing upload endpoint and request behavior unchanged.

---

Duplicate comments:
In `@scripts/manual_reviewer_smoke_test.py`:
- Around line 32-38: Remove the unnecessary f-string prefixes from the constant
CLI argument strings and fixed output messages in the smoke test, including the
arguments near the command construction and the success/listing messages in the
relevant output section; preserve their exact string contents.
- Around line 47-73: Update wait_for_server_ready to use non-blocking readiness
checks with a hard timeout, report subprocess errors instead of swallowing them,
and call proc.poll() to detect early process exit. Confirm the spawned server
itself is serving on port 8079 before returning, preventing an existing process
on that port from being treated as ready.
- Around line 157-161: Update the download success checks around the image and
label response handlers to require both HTTP 200 and a non-empty response body
before returning success. Apply the same validation to the additional block
referenced by the comment, preserving the existing logging and failure behavior
for empty downloads.

---

Nitpick comments:
In `@monailabel/endpoints/datastore_review.py`:
- Around line 104-133: Update _list_review_cases to reuse a cache of aggregated
review-case metadata instead of scanning the datastore and fetching per-image
details on every request. Add cache invalidation tied to label updates so
/review/cases and /review/report observe updated statuses, reviewers, dates, and
tags while avoiding repeated full datastore I/O.
- Around line 80-101: Update _review_case to resolve label_id from
labels.get(tag) before calling _safe_label_info, and pass that label ID to the
label-info lookup instead of image_id. Preserve the existing fallback behavior
for missing labels and keep the remaining status and response construction
unchanged.

In `@tests/unit/datastore/test_local.py`:
- Around line 99-120: Update
test_update_label_info_falls_back_to_existing_ts_for_legacy_reviews so the
legacy ts and approved status are persisted through LocalDatastore’s public
APIs, such as save_label or update_label_info, rather than mutating the
dictionary returned by get_label_info. Keep the test focused on verifying that
update_label_info preserves the existing timestamp as last_reviewed for legacy
reviews.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

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 Plus

Run ID: 21163d30-4e15-40ed-86d2-b6fdb05067ce

📥 Commits

Reviewing files that changed from the base of the PR and between 6ed8f8c and 09e93a3.

📒 Files selected for processing (17)
  • README.md
  • monailabel/app.py
  • monailabel/datastore/local.py
  • monailabel/endpoints/datastore_review.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py
  • sample-apps/reviewer/README.md
  • sample-apps/reviewer/app.py
  • sample-apps/reviewer/client.py
  • sample-apps/reviewer/lib/__init__.py
  • sample-apps/reviewer/lib/config.py
  • sample-apps/reviewer/main.py
  • scripts/manual_reviewer_smoke_test.py
  • tests/unit/datastore/test_local.py
  • tests/unit/sample_apps/test_reviewer_app.py
  • tests/unit/sample_apps/test_reviewer_client.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • sample-apps/reviewer/main.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py
  • monailabel/app.py
  • sample-apps/reviewer/lib/config.py
  • tests/unit/sample_apps/test_reviewer_app.py
  • monailabel/datastore/local.py

Comment thread monailabel/endpoints/datastore_review.py
Comment thread sample-apps/reviewer/app.py Outdated
Comment thread sample-apps/reviewer/client.py
Comment thread sample-apps/reviewer/client.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sample-apps/reviewer/client.py (1)

359-379: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle non-JSON report formats separately.

generate_report() supports csv and html, but the successful response is always passed through _json_dict(). CSV/HTML responses will raise response.json() failures and bubble back as {"status": "error", ...}; use raw payload handling or restrict the client method to JSON.

🤖 Prompt for 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.

In `@sample-apps/reviewer/client.py` around lines 359 - 379, Update
generate_report to handle fmt values csv and html without passing successful
responses through _json_dict: return the raw response payload in the appropriate
form for non-JSON formats, while preserving _json_dict for json reports.
Alternatively, explicitly restrict supported formats to json and reject csv/html
before making the request, keeping the method’s documented behavior consistent.
🧹 Nitpick comments (1)
sample-apps/reviewer/client.py (1)

355-357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the broad exception handler.

Catching Exception masks programming errors and makes unexpected failures indistinguishable from request failures. Catch requests.exceptions.RequestException (plus the specific JSON-decoding exception if required by the response contract).

Proposed fix
-        except Exception as e:
+        except requests.exceptions.RequestException as e:
🤖 Prompt for 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.

In `@sample-apps/reviewer/client.py` around lines 355 - 357, Update the exception
handler surrounding the version request in the image-version retrieval flow to
catch requests.exceptions.RequestException instead of broad Exception; include
the specific JSON-decoding exception only if the response contract requires
handling malformed JSON. Preserve the existing logger.error message and error
response for the handled request failures.

Source: Linters/SAST tools

🤖 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.

Outside diff comments:
In `@sample-apps/reviewer/client.py`:
- Around line 359-379: Update generate_report to handle fmt values csv and html
without passing successful responses through _json_dict: return the raw response
payload in the appropriate form for non-JSON formats, while preserving
_json_dict for json reports. Alternatively, explicitly restrict supported
formats to json and reject csv/html before making the request, keeping the
method’s documented behavior consistent.

---

Nitpick comments:
In `@sample-apps/reviewer/client.py`:
- Around line 355-357: Update the exception handler surrounding the version
request in the image-version retrieval flow to catch
requests.exceptions.RequestException instead of broad Exception; include the
specific JSON-decoding exception only if the response contract requires handling
malformed JSON. Preserve the existing logger.error message and error response
for the handled request failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba0e6095-1a31-4f71-ac9b-e15fca884d09

📥 Commits

Reviewing files that changed from the base of the PR and between 09e93a3 and 0ae13f8.

📒 Files selected for processing (4)
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • sample-apps/reviewer/app.py
  • sample-apps/reviewer/client.py
  • scripts/manual_reviewer_smoke_test.py
💤 Files with no reviewable changes (1)
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • sample-apps/reviewer/app.py
  • scripts/manual_reviewer_smoke_test.py

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

⚠️ Branch updated during autofix.

The branch was updated while autofix was in progress. Please try again.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

⚠️ Branch updated during autofix.

The branch was updated while autofix was in progress. Please try again.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

⚠️ Branch updated during autofix.

The branch was updated while autofix was in progress. Please try again.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a lightweight “reviewer” workflow to MONAILabel for reviewing existing segmentations without loading AI inference/training tasks. It introduces a reviewer sample app and client, new authenticated /review/* aggregate endpoints, and corresponding 3D Slicer reviewer plugin fixes, plus datastore updates to better handle reviewer label conventions.

Changes:

  • Added a new sample-apps/reviewer app + lightweight client for review-only workflows.
  • Introduced /review/cases, /review/versions, and /review/report endpoints and mounted them in the main FastAPI app.
  • Updated local datastore reconciliation for .seg.nrrd labels and enhanced review timestamp tracking; updated Slicer plugin URL normalization and image download behavior; added tests and docs.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/unit/sample_apps/test_reviewer_client.py Unit tests validating reviewer client timeout and URL normalization behavior
tests/unit/sample_apps/test_reviewer_app.py Unit tests validating reviewer app config/env override behavior
tests/unit/datastore/test_local.py Unit tests for .seg.nrrd label handling and last_reviewed behavior in LocalDatastore
scripts/manual_reviewer_smoke_test.py Manual end-to-end smoke test script for the reviewer server endpoints
sample-apps/reviewer/README.md Reviewer sample app documentation and workflow guide
sample-apps/reviewer/main.py MONAILabel app entrypoint for the reviewer sample app
sample-apps/reviewer/lib/config.py Reviewer app configuration helper module
sample-apps/reviewer/lib/init.py Package init for reviewer sample’s lib module
sample-apps/reviewer/client.py Lightweight Python client for reviewer workflow routes
sample-apps/reviewer/app.py Reviewer MONAILabelApp implementation with no AI tasks
README.md Top-level documentation updated to include the reviewer workflow
plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py Slicer plugin REST layer: URL normalization and direct image request helper
plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py Slicer plugin controller: added image request passthrough
plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py Slicer plugin UI/logic updates: URL normalization, safer temp-file handling, direct image downloads
monailabel/endpoints/datastore_review.py New /review aggregate endpoints (cases, versions, report)
monailabel/datastore/local.py LocalDatastore updates for .seg.nrrd label IDs/extensions and last_reviewed handling
monailabel/app.py Main FastAPI app updated to mount the new review router

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sample-apps/reviewer/client.py Outdated
Comment thread plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py Outdated
Comment thread plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py Outdated
Comment thread plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py Outdated
Comment thread monailabel/endpoints/datastore_review.py
Comment thread sample-apps/reviewer/lib/config.py Outdated
Comment thread sample-apps/reviewer/README.md Outdated
Comment thread scripts/manual_reviewer_smoke_test.py Outdated
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 2 file(s) based on 2 unresolved review comments.

Files modified:

  • monailabel/datastore/local.py
  • tests/unit/sample_apps/test_reviewer_app.py

Commit: c3fe7bdccb7980ad0f4cde4e482fdd11264f2987

The changes have been pushed to the feature/lightweight-reviewer branch.

Time taken: 2m 29s

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

⚠️ Branch updated during autofix.

The branch was updated while autofix was in progress. Please try again.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

⚠️ Branch updated during autofix.

The branch was updated while autofix was in progress. Please try again.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
monailabel/endpoints/datastore_review.py (2)

40-48: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not swallow datastore failures as empty metadata.

Only LabelNotFoundException represents an expected missing-label case. Catching every other exception makes I/O, corruption, or storage failures look like unlabeled/pending cases and silently produces incorrect reports. Re-raise unexpected exceptions or catch only documented datastore exceptions.

🤖 Prompt for 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.

In `@monailabel/endpoints/datastore_review.py` around lines 40 - 48, Update
_safe_label_info to handle only LabelNotFoundException as an empty metadata
result; remove the broad Exception catch and allow unexpected datastore failures
to propagate, or narrowly catch only documented datastore exceptions without
converting them to {}.

65-77: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Normalize timezone awareness before comparing timestamps.

date_range and last_reviewed can become offset-aware vs offset-naive (for example 2026-01-01T00:00:00+00:00 vs 2026-01-01T00:00:00), causing TypeError around line 77 instead of a handled response. Normalize both values to UTC before comparison, or require and reject mixed-awareness input with HTTP 400.

🤖 Prompt for 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.

In `@monailabel/endpoints/datastore_review.py` around lines 65 - 77, Update
_matches_date_range to normalize the parsed current timestamp and both
date-range bounds to a consistent UTC timezone awareness before evaluating start
<= current <= end. Ensure mixed-aware and naive inputs no longer raise
TypeError; handle them according to the endpoint’s existing invalid-input
behavior.
🤖 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 `@monailabel/endpoints/datastore_review.py`:
- Line 263: Remove the stray backtick immediately before the `@router.get`
decorator for the "/report" endpoint so the decorator is valid Python syntax and
the datastore review routes can import successfully.

---

Outside diff comments:
In `@monailabel/endpoints/datastore_review.py`:
- Around line 40-48: Update _safe_label_info to handle only
LabelNotFoundException as an empty metadata result; remove the broad Exception
catch and allow unexpected datastore failures to propagate, or narrowly catch
only documented datastore exceptions without converting them to {}.
- Around line 65-77: Update _matches_date_range to normalize the parsed current
timestamp and both date-range bounds to a consistent UTC timezone awareness
before evaluating start <= current <= end. Ensure mixed-aware and naive inputs
no longer raise TypeError; handle them according to the endpoint’s existing
invalid-input 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ee58f65-64c4-4b77-967c-ef432821e23c

📥 Commits

Reviewing files that changed from the base of the PR and between 0ae13f8 and b8b186d.

📒 Files selected for processing (1)
  • monailabel/endpoints/datastore_review.py

Comment thread monailabel/endpoints/datastore_review.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@scripts/manual_reviewer_smoke_test.py`:
- Around line 49-50: Update start_server() to return the process already created
by the initial subprocess.Popen call instead of invoking Popen a second time.
Preserve the readiness check via wait_for_server_ready(), and remove unused PIPE
captures unless the existing process output is explicitly consumed.
🪄 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 Plus

Run ID: fe53f4ef-bdea-46dd-8ed3-cae7723ad506

📥 Commits

Reviewing files that changed from the base of the PR and between c3fe7bd and e082360.

📒 Files selected for processing (2)
  • sample-apps/reviewer/README.md
  • scripts/manual_reviewer_smoke_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • sample-apps/reviewer/README.md

Comment thread scripts/manual_reviewer_smoke_test.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py (1)

240-247: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Initialize img_blob before using it.

img_blob is undefined, so every image load raises NameError instead of returning the REST response or None.

Proposed fix
 def requestImage(self, image_id: str):
+    img_blob = self.monaiServerREST.requestImage(image_id)
     if img_blob is not None:
🤖 Prompt for 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.

In
`@plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py`
around lines 240 - 247, Initialize img_blob at the start of requestImage before
the None check, then assign it from the REST image request result while
preserving the existing successful-request logging and returning the response or
None.
🤖 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 `@scripts/manual_reviewer_smoke_test.py`:
- Around line 218-220: Update the fallback request in the manual reviewer smoke
test around download_response so it always includes a valid tag parameter
required by /datastore/label. Retain the case’s existing tag or select a valid
tag from its advertised versions, while preserving the current image_id and
retry behavior.

---

Outside diff comments:
In
`@plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py`:
- Around line 240-247: Initialize img_blob at the start of requestImage before
the None check, then assign it from the REST image request result while
preserving the existing successful-request logging and returning the response or
None.
🪄 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 Plus

Run ID: be3d0cfc-6a82-4b13-9473-41df3a103573

📥 Commits

Reviewing files that changed from the base of the PR and between e082360 and 55e4683.

📒 Files selected for processing (12)
  • monailabel/datastore/local.py
  • monailabel/endpoints/datastore_review.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py
  • sample-apps/reviewer/client.py
  • sample-apps/reviewer/lib/config.py
  • scripts/manual_reviewer_smoke_test.py
  • tests/unit/datastore/test_local.py
  • tests/unit/endpoints/test_datastore_review.py
  • tests/unit/sample_apps/test_reviewer_app.py
  • tests/unit/sample_apps/test_reviewer_client.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/unit/sample_apps/test_reviewer_client.py
  • tests/unit/sample_apps/test_reviewer_app.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • monailabel/datastore/local.py
  • monailabel/endpoints/datastore_review.py

Comment on lines +218 to +220
download_response = requests.get(
f"http://localhost:{PORT}/datastore/label", params={"label": image_id}, timeout=10
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Always supply a valid label tag in the fallback.

/datastore/label requires tag, so this retry returns HTTP 422 and can never recover. Retain a tag (or select one from the case’s advertised versions) rather than issuing an untagged request.

🤖 Prompt for 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.

In `@scripts/manual_reviewer_smoke_test.py` around lines 218 - 220, Update the
fallback request in the manual reviewer smoke test around download_response so
it always includes a valid tag parameter required by /datastore/label. Retain
the case’s existing tag or select a valid tag from its advertised versions,
while preserving the current image_id and retry behavior.

@chezhia
chezhia force-pushed the feature/lightweight-reviewer branch from d47367c to fe6b02c Compare July 29, 2026 14:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
scripts/manual_reviewer_smoke_test.py (1)

216-228: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fallback retry omits the required tag param — it can never succeed.

The retry drops the tag query param entirely (params={"label": image_id}), but /datastore/label requires tag; this fallback path will hit the same failure (e.g. 422) rather than recovering.

🔧 Proposed fix
             # Try without tag
             download_response = requests.get(
-                f"http://localhost:{PORT}/datastore/label", params={"label": image_id}, timeout=10
+                f"http://localhost:{PORT}/datastore/label", params={"label": image_id, "tag": "original"}, timeout=10
             )
🤖 Prompt for 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.

In `@scripts/manual_reviewer_smoke_test.py` around lines 216 - 228, Update the
fallback request in the manual reviewer smoke test to include the required tag
query parameter alongside label, using the intended default-tag value. Keep the
existing success and failure handling unchanged.
🤖 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 `@plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py`:
- Around line 1733-1744: Update requestDicomImage to remove the temporary DICOM
file at destination after slicer.util.loadVolume completes, matching the cleanup
behavior used by the segmentation display path while preserving the existing
download and loading flow.

In
`@plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py`:
- Around line 240-247: Update requestImage to call
self.monaiServerREST.requestImage(image_id) and assign its result to img_blob
before checking or returning it, matching the request pattern used by
reuqestSegmentation. Ensure the downstream MonaiServerREST.requestImage
undefined download_uri issue is also corrected so the image download call chain
works end to end.

In `@plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py`:
- Around line 60-81: Update requestImage to construct download_uri before
calling requests.get, using self.serverUrl and quote_plus(image_id) consistently
with getDicomDownloadUri and requestSegmentation. Preserve the existing timeout,
exception handling, response-status validation, and return behavior.

In `@sample-apps/reviewer/README.md`:
- Around line 101-110: Update the CSV and HTML report commands in the README to
write the raw responses from /review/report directly to their respective files,
removing the Python json.load unwrapping step. Keep the JSON report command
unchanged.

---

Duplicate comments:
In `@scripts/manual_reviewer_smoke_test.py`:
- Around line 216-228: Update the fallback request in the manual reviewer smoke
test to include the required tag query parameter alongside label, using the
intended default-tag value. Keep the existing success and failure handling
unchanged.
🪄 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 Plus

Run ID: cce92377-7f81-4c55-8b9c-3dc8b660d4bf

📥 Commits

Reviewing files that changed from the base of the PR and between 6ed8f8c and e063fc9.

📒 Files selected for processing (21)
  • .pre-commit-config.yaml
  • README.md
  • monailabel/_version.py
  • monailabel/app.py
  • monailabel/datastore/local.py
  • monailabel/endpoints/datastore_review.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py
  • plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py
  • sample-apps/reviewer/README.md
  • sample-apps/reviewer/app.py
  • sample-apps/reviewer/client.py
  • sample-apps/reviewer/lib/__init__.py
  • sample-apps/reviewer/lib/config.py
  • sample-apps/reviewer/main.py
  • scripts/manual_reviewer_smoke_test.py
  • tests/unit/datastore/test_convert.py
  • tests/unit/datastore/test_local.py
  • tests/unit/endpoints/test_datastore_review.py
  • tests/unit/sample_apps/test_reviewer_app.py
  • tests/unit/sample_apps/test_reviewer_client.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • sample-apps/reviewer/main.py
  • tests/unit/sample_apps/test_reviewer_app.py
  • README.md
  • .pre-commit-config.yaml
  • sample-apps/reviewer/lib/config.py
  • monailabel/datastore/local.py
  • sample-apps/reviewer/app.py

Comment on lines 1733 to 1744
def requestDicomImage(self, image_id: str, image_name: str, node_name: str):
download_uri = self.imageDataController.getDicomDownloadUri(image_id)
SampleData.SampleDataLogic().downloadFromURL(nodeNames=node_name, fileNames=image_name, uris=download_uri)
response = self.imageDataController.requestImage(image_id)
if response is None:
raise RuntimeError(f"Failed to download image '{image_id}' from MONAI Label server")

destination = self.getPathToStore(image_name, self.temp_dir.name)
with open(destination, "wb") as img_file:
img_file.write(response.content)

logging.info(f"{self.getCurrentTime()}: Image stored temporarily in: {destination}")
slicer.util.loadVolume(destination, properties={"name": node_name})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Downloaded DICOM temp file is never cleaned up.

Unlike the segmentation path (storeSegmentationdisplaySegmentionos.remove(destination)), requestDicomImage writes the downloaded image to self.temp_dir and never removes it after slicer.util.loadVolume finishes. Over a long review session (many Previous/Next navigations), this accumulates files in the temp directory until the widget reconnects/cleanCache() is called.

🧹 Proposed fix
         destination = self.getPathToStore(image_name, self.temp_dir.name)
         with open(destination, "wb") as img_file:
             img_file.write(response.content)
 
         logging.info(f"{self.getCurrentTime()}: Image stored temporarily in: {destination}")
         slicer.util.loadVolume(destination, properties={"name": node_name})
+        os.remove(destination)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def requestDicomImage(self, image_id: str, image_name: str, node_name: str):
download_uri = self.imageDataController.getDicomDownloadUri(image_id)
SampleData.SampleDataLogic().downloadFromURL(nodeNames=node_name, fileNames=image_name, uris=download_uri)
response = self.imageDataController.requestImage(image_id)
if response is None:
raise RuntimeError(f"Failed to download image '{image_id}' from MONAI Label server")
destination = self.getPathToStore(image_name, self.temp_dir.name)
with open(destination, "wb") as img_file:
img_file.write(response.content)
logging.info(f"{self.getCurrentTime()}: Image stored temporarily in: {destination}")
slicer.util.loadVolume(destination, properties={"name": node_name})
def requestDicomImage(self, image_id: str, image_name: str, node_name: str):
response = self.imageDataController.requestImage(image_id)
if response is None:
raise RuntimeError(f"Failed to download image '{image_id}' from MONAI Label server")
destination = self.getPathToStore(image_name, self.temp_dir.name)
with open(destination, "wb") as img_file:
img_file.write(response.content)
logging.info(f"{self.getCurrentTime()}: Image stored temporarily in: {destination}")
slicer.util.loadVolume(destination, properties={"name": node_name})
os.remove(destination)
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 1738-1738: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(destination, "wb")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for 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.

In `@plugins/slicer/MONAILabelReviewer/MONAILabelReviewer.py` around lines 1733 -
1744, Update requestDicomImage to remove the temporary DICOM file at destination
after slicer.util.loadVolume completes, matching the cleanup behavior used by
the segmentation display path while preserving the existing download and loading
flow.

Comment on lines +240 to +247
def requestImage(self, image_id: str):
if img_blob is not None:
logging.info(
"{}: Image successfully requested from MONAIServer (image id: {})".format(
self.getCurrentTime(), image_id
)
)
return img_blob

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

img_blob is undefined — requestImage will raise NameError on every call.

The method never calls self.monaiServerREST.requestImage(image_id) (unlike the sibling reuqestSegmentation above it, which does the equivalent call before referencing its result). This breaks DICOM image downloads entirely in the reviewer plugin, confirmed by Ruff's F821 Undefined name 'img_blob'.

🐛 Proposed fix
     def requestImage(self, image_id: str):
+        img_blob = self.monaiServerREST.requestImage(image_id)
         if img_blob is not None:
             logging.info(
                 "{}: Image successfully requested from MONAIServer (image id: {})".format(
                     self.getCurrentTime(), image_id
                 )
             )
         return img_blob

Note: MonaiServerREST.requestImage (in MonaiServerREST.py) has its own, separate bug (undefined download_uri) that must also be fixed for this call chain to work end-to-end.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def requestImage(self, image_id: str):
if img_blob is not None:
logging.info(
"{}: Image successfully requested from MONAIServer (image id: {})".format(
self.getCurrentTime(), image_id
)
)
return img_blob
def requestImage(self, image_id: str):
img_blob = self.monaiServerREST.requestImage(image_id)
if img_blob is not None:
logging.info(
"{}: Image successfully requested from MONAIServer (image id: {})".format(
self.getCurrentTime(), image_id
)
)
return img_blob
🧰 Tools
🪛 Ruff (0.16.0)

[error] 241-241: Undefined name img_blob

(F821)


[error] 247-247: Undefined name img_blob

(F821)

🤖 Prompt for 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.

In
`@plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/ImageDataController.py`
around lines 240 - 247, Update requestImage to call
self.monaiServerREST.requestImage(image_id) and assign its result to img_blob
before checking or returning it, matching the request pattern used by
reuqestSegmentation. Ensure the downstream MonaiServerREST.requestImage
undefined download_uri issue is also corrected so the image download call chain
works end to end.

Comment on lines +60 to +81
def requestImage(self, image_id: str):

try:
response = requests.get(download_uri, timeout=30)
except Exception as exception:
logging.warning(
"{}: Image request (image id: '{}') failed due to '{}'".format(
self.getCurrentTime(), image_id, exception
)
)
return None

if response.status_code != 200:
logging.warning(
"{}: Image request (image id: '{}') failed due to response code: '{}'".format(
self.getCurrentTime(), image_id, response.status_code
)
)
return None

return response

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

download_uri is undefined — requestImage will raise NameError on every call.

requests.get(download_uri, timeout=30) references download_uri, but it's never built in this method. Every sibling method (e.g. getDicomDownloadUri, requestSegmentation) constructs the URI from self.serverUrl and quote_plus(image_id) first. Ruff confirms F821 Undefined name 'download_uri'.

🐛 Proposed fix
     def requestImage(self, image_id: str):
+        download_uri = f"{self.serverUrl}/datastore/image?image={quote_plus(image_id)}"
+        logging.info(f"{self.getCurrentTime()}: REST: request dicom image '{download_uri}'")
 
         try:
             response = requests.get(download_uri, timeout=30)

This, combined with the missing call in ImageDataController.requestImage, means DICOM image download is currently completely broken end-to-end.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def requestImage(self, image_id: str):
try:
response = requests.get(download_uri, timeout=30)
except Exception as exception:
logging.warning(
"{}: Image request (image id: '{}') failed due to '{}'".format(
self.getCurrentTime(), image_id, exception
)
)
return None
if response.status_code != 200:
logging.warning(
"{}: Image request (image id: '{}') failed due to response code: '{}'".format(
self.getCurrentTime(), image_id, response.status_code
)
)
return None
return response
def requestImage(self, image_id: str):
download_uri = f"{self.serverUrl}/datastore/image?image={quote_plus(image_id)}"
logging.info(f"{self.getCurrentTime()}: REST: request dicom image '{download_uri}'")
try:
response = requests.get(download_uri, timeout=30)
except Exception as exception:
logging.warning(
"{}: Image request (image id: '{}') failed due to '{}'".format(
self.getCurrentTime(), image_id, exception
)
)
return None
if response.status_code != 200:
logging.warning(
"{}: Image request (image id: '{}') failed due to response code: '{}'".format(
self.getCurrentTime(), image_id, response.status_code
)
)
return None
return response
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 62-62: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.get(download_uri, timeout=30)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)

🪛 Ruff (0.16.0)

[error] 63-63: Undefined name download_uri

(F821)


[warning] 64-64: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for 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.

In `@plugins/slicer/MONAILabelReviewer/MONAILabelReviewerLib/MonaiServerREST.py`
around lines 60 - 81, Update requestImage to construct download_uri before
calling requests.get, using self.serverUrl and quote_plus(image_id) consistently
with getDicomDownloadUri and requestSegmentation. Preserve the existing timeout,
exception handling, response-status validation, and return behavior.

Comment thread sample-apps/reviewer/README.md
@chezhia chezhia assigned chezhia and sanadeem and unassigned chezhia Jul 29, 2026
Fixes:
- test_binary_to_image cleanup FileNotFoundError (guarded addCleanup)
- highdicom Python 3.9 compatibility (version guard in requirements.txt)
- build-docs (3.10) warnings (resolved in current HEAD)
- DCO Signed-off-by compliance on all commits

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Signed-off-by: Elanchezhian <chezhipower@gmail.com>
@chezhia
chezhia force-pushed the feature/lightweight-reviewer branch 2 times, most recently from c0c16a0 to 775d40b Compare July 29, 2026 16:41
chezhia added 2 commits July 29, 2026 17:44
The packaging Verify Package step runs runtests.sh --net which requires
GPU for deepedit/segmentation integration tests. On ubuntu-latest runners
without GPUs, these tests would always fail. Add an nvidia-smi check to
skip gracefully and explain why.

Signed-off-by: Elanchezhian <chezhipower@gmail.com>
The concurrency group cancels all matrix jobs when a new commit lands,
causing most jobs (packaging, build, deps_check) to be cancelled before
completion. This makes CI results unreliably incomplete — only the 4
fastest-completing jobs show results (all failures), masking the actual
state.

Disabling cancel-in-progress ensures all matrix jobs complete fully and
the real results are visible, even if it means older runs complete after
a new one starts.

Signed-off-by: Elanchezhian <chezhipower@gmail.com>
@chezhia

chezhia commented Jul 29, 2026

Copy link
Copy Markdown
Author

Most of the pending CI failures are not from the feature implementation itself, appreciate any help on proceeding further! Thanks

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