Skip to content

feat(annotation): panel-status core engine#248

Merged
henrycgbaker merged 1 commit into
mainfrom
feat/annotation-retrieval-completeness/03-status-command
Jul 7, 2026
Merged

feat(annotation): panel-status core engine#248
henrycgbaker merged 1 commit into
mainfrom
feat/annotation-retrieval-completeness/03-status-command

Conversation

@henrycgbaker

@henrycgbaker henrycgbaker commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

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

This PR (4/6): Core engine for live per-panel completeness + all-task progress, computed directly against Argilla (no export round-trip). Exposed via CLI/API in #269. Stacked on #268.

  • All tasks: walks every dataset and computes record progress (total / completed) across retrieval / grounding / generation.
  • Retrieval panels: the retrieval row also carries panel-completeness (panel_complete = all K chunks have ≥1 submitted, overlap_satisfied = each chunk meets its min_submitted, integrity vs n_retrieved_chunks).
  • Config-free + multi-domain: selects datasets by name-prefix (no AnnotationSettings), covering every workspace in one pass. Panels keyed by (workspace, record_uuid). min_submitted read live from each dataset's Argilla settings.

The CLI/API surface (pragmata annotation status) ships in #269. The --tag-partial-panels write ships in #249.

@github-actions github-actions Bot added feature Adds or expands user-facing functionality size: L 500-999 LOC labels Jun 1, 2026
@henrycgbaker henrycgbaker changed the title feat(annotation): add 'annotation status' command for live panel completeness feat(annotation): live panel-status command (config-free, multi-domain) Jul 1, 2026
@henrycgbaker henrycgbaker changed the title feat(annotation): live panel-status command (config-free, multi-domain) feat(annotation): live annotation status (all-task progress + retrieval panels) Jul 1, 2026
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/02-export-completeness branch from 42bd0be to 2791caa Compare July 1, 2026 16:37
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/03-status-command branch from 481a9fd to b8f430b Compare July 1, 2026 16:37
@github-actions github-actions Bot added size: XL 1000+ LOC and removed size: L 500-999 LOC labels Jul 1, 2026
@henrycgbaker henrycgbaker changed the base branch from feat/annotation-retrieval-completeness/02-export-completeness to feat/annotation-retrieval-completeness/02b-wire-export-completeness July 1, 2026 16:38
@henrycgbaker henrycgbaker changed the title feat(annotation): live annotation status (all-task progress + retrieval panels) feat(annotation): panel-status core engine Jul 1, 2026
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/03-status-command branch from b8f430b to c80a597 Compare July 3, 2026 13:34
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/02b-wire-export-completeness branch from fd0eb3d to 2d18117 Compare July 3, 2026 13:34
@github-actions github-actions Bot added size: L 500-999 LOC and removed size: XL 1000+ LOC labels Jul 3, 2026
@henrycgbaker henrycgbaker requested a review from ddimmery July 3, 2026 13:46
@henrycgbaker henrycgbaker marked this pull request as ready for review July 3, 2026 13:46
@saschagobel saschagobel added the annotation Changes affecting the annotation tool label Jul 4, 2026

@ddimmery ddimmery left a comment

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.

The naming of ProgressReport / StatusReport etc could be made more clear to distinguish the hierarchy.

)
n_integrity_warnings += 1
if facts.k_metadata == 0:
n_panels_unknown_k += 1

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.

Is this worth exposing in the StatusReport?

integrity_ok = facts.k_metadata == 0 or facts.k_metadata == facts.k_records
if not integrity_ok:
logger.warning(
"record_uuid=%s: integrity warning - %d records but n_retrieved_chunks metadata=%d",

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.

shouldn't we also print out ws_name?

n_overlap_satisfied: int
n_integrity_warnings: int
n_orphans_skipped: int
progress: "ProgressReport | None" = None

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.

Is there a reason for this to be optional?

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 henrycgbaker force-pushed the feat/annotation-retrieval-completeness/02b-wire-export-completeness branch from 2d18117 to de3f8ff Compare July 7, 2026 07:46
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`
Base automatically changed from feat/annotation-retrieval-completeness/02b-wire-export-completeness to main July 7, 2026 08:43
@henrycgbaker henrycgbaker force-pushed the feat/annotation-retrieval-completeness/03-status-command branch from c80a597 to 7597597 Compare July 7, 2026 11:45
Computes live per-panel completeness across every retrieval dataset in one
pass: walks all datasets by name-prefix (no AnnotationSettings, so it's
config-free and multi-domain), reads each dataset's min_submitted live from
its Argilla distribution settings, and keys panels by (workspace,
record_uuid) so sibling panels across tasks are never fused.

Two distinct notions of complete are computed side by side so operators
don't conflate them:
- panel_complete (metric-facing, STRICT): every K chunk has at least one
  SUBMITTED response.
- overlap_satisfied (operational): every chunk's submitted-response count
  is >= its per-purpose Argilla min_submitted threshold (1 prod / 3 cal).
A panel can be metric-complete but overlap-unsatisfied, or vice versa.

K is computed live by counting distinct chunk-records per record_uuid and
cross-checked against the n_retrieved_chunks metadata for integrity;
mismatches log a warning and surface in the report.

compute_task_progress adds an all-task record-progress summary
(total/completed across retrieval/grounding/generation) via
dataset.progress(), aggregated grand/by-task/by-workspace/by-dataset.
Orphan records (empty record_uuid) are excluded from panel grouping and
reported separately.
@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 merged commit 9f4b5b2 into main Jul 7, 2026
4 checks passed
@henrycgbaker henrycgbaker deleted the feat/annotation-retrieval-completeness/03-status-command branch July 7, 2026 12:27
@henrycgbaker henrycgbaker restored the feat/annotation-retrieval-completeness/03-status-command branch July 7, 2026 12:29
@henrycgbaker henrycgbaker deleted the feat/annotation-retrieval-completeness/03-status-command branch July 7, 2026 12:29
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 feature Adds or expands user-facing functionality size: L 500-999 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants