Spaces:
Sleeping
Sleeping
Upload 18 files
Browse files- DATASET_COLLECTION_GUIDANCE.md +43 -27
- README.md +73 -32
- _utils/_dataset_schema.py +127 -24
- _utils/_shared_logic.py +1 -1
- app.py +41 -6
DATASET_COLLECTION_GUIDANCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Dataset Collection and Multi-Store Operations Guide
|
| 2 |
|
| 3 |
-
## Schema
|
| 4 |
|
| 5 |
> **Run 6 policy:** where older historical sections below describe feedback as
|
| 6 |
> training input, cross-source feedback/contribution joins, unversioned consent,
|
|
@@ -9,6 +9,19 @@
|
|
| 9 |
- Ordinary `/v1/feedback` is rating telemetry only. Query, answer, note, model,
|
| 10 |
page and conversation identity are discarded server-side; durable feedback is
|
| 11 |
opt-in and marked `trainingStatus="telemetry"`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
- `/v1/contribute` requires `consentFlag=true` and the current
|
| 13 |
`consentVersion`. Accepted content enters the mutable contribution receipt
|
| 14 |
ledger with `trainingStatus="quarantined"`; it is not written directly to
|
|
@@ -175,7 +188,7 @@ are readable; Secret values are write-only in the settings interface.
|
|
| 175 |
| `TRAINING_DATASET_REPO` | **Variable** | Legacy HF mode only | Repo ID is normally non-sensitive |
|
| 176 |
| `HF_DATASET_TOKEN` | **Secret** | Legacy HF mode | Actual HF credential |
|
| 177 |
| `HF_DATASET_TOKEN_TYPE` | **Variable** | Legacy HF mode | Classification only |
|
| 178 |
-
| `FEEDBACK_PERSIST_ENABLED` | **Variable** | Recommended |
|
| 179 |
|
| 180 |
> **Attention β your existing `TRAINING_DATASET_REPO` Secret works, but it does
|
| 181 |
> not normally need to be a Secret.**
|
|
@@ -770,8 +783,12 @@ Both files should contain identical bytes.
|
|
| 770 |
### Rating telemetry
|
| 771 |
|
| 772 |
The browser does not send rating telemetry until the reader explicitly enables
|
| 773 |
-
**Send rating telemetry**.
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
```text
|
| 777 |
feedback/YYYY/MM/DD/fb_<recordId>.jsonl
|
|
@@ -779,7 +796,10 @@ feedback/YYYY/MM/DD/fb_<recordId>.jsonl
|
|
| 779 |
|
| 780 |
Inspect the row and verify it contains rating/event mechanics only: no query,
|
| 781 |
answer, note, model, page URL, or conversation identifier, and
|
| 782 |
-
`trainingStatus="telemetry"`.
|
|
|
|
|
|
|
|
|
|
| 783 |
|
| 784 |
### Contribution receipt lifecycle
|
| 785 |
|
|
@@ -790,7 +810,7 @@ delete/withdraw capability. The content is training-ineligible at this stage.
|
|
| 790 |
Only an authorized operator review/promotion request may create durable
|
| 791 |
`trainingStatus="eligible"` rows. Promotion uses an atomic lifecycle claim so a
|
| 792 |
second concurrent reviewer cannot write the same receipt again. After promotion,
|
| 793 |
-
raw
|
| 794 |
dedup keys, provider paths, and only the delete-capability hash remain.
|
| 795 |
|
| 796 |
The same receipt capability remains valid after promotion. DELETE then means
|
|
@@ -904,35 +924,31 @@ empty segments, excessive depth, and unsafe names are rejected.
|
|
| 904 |
|
| 905 |
---
|
| 906 |
|
| 907 |
-
## 14.
|
| 908 |
|
| 909 |
-
There are two
|
| 910 |
|
| 911 |
-
| Path | Endpoint | Typical folder |
|
| 912 |
|---|---|---|---|
|
| 913 |
-
|
|
| 914 |
-
| Explicit contribution | `POST /v1/contribute` | `contributions/` | user
|
| 915 |
|
| 916 |
-
|
| 917 |
-
|
|
|
|
| 918 |
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
### Canonical dedup key
|
| 922 |
-
|
| 923 |
-
```text
|
| 924 |
-
_dedup_key = <conversationId>:<answerIndex>
|
| 925 |
-
```
|
| 926 |
-
|
| 927 |
-
### Priority
|
| 928 |
|
| 929 |
```text
|
| 930 |
-
|
|
|
|
| 931 |
```
|
| 932 |
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
|
|
|
| 936 |
|
| 937 |
---
|
| 938 |
|
|
@@ -1624,7 +1640,7 @@ conflict resolution could hide corruption or unauthorized modification.
|
|
| 1624 |
- exact legacy duplicate records across mirrors suppressed;
|
| 1625 |
- same canonical record ID + different bytes fails closed;
|
| 1626 |
- guarded remote archive extraction;
|
| 1627 |
-
- historical schema normalization to current schema
|
| 1628 |
- default exclusion of feedback telemetry/quarantined/legacy-unreviewed records;
|
| 1629 |
- training acceptance only for `trainingStatus="eligible"` contributions;
|
| 1630 |
- tombstone/retraction handling where applicable;
|
|
|
|
| 1 |
# Dataset Collection and Multi-Store Operations Guide
|
| 2 |
|
| 3 |
+
## Schema v4 privacy and contribution lifecycle β authoritative
|
| 4 |
|
| 5 |
> **Run 6 policy:** where older historical sections below describe feedback as
|
| 6 |
> training input, cross-source feedback/contribution joins, unversioned consent,
|
|
|
|
| 9 |
- Ordinary `/v1/feedback` is rating telemetry only. Query, answer, note, model,
|
| 10 |
page and conversation identity are discarded server-side; durable feedback is
|
| 11 |
opt-in and marked `trainingStatus="telemetry"`.
|
| 12 |
+
- Current schema v4 keeps feedback telemetry and explicit content contribution
|
| 13 |
+
structurally separate. Contribution has two record families: `recordType="qa"`
|
| 14 |
+
for selected/rated Q&A and `recordType="conversation"` for one ordered
|
| 15 |
+
`messages[]` record. Whole-conversation mode does not explode turns into
|
| 16 |
+
independent training rows.
|
| 17 |
+
- The browser's dedicated **Contribute to dataset** sheet is the only content-bearing
|
| 18 |
+
contribution surface. **This Q&A**, **Rated answers**, and **Whole conversation**
|
| 19 |
+
converge on the same exact-JSON inspection, privacy preflight, consent, quarantine,
|
| 20 |
+
receipt-delete, and post-promotion withdrawal lifecycle. Share/export owns no
|
| 21 |
+
contribution controls.
|
| 22 |
+
- Schema v4 requires contribution consent `2.0.0`. Legacy schema v2/v3 clients may
|
| 23 |
+
continue using historical consent `1.0.0` for compatibility; that legacy consent
|
| 24 |
+
is not accepted for schema v4's broader conversation-record contract.
|
| 25 |
- `/v1/contribute` requires `consentFlag=true` and the current
|
| 26 |
`consentVersion`. Accepted content enters the mutable contribution receipt
|
| 27 |
ledger with `trainingStatus="quarantined"`; it is not written directly to
|
|
|
|
| 188 |
| `TRAINING_DATASET_REPO` | **Variable** | Legacy HF mode only | Repo ID is normally non-sensitive |
|
| 189 |
| `HF_DATASET_TOKEN` | **Secret** | Legacy HF mode | Actual HF credential |
|
| 190 |
| `HF_DATASET_TOKEN_TYPE` | **Variable** | Legacy HF mode | Classification only |
|
| 191 |
+
| `FEEDBACK_PERSIST_ENABLED` | **Variable** | Recommended | Server persistence flag; never substitutes for reader telemetry consent |
|
| 192 |
|
| 193 |
> **Attention β your existing `TRAINING_DATASET_REPO` Secret works, but it does
|
| 194 |
> not normally need to be a Secret.**
|
|
|
|
| 783 |
### Rating telemetry
|
| 784 |
|
| 785 |
The browser does not send rating telemetry until the reader explicitly enables
|
| 786 |
+
**Send rating telemetry**. The permission is versioned and stored as a structured
|
| 787 |
+
browser consent record; missing, malformed, stale-version, or historical boolean
|
| 788 |
+
preferences fail closed to **Off**. Every network telemetry request carries
|
| 789 |
+
`telemetryConsent=true`, `telemetryConsentVersion="1.0.0"`, and the grant timestamp;
|
| 790 |
+
the bundled proxy/Worker reject requests that omit the current consent contract.
|
| 791 |
+
With that user permission active and `FEEDBACK_PERSIST_ENABLED=true`, a rating may produce:
|
| 792 |
|
| 793 |
```text
|
| 794 |
feedback/YYYY/MM/DD/fb_<recordId>.jsonl
|
|
|
|
| 796 |
|
| 797 |
Inspect the row and verify it contains rating/event mechanics only: no query,
|
| 798 |
answer, note, model, page URL, or conversation identifier, and
|
| 799 |
+
`trainingStatus="telemetry"`. Turning telemetry off must stop future rating and
|
| 800 |
+
retraction network requests. Storage failure must not break the local rating UI.
|
| 801 |
+
The public `ai-assistant-feedback` DOM event is content-free for the same reason:
|
| 802 |
+
page listeners receive bounded rating mechanics, not the Q&A/note/model/page tuple.
|
| 803 |
|
| 804 |
### Contribution receipt lifecycle
|
| 805 |
|
|
|
|
| 810 |
Only an authorized operator review/promotion request may create durable
|
| 811 |
`trainingStatus="eligible"` rows. Promotion uses an atomic lifecycle claim so a
|
| 812 |
second concurrent reviewer cannot write the same receipt again. After promotion,
|
| 813 |
+
raw contributed content is cleared from the receipt ledger; lifecycle metadata, server-owned
|
| 814 |
dedup keys, provider paths, and only the delete-capability hash remain.
|
| 815 |
|
| 816 |
The same receipt capability remains valid after promotion. DELETE then means
|
|
|
|
| 924 |
|
| 925 |
---
|
| 926 |
|
| 927 |
+
## 14. Contribution record identity and deduplication
|
| 928 |
|
| 929 |
+
There are two intentionally separate collection paths:
|
| 930 |
|
| 931 |
+
| Path | Endpoint | Typical folder | Content semantics |
|
| 932 |
|---|---|---|---|
|
| 933 |
+
| Rating telemetry | `POST /v1/feedback` | `feedback/` | bounded rating/event mechanics only; never training-eligible |
|
| 934 |
+
| Explicit contribution | `POST /v1/contribute` | `contributions/` | user-reviewed Q&A or one ordered conversation record |
|
| 935 |
|
| 936 |
+
Feedback telemetry no longer carries query/answer content, so it is **not** joined
|
| 937 |
+
with contributions to form training examples. It remains `trainingStatus="telemetry"`
|
| 938 |
+
and is excluded from ordinary training output.
|
| 939 |
|
| 940 |
+
Contribution dedup keys are server-owned and receipt-scoped rather than stable
|
| 941 |
+
browser conversation identifiers:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 942 |
|
| 943 |
```text
|
| 944 |
+
Q&A record: <receipt-id>:<answer-index>
|
| 945 |
+
Conversation record: <receipt-id>:conversation
|
| 946 |
```
|
| 947 |
|
| 948 |
+
The same key is reused by later eligible/withdrawal lifecycle records so
|
| 949 |
+
last-write-wins can suppress a withdrawn contribution without exposing a stable
|
| 950 |
+
client identity. Within a key, the newest server `_ts` wins. A whole conversation
|
| 951 |
+
remains one record throughout quarantine, promotion, and withdrawal.
|
| 952 |
|
| 953 |
---
|
| 954 |
|
|
|
|
| 1640 |
- exact legacy duplicate records across mirrors suppressed;
|
| 1641 |
- same canonical record ID + different bytes fails closed;
|
| 1642 |
- guarded remote archive extraction;
|
| 1643 |
+
- historical schema normalization to current schema v4 when `_utils/_dataset_schema.py` is available;
|
| 1644 |
- default exclusion of feedback telemetry/quarantined/legacy-unreviewed records;
|
| 1645 |
- training acceptance only for `trainingStatus="eligible"` contributions;
|
| 1646 |
- tombstone/retraction handling where applicable;
|
README.md
CHANGED
|
@@ -146,31 +146,72 @@ caller-supplied forwarding headers.
|
|
| 146 |
|
| 147 |
### `POST /v1/contribute` β payload schema
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
```json
|
| 150 |
{
|
| 151 |
-
"schemaVersion":
|
| 152 |
-
"consentFlag":
|
| 153 |
-
"consentVersion": "
|
| 154 |
-
"
|
| 155 |
-
"
|
| 156 |
-
"model": { "id": "...", "provider": "...", "model": "..." },
|
| 157 |
"records": [
|
| 158 |
{
|
| 159 |
-
"
|
| 160 |
-
"
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
]
|
| 168 |
}
|
| 169 |
```
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
---
|
| 176 |
|
|
@@ -280,7 +321,7 @@ Effort / Thinking fields and opens a per-model in-memory fallback circuit.
|
|
| 280 |
|---|---|---:|---|
|
| 281 |
| `RECORD_STORAGE_TARGETS` | Variable | New multi-store mode | Provider-neutral JSON topology with exactly one Primary and optional Mirrors. Contains token environment-variable **names**, never token values. |
|
| 282 |
| `TRAINING_DATASET_REPO` | Variable | Legacy HF mode only | Backward-compatible HF Dataset repo ID. When `RECORD_STORAGE_TARGETS` is present, the explicit target topology is authoritative; this value may remain for rollback/older discovery consumers. |
|
| 283 |
-
| `FEEDBACK_PERSIST_ENABLED` | Variable | No |
|
| 284 |
| `CONTRIBUTION_REVIEW_TOKEN` | Secret | For promotion | Operator-only token required to promote quarantined contribution intake to durable `trainingStatus=eligible` storage. |
|
| 285 |
| `CONTRIBUTION_LEDGER_BACKEND` | Variable | No | `memory` (default), `sqlite` (local transactional/restart-durable), or `redis` (shared transactional authority across replicas in one Redis consistency domain). Redis persistence durability is deployment-conditional and is not inferred by the proxy. |
|
| 286 |
| `CONTRIBUTION_LEDGER_SQLITE_PATH` | Variable | When using SQLite | Local SQLite database path. Keep it on deployment-owned persistent storage if restart durability is required. |
|
|
@@ -625,7 +666,7 @@ BASE=https://scikit-plots-ai.hf.space
|
|
| 625 |
|
| 626 |
# 1. Liveness probe
|
| 627 |
curl $BASE/health
|
| 628 |
-
# {"status":"ok","version":"6.
|
| 629 |
|
| 630 |
# Optional deterministic stub rig status
|
| 631 |
curl -s $BASE/health | python3 -m json.tool
|
|
@@ -664,23 +705,23 @@ curl -N $BASE/v1/chat/completions \
|
|
| 664 |
curl $BASE/v1/contribute \
|
| 665 |
-H "Content-Type: application/json" \
|
| 666 |
-d '{
|
| 667 |
-
"schemaVersion":
|
| 668 |
-
"consentFlag":
|
| 669 |
-
"consentVersion": "
|
| 670 |
-
"
|
| 671 |
-
"
|
| 672 |
-
"model": null,
|
| 673 |
"records": [{
|
| 674 |
-
"
|
| 675 |
-
"
|
| 676 |
-
"
|
| 677 |
-
"
|
| 678 |
-
"
|
| 679 |
-
"
|
| 680 |
-
"
|
|
|
|
| 681 |
}]
|
| 682 |
}'
|
| 683 |
-
# {"contributed": true, "
|
| 684 |
```
|
| 685 |
|
| 686 |
---
|
|
|
|
| 146 |
|
| 147 |
### `POST /v1/contribute` β payload schema
|
| 148 |
|
| 149 |
+
The current browser sends **schema v4** with consent **2.0.0**. The endpoint
|
| 150 |
+
supports two explicit contribution record families. A whole conversation is one
|
| 151 |
+
ordered record:
|
| 152 |
+
|
| 153 |
```json
|
| 154 |
{
|
| 155 |
+
"schemaVersion": 4,
|
| 156 |
+
"consentFlag": true,
|
| 157 |
+
"consentVersion": "2.0.0",
|
| 158 |
+
"page": "https://your-docs-site/index.html",
|
| 159 |
+
"model": null,
|
|
|
|
| 160 |
"records": [
|
| 161 |
{
|
| 162 |
+
"recordType": "conversation",
|
| 163 |
+
"messages": [
|
| 164 |
+
{"role": "user", "content": "What is a confusion matrix?", "ts": 1781002584000},
|
| 165 |
+
{
|
| 166 |
+
"role": "assistant",
|
| 167 |
+
"content": "A confusion matrix is...",
|
| 168 |
+
"ts": 1781002584724,
|
| 169 |
+
"model": {"id": "...", "provider": "...", "model": "..."},
|
| 170 |
+
"feedback": {"ratingValue": 2, "ratingLabel": "helpful", "note": "optional"}
|
| 171 |
+
}
|
| 172 |
+
],
|
| 173 |
+
"message": "optional contribution-level reviewer note",
|
| 174 |
+
"ts": 1781002585000
|
| 175 |
}
|
| 176 |
]
|
| 177 |
}
|
| 178 |
```
|
| 179 |
|
| 180 |
+
The dedicated **Contribute to dataset** sheet also supports **This Q&A** and
|
| 181 |
+
**Rated answers** scopes. Those produce `recordType="qa"` records using the
|
| 182 |
+
historical `query` / `answer` shape. The current v4 envelope always requires
|
| 183 |
+
`consentVersion="2.0.0"`; legacy schema v2/v3 clients remain accepted with their
|
| 184 |
+
historical `1.0.0` consent for compatibility, but legacy consent is not accepted
|
| 185 |
+
for the broader v4 contract.
|
| 186 |
+
|
| 187 |
+
Ordinary `/v1/feedback` remains privacy-minimal rating telemetry and never turns
|
| 188 |
+
into content contribution automatically. The browser keeps ratings local unless
|
| 189 |
+
the reader explicitly enables **Send rating telemetry**. Current telemetry uses a
|
| 190 |
+
versioned permission (`telemetryConsent=true`, `telemetryConsentVersion="1.0.0"`)
|
| 191 |
+
and the proxy rejects feedback requests that do not carry that current consent
|
| 192 |
+
contract. Enabling telemetry does not contribute the question, answer, note,
|
| 193 |
+
page, model, or conversation content. Turning telemetry off stops future sends;
|
| 194 |
+
it does not claim erasure of telemetry already accepted by a remote provider.
|
| 195 |
+
|
| 196 |
+
Each accepted contribution record receives a server receipt-scoped dedup key,
|
| 197 |
+
but raw accepted content is **not** written to provider repositories immediately.
|
| 198 |
+
It enters the mutable contribution receipt ledger first with
|
| 199 |
+
`trainingStatus="quarantined"`. Only an independently authorized promotion writes
|
| 200 |
+
an `eligible` row beneath `contributions/YYYY/MM/DD/<record-id>.jsonl` in the
|
| 201 |
+
Primary/Mirrors. Whole-conversation mode contributes one ordered conversation
|
| 202 |
+
record; rated-answer mode contributes only explicitly rated Q&A records; the
|
| 203 |
+
single-Q&A shortcut contributes only the selected Q&A after the same inspect,
|
| 204 |
+
privacy-review, and consent flow.
|
| 205 |
+
|
| 206 |
+
The receipt's delete capability remains meaningful after promotion. While
|
| 207 |
+
pending it removes the content from the active review ledger. After promotion
|
| 208 |
+
the same capability records a privacy-minimal `withdraw` tombstone keyed by the
|
| 209 |
+
server-owned contribution dedup key and attempts best-effort removal from each
|
| 210 |
+
provider's current branch/view. Dataset construction applies the withdrawal
|
| 211 |
+
through last-write-wins and excludes both the withdrawn eligible row and the
|
| 212 |
+
tombstone itself from training output. Versioned repository history, database
|
| 213 |
+
pages/WAL remnants, backups, CDN/provider logs, and infrastructure snapshots are
|
| 214 |
+
**not** claimed physically erased.
|
| 215 |
|
| 216 |
---
|
| 217 |
|
|
|
|
| 321 |
|---|---|---:|---|
|
| 322 |
| `RECORD_STORAGE_TARGETS` | Variable | New multi-store mode | Provider-neutral JSON topology with exactly one Primary and optional Mirrors. Contains token environment-variable **names**, never token values. |
|
| 323 |
| `TRAINING_DATASET_REPO` | Variable | Legacy HF mode only | Backward-compatible HF Dataset repo ID. When `RECORD_STORAGE_TARGETS` is present, the explicit target topology is authoritative; this value may remain for rollback/older discovery consumers. |
|
| 324 |
+
| `FEEDBACK_PERSIST_ENABLED` | Variable | No | Server-side persistence permission for privacy-minimal `/v1/feedback` telemetry. Default is `false`; it cannot override the browser user-consent gate, and feedback is never training-eligible. |
|
| 325 |
| `CONTRIBUTION_REVIEW_TOKEN` | Secret | For promotion | Operator-only token required to promote quarantined contribution intake to durable `trainingStatus=eligible` storage. |
|
| 326 |
| `CONTRIBUTION_LEDGER_BACKEND` | Variable | No | `memory` (default), `sqlite` (local transactional/restart-durable), or `redis` (shared transactional authority across replicas in one Redis consistency domain). Redis persistence durability is deployment-conditional and is not inferred by the proxy. |
|
| 327 |
| `CONTRIBUTION_LEDGER_SQLITE_PATH` | Variable | When using SQLite | Local SQLite database path. Keep it on deployment-owned persistent storage if restart durability is required. |
|
|
|
|
| 666 |
|
| 667 |
# 1. Liveness probe
|
| 668 |
curl $BASE/health
|
| 669 |
+
# {"status":"ok","version":"6.6.1"}
|
| 670 |
|
| 671 |
# Optional deterministic stub rig status
|
| 672 |
curl -s $BASE/health | python3 -m json.tool
|
|
|
|
| 705 |
curl $BASE/v1/contribute \
|
| 706 |
-H "Content-Type: application/json" \
|
| 707 |
-d '{
|
| 708 |
+
"schemaVersion": 4,
|
| 709 |
+
"consentFlag": true,
|
| 710 |
+
"consentVersion": "2.0.0",
|
| 711 |
+
"page": "https://example.com",
|
| 712 |
+
"model": {"id":"test-model","provider":"test"},
|
|
|
|
| 713 |
"records": [{
|
| 714 |
+
"recordType": "qa",
|
| 715 |
+
"answerIndex": 0,
|
| 716 |
+
"query": "test query",
|
| 717 |
+
"answer": "test answer",
|
| 718 |
+
"ratingValue": 2,
|
| 719 |
+
"ratingLabel": "helpful",
|
| 720 |
+
"message": "",
|
| 721 |
+
"ts": 1781002584724
|
| 722 |
}]
|
| 723 |
}'
|
| 724 |
+
# {"contributed": true, "status": "quarantined", "rows": 1, "receiptId": "...", "deleteToken": "..."}
|
| 725 |
```
|
| 726 |
|
| 727 |
---
|
_utils/_dataset_schema.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/
|
| 2 |
#
|
| 3 |
# flake8: noqa: D213
|
| 4 |
#
|
|
@@ -7,16 +7,17 @@
|
|
| 7 |
|
| 8 |
"""Canonical schema and normalization for collection records.
|
| 9 |
|
| 10 |
-
Schema
|
| 11 |
|
| 12 |
* ``feedback`` is privacy-minimal rating telemetry. Content, model, page and
|
| 13 |
conversation identity are discarded even when legacy/direct callers submit them;
|
| 14 |
``trainingStatus`` is always ``telemetry``.
|
| 15 |
-
* ``contribution`` is explicit-content intake.
|
| 16 |
-
|
| 17 |
-
|
|
|
|
| 18 |
|
| 19 |
-
Historical v1/v2 rows remain readable through :func:`normalize_record`, but old
|
| 20 |
contributions become ``legacy_unreviewed`` rather than silently entering training.
|
| 21 |
Client IP addresses are never dataset fields. See ``DATASET_COLLECTION_GUIDANCE.md``
|
| 22 |
for lifecycle and retention policy.
|
|
@@ -40,8 +41,8 @@ logger = logging.getLogger(__name__)
|
|
| 40 |
#: Increment when a breaking field-name change is introduced; additive
|
| 41 |
#: changes (new optional columns, wider population of existing columns) bump
|
| 42 |
#: this too so consumers can branch on ``schemaVersion`` to know which fields
|
| 43 |
-
#: to expect. See
|
| 44 |
-
SCHEMA_VERSION: int =
|
| 45 |
|
| 46 |
#: Ordered list of canonical column names. Every stored JSONL row and every
|
| 47 |
#: row in the pandas DataFrame will have these columns in exactly this order.
|
|
@@ -56,6 +57,7 @@ CANONICAL_COLUMNS: list[str] = [
|
|
| 56 |
"conversationId", # legacy field; v3 feedback/contribution normalization writes None
|
| 57 |
"feedbackId", # feedback event id only; contributions write None
|
| 58 |
# ββ Record descriptor βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 59 |
"answerIndex", # 0-based position of answer in the conversation
|
| 60 |
"action", # "rate" | "retract"
|
| 61 |
"prevFeedbackId", # feedbackId of the record this one supersedes/invalidates.
|
|
@@ -76,7 +78,8 @@ CANONICAL_COLUMNS: list[str] = [
|
|
| 76 |
"message", # contribution text only; feedback telemetry writes empty string
|
| 77 |
# ββ Conversation content ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
"query", # contribution user question; feedback telemetry writes empty string
|
| 79 |
-
"answer", # contribution model response; feedback telemetry
|
|
|
|
| 80 |
# ββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 81 |
"model", # dict | None: normalised 8-key model object (see MODEL_KEYS)
|
| 82 |
"modelEvidence", # None | "client_reported" | "legacy_unverified"
|
|
@@ -88,7 +91,7 @@ CANONICAL_COLUMNS: list[str] = [
|
|
| 88 |
]
|
| 89 |
|
| 90 |
#: Required keys for the normalised model sub-object.
|
| 91 |
-
#:
|
| 92 |
#: this full set; keys absent in the source are filled with ``None``.
|
| 93 |
MODEL_KEYS: list[str] = [
|
| 94 |
"id", # canonical model identifier (e.g. "Qwen2.5-Coder-7B-Instruct-hf")
|
|
@@ -109,7 +112,10 @@ MODEL_KEYS: list[str] = [
|
|
| 109 |
#: whenever the displayed contribution terms change materially and update the
|
| 110 |
#: browser ``CONSENT_VERSION`` in the same run.
|
| 111 |
CONSENT_VERSION_ENABLED: bool = True
|
| 112 |
-
RESERVED_CONSENT_VERSION: str = "
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
|
| 115 |
def _resolve_consent_version(raw: Any) -> str | None:
|
|
@@ -139,8 +145,8 @@ def _resolve_consent_version(raw: Any) -> str | None:
|
|
| 139 |
|
| 140 |
Examples
|
| 141 |
--------
|
| 142 |
-
>>> _resolve_consent_version("
|
| 143 |
-
'
|
| 144 |
>>> _resolve_consent_version(None)
|
| 145 |
"""
|
| 146 |
if not CONSENT_VERSION_ENABLED:
|
|
@@ -562,6 +568,7 @@ def normalize_feedback_record(
|
|
| 562 |
"_dedup_key": dedup,
|
| 563 |
"conversationId": None,
|
| 564 |
"feedbackId": feedback_id,
|
|
|
|
| 565 |
"answerIndex": answer_index,
|
| 566 |
"action": "retract" if is_retract else "rate",
|
| 567 |
"prevFeedbackId": prev_feedback_id,
|
|
@@ -575,6 +582,7 @@ def normalize_feedback_record(
|
|
| 575 |
"message": "",
|
| 576 |
"query": "",
|
| 577 |
"answer": "",
|
|
|
|
| 578 |
"model": None,
|
| 579 |
"modelEvidence": None,
|
| 580 |
"page": "",
|
|
@@ -582,6 +590,65 @@ def normalize_feedback_record(
|
|
| 582 |
"ts": payload.get("ts"),
|
| 583 |
})
|
| 584 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
def normalize_contribution_record(
|
| 586 |
rec: dict[str, Any],
|
| 587 |
*,
|
|
@@ -590,12 +657,46 @@ def normalize_contribution_record(
|
|
| 590 |
training_status: str = "quarantined",
|
| 591 |
submission_id: str | None = None,
|
| 592 |
) -> dict[str, Any]:
|
| 593 |
-
"""Normalize one explicitly consented
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
|
| 595 |
-
Model/provider metadata is a client report, not verified provenance.
|
| 596 |
-
``training_status`` is server-owned and defaults to ``quarantined``; only an
|
| 597 |
-
authorised review/promotion path may create ``eligible`` records.
|
| 598 |
-
"""
|
| 599 |
answer_index = rec.get("answerIndex")
|
| 600 |
try:
|
| 601 |
answer_index = int(answer_index) if answer_index is not None else None
|
|
@@ -606,9 +707,6 @@ def normalize_contribution_record(
|
|
| 606 |
rating_mode=rec.get("ratingMode"), rating_title=rec.get("ratingTitle"),
|
| 607 |
feedback_id=None,
|
| 608 |
)
|
| 609 |
-
if training_status not in {"quarantined", "eligible", "legacy_unreviewed"}:
|
| 610 |
-
training_status = "quarantined"
|
| 611 |
-
dedup_base = _safe_id(submission_id) or "pending"
|
| 612 |
return _ordered({
|
| 613 |
"schemaVersion": SCHEMA_VERSION,
|
| 614 |
"_source": "contribution",
|
|
@@ -616,6 +714,7 @@ def normalize_contribution_record(
|
|
| 616 |
"_dedup_key": f"{dedup_base}:{answer_index}",
|
| 617 |
"conversationId": None,
|
| 618 |
"feedbackId": None,
|
|
|
|
| 619 |
"answerIndex": answer_index,
|
| 620 |
"action": "rate",
|
| 621 |
"prevFeedbackId": None,
|
|
@@ -626,9 +725,10 @@ def normalize_contribution_record(
|
|
| 626 |
"ratingSlug": rating_fields["ratingSlug"],
|
| 627 |
"ratingTitle": rating_fields["ratingTitle"],
|
| 628 |
"ratingMode": rating_fields["ratingMode"],
|
| 629 |
-
"message": rec.get("message")
|
| 630 |
-
"query": rec.get("query")
|
| 631 |
-
"answer": rec.get("answer")
|
|
|
|
| 632 |
"model": normalize_model(envelope.get("model")),
|
| 633 |
"modelEvidence": "client_reported" if envelope.get("model") else None,
|
| 634 |
"page": envelope.get("page") or "",
|
|
@@ -665,6 +765,7 @@ def normalize_contribution_withdrawal_record(
|
|
| 665 |
"_dedup_key": key,
|
| 666 |
"conversationId": None,
|
| 667 |
"feedbackId": None,
|
|
|
|
| 668 |
"answerIndex": answer_index,
|
| 669 |
"action": "withdraw",
|
| 670 |
"prevFeedbackId": None,
|
|
@@ -770,6 +871,7 @@ def normalize_record(raw: dict[str, Any]) -> dict[str, Any]: # noqa: PLR0912
|
|
| 770 |
# ββ Back-fill missing canonical fields (schemaVersion: 1 β 2) βββββββββββββ
|
| 771 |
out["schemaVersion"] = SCHEMA_VERSION
|
| 772 |
out.setdefault("feedbackId", None)
|
|
|
|
| 773 |
out.setdefault("action", "rate")
|
| 774 |
out.setdefault("prevFeedbackId", None)
|
| 775 |
# editCount: None for retraction tombstones (not applicable), 0 for any
|
|
@@ -780,6 +882,7 @@ def normalize_record(raw: dict[str, Any]) -> dict[str, Any]: # noqa: PLR0912
|
|
| 780 |
out.setdefault("message", "")
|
| 781 |
out.setdefault("query", "")
|
| 782 |
out.setdefault("answer", "")
|
|
|
|
| 783 |
out.setdefault("page", "")
|
| 784 |
out.setdefault("modelEvidence", "legacy_unverified" if out.get("model") else None)
|
| 785 |
|
|
|
|
| 1 |
+
# scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/_utils/_dataset_schema.py
|
| 2 |
#
|
| 3 |
# flake8: noqa: D213
|
| 4 |
#
|
|
|
|
| 7 |
|
| 8 |
"""Canonical schema and normalization for collection records.
|
| 9 |
|
| 10 |
+
Schema v4 separates telemetry from two explicit contribution record families:
|
| 11 |
|
| 12 |
* ``feedback`` is privacy-minimal rating telemetry. Content, model, page and
|
| 13 |
conversation identity are discarded even when legacy/direct callers submit them;
|
| 14 |
``trainingStatus`` is always ``telemetry``.
|
| 15 |
+
* ``contribution`` is explicit-content intake. Q&A records retain the historical
|
| 16 |
+
``query``/``answer`` shape while conversation records carry one ordered ``messages``
|
| 17 |
+
array. Both carry versioned consent, enter ``quarantined`` state, and are
|
| 18 |
+
training-eligible only after an authorised review promotes them.
|
| 19 |
|
| 20 |
+
Historical v1/v2/v3 rows remain readable through :func:`normalize_record`, but old
|
| 21 |
contributions become ``legacy_unreviewed`` rather than silently entering training.
|
| 22 |
Client IP addresses are never dataset fields. See ``DATASET_COLLECTION_GUIDANCE.md``
|
| 23 |
for lifecycle and retention policy.
|
|
|
|
| 41 |
#: Increment when a breaking field-name change is introduced; additive
|
| 42 |
#: changes (new optional columns, wider population of existing columns) bump
|
| 43 |
#: this too so consumers can branch on ``schemaVersion`` to know which fields
|
| 44 |
+
#: to expect. See the module docstring and collection guidance for version semantics.
|
| 45 |
+
SCHEMA_VERSION: int = 4
|
| 46 |
|
| 47 |
#: Ordered list of canonical column names. Every stored JSONL row and every
|
| 48 |
#: row in the pandas DataFrame will have these columns in exactly this order.
|
|
|
|
| 57 |
"conversationId", # legacy field; v3 feedback/contribution normalization writes None
|
| 58 |
"feedbackId", # feedback event id only; contributions write None
|
| 59 |
# ββ Record descriptor βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
+
"recordType", # "qa" | "conversation" (feedback writes None)
|
| 61 |
"answerIndex", # 0-based position of answer in the conversation
|
| 62 |
"action", # "rate" | "retract"
|
| 63 |
"prevFeedbackId", # feedbackId of the record this one supersedes/invalidates.
|
|
|
|
| 78 |
"message", # contribution text only; feedback telemetry writes empty string
|
| 79 |
# ββ Conversation content ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 80 |
"query", # contribution user question; feedback telemetry writes empty string
|
| 81 |
+
"answer", # Q&A contribution model response; feedback telemetry/conversations write empty string
|
| 82 |
+
"messages", # conversation contribution ordered message list; otherwise None
|
| 83 |
# ββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
"model", # dict | None: normalised 8-key model object (see MODEL_KEYS)
|
| 85 |
"modelEvidence", # None | "client_reported" | "legacy_unverified"
|
|
|
|
| 91 |
]
|
| 92 |
|
| 93 |
#: Required keys for the normalised model sub-object.
|
| 94 |
+
#: Legacy/model-bearing contribution shapes are expanded to
|
| 95 |
#: this full set; keys absent in the source are filled with ``None``.
|
| 96 |
MODEL_KEYS: list[str] = [
|
| 97 |
"id", # canonical model identifier (e.g. "Qwen2.5-Coder-7B-Instruct-hf")
|
|
|
|
| 112 |
#: whenever the displayed contribution terms change materially and update the
|
| 113 |
#: browser ``CONSENT_VERSION`` in the same run.
|
| 114 |
CONSENT_VERSION_ENABLED: bool = True
|
| 115 |
+
RESERVED_CONSENT_VERSION: str = "2.0.0"
|
| 116 |
+
FEEDBACK_TELEMETRY_CONSENT_VERSION: str = "1.0.0"
|
| 117 |
+
FEEDBACK_TELEMETRY_SCHEMA_VERSION: int = 4
|
| 118 |
+
LEGACY_CONSENT_VERSIONS: frozenset[str] = frozenset({"1.0.0"})
|
| 119 |
|
| 120 |
|
| 121 |
def _resolve_consent_version(raw: Any) -> str | None:
|
|
|
|
| 145 |
|
| 146 |
Examples
|
| 147 |
--------
|
| 148 |
+
>>> _resolve_consent_version("2.0.0")
|
| 149 |
+
'2.0.0'
|
| 150 |
>>> _resolve_consent_version(None)
|
| 151 |
"""
|
| 152 |
if not CONSENT_VERSION_ENABLED:
|
|
|
|
| 568 |
"_dedup_key": dedup,
|
| 569 |
"conversationId": None,
|
| 570 |
"feedbackId": feedback_id,
|
| 571 |
+
"recordType": None,
|
| 572 |
"answerIndex": answer_index,
|
| 573 |
"action": "retract" if is_retract else "rate",
|
| 574 |
"prevFeedbackId": prev_feedback_id,
|
|
|
|
| 582 |
"message": "",
|
| 583 |
"query": "",
|
| 584 |
"answer": "",
|
| 585 |
+
"messages": None,
|
| 586 |
"model": None,
|
| 587 |
"modelEvidence": None,
|
| 588 |
"page": "",
|
|
|
|
| 590 |
"ts": payload.get("ts"),
|
| 591 |
})
|
| 592 |
|
| 593 |
+
_MAX_CONVERSATION_MESSAGES: int = 100
|
| 594 |
+
_MAX_CONVERSATION_MESSAGE_CHARS: int = 100_000
|
| 595 |
+
_MAX_CONTRIBUTION_NOTE_CHARS: int = 2_000
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def _bounded_text(value: Any, *, limit: int) -> str:
|
| 599 |
+
"""Return a bounded string for explicit contribution content."""
|
| 600 |
+
if not isinstance(value, str):
|
| 601 |
+
return ""
|
| 602 |
+
return value[:limit]
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def normalize_conversation_messages(value: Any) -> list[dict[str, Any]]:
|
| 606 |
+
"""Normalize one explicit whole-conversation message array.
|
| 607 |
+
|
| 608 |
+
Only ``user`` and ``assistant`` roles are accepted. Error/tool/system rows are
|
| 609 |
+
deliberately excluded from this training/evaluation contribution family.
|
| 610 |
+
Per-assistant model and rating metadata remain client-reported evidence.
|
| 611 |
+
"""
|
| 612 |
+
if not isinstance(value, list):
|
| 613 |
+
return []
|
| 614 |
+
out: list[dict[str, Any]] = []
|
| 615 |
+
for raw in value[:_MAX_CONVERSATION_MESSAGES]:
|
| 616 |
+
if not isinstance(raw, dict):
|
| 617 |
+
continue
|
| 618 |
+
role = raw.get("role")
|
| 619 |
+
if role not in {"user", "assistant"}:
|
| 620 |
+
continue
|
| 621 |
+
content = _bounded_text(raw.get("content"), limit=_MAX_CONVERSATION_MESSAGE_CHARS)
|
| 622 |
+
if not content:
|
| 623 |
+
continue
|
| 624 |
+
item: dict[str, Any] = {
|
| 625 |
+
"role": role,
|
| 626 |
+
"content": content,
|
| 627 |
+
"ts": raw.get("ts") if isinstance(raw.get("ts"), (int, float)) and not isinstance(raw.get("ts"), bool) else None,
|
| 628 |
+
}
|
| 629 |
+
if role == "assistant":
|
| 630 |
+
raw_model = raw.get("model")
|
| 631 |
+
item["model"] = normalize_model(raw_model) if isinstance(raw_model, dict) else None
|
| 632 |
+
raw_feedback = raw.get("feedback")
|
| 633 |
+
if isinstance(raw_feedback, dict):
|
| 634 |
+
rating = normalize_rating(
|
| 635 |
+
raw_feedback.get("ratingValue"), raw_feedback.get("ratingLabel"),
|
| 636 |
+
rating_mode=raw_feedback.get("ratingMode"),
|
| 637 |
+
rating_title=raw_feedback.get("ratingTitle"), feedback_id=None,
|
| 638 |
+
)
|
| 639 |
+
item["feedback"] = {
|
| 640 |
+
"ratingValue": raw_feedback.get("ratingValue"),
|
| 641 |
+
"ratingSlug": rating["ratingSlug"],
|
| 642 |
+
"ratingTitle": rating["ratingTitle"],
|
| 643 |
+
"ratingMode": rating["ratingMode"],
|
| 644 |
+
"note": _bounded_text(raw_feedback.get("note"), limit=_MAX_CONTRIBUTION_NOTE_CHARS),
|
| 645 |
+
}
|
| 646 |
+
else:
|
| 647 |
+
item["feedback"] = None
|
| 648 |
+
out.append(item)
|
| 649 |
+
return out
|
| 650 |
+
|
| 651 |
+
|
| 652 |
def normalize_contribution_record(
|
| 653 |
rec: dict[str, Any],
|
| 654 |
*,
|
|
|
|
| 657 |
training_status: str = "quarantined",
|
| 658 |
submission_id: str | None = None,
|
| 659 |
) -> dict[str, Any]:
|
| 660 |
+
"""Normalize one explicitly consented Q&A or conversation contribution."""
|
| 661 |
+
if training_status not in {"quarantined", "eligible", "legacy_unreviewed"}:
|
| 662 |
+
training_status = "quarantined"
|
| 663 |
+
dedup_base = _safe_id(submission_id) or "pending"
|
| 664 |
+
declared_type = rec.get("recordType")
|
| 665 |
+
record_type = "conversation" if declared_type == "conversation" else "qa"
|
| 666 |
+
|
| 667 |
+
if record_type == "conversation":
|
| 668 |
+
messages = normalize_conversation_messages(rec.get("messages"))
|
| 669 |
+
return _ordered({
|
| 670 |
+
"schemaVersion": SCHEMA_VERSION,
|
| 671 |
+
"_source": "contribution",
|
| 672 |
+
"_ts": server_ts_ms,
|
| 673 |
+
"_dedup_key": f"{dedup_base}:conversation",
|
| 674 |
+
"conversationId": None,
|
| 675 |
+
"feedbackId": None,
|
| 676 |
+
"recordType": "conversation",
|
| 677 |
+
"answerIndex": None,
|
| 678 |
+
"action": "rate",
|
| 679 |
+
"prevFeedbackId": None,
|
| 680 |
+
"editCount": 0,
|
| 681 |
+
"status": "active",
|
| 682 |
+
"trainingStatus": training_status,
|
| 683 |
+
"ratingValue": None,
|
| 684 |
+
"ratingSlug": None,
|
| 685 |
+
"ratingTitle": None,
|
| 686 |
+
"ratingMode": None,
|
| 687 |
+
"message": _bounded_text(rec.get("message"), limit=_MAX_CONTRIBUTION_NOTE_CHARS),
|
| 688 |
+
"query": "",
|
| 689 |
+
"answer": "",
|
| 690 |
+
"messages": messages,
|
| 691 |
+
"model": None,
|
| 692 |
+
"modelEvidence": "client_reported_per_message" if any(
|
| 693 |
+
isinstance(m.get("model"), dict) for m in messages if m.get("role") == "assistant"
|
| 694 |
+
) else None,
|
| 695 |
+
"page": envelope.get("page") or "",
|
| 696 |
+
"consentVersion": _resolve_consent_version(envelope.get("consentVersion")),
|
| 697 |
+
"ts": rec.get("ts"),
|
| 698 |
+
})
|
| 699 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
answer_index = rec.get("answerIndex")
|
| 701 |
try:
|
| 702 |
answer_index = int(answer_index) if answer_index is not None else None
|
|
|
|
| 707 |
rating_mode=rec.get("ratingMode"), rating_title=rec.get("ratingTitle"),
|
| 708 |
feedback_id=None,
|
| 709 |
)
|
|
|
|
|
|
|
|
|
|
| 710 |
return _ordered({
|
| 711 |
"schemaVersion": SCHEMA_VERSION,
|
| 712 |
"_source": "contribution",
|
|
|
|
| 714 |
"_dedup_key": f"{dedup_base}:{answer_index}",
|
| 715 |
"conversationId": None,
|
| 716 |
"feedbackId": None,
|
| 717 |
+
"recordType": "qa",
|
| 718 |
"answerIndex": answer_index,
|
| 719 |
"action": "rate",
|
| 720 |
"prevFeedbackId": None,
|
|
|
|
| 725 |
"ratingSlug": rating_fields["ratingSlug"],
|
| 726 |
"ratingTitle": rating_fields["ratingTitle"],
|
| 727 |
"ratingMode": rating_fields["ratingMode"],
|
| 728 |
+
"message": _bounded_text(rec.get("message"), limit=_MAX_CONTRIBUTION_NOTE_CHARS),
|
| 729 |
+
"query": _bounded_text(rec.get("query"), limit=_MAX_CONVERSATION_MESSAGE_CHARS),
|
| 730 |
+
"answer": _bounded_text(rec.get("answer"), limit=_MAX_CONVERSATION_MESSAGE_CHARS),
|
| 731 |
+
"messages": None,
|
| 732 |
"model": normalize_model(envelope.get("model")),
|
| 733 |
"modelEvidence": "client_reported" if envelope.get("model") else None,
|
| 734 |
"page": envelope.get("page") or "",
|
|
|
|
| 765 |
"_dedup_key": key,
|
| 766 |
"conversationId": None,
|
| 767 |
"feedbackId": None,
|
| 768 |
+
"recordType": None,
|
| 769 |
"answerIndex": answer_index,
|
| 770 |
"action": "withdraw",
|
| 771 |
"prevFeedbackId": None,
|
|
|
|
| 871 |
# ββ Back-fill missing canonical fields (schemaVersion: 1 β 2) βββββββββββββ
|
| 872 |
out["schemaVersion"] = SCHEMA_VERSION
|
| 873 |
out.setdefault("feedbackId", None)
|
| 874 |
+
out.setdefault("recordType", "qa" if source == "contribution" else None)
|
| 875 |
out.setdefault("action", "rate")
|
| 876 |
out.setdefault("prevFeedbackId", None)
|
| 877 |
# editCount: None for retraction tombstones (not applicable), 0 for any
|
|
|
|
| 882 |
out.setdefault("message", "")
|
| 883 |
out.setdefault("query", "")
|
| 884 |
out.setdefault("answer", "")
|
| 885 |
+
out.setdefault("messages", None)
|
| 886 |
out.setdefault("page", "")
|
| 887 |
out.setdefault("modelEvidence", "legacy_unverified" if out.get("model") else None)
|
| 888 |
|
_utils/_shared_logic.py
CHANGED
|
@@ -220,7 +220,7 @@ __all__ = [ # noqa: RUF022
|
|
| 220 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
|
| 222 |
#: Proxy release version β bump on every breaking change.
|
| 223 |
-
PROXY_VERSION: str = "6.
|
| 224 |
|
| 225 |
#: HuggingFace Inference Providers router base URL (no trailing slash).
|
| 226 |
#: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
|
|
|
|
| 220 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
|
| 222 |
#: Proxy release version β bump on every breaking change.
|
| 223 |
+
PROXY_VERSION: str = "6.6.1"
|
| 224 |
|
| 225 |
#: HuggingFace Inference Providers router base URL (no trailing slash).
|
| 226 |
#: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
|
app.py
CHANGED
|
@@ -110,7 +110,7 @@
|
|
| 110 |
# RATE_LIMIT_REQUIRE_SHARED When true, local rate limiting is rejected and
|
| 111 |
# Redis initialization/runtime failure returns HTTP 503.
|
| 112 |
# RATE_LIMIT_REDIS_TIMEOUT_SECONDS Redis operation timeout; clamped 0.25..10 s.
|
| 113 |
-
# FEEDBACK_PERSIST_ENABLED Persist privacy-minimal rating telemetry only. Default false.
|
| 114 |
# CONTRIBUTION_REVIEW_TOKEN Server/operator token required to promote quarantined
|
| 115 |
# contributions into durable training-eligible storage.
|
| 116 |
# CONTRIBUTION_QUARANTINE_TTL_SECONDS Pending contribution lifetime. Default 86400.
|
|
@@ -180,6 +180,9 @@ try:
|
|
| 180 |
from ._utils._dataset_schema import ( # type: ignore[import]
|
| 181 |
CONSENT_VERSION_ENABLED,
|
| 182 |
RESERVED_CONSENT_VERSION,
|
|
|
|
|
|
|
|
|
|
| 183 |
normalize_contribution_record,
|
| 184 |
normalize_contribution_withdrawal_record,
|
| 185 |
normalize_feedback_record,
|
|
@@ -188,6 +191,9 @@ except Exception: # noqa: BLE001
|
|
| 188 |
from _utils._dataset_schema import ( # type: ignore[import]
|
| 189 |
CONSENT_VERSION_ENABLED,
|
| 190 |
RESERVED_CONSENT_VERSION,
|
|
|
|
|
|
|
|
|
|
| 191 |
normalize_contribution_record,
|
| 192 |
normalize_contribution_withdrawal_record,
|
| 193 |
normalize_feedback_record,
|
|
@@ -2069,6 +2075,8 @@ async def root() -> JSONResponse:
|
|
| 2069 |
"dataset_repo": None,
|
| 2070 |
"contribute_ready": _contribution_pipeline_ready(),
|
| 2071 |
"feedback_persist_enabled": FEEDBACK_PERSIST_ENABLED,
|
|
|
|
|
|
|
| 2072 |
},
|
| 2073 |
"tokens": {
|
| 2074 |
"hf_token_type": "unknown",
|
|
@@ -2430,13 +2438,19 @@ async def contribute(request: Request) -> JSONResponse:
|
|
| 2430 |
raise HTTPException(status_code=422, detail="Contribution body must be a JSON object.")
|
| 2431 |
if payload.get("consentFlag") is not True:
|
| 2432 |
raise HTTPException(status_code=422, detail="Explicit contribution consent is required.")
|
| 2433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2434 |
raise HTTPException(
|
| 2435 |
status_code=422,
|
| 2436 |
detail="Consent text changed or is missing. Reload the page and review consent again.",
|
| 2437 |
)
|
| 2438 |
-
if payload.get("schemaVersion") not in {2, 3}:
|
| 2439 |
-
raise HTTPException(status_code=422, detail="Unsupported contribution schemaVersion.")
|
| 2440 |
records = payload.get("records")
|
| 2441 |
if not isinstance(records, list) or not records:
|
| 2442 |
raise HTTPException(status_code=422, detail="records must be a non-empty list.")
|
|
@@ -2462,6 +2476,10 @@ async def contribute(request: Request) -> JSONResponse:
|
|
| 2462 |
)
|
| 2463 |
for rec in records if isinstance(rec, dict)
|
| 2464 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2465 |
if not normalized:
|
| 2466 |
raise HTTPException(status_code=422, detail="No valid contribution records were supplied.")
|
| 2467 |
encoded = ("\n".join(json.dumps(r, ensure_ascii=False) for r in normalized)).encode("utf-8")
|
|
@@ -3135,9 +3153,10 @@ async def share_delete(share_id: str, request: Request) -> JSONResponse:
|
|
| 3135 |
async def feedback(request: Request) -> JSONResponse:
|
| 3136 |
"""Accept privacy-minimal rating telemetry.
|
| 3137 |
|
| 3138 |
-
|
|
|
|
| 3139 |
conversation identifiers, but the server normalizer discards them before
|
| 3140 |
-
optional persistence.
|
| 3141 |
(``FEEDBACK_PERSIST_ENABLED=false`` by default) and persisted rows are
|
| 3142 |
``trainingStatus=telemetry`` so the training builder excludes them.
|
| 3143 |
"""
|
|
@@ -3154,6 +3173,22 @@ async def feedback(request: Request) -> JSONResponse:
|
|
| 3154 |
status_code=422, detail="Feedback body must be a JSON object."
|
| 3155 |
)
|
| 3156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3157 |
# ββ Distinguish retraction tombstones from regular ratings βββββββββββββββ
|
| 3158 |
# Retractions are system-generated housekeeping records that invalidate a
|
| 3159 |
# previous rating in the training dataset. They carry action="retract" and
|
|
|
|
| 110 |
# RATE_LIMIT_REQUIRE_SHARED When true, local rate limiting is rejected and
|
| 111 |
# Redis initialization/runtime failure returns HTTP 503.
|
| 112 |
# RATE_LIMIT_REDIS_TIMEOUT_SECONDS Redis operation timeout; clamped 0.25..10 s.
|
| 113 |
+
# FEEDBACK_PERSIST_ENABLED Persist privacy-minimal consent-gated rating telemetry only. Default false.
|
| 114 |
# CONTRIBUTION_REVIEW_TOKEN Server/operator token required to promote quarantined
|
| 115 |
# contributions into durable training-eligible storage.
|
| 116 |
# CONTRIBUTION_QUARANTINE_TTL_SECONDS Pending contribution lifetime. Default 86400.
|
|
|
|
| 180 |
from ._utils._dataset_schema import ( # type: ignore[import]
|
| 181 |
CONSENT_VERSION_ENABLED,
|
| 182 |
RESERVED_CONSENT_VERSION,
|
| 183 |
+
LEGACY_CONSENT_VERSIONS,
|
| 184 |
+
FEEDBACK_TELEMETRY_CONSENT_VERSION,
|
| 185 |
+
FEEDBACK_TELEMETRY_SCHEMA_VERSION,
|
| 186 |
normalize_contribution_record,
|
| 187 |
normalize_contribution_withdrawal_record,
|
| 188 |
normalize_feedback_record,
|
|
|
|
| 191 |
from _utils._dataset_schema import ( # type: ignore[import]
|
| 192 |
CONSENT_VERSION_ENABLED,
|
| 193 |
RESERVED_CONSENT_VERSION,
|
| 194 |
+
LEGACY_CONSENT_VERSIONS,
|
| 195 |
+
FEEDBACK_TELEMETRY_CONSENT_VERSION,
|
| 196 |
+
FEEDBACK_TELEMETRY_SCHEMA_VERSION,
|
| 197 |
normalize_contribution_record,
|
| 198 |
normalize_contribution_withdrawal_record,
|
| 199 |
normalize_feedback_record,
|
|
|
|
| 2075 |
"dataset_repo": None,
|
| 2076 |
"contribute_ready": _contribution_pipeline_ready(),
|
| 2077 |
"feedback_persist_enabled": FEEDBACK_PERSIST_ENABLED,
|
| 2078 |
+
"feedback_telemetry_schema_version": FEEDBACK_TELEMETRY_SCHEMA_VERSION,
|
| 2079 |
+
"feedback_telemetry_consent_version": FEEDBACK_TELEMETRY_CONSENT_VERSION,
|
| 2080 |
},
|
| 2081 |
"tokens": {
|
| 2082 |
"hf_token_type": "unknown",
|
|
|
|
| 2438 |
raise HTTPException(status_code=422, detail="Contribution body must be a JSON object.")
|
| 2439 |
if payload.get("consentFlag") is not True:
|
| 2440 |
raise HTTPException(status_code=422, detail="Explicit contribution consent is required.")
|
| 2441 |
+
schema_version = payload.get("schemaVersion")
|
| 2442 |
+
if schema_version not in {2, 3, 4}:
|
| 2443 |
+
raise HTTPException(status_code=422, detail="Unsupported contribution schemaVersion.")
|
| 2444 |
+
consent_version = payload.get("consentVersion")
|
| 2445 |
+
if schema_version == 4:
|
| 2446 |
+
consent_ok = consent_version == RESERVED_CONSENT_VERSION
|
| 2447 |
+
else:
|
| 2448 |
+
consent_ok = consent_version in (LEGACY_CONSENT_VERSIONS | {RESERVED_CONSENT_VERSION})
|
| 2449 |
+
if not consent_ok:
|
| 2450 |
raise HTTPException(
|
| 2451 |
status_code=422,
|
| 2452 |
detail="Consent text changed or is missing. Reload the page and review consent again.",
|
| 2453 |
)
|
|
|
|
|
|
|
| 2454 |
records = payload.get("records")
|
| 2455 |
if not isinstance(records, list) or not records:
|
| 2456 |
raise HTTPException(status_code=422, detail="records must be a non-empty list.")
|
|
|
|
| 2476 |
)
|
| 2477 |
for rec in records if isinstance(rec, dict)
|
| 2478 |
]
|
| 2479 |
+
normalized = [
|
| 2480 |
+
row for row in normalized
|
| 2481 |
+
if row.get("recordType") != "conversation" or bool(row.get("messages"))
|
| 2482 |
+
]
|
| 2483 |
if not normalized:
|
| 2484 |
raise HTTPException(status_code=422, detail="No valid contribution records were supplied.")
|
| 2485 |
encoded = ("\n".join(json.dumps(r, ensure_ascii=False) for r in normalized)).encode("utf-8")
|
|
|
|
| 3153 |
async def feedback(request: Request) -> JSONResponse:
|
| 3154 |
"""Accept privacy-minimal rating telemetry.
|
| 3155 |
|
| 3156 |
+
Every request must carry the current explicit telemetry-consent marker.
|
| 3157 |
+
Direct callers may still submit query, answer, comment, model, page or
|
| 3158 |
conversation identifiers, but the server normalizer discards them before
|
| 3159 |
+
optional persistence. Persistence is server opt-in
|
| 3160 |
(``FEEDBACK_PERSIST_ENABLED=false`` by default) and persisted rows are
|
| 3161 |
``trainingStatus=telemetry`` so the training builder excludes them.
|
| 3162 |
"""
|
|
|
|
| 3173 |
status_code=422, detail="Feedback body must be a JSON object."
|
| 3174 |
)
|
| 3175 |
|
| 3176 |
+
# Network feedback is user-consented telemetry, not an implicit side effect
|
| 3177 |
+
# of clicking a local rating button. Reject legacy/misbehaving clients that
|
| 3178 |
+
# do not carry the current explicit consent marker. This check occurs before
|
| 3179 |
+
# rate-limit/storage work so denied telemetry consumes minimal server state.
|
| 3180 |
+
if payload.get("telemetryConsent") is not True or payload.get("telemetryConsentVersion") != FEEDBACK_TELEMETRY_CONSENT_VERSION:
|
| 3181 |
+
logger.warning(json.dumps({"event": "feedback.consent_required"}))
|
| 3182 |
+
raise HTTPException(
|
| 3183 |
+
status_code=403,
|
| 3184 |
+
detail="Explicit feedback telemetry permission is required.",
|
| 3185 |
+
)
|
| 3186 |
+
if payload.get("schemaVersion") != FEEDBACK_TELEMETRY_SCHEMA_VERSION:
|
| 3187 |
+
raise HTTPException(status_code=422, detail="Unsupported feedback telemetry schema version.")
|
| 3188 |
+
consent_at = payload.get("telemetryConsentAt")
|
| 3189 |
+
if not isinstance(consent_at, (int, float)) or isinstance(consent_at, bool) or consent_at <= 0:
|
| 3190 |
+
raise HTTPException(status_code=422, detail="Feedback telemetry consent timestamp is required.")
|
| 3191 |
+
|
| 3192 |
# ββ Distinguish retraction tombstones from regular ratings βββββββββββββββ
|
| 3193 |
# Retractions are system-generated housekeeping records that invalidate a
|
| 3194 |
# previous rating in the training dataset. They carry action="retract" and
|