feat(annotation): tag partial panels via status --tag-partial-panels#249
Conversation
7c11d13 to
bbe639d
Compare
17d1d81 to
fc37f6d
Compare
481a9fd to
b8f430b
Compare
fc37f6d to
139d097
Compare
139d097 to
9d28183
Compare
0f63545 to
4624141
Compare
**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) ---
**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)
…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`
461de42 to
15c0a59
Compare
9d28183 to
de870d4
Compare
15c0a59 to
fe70147
Compare
de870d4 to
63acdac
Compare
Add the config-free partial-panel tag write onto 'annotation status': it stamps 'needs_completion' on the unresolved chunks of PARTIAL panels (0 < submitted < K) and clears stale tags, sharing the single status walk. Reuses metadata_ops for the safe upsert.
StatusReport.tag_result is a public field typed TagResult, but the facade never gained a lazy-export entry for it (it was pre-declared in an earlier PR before panel_status.py existed, dropped when that PR's export target still didn't define it, and never re-added once this PR added the class itself). pragmata.annotation.TagResult now resolves like its sibling panel_status exports.
63acdac to
789bb9e
Compare
| def build_metadata_upsert( | ||
| record: rg.Record, | ||
| updates: Mapping[str, object], | ||
| *, | ||
| remove_keys: Iterable[str] = (), | ||
| ) -> rg.Record | None: |
There was a problem hiding this comment.
Hm. This is all non-obvious behavior, but as best I can tell without testing, this is also recommended for v2.8.0. We're still just saying "argilla>=2.0,<3.0" -- maybe it would be better to pin to 2.8 and force intentional version changes to reduce the risk of something changing about upsert behavior and clobbering information we want to retain?
|
|
||
| batched: dict[tuple[str, str], list[rg.Record]] = {} | ||
| n_tagged = n_cleared = n_already = 0 | ||
| for (_ws, uuid), group in _group_by_panel(collected.records).items(): |
There was a problem hiding this comment.
So we're completely separating calibration from production here, and "partial" means "within the context of this particular dataset"? This is ok, but it should be documented.
| """Shared safe metadata operations for live Argilla mutations. | ||
|
|
||
| Used by both the ``--tag-partial-panels`` write path in ``panel_status`` and the | ||
| one-off backfill script under ``scripts/``. Centralises the two safety |
There was a problem hiding this comment.
I don't see this script, is it meant to be here?
| upsert = build_metadata_upsert(rec.record, {}, remove_keys=[NEEDS_COMPLETION_KEY]) | ||
| else: | ||
| continue | ||
| if upsert is None: |
There was a problem hiding this comment.
This should never be the case, should it?
| NEEDS_COMPLETION_KEY = "needs_completion" | ||
| NEEDS_COMPLETION_VALUE = "true" |
There was a problem hiding this comment.
I think there should be a comment indicating that if these are changed, there may need to be manual modifications to the Argilla data to remove old, stale metadata (the tools here will not get rid of it afaict). I can't imagine there will ever be a reason to change this mid-run, though.
Clarify that PARTIAL tagging is dataset-local and ignores min_submitted overlap targets, drop the stale scripts/ backfill reference, and note the stale-metadata risk if the needs_completion tag constants change.
Stack (6 PRs): #246 → #247 → #268 → #248 → #269 → #249
This PR (6/6): Adds the advisory
needs_completiontag write ontoannotation status.status --tag-partial-panels: stampsneeds_completionon the unresolved chunks of PARTIAL panels (0 < submitted < K) and clears stale tags, sharing the single status walk. Annotators filterneeds_completion=truein the Argilla UI to focus on the records that will complete a partially-done panel.Reuses
metadata_opsfor the replace-safe upsert. Stacked on #269.Also re-exports
TagResultfrom thepragmata.annotationfacade:StatusReport.tag_resultis a public field of this type, but the facade export was pre-declared two PRs ago (beforepanel_status.pyexisted), 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.