Skip to content

feat(annotation): status API + CLI command#269

Merged
henrycgbaker merged 2 commits into
mainfrom
feat/annotation-retrieval-completeness/03b-status-api-cli
Jul 7, 2026
Merged

feat(annotation): status API + CLI command#269
henrycgbaker merged 2 commits into
mainfrom
feat/annotation-retrieval-completeness/03b-status-api-cli

Conversation

@henrycgbaker

@henrycgbaker henrycgbaker commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Stack (6 PRs): #246#247#268#248#269#249

This PR (5/6): Split out of #248 (was 975 lines / L). Exposes the panel-status core engine (#248) as a CLI command and API function. Stacked on #248.


Scope

  • New pragmata annotation status CLI: reads live retrieval datasets and reports per-panel completeness across prod + cal in one call, plus the all-task record-progress summary (total/completed across retrieval/grounding/generation) as an aligned table, grouped by task by default, with --by-workspace / --by-dataset for finer breakdowns. Pure read; no Argilla mutations.
  • CLI: --workspace to scope; credentials via --api-url/--api-key or env.
  • api/annotation_status.py: report_status() wraps compute_panel_status + compute_task_progress for the CLI/API surface.

API

  • annotation/__init__.py: lazy re-exports for HeadlineTotals, PanelStatus, StatusReport, report_status.

Tests

  • test_status_api.py: report_status() assembly of panel + progress reports.
  • test_cli_annotation.py: CLI output formatting, --workspace scoping, --by-workspace/--by-dataset breakdowns.

The --tag-partial-panels advisory write ships in #249.

Test plan

  • uv run python -m pytest tests/unit/api/test_status_api.py tests/unit/cli/test_cli_annotation.py

@saschagobel

Copy link
Copy Markdown
Collaborator

_render_table feels like generic CLI presentation formatting rather than annotation-command behavior. Since cli/parsing.py already keeps reusable CLI helpers outside command modules, maybe this should live in a small pragmata.cli.formatting / output helper instead and be imported here. Fine to keep local if this is intentionally private to this one status command, but extracting it would keep the command module boundary cleaner.

@saschagobel saschagobel added api Changes affecting the public Python API or user-facing function signatures cli Changes affecting the command-line interface, CLI options, or terminal behavior annotation Changes affecting the annotation tool labels Jul 4, 2026
Comment on lines +43 to +44
progress = compute_task_progress(client, workspace=workspace)
report = compute_panel_status(client, workspace=workspace).with_progress(progress)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Each of these separately crawl the Argilla data. This shouldn't be a problem, but I suppose you could get a weird race condition that leaves them slightly inconsistent. I think in practice this is only an efficiency issue that isn't really worth fixing unless something comes up.

Comment thread src/pragmata/annotation/__init__.py Outdated
Comment on lines +26 to +28
"PanelStatus",
"SetupResult",
"StatusReport",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ProgressReport and ProgressRow should probably also be exported, since they're constituent components of StatusReport. Consuming code that works with this might want to use these types, so they should probably be exported.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we really export ProgressReport and ProgressRow here?

I’m wondering whether we should be cautious about expanding pragmata.annotation further, since the public annotation surface already seems larger than it strictly needs to be. My intuition is that nested/component types should only be re-exported when users are expected to construct them or depend on them directly. Since these are accessible through the returned StatusReport, I’d lean toward keeping them internal unless there is a concrete external use case.

Comment thread src/pragmata/cli/commands/annotation.py Outdated
else:
panels = ("–", "–", "–")
task_rows.append((row.label, _num(row.total), _num(row.completed), _pct(row.completed, row.total), *panels))
for line in _render_table(["TASK", "TOTAL", "COMPLETED", "%", "PANELS", "COMPL", "OVERLAP"], task_rows, "lrrrrrr"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

COMPLETED versus COMPL is very confusing. I would suggest being more specific on the latter with PANEL-COMPL or something.

henrycgbaker added a commit that referenced this pull request Jul 6, 2026
**Stack (6 PRs):** #246#247#268#248#269#249

**Chain goal:** Persist K (number of retrieved chunks per query) on retrieval records, surface it as panel-completeness columns + sidecar at export time, ship a live `annotation status` CLI on top, and add an opt-in `--tag-partial-panels` advisory write so annotators can filter partially-done panels in the Argilla UI.

**This PR (1/6):** Foundation. Stamps `n_retrieved_chunks` on retrieval record metadata at import and surfaces it on `RetrievalAnnotation`. Ships nothing user-visible on its own — the export columns (#247/#268) and live status CLI (#248/#269) consume it.

---

## Scope

### Record building
- `core/annotation/record_builder.py`: add `"n_retrieved_chunks": len(pair.chunks)` to every chunk-record's metadata dict.

### Task definition
- `core/annotation/argilla_task_definitions.py`: declare `IntegerMetadataProperty("n_retrieved_chunks", min=1, visible_for_annotators=False)` on retrieval task settings.

### Schema
- `core/schemas/annotation_export.py`: `RetrievalAnnotation.n_retrieved_chunks: int` field (mirrors the existing `chunk_rank: int`).

### Export readback
- `core/annotation/export_fetcher.py`: `_build_row` retrieval branch reads `metadata.get("n_retrieved_chunks", 0)` (defensive default for records imported before this PR).

### Tests
- New `test_n_retrieved_chunks_stamped_on_every_chunk_record` in `test_import.py` locks the persist contract.
- Metadata-name list in `test_argilla_task_definitions.py` updated.
- Fixture refresh across `test_annotation_export.py`, `test_export_runner.py`, `test_export_constraint_checks.py`, `test_iaa_runner.py`, `test_export_api.py`.

## Why

K varies per query (~15% K<5, 61% K=5, 24% K>5; the retriever is threshold-based, not top-K), so the export side can't infer K from `chunk_rank` or from record-counting alone — it needs a persisted SSOT to drive true top-K metrics in the consumer pipeline (precision@K, recall@K, NDCG@K, MRR@K).

End-to-end mirror of the existing `chunk_rank` field: stamped at import, declared on the task, modelled on `RetrievalAnnotation`, read back by the fetcher.

A one-off backfill script for records imported before this PR (in-flight batches) lives outside the supported CLI/API surface — retrospective ops migration, not packaged functionality.

## Base

`feat/querygen-resumable-stack` — kitchen-sink integration branch carrying the full dep surface this stack assumes (`Locale`, `atomic_write_text`, `LogicalConstraint`, `WIDGET_FIELD_PLACEHOLDERS`, the `export_constraint_checks` rename). Rebases cleanly to `main` once those upstream stacks land.

## Test plan
- [x] `uv run python -m pytest tests/unit` (863 passing)

---
henrycgbaker added a commit that referenced this pull request Jul 6, 2026
**Stack (6 PRs):** #246 (merged) → #247#268#248#269#249

**Chain goal:** Persist K (number of retrieved chunks per query) on
retrieval records, surface it as panel-completeness columns + sidecar at
export time, ship a live `annotation status` CLI on top, and add an
opt-in `--tag-partial-panels` advisory write so annotators can filter
partially-done panels in the Argilla UI.

**This PR (2/6):** The standalone, export-agnostic panel-completeness
calculator, plus the shared retrieval-walk primitive it and #268 both
consume. Split out of what was originally a single ~1000-line PR; wiring
completeness into the retrieval export path ships separately in #268.

---

## Scope

### Completeness aggregator
- New `core/annotation/completeness.py`: groups retrieval snapshots by
`record_uuid`, distinct-by-`chunk_id`, derives per-panel facts, emits
`CompletenessReport` (`by_uuid` + `summary`). Aggregation is split into
a typed `_PanelAccumulator` plus `_aggregate_snapshots` (grouping) /
`_summarize_groups` (transform) passes.

### Shared retrieval walk
- `core/annotation/export_fetcher.py`: `RetrievalRecordSnapshot` +
`walk_retrieval_records` — one Argilla scroll, no status filter (so
records lacking terminal responses are still seen for the integrity
check), feeding both this module's completeness pass and #268's row
emission. Plus `resolve_task_purposes`, a topology lookup extracted from
`fetch_task` (no behaviour change).

### Schema
- `core/schemas/annotation_export.py`: `CompletenessSummary` +
`KBucketStat` models for the export sidecar (populated by #268).

### Tests
- New `test_completeness.py`: happy path, distinct-by-chunk_id, orphan
exclusion, integrity warnings (records-vs-K + mixed-backfill within
panel), all-unknown-K warning, K-bucket cross-tab + per-K histogram,
STRICT `panel_complete` (discards do NOT count), `n_discarded_chunks`
subset of `n_annotated_chunks`, calibration walked when topology
declares it.

## Why STRICT `panel_complete`

The computed `panel_complete: bool` is STRICT: True iff every K chunk in
the panel has at least one **submitted** response. Discarded responses
are abstentions, not judgements (pragmata's `DiscardReason` enum is
refusal-only: `INVALID_OR_UNREALISTIC` / `UNCLEAR` /
`OUTSIDE_REVIEWER_EXPERTISE`), so treating them as covered would feed
unjudged chunks into NDCG@K / precision@K denominators as 0-relevance
labels. The bool is derivable from the count columns, so a consumer
wanting the permissive policy computes `n_annotated_chunks ==
n_retrieved_chunks` in one line.

The retriever is threshold-based, so dropping partial panels biases
metrics toward small K (MNAR); this module is the shared calculation
both the export path (#268) and the live status path (#248) build on —
one aggregator, two consumers.

## Test plan
- [x] `uv run python -m pytest tests/unit` (1040 passing)
henrycgbaker added a commit that referenced this pull request Jul 7, 2026
…ry) (#268)

**Stack (6 PRs):** #246#247#268#248#269#249

**This PR (3/6):** Split out of #247 (was 1001 lines / XL). Wires the
panel-completeness calculator (#247) into the retrieval export path.
Stacked on #247.

---

## Scope

### Schema
- `core/schemas/annotation_export.py`:
- `RetrievalExportRow` += `panel_complete: bool`, `n_annotated_chunks:
int`, `n_submitted_chunks: int`, `n_discarded_chunks: int`,
`n_records_seen: int`. All defaulted; appended at the tail so existing
column order is preserved.
- New `KBucketStat` (per-K-bucket panel counts) and
`CompletenessSummary` (aggregates + `by_k_bucket` cross-tab + full per-K
histogram) Pydantic models.
- `AnnotationExportMeta` += `completeness_summary: CompletenessSummary |
None` and `completeness_status: Literal["ok","failed","not_requested"]`.

### Single-walk retrieval pipeline
- `core/annotation/export_fetcher.py`: new `walk_retrieval_records` (one
Argilla scroll per dataset, no status filter — sees discards-only
records too so the integrity check has the full record set) +
`RetrievalRecordSnapshot` dataclass + `fetch_retrieval_from_records`
(status filter applied in Python at typed-row construction).
- `core/annotation/export_runner.py`: when `Task.RETRIEVAL in tasks`,
walks once and feeds both the typed-row projection and the completeness
aggregator. `compute_completeness` failures degrade the sidecar (sets
`completeness_status="failed"`) without losing the already-fetched
export.

### API
- `annotation/__init__.py`: lazy re-exports for `CompletenessSummary`,
`KBucketStat`.

### Tests
- `test_export_runner.py`: `write_export_csv` stamps completeness
columns by `record_uuid`; sidecar carries `completeness_summary`;
`completeness_status` ok/failed/not_requested set explicitly.
- `test_annotation_export.py`: `RetrievalExportRow` field-order locked.

## Why

The retriever is threshold-based, so dropping partial panels biases the
metrics toward small K (MNAR). Surfacing both per-row predicate columns
AND a bucketed sidecar aggregate lets the eval pipeline either filter
complete panels per-K or apply condensed-list scoring without
re-querying the data layer.

The single-walk shape (one retrieval scroll feeds both fetch_task and
compute_completeness) costs one extra in-memory pass over
already-fetched records but spares the Argilla server a second full
scroll per export — meaningful on the in-flight batches (~hundreds of
panels).

The STRICT `panel_complete` policy question (which shape to ship — this
PR assumes (A) from #247) is discussed on #247, since that's where the
predicate is actually computed; this PR just surfaces it as a column.

## Test plan
- [ ] `uv run python -m pytest
tests/unit/core/annotation/test_export_runner.py
tests/unit/core/schemas/test_annotation_export.py`
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/03-status-command branch from c80a597 to 7597597 Compare July 7, 2026 11:45
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/03b-status-api-cli branch 2 times, most recently from 461de42 to 15c0a59 Compare July 7, 2026 12:09
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/03-status-command branch from 7597597 to 751883c Compare July 7, 2026 12:25
@henrycgbaker henrycgbaker deleted the branch main July 7, 2026 12:27
@henrycgbaker henrycgbaker reopened this Jul 7, 2026
@henrycgbaker henrycgbaker changed the base branch from feat/annotation-retrieval-completeness/03-status-command to main July 7, 2026 12:29
@github-actions github-actions Bot added size: L 500-999 LOC and removed size: M 200-499 LOC labels Jul 7, 2026
New 'pragmata annotation status' CLI reads live retrieval datasets and
reports per-panel completeness across prod + cal in one call, plus an
all-task record-progress summary across retrieval/grounding/generation.
Pure read; no Argilla mutations.

CLI: --workspace to scope; --by-workspace / --by-dataset for finer
progress breakdowns; credentials via --api-url/--api-key or env.
- Narrow report.progress (always set by report_status) before use; loosen
  _render_table rows to Sequence to fix list-invariance under mypy strict.
- Rename task-table panel column COMPL -> PANEL-COMPL to disambiguate from
  the adjacent COMPLETED (record) column (review: @ddimmery).
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/03b-status-api-cli branch from 15c0a59 to fe70147 Compare July 7, 2026 12:38
@github-actions github-actions Bot added size: M 200-499 LOC and removed size: L 500-999 LOC labels Jul 7, 2026
@henrycgbaker henrycgbaker merged commit f31f7e7 into main Jul 7, 2026
4 checks passed
@henrycgbaker henrycgbaker deleted the feat/annotation-retrieval-completeness/03b-status-api-cli branch July 7, 2026 12:48
henrycgbaker added a commit that referenced this pull request Jul 7, 2026
…249)

**Stack (6 PRs):** #246#247#268#248#269#249

**This PR (6/6):** Adds the advisory `needs_completion` tag write onto
`annotation status`.

- `status --tag-partial-panels`: stamps `needs_completion` on the
**unresolved chunks of PARTIAL panels** (`0 < submitted < K`) and clears
stale tags, sharing the single status walk. Annotators filter
`needs_completion=true` in the Argilla UI to focus on the records that
will complete a partially-done panel.
- **Partial-only gate** (not "any incomplete panel"): a fully-unstarted
panel is never tagged, so the tag stays a selective mop-up signal
instead of ≈ everything pending.
- **Multi-domain + prod/cal-split aware**: a panel whose chunks are
split across a workspace's production + calibration datasets (per-item
calibration) is evaluated as one unit; writes route to each chunk's
owning dataset.

Reuses `metadata_ops` for the replace-safe upsert. Stacked on #269.

Also re-exports `TagResult` from the `pragmata.annotation` facade:
`StatusReport.tag_result` is a public field of this type, but the facade
export was pre-declared two PRs ago (before `panel_status.py` existed),
dropped once that PR's version of the module still didn't define the
class, and never re-added once this PR added it for real.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

annotation Changes affecting the annotation tool api Changes affecting the public Python API or user-facing function signatures cli Changes affecting the command-line interface, CLI options, or terminal behavior feature Adds or expands user-facing functionality size: M 200-499 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants