GitHub Actions commited on
Commit ·
feebf4d
1
Parent(s): c667464
Sync from GitHub
Browse files- README.md +9 -0
- hf-space/.env.example +7 -0
- hf-space/.github/workflows/sync-to-hf.yml +35 -0
- hf-space/.gitignore +7 -0
- hf-space/Dockerfile +11 -0
- hf-space/README.md +84 -10
- hf-space/app.py +853 -0
- hf-space/hf-space/.gitattributes +35 -0
- hf-space/hf-space/README.md +10 -0
- hf-space/requirements.txt +5 -0
- hf-space/scripts/seed.py +28 -0
README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# LLM Annotation Platform — Hugging Face native
|
| 2 |
|
| 3 |
This version removes the external database layer.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: LLM Annotation Platform
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
# LLM Annotation Platform — Hugging Face native
|
| 11 |
|
| 12 |
This version removes the external database layer.
|
hf-space/.env.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SOURCE_DATASET_REPO=nvidia/CantTalkAboutThis-Topic-Control-Dataset
|
| 2 |
+
SOURCE_DATASET_SPLIT=train
|
| 3 |
+
ANNOTATION_REPO_ID=YOUR_ORG/llm-distractor-annotations
|
| 4 |
+
HF_TOKEN=
|
| 5 |
+
CACHE_DIR=/data/hf_annotation_cache
|
| 6 |
+
DRAFT_DIR=/data/hf_annotation_drafts
|
| 7 |
+
EXPORT_DIR=/data/hf_annotation_exports
|
hf-space/.github/workflows/sync-to-hf.yml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face Space
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
sync-to-hub:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout repository
|
| 14 |
+
uses: actions/checkout@v4
|
| 15 |
+
with:
|
| 16 |
+
lfs: true
|
| 17 |
+
|
| 18 |
+
- name: Push to Hugging Face
|
| 19 |
+
env:
|
| 20 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 21 |
+
run: |
|
| 22 |
+
git config --global user.email "github-actions@github.com"
|
| 23 |
+
git config --global user.name "GitHub Actions"
|
| 24 |
+
|
| 25 |
+
git clone https://user:$HF_TOKEN@huggingface.co/spaces/keepingLLMontrack/llm-annotation-platform hf-space
|
| 26 |
+
|
| 27 |
+
rsync -av --exclude '.git' ./ hf-space/
|
| 28 |
+
|
| 29 |
+
cd hf-space
|
| 30 |
+
|
| 31 |
+
git add .
|
| 32 |
+
|
| 33 |
+
git commit -m "Sync from GitHub" || echo "No changes to commit"
|
| 34 |
+
|
| 35 |
+
git push
|
hf-space/.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.streamlit/
|
| 4 |
+
data/
|
| 5 |
+
exports/
|
| 6 |
+
.env
|
| 7 |
+
.DS_Store
|
hf-space/Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . /app
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
EXPOSE 7860
|
| 10 |
+
|
| 11 |
+
CMD ["streamlit", "run", "app.py", "--server.port", "7860", "--server.address", "0.0.0.0"]
|
hf-space/README.md
CHANGED
|
@@ -1,10 +1,84 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LLM Annotation Platform — Hugging Face native
|
| 2 |
+
|
| 3 |
+
This version removes the external database layer.
|
| 4 |
+
|
| 5 |
+
## What it uses
|
| 6 |
+
|
| 7 |
+
- **Hugging Face Space** for the Streamlit app
|
| 8 |
+
- **Hugging Face dataset repo** for the canonical annotation store
|
| 9 |
+
- **Hugging Face Storage Bucket** only for persistent local cache / drafts in the Space
|
| 10 |
+
- **No Supabase**
|
| 11 |
+
- **No separate backend platform**
|
| 12 |
+
|
| 13 |
+
Hugging Face Spaces provide ephemeral disk by default, and Hugging Face recommends attaching Storage Buckets to persist data across restarts. Buckets are mounted into the Space container as local volumes. citeturn322583view0
|
| 14 |
+
|
| 15 |
+
## Repository structure
|
| 16 |
+
|
| 17 |
+
```text
|
| 18 |
+
app.py
|
| 19 |
+
scripts/seed.py
|
| 20 |
+
requirements.txt
|
| 21 |
+
README.md
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## Behavior
|
| 25 |
+
|
| 26 |
+
Each annotation is written as its own JSON file into the dataset repository:
|
| 27 |
+
```text
|
| 28 |
+
annotations/<annotator>/<timestamp>_<item_id>_<uuid>.json
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
That design avoids write conflicts between annotators because each submission is a new file, not an overwrite of a shared database row. Repository files on the Hub are versioned, and the Hub supports uploading files to dataset repositories. citeturn322583view1turn322583view4
|
| 32 |
+
|
| 33 |
+
## Local run
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
pip install -r requirements.txt
|
| 37 |
+
streamlit run app.py
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## How to set it up on Hugging Face
|
| 41 |
+
|
| 42 |
+
### 1. Create two dataset repositories
|
| 43 |
+
|
| 44 |
+
Create:
|
| 45 |
+
- one dataset repo for the **source / seed data**
|
| 46 |
+
- one dataset repo for the **annotations**
|
| 47 |
+
|
| 48 |
+
Hugging Face dataset repositories are created from the Hub UI, and dataset files plus revision history are stored in the repository. citeturn322583view1
|
| 49 |
+
|
| 50 |
+
### 2. Create a Space
|
| 51 |
+
|
| 52 |
+
Create a **Streamlit** Space and connect it to your GitHub repository. Spaces host apps directly on the Hub and support Streamlit as a built-in SDK. citeturn322583view2
|
| 53 |
+
|
| 54 |
+
### 3. Attach a Storage Bucket
|
| 55 |
+
|
| 56 |
+
Attach a Storage Bucket to the Space and mount it at `/data`.
|
| 57 |
+
|
| 58 |
+
This is the only stateful storage used by the app. It stores drafts and cache files and survives restarts. Hugging Face documents Storage Buckets as the recommended persistence mechanism for Spaces. citeturn322583view0
|
| 59 |
+
|
| 60 |
+
### 4. Add secrets
|
| 61 |
+
|
| 62 |
+
In the Space settings, add:
|
| 63 |
+
- `HF_TOKEN` — a Hugging Face token with **write** permission
|
| 64 |
+
- `SOURCE_DATASET_REPO`
|
| 65 |
+
- `SOURCE_DATASET_SPLIT`
|
| 66 |
+
- `ANNOTATION_REPO_ID`
|
| 67 |
+
|
| 68 |
+
Hugging Face recommends using Space secrets or environment variables instead of hard-coding sensitive values. A write token is required to create repositories or push content to the Hub. citeturn322583view2turn322583view4
|
| 69 |
+
|
| 70 |
+
### 5. Deploy
|
| 71 |
+
|
| 72 |
+
Commit the repo to GitHub. Once the Space is linked, it will build from the repository, and the app can upload annotation files to the dataset repo using the Hub API. Hugging Face’s Hub client supports `upload_file()` and `create_commit()` for repository writes. citeturn322583view3turn322583view4
|
| 73 |
+
|
| 74 |
+
## Suggested workflow for your group
|
| 75 |
+
|
| 76 |
+
- each person uses a stable annotator name
|
| 77 |
+
- each submission creates a new JSON file in the annotation repo
|
| 78 |
+
- the Review page shows items with 2+ annotations
|
| 79 |
+
- the Dashboard shows per-annotator and per-domain progress
|
| 80 |
+
- exports are generated from the merged source + annotation view
|
| 81 |
+
|
| 82 |
+
## Why this is a good fit
|
| 83 |
+
|
| 84 |
+
The original source dataset can still be loaded with `datasets.load_dataset(...)`, and the Hugging Face ecosystem is designed for pushing and versioning datasets directly on the Hub. The `datasets` library also provides a `push_to_hub()` path for dataset publishing, while `huggingface_hub` provides lower-level file upload methods when you want more control over file layout. citeturn674332search1turn674332search3turn322583view3
|
hf-space/app.py
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import uuid
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 9 |
+
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import streamlit as st
|
| 12 |
+
from datasets import load_dataset
|
| 13 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 14 |
+
|
| 15 |
+
APP_TITLE = "🧭 LLM Annotation Platform"
|
| 16 |
+
DEFAULT_SOURCE_DATASET = os.environ.get(
|
| 17 |
+
"SOURCE_DATASET_REPO",
|
| 18 |
+
"nvidia/CantTalkAboutThis-Topic-Control-Dataset",
|
| 19 |
+
)
|
| 20 |
+
DEFAULT_SOURCE_SPLIT = os.environ.get("SOURCE_DATASET_SPLIT", "train")
|
| 21 |
+
DEFAULT_ANNOTATION_REPO = os.environ.get(
|
| 22 |
+
"ANNOTATION_REPO_ID",
|
| 23 |
+
"YOUR_ORG/llm-distractor-annotations",
|
| 24 |
+
)
|
| 25 |
+
DEFAULT_CACHE_DIR = Path(os.environ.get("CACHE_DIR", "/data/hf_annotation_cache"))
|
| 26 |
+
DEFAULT_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
| 27 |
+
LOCAL_DRAFT_DIR = Path(os.environ.get("DRAFT_DIR", "/data/hf_annotation_drafts"))
|
| 28 |
+
LOCAL_DRAFT_DIR.mkdir(parents=True, exist_ok=True)
|
| 29 |
+
LOCAL_EXPORT_DIR = Path(os.environ.get("EXPORT_DIR", "/data/hf_annotation_exports"))
|
| 30 |
+
LOCAL_EXPORT_DIR.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
|
| 32 |
+
LABEL_OPTIONS = {
|
| 33 |
+
"distractor_kind": [
|
| 34 |
+
"benign off-topic",
|
| 35 |
+
"smooth bridge",
|
| 36 |
+
"policy-evasive",
|
| 37 |
+
"roleplay / impersonation",
|
| 38 |
+
"pressure / persistence",
|
| 39 |
+
"urgency / time pressure",
|
| 40 |
+
"loophole seeking",
|
| 41 |
+
"multi-turn escalation",
|
| 42 |
+
"other",
|
| 43 |
+
],
|
| 44 |
+
"assistant_behavior": [
|
| 45 |
+
"perfect refusal + redirect",
|
| 46 |
+
"helpful redirection",
|
| 47 |
+
"partial engagement",
|
| 48 |
+
"full engagement / derailment",
|
| 49 |
+
"over-refusal",
|
| 50 |
+
"unclear",
|
| 51 |
+
],
|
| 52 |
+
"transition_style": [
|
| 53 |
+
"abrupt",
|
| 54 |
+
"smooth bridge",
|
| 55 |
+
"follow-up clarification",
|
| 56 |
+
"rephrasing",
|
| 57 |
+
"escalation",
|
| 58 |
+
"roleplay",
|
| 59 |
+
"ambiguity exploitation",
|
| 60 |
+
"other",
|
| 61 |
+
],
|
| 62 |
+
"policy_target": [
|
| 63 |
+
"medical advice",
|
| 64 |
+
"financial advice",
|
| 65 |
+
"legal advice",
|
| 66 |
+
"competitor discussion",
|
| 67 |
+
"politics",
|
| 68 |
+
"unsafe content",
|
| 69 |
+
"personal data",
|
| 70 |
+
"company-specific policy",
|
| 71 |
+
"tone / style policy",
|
| 72 |
+
"other",
|
| 73 |
+
],
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def now_iso() -> str:
|
| 78 |
+
return datetime.now(timezone.utc).isoformat()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def token() -> Optional[str]:
|
| 82 |
+
return os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def api() -> HfApi:
|
| 86 |
+
return HfApi(token=token())
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def annotation_file_name(item_id: str, annotator: str) -> str:
|
| 90 |
+
safe_annotator = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in annotator.strip().lower()) or "annotator"
|
| 91 |
+
safe_item = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in item_id.strip()) or "item"
|
| 92 |
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
| 93 |
+
return f"annotations/{safe_annotator}/{stamp}_{safe_item}_{uuid.uuid4().hex[:8]}.json"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def draft_path(annotator: str) -> Path:
|
| 97 |
+
safe_annotator = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in annotator.strip().lower()) or "annotator"
|
| 98 |
+
return LOCAL_DRAFT_DIR / f"{safe_annotator}.json"
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def cache_annotations_dir() -> Path:
|
| 102 |
+
path = DEFAULT_CACHE_DIR / "annotations_snapshot"
|
| 103 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 104 |
+
return path
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def ensure_repo_exists(repo_id: str) -> None:
|
| 108 |
+
if repo_id.startswith("YOUR_ORG/") or not repo_id.strip():
|
| 109 |
+
return
|
| 110 |
+
api().create_repo(repo_id=repo_id, repo_type="dataset", private=True, exist_ok=True)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def load_source_dataset(repo_id: str, split: str) -> List[Dict[str, Any]]:
|
| 114 |
+
ds = load_dataset(repo_id, split=split)
|
| 115 |
+
return [dict(row) for row in ds]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def normalize_turns(turns: Any) -> List[Dict[str, Any]]:
|
| 119 |
+
if turns is None:
|
| 120 |
+
return []
|
| 121 |
+
if isinstance(turns, str):
|
| 122 |
+
try:
|
| 123 |
+
turns = json.loads(turns)
|
| 124 |
+
except Exception:
|
| 125 |
+
return []
|
| 126 |
+
if not isinstance(turns, list):
|
| 127 |
+
return []
|
| 128 |
+
out = []
|
| 129 |
+
for turn in turns:
|
| 130 |
+
if isinstance(turn, dict):
|
| 131 |
+
role = turn.get("role") or turn.get("speaker") or turn.get("type") or "unknown"
|
| 132 |
+
content = turn.get("content") or turn.get("text") or turn.get("utterance") or ""
|
| 133 |
+
out.append({"role": str(role), "content": str(content)})
|
| 134 |
+
else:
|
| 135 |
+
out.append({"role": "unknown", "content": str(turn)})
|
| 136 |
+
return out
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def safe_sample_id(record: Dict[str, Any], fallback_index: int) -> str:
|
| 140 |
+
for key in ("sample_id", "id", "_id", "row_id"):
|
| 141 |
+
if record.get(key) not in (None, ""):
|
| 142 |
+
return str(record[key])
|
| 143 |
+
domain = str(record.get("domain", "sample")).replace(" ", "_")
|
| 144 |
+
scenario = str(record.get("scenario", "")).replace(" ", "_")
|
| 145 |
+
return f"{domain}-{scenario}-{fallback_index}"
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def expand_record(record: Dict[str, Any], idx: int) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
| 149 |
+
sample_id = safe_sample_id(record, idx)
|
| 150 |
+
conversation = normalize_turns(record.get("conversation"))
|
| 151 |
+
distractors = record.get("distractors") or []
|
| 152 |
+
if isinstance(distractors, str):
|
| 153 |
+
try:
|
| 154 |
+
distractors = json.loads(distractors)
|
| 155 |
+
except Exception:
|
| 156 |
+
distractors = []
|
| 157 |
+
if not isinstance(distractors, list):
|
| 158 |
+
distractors = []
|
| 159 |
+
|
| 160 |
+
sample = {
|
| 161 |
+
"sample_id": sample_id,
|
| 162 |
+
"domain": str(record.get("domain", "")),
|
| 163 |
+
"scenario": str(record.get("scenario", "")),
|
| 164 |
+
"system_instruction": str(record.get("system_instruction", "")),
|
| 165 |
+
"conversation_json": json.dumps(conversation, ensure_ascii=False),
|
| 166 |
+
"distractors_json": json.dumps(distractors, ensure_ascii=False),
|
| 167 |
+
"conversation_with_distractors_json": json.dumps(record.get("conversation_with_distractors", []), ensure_ascii=False),
|
| 168 |
+
"raw_json": json.dumps(record, ensure_ascii=False),
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
items = []
|
| 172 |
+
for distractor_index, d in enumerate(distractors):
|
| 173 |
+
bot_turn = ""
|
| 174 |
+
distractor_text = ""
|
| 175 |
+
if isinstance(d, dict):
|
| 176 |
+
bot_turn = str(
|
| 177 |
+
d.get("bot turn")
|
| 178 |
+
or d.get("bot_turn")
|
| 179 |
+
or d.get("assistant_turn")
|
| 180 |
+
or d.get("assistant")
|
| 181 |
+
or ""
|
| 182 |
+
)
|
| 183 |
+
distractor_text = str(
|
| 184 |
+
d.get("distractor")
|
| 185 |
+
or d.get("distractor user turn")
|
| 186 |
+
or d.get("user_turn")
|
| 187 |
+
or d.get("user")
|
| 188 |
+
or d.get("text")
|
| 189 |
+
or ""
|
| 190 |
+
)
|
| 191 |
+
else:
|
| 192 |
+
distractor_text = str(d)
|
| 193 |
+
|
| 194 |
+
items.append(
|
| 195 |
+
{
|
| 196 |
+
"item_id": f"{sample_id}::{distractor_index}",
|
| 197 |
+
"sample_id": sample_id,
|
| 198 |
+
"distractor_index": distractor_index,
|
| 199 |
+
"bot_turn": bot_turn,
|
| 200 |
+
"distractor_text": distractor_text,
|
| 201 |
+
}
|
| 202 |
+
)
|
| 203 |
+
return sample, items
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def seed_source_index(records: List[Dict[str, Any]]) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
| 207 |
+
samples = []
|
| 208 |
+
items = []
|
| 209 |
+
for idx, record in enumerate(records):
|
| 210 |
+
sample, record_items = expand_record(record, idx)
|
| 211 |
+
samples.append(sample)
|
| 212 |
+
items.extend(record_items)
|
| 213 |
+
return pd.DataFrame(samples), pd.DataFrame(items)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def read_json_file(path: Path) -> Dict[str, Any]:
|
| 217 |
+
with path.open("r", encoding="utf-8") as f:
|
| 218 |
+
return json.load(f)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def load_all_hub_annotations(annotation_repo_id: str) -> pd.DataFrame:
|
| 222 |
+
"""
|
| 223 |
+
Each submission is stored as a separate JSON file, which avoids write conflicts.
|
| 224 |
+
"""
|
| 225 |
+
if annotation_repo_id.startswith("YOUR_ORG/") or not annotation_repo_id.strip():
|
| 226 |
+
return pd.DataFrame(columns=["item_id", "annotator", "labels", "notes", "status", "created_at", "file_path"])
|
| 227 |
+
|
| 228 |
+
cache_dir = cache_annotations_dir()
|
| 229 |
+
file_list = api().list_repo_files(annotation_repo_id, repo_type="dataset")
|
| 230 |
+
ann_files = [f for f in file_list if f.startswith("annotations/") and f.endswith(".json")]
|
| 231 |
+
|
| 232 |
+
rows = []
|
| 233 |
+
for file_path in ann_files:
|
| 234 |
+
try:
|
| 235 |
+
local_path = hf_hub_download(
|
| 236 |
+
repo_id=annotation_repo_id,
|
| 237 |
+
repo_type="dataset",
|
| 238 |
+
filename=file_path,
|
| 239 |
+
token=token(),
|
| 240 |
+
local_dir=str(cache_dir),
|
| 241 |
+
local_dir_use_symlinks=False,
|
| 242 |
+
)
|
| 243 |
+
payload = read_json_file(Path(local_path))
|
| 244 |
+
rows.append(
|
| 245 |
+
{
|
| 246 |
+
"item_id": payload.get("item_id", ""),
|
| 247 |
+
"sample_id": payload.get("sample_id", ""),
|
| 248 |
+
"annotator": payload.get("annotator", ""),
|
| 249 |
+
"labels": payload.get("labels", {}),
|
| 250 |
+
"notes": payload.get("notes", ""),
|
| 251 |
+
"status": payload.get("status", "submitted"),
|
| 252 |
+
"created_at": payload.get("created_at", ""),
|
| 253 |
+
"file_path": file_path,
|
| 254 |
+
}
|
| 255 |
+
)
|
| 256 |
+
except Exception as e:
|
| 257 |
+
rows.append(
|
| 258 |
+
{
|
| 259 |
+
"item_id": "",
|
| 260 |
+
"sample_id": "",
|
| 261 |
+
"annotator": "",
|
| 262 |
+
"labels": {},
|
| 263 |
+
"notes": f"Failed to load {file_path}: {e}",
|
| 264 |
+
"status": "load_error",
|
| 265 |
+
"created_at": "",
|
| 266 |
+
"file_path": file_path,
|
| 267 |
+
}
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
return pd.DataFrame(rows) if rows else pd.DataFrame(columns=["item_id", "sample_id", "annotator", "labels", "notes", "status", "created_at", "file_path"])
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def save_draft(annotator: str, payload: Dict[str, Any]) -> Path:
|
| 274 |
+
path = draft_path(annotator)
|
| 275 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 276 |
+
with path.open("w", encoding="utf-8") as f:
|
| 277 |
+
json.dump(payload, f, ensure_ascii=False, indent=2)
|
| 278 |
+
return path
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def load_draft(annotator: str) -> Dict[str, Any]:
|
| 282 |
+
path = draft_path(annotator)
|
| 283 |
+
if not path.exists():
|
| 284 |
+
return {}
|
| 285 |
+
try:
|
| 286 |
+
return read_json_file(path)
|
| 287 |
+
except Exception:
|
| 288 |
+
return {}
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def build_labels_from_state(prefix: str = "") -> Dict[str, Any]:
|
| 292 |
+
return {
|
| 293 |
+
"distractor_kind": st.session_state.get(f"{prefix}distractor_kind", LABEL_OPTIONS["distractor_kind"][0]),
|
| 294 |
+
"transition_style": st.session_state.get(f"{prefix}transition_style", LABEL_OPTIONS["transition_style"][0]),
|
| 295 |
+
"policy_target": st.session_state.get(f"{prefix}policy_target", []),
|
| 296 |
+
"difficulty": int(st.session_state.get(f"{prefix}difficulty", 3)),
|
| 297 |
+
"realism": int(st.session_state.get(f"{prefix}realism", 3)),
|
| 298 |
+
"assistant_behavior": st.session_state.get(f"{prefix}assistant_behavior", LABEL_OPTIONS["assistant_behavior"][0]),
|
| 299 |
+
"multi_turn_escalation": bool(st.session_state.get(f"{prefix}multi_turn_escalation", False)),
|
| 300 |
+
"rule_followed": bool(st.session_state.get(f"{prefix}rule_followed", True)),
|
| 301 |
+
"needs_review": bool(st.session_state.get(f"{prefix}needs_review", False)),
|
| 302 |
+
"confidence": int(st.session_state.get(f"{prefix}confidence", 3)),
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def preview_text(text: str, limit: int = 280) -> str:
|
| 307 |
+
txt = (text or "").strip().replace("\n", " ")
|
| 308 |
+
if len(txt) <= limit:
|
| 309 |
+
return txt
|
| 310 |
+
return txt[:limit - 1] + "…"
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def render_turns(turns: List[Dict[str, Any]]) -> None:
|
| 314 |
+
if not turns:
|
| 315 |
+
st.info("No conversation turns found.")
|
| 316 |
+
return
|
| 317 |
+
for i, turn in enumerate(turns, 1):
|
| 318 |
+
role = str(turn.get("role", "unknown")).lower()
|
| 319 |
+
content = str(turn.get("content", "")).strip()
|
| 320 |
+
css_cls = "user" if role == "user" else "assistant" if role in {"assistant", "bot"} else "system"
|
| 321 |
+
st.markdown(
|
| 322 |
+
f"""
|
| 323 |
+
<div class="turn {css_cls}">
|
| 324 |
+
<span class="badge">{role.upper()}</span>
|
| 325 |
+
<span class="smallmono">Turn {i}</span>
|
| 326 |
+
<div style="margin-top:0.35rem; white-space:pre-wrap;">{content.replace(chr(10), '<br>')}</div>
|
| 327 |
+
</div>
|
| 328 |
+
""",
|
| 329 |
+
unsafe_allow_html=True,
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def annotation_exists_for_item(df_anns: pd.DataFrame, item_id: str, annotator: str) -> bool:
|
| 334 |
+
if df_anns.empty:
|
| 335 |
+
return False
|
| 336 |
+
sub = df_anns[(df_anns["item_id"] == item_id) & (df_anns["annotator"] == annotator)]
|
| 337 |
+
return not sub.empty
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def compute_agreement(df_anns: pd.DataFrame, label_key: str = "assistant_behavior") -> Dict[str, Any]:
|
| 341 |
+
if df_anns.empty:
|
| 342 |
+
return {"paired_items": 0, "raw_agreement": None, "cohen_kappa": None}
|
| 343 |
+
|
| 344 |
+
rows = []
|
| 345 |
+
for _, r in df_anns.iterrows():
|
| 346 |
+
labels = r.get("labels", {}) or {}
|
| 347 |
+
rows.append({"item_id": r["item_id"], "annotator": r["annotator"], label_key: labels.get(label_key)})
|
| 348 |
+
tmp = pd.DataFrame(rows)
|
| 349 |
+
pivot = tmp.pivot_table(index="item_id", columns="annotator", values=label_key, aggfunc="first")
|
| 350 |
+
pivot = pivot.dropna(axis=0, how="any")
|
| 351 |
+
if pivot.shape[0] < 2 or pivot.shape[1] < 2:
|
| 352 |
+
return {"paired_items": int(pivot.shape[0]), "raw_agreement": None, "cohen_kappa": None}
|
| 353 |
+
|
| 354 |
+
from sklearn.metrics import cohen_kappa_score
|
| 355 |
+
|
| 356 |
+
a = pivot.iloc[:, 0].astype(str)
|
| 357 |
+
b = pivot.iloc[:, 1].astype(str)
|
| 358 |
+
return {
|
| 359 |
+
"paired_items": int(pivot.shape[0]),
|
| 360 |
+
"raw_agreement": float((a == b).mean()),
|
| 361 |
+
"cohen_kappa": float(cohen_kappa_score(a, b)),
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def push_annotation_to_hub(annotation_repo_id: str, payload: Dict[str, Any]) -> str:
|
| 366 |
+
ensure_repo_exists(annotation_repo_id)
|
| 367 |
+
file_rel_path = annotation_file_name(payload["item_id"], payload["annotator"])
|
| 368 |
+
local_path = LOCAL_DRAFT_DIR / file_rel_path.replace("/", "__")
|
| 369 |
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
| 370 |
+
with local_path.open("w", encoding="utf-8") as f:
|
| 371 |
+
json.dump(payload, f, ensure_ascii=False, indent=2)
|
| 372 |
+
|
| 373 |
+
api().upload_file(
|
| 374 |
+
path_or_fileobj=str(local_path),
|
| 375 |
+
path_in_repo=file_rel_path,
|
| 376 |
+
repo_id=annotation_repo_id,
|
| 377 |
+
repo_type="dataset",
|
| 378 |
+
token=token(),
|
| 379 |
+
commit_message=f"Add annotation for {payload['item_id']} by {payload['annotator']}",
|
| 380 |
+
)
|
| 381 |
+
return file_rel_path
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def get_current_item_id() -> Optional[str]:
|
| 385 |
+
return st.session_state.get("current_item_id")
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def set_current_item_id(item_id: Optional[str]) -> None:
|
| 389 |
+
st.session_state["current_item_id"] = item_id
|
| 390 |
+
try:
|
| 391 |
+
st.query_params["item_id"] = item_id or ""
|
| 392 |
+
except Exception:
|
| 393 |
+
pass
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def main() -> None:
|
| 397 |
+
st.set_page_config(page_title="LLM Annotation Platform", page_icon="🧭", layout="wide")
|
| 398 |
+
st.markdown(
|
| 399 |
+
"""
|
| 400 |
+
<style>
|
| 401 |
+
.block-container {padding-top: 1rem; padding-bottom: 2rem;}
|
| 402 |
+
.smallmono {font-size: 0.84rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;}
|
| 403 |
+
.cardbox {
|
| 404 |
+
border: 1px solid rgba(120,120,120,0.22);
|
| 405 |
+
border-radius: 18px;
|
| 406 |
+
padding: 1rem 1rem 0.75rem 1rem;
|
| 407 |
+
background: rgba(255,255,255,0.03);
|
| 408 |
+
}
|
| 409 |
+
.turn {
|
| 410 |
+
border-left: 4px solid rgba(120,120,120,0.45);
|
| 411 |
+
padding: 0.6rem 0.85rem;
|
| 412 |
+
margin: 0.55rem 0;
|
| 413 |
+
border-radius: 0.6rem;
|
| 414 |
+
background: rgba(128,128,128,0.06);
|
| 415 |
+
}
|
| 416 |
+
.turn.user {border-left-color: #8b5cf6;}
|
| 417 |
+
.turn.assistant, .turn.bot {border-left-color: #06b6d4;}
|
| 418 |
+
.turn.system {border-left-color: #f59e0b;}
|
| 419 |
+
.badge {
|
| 420 |
+
display:inline-block; padding:0.18rem 0.5rem; border-radius: 999px;
|
| 421 |
+
background: rgba(120,120,120,0.16); margin-right: 0.35rem; font-size: 0.78rem;
|
| 422 |
+
}
|
| 423 |
+
hr {margin: 0.7rem 0 0.9rem 0;}
|
| 424 |
+
</style>
|
| 425 |
+
""",
|
| 426 |
+
unsafe_allow_html=True,
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
st.title(APP_TITLE)
|
| 430 |
+
st.caption("A Hugging Face–native annotation tool for multi-turn distractors, inter-rater review, and dataset versioning.")
|
| 431 |
+
|
| 432 |
+
if "annotator" not in st.session_state:
|
| 433 |
+
st.session_state["annotator"] = "annotator_1"
|
| 434 |
+
if "current_item_id" not in st.session_state:
|
| 435 |
+
st.session_state["current_item_id"] = None
|
| 436 |
+
if "source_records" not in st.session_state:
|
| 437 |
+
st.session_state["source_records"] = None
|
| 438 |
+
if "source_index" not in st.session_state:
|
| 439 |
+
st.session_state["source_index"] = None
|
| 440 |
+
if "annotations_df" not in st.session_state:
|
| 441 |
+
st.session_state["annotations_df"] = None
|
| 442 |
+
if "draft_loaded" not in st.session_state:
|
| 443 |
+
st.session_state["draft_loaded"] = False
|
| 444 |
+
|
| 445 |
+
with st.sidebar:
|
| 446 |
+
st.header("Workspace")
|
| 447 |
+
annotator = st.text_input("Annotator name", value=st.session_state["annotator"])
|
| 448 |
+
st.session_state["annotator"] = annotator.strip() or "annotator_1"
|
| 449 |
+
|
| 450 |
+
source_repo = st.text_input("Source dataset repo", value=DEFAULT_SOURCE_DATASET)
|
| 451 |
+
source_split = st.text_input("Source split", value=DEFAULT_SOURCE_SPLIT)
|
| 452 |
+
annotation_repo = st.text_input("Annotation dataset repo", value=DEFAULT_ANNOTATION_REPO)
|
| 453 |
+
|
| 454 |
+
st.divider()
|
| 455 |
+
st.caption("HF token is needed only for upload / repo creation.")
|
| 456 |
+
st.write("HF token present:", "yes" if token() else "no")
|
| 457 |
+
st.write("Cache:", str(DEFAULT_CACHE_DIR))
|
| 458 |
+
st.write("Drafts:", str(LOCAL_DRAFT_DIR))
|
| 459 |
+
|
| 460 |
+
if st.button("Reload Hub data", use_container_width=True):
|
| 461 |
+
st.session_state["source_records"] = None
|
| 462 |
+
st.session_state["source_index"] = None
|
| 463 |
+
st.session_state["annotations_df"] = None
|
| 464 |
+
st.rerun()
|
| 465 |
+
|
| 466 |
+
page = st.radio("Page", ["Annotate", "Review", "Dashboard", "Export"], index=0)
|
| 467 |
+
|
| 468 |
+
if st.session_state["source_records"] is None:
|
| 469 |
+
with st.spinner("Loading source dataset from the Hub..."):
|
| 470 |
+
source_records = load_source_dataset(source_repo, source_split)
|
| 471 |
+
samples_df, items_df = seed_source_index(source_records)
|
| 472 |
+
st.session_state["source_records"] = source_records
|
| 473 |
+
st.session_state["source_index"] = {"samples_df": samples_df, "items_df": items_df}
|
| 474 |
+
|
| 475 |
+
if st.session_state["annotations_df"] is None:
|
| 476 |
+
with st.spinner("Loading annotations from the annotation dataset repo..."):
|
| 477 |
+
try:
|
| 478 |
+
anns_df = load_all_hub_annotations(annotation_repo)
|
| 479 |
+
except Exception as e:
|
| 480 |
+
anns_df = pd.DataFrame(columns=["item_id", "sample_id", "annotator", "labels", "notes", "status", "created_at", "file_path"])
|
| 481 |
+
st.warning(f"Could not load annotations from Hub yet: {e}")
|
| 482 |
+
st.session_state["annotations_df"] = anns_df
|
| 483 |
+
|
| 484 |
+
samples_df = st.session_state["source_index"]["samples_df"]
|
| 485 |
+
items_df = st.session_state["source_index"]["items_df"]
|
| 486 |
+
anns_df = st.session_state["annotations_df"]
|
| 487 |
+
|
| 488 |
+
if not st.session_state["draft_loaded"]:
|
| 489 |
+
try:
|
| 490 |
+
q_item = st.query_params.get("item_id")
|
| 491 |
+
except Exception:
|
| 492 |
+
q_item = None
|
| 493 |
+
if q_item:
|
| 494 |
+
st.session_state["current_item_id"] = q_item
|
| 495 |
+
draft = load_draft(st.session_state["annotator"])
|
| 496 |
+
if draft.get("current_item_id") and not st.session_state["current_item_id"]:
|
| 497 |
+
st.session_state["current_item_id"] = draft["current_item_id"]
|
| 498 |
+
st.session_state["draft_loaded"] = True
|
| 499 |
+
|
| 500 |
+
my_annotated_item_ids = set(
|
| 501 |
+
anns_df.loc[anns_df["annotator"] == st.session_state["annotator"], "item_id"].dropna().astype(str).tolist()
|
| 502 |
+
) if not anns_df.empty else set()
|
| 503 |
+
|
| 504 |
+
def current_item_row() -> Optional[Dict[str, Any]]:
|
| 505 |
+
item_id = get_current_item_id()
|
| 506 |
+
if not item_id:
|
| 507 |
+
return None
|
| 508 |
+
match = items_df[items_df["item_id"] == item_id]
|
| 509 |
+
if match.empty:
|
| 510 |
+
return None
|
| 511 |
+
row = match.iloc[0].to_dict()
|
| 512 |
+
sample = samples_df[samples_df["sample_id"] == row["sample_id"]]
|
| 513 |
+
if not sample.empty:
|
| 514 |
+
row.update(sample.iloc[0].to_dict())
|
| 515 |
+
return row
|
| 516 |
+
|
| 517 |
+
def queue_df() -> pd.DataFrame:
|
| 518 |
+
return items_df[~items_df["item_id"].astype(str).isin(my_annotated_item_ids)].copy()
|
| 519 |
+
|
| 520 |
+
if page == "Annotate":
|
| 521 |
+
st.subheader("Annotate a distractor item")
|
| 522 |
+
left, right = st.columns([1.05, 0.95], gap="large")
|
| 523 |
+
|
| 524 |
+
with left:
|
| 525 |
+
top_a, top_b, top_c = st.columns([1, 1, 1])
|
| 526 |
+
with top_a:
|
| 527 |
+
if st.button("Claim next item", use_container_width=True):
|
| 528 |
+
q = queue_df()
|
| 529 |
+
if q.empty:
|
| 530 |
+
st.warning("No remaining items in your queue.")
|
| 531 |
+
else:
|
| 532 |
+
set_current_item_id(q.iloc[0]["item_id"])
|
| 533 |
+
st.rerun()
|
| 534 |
+
with top_b:
|
| 535 |
+
if st.button("Reload annotations from Hub", use_container_width=True):
|
| 536 |
+
st.session_state["annotations_df"] = load_all_hub_annotations(annotation_repo)
|
| 537 |
+
st.rerun()
|
| 538 |
+
with top_c:
|
| 539 |
+
if st.button("Clear current", use_container_width=True):
|
| 540 |
+
set_current_item_id(None)
|
| 541 |
+
st.rerun()
|
| 542 |
+
|
| 543 |
+
item = current_item_row()
|
| 544 |
+
if item is None:
|
| 545 |
+
st.info("Claim an item to start. The app keeps a per-annotator queue so multiple people can work in parallel.")
|
| 546 |
+
q = queue_df().head(10)
|
| 547 |
+
if not q.empty:
|
| 548 |
+
display = q[["item_id", "sample_id", "domain", "scenario", "distractor_index"]].copy()
|
| 549 |
+
display["preview"] = q["distractor_text"].map(preview_text)
|
| 550 |
+
st.dataframe(display, use_container_width=True, hide_index=True)
|
| 551 |
+
return
|
| 552 |
+
|
| 553 |
+
st.markdown(
|
| 554 |
+
f"""
|
| 555 |
+
<div class="cardbox">
|
| 556 |
+
<div><span class="badge">Domain</span> {item.get("domain", "")}</div>
|
| 557 |
+
<div style="margin-top:0.35rem;"><span class="badge">Scenario</span> {item.get("scenario", "")}</div>
|
| 558 |
+
<div style="margin-top:0.35rem;"><span class="badge">Sample</span> <span class="smallmono">{item.get("sample_id", "")}</span></div>
|
| 559 |
+
<div style="margin-top:0.35rem;"><span class="badge">Item</span> <span class="smallmono">{item.get("item_id", "")}</span></div>
|
| 560 |
+
</div>
|
| 561 |
+
""",
|
| 562 |
+
unsafe_allow_html=True,
|
| 563 |
+
)
|
| 564 |
+
st.divider()
|
| 565 |
+
|
| 566 |
+
tabs = st.tabs(["Context", "Distractor", "Existing annotations"])
|
| 567 |
+
with tabs[0]:
|
| 568 |
+
st.markdown("**System instruction**")
|
| 569 |
+
st.code(item.get("system_instruction", ""), language="text")
|
| 570 |
+
st.markdown("**Conversation**")
|
| 571 |
+
render_turns(json.loads(item.get("conversation_json", "[]")))
|
| 572 |
+
with tabs[1]:
|
| 573 |
+
st.markdown("**Previous assistant turn**")
|
| 574 |
+
st.code(item.get("bot_turn", "") or "(missing)", language="text")
|
| 575 |
+
st.markdown("**Distractor user turn**")
|
| 576 |
+
st.code(item.get("distractor_text", "") or "(missing)", language="text")
|
| 577 |
+
with tabs[2]:
|
| 578 |
+
existing = anns_df[anns_df["item_id"] == item["item_id"]].copy()
|
| 579 |
+
if existing.empty:
|
| 580 |
+
st.caption("No annotations yet.")
|
| 581 |
+
else:
|
| 582 |
+
for _, row in existing.iterrows():
|
| 583 |
+
st.write(f"**{row['annotator']}** · {row['status']} · {row['created_at']}")
|
| 584 |
+
st.json(row["labels"])
|
| 585 |
+
if row.get("notes"):
|
| 586 |
+
st.caption(row["notes"])
|
| 587 |
+
st.divider()
|
| 588 |
+
|
| 589 |
+
with right:
|
| 590 |
+
st.markdown("### Annotation form")
|
| 591 |
+
current_draft = load_draft(st.session_state["annotator"])
|
| 592 |
+
draft_labels = current_draft.get("labels", {}) if current_draft else {}
|
| 593 |
+
|
| 594 |
+
with st.form("annotation_form", clear_on_submit=False):
|
| 595 |
+
st.selectbox(
|
| 596 |
+
"Distractor kind",
|
| 597 |
+
LABEL_OPTIONS["distractor_kind"],
|
| 598 |
+
index=LABEL_OPTIONS["distractor_kind"].index(draft_labels.get("distractor_kind", LABEL_OPTIONS["distractor_kind"][0]))
|
| 599 |
+
if draft_labels.get("distractor_kind") in LABEL_OPTIONS["distractor_kind"]
|
| 600 |
+
else 0,
|
| 601 |
+
key="distractor_kind",
|
| 602 |
+
)
|
| 603 |
+
st.selectbox(
|
| 604 |
+
"Transition style",
|
| 605 |
+
LABEL_OPTIONS["transition_style"],
|
| 606 |
+
index=LABEL_OPTIONS["transition_style"].index(draft_labels.get("transition_style", LABEL_OPTIONS["transition_style"][0]))
|
| 607 |
+
if draft_labels.get("transition_style") in LABEL_OPTIONS["transition_style"]
|
| 608 |
+
else 0,
|
| 609 |
+
key="transition_style",
|
| 610 |
+
)
|
| 611 |
+
st.multiselect(
|
| 612 |
+
"Policy target(s)",
|
| 613 |
+
LABEL_OPTIONS["policy_target"],
|
| 614 |
+
default=draft_labels.get("policy_target", []),
|
| 615 |
+
key="policy_target",
|
| 616 |
+
)
|
| 617 |
+
c1, c2 = st.columns(2)
|
| 618 |
+
with c1:
|
| 619 |
+
st.slider("Difficulty", 1, 5, value=int(draft_labels.get("difficulty", 3)), key="difficulty")
|
| 620 |
+
st.slider("Realism", 1, 5, value=int(draft_labels.get("realism", 3)), key="realism")
|
| 621 |
+
with c2:
|
| 622 |
+
st.selectbox(
|
| 623 |
+
"Assistant behavior",
|
| 624 |
+
LABEL_OPTIONS["assistant_behavior"],
|
| 625 |
+
index=LABEL_OPTIONS["assistant_behavior"].index(draft_labels.get("assistant_behavior", LABEL_OPTIONS["assistant_behavior"][0]))
|
| 626 |
+
if draft_labels.get("assistant_behavior") in LABEL_OPTIONS["assistant_behavior"]
|
| 627 |
+
else 0,
|
| 628 |
+
key="assistant_behavior",
|
| 629 |
+
)
|
| 630 |
+
st.slider("Confidence", 1, 5, value=int(draft_labels.get("confidence", 3)), key="confidence")
|
| 631 |
+
|
| 632 |
+
st.checkbox(
|
| 633 |
+
"Multi-turn escalation / persistence",
|
| 634 |
+
value=bool(draft_labels.get("multi_turn_escalation", False)),
|
| 635 |
+
key="multi_turn_escalation",
|
| 636 |
+
)
|
| 637 |
+
st.checkbox(
|
| 638 |
+
"Assistant followed the rule",
|
| 639 |
+
value=bool(draft_labels.get("rule_followed", True)),
|
| 640 |
+
key="rule_followed",
|
| 641 |
+
)
|
| 642 |
+
st.checkbox(
|
| 643 |
+
"Borderline / needs review",
|
| 644 |
+
value=bool(draft_labels.get("needs_review", False)),
|
| 645 |
+
key="needs_review",
|
| 646 |
+
)
|
| 647 |
+
notes = st.text_area(
|
| 648 |
+
"Notes",
|
| 649 |
+
value=current_draft.get("notes", ""),
|
| 650 |
+
height=150,
|
| 651 |
+
placeholder="Explain ambiguity, likely disagreement, or policy edge cases.",
|
| 652 |
+
)
|
| 653 |
+
submitted = st.form_submit_button("Submit to Hugging Face", use_container_width=True)
|
| 654 |
+
|
| 655 |
+
c1, c2 = st.columns(2)
|
| 656 |
+
with c1:
|
| 657 |
+
if st.button("Save draft locally", use_container_width=True):
|
| 658 |
+
payload = {
|
| 659 |
+
"current_item_id": item["item_id"],
|
| 660 |
+
"labels": build_labels_from_state(),
|
| 661 |
+
"notes": notes,
|
| 662 |
+
"saved_at": now_iso(),
|
| 663 |
+
}
|
| 664 |
+
path = save_draft(st.session_state["annotator"], payload)
|
| 665 |
+
st.success(f"Draft saved to {path}")
|
| 666 |
+
with c2:
|
| 667 |
+
if st.button("Sync annotation cache", use_container_width=True):
|
| 668 |
+
st.session_state["annotations_df"] = load_all_hub_annotations(annotation_repo)
|
| 669 |
+
st.success("Reloaded annotation index from Hub.")
|
| 670 |
+
|
| 671 |
+
if submitted:
|
| 672 |
+
labels = build_labels_from_state()
|
| 673 |
+
payload = {
|
| 674 |
+
"annotation_id": str(uuid.uuid4()),
|
| 675 |
+
"item_id": item["item_id"],
|
| 676 |
+
"sample_id": item["sample_id"],
|
| 677 |
+
"annotator": st.session_state["annotator"],
|
| 678 |
+
"created_at": now_iso(),
|
| 679 |
+
"status": "submitted",
|
| 680 |
+
"labels": labels,
|
| 681 |
+
"notes": notes,
|
| 682 |
+
"source": {
|
| 683 |
+
"source_dataset_repo": source_repo,
|
| 684 |
+
"source_dataset_split": source_split,
|
| 685 |
+
"domain": item.get("domain", ""),
|
| 686 |
+
"scenario": item.get("scenario", ""),
|
| 687 |
+
"distractor_index": int(item.get("distractor_index", 0)),
|
| 688 |
+
},
|
| 689 |
+
}
|
| 690 |
+
try:
|
| 691 |
+
path_in_repo = push_annotation_to_hub(annotation_repo, payload)
|
| 692 |
+
st.session_state["annotations_df"] = pd.concat(
|
| 693 |
+
[
|
| 694 |
+
anns_df,
|
| 695 |
+
pd.DataFrame(
|
| 696 |
+
[
|
| 697 |
+
{
|
| 698 |
+
"item_id": payload["item_id"],
|
| 699 |
+
"sample_id": payload["sample_id"],
|
| 700 |
+
"annotator": payload["annotator"],
|
| 701 |
+
"labels": payload["labels"],
|
| 702 |
+
"notes": payload["notes"],
|
| 703 |
+
"status": payload["status"],
|
| 704 |
+
"created_at": payload["created_at"],
|
| 705 |
+
"file_path": path_in_repo,
|
| 706 |
+
}
|
| 707 |
+
]
|
| 708 |
+
),
|
| 709 |
+
],
|
| 710 |
+
ignore_index=True,
|
| 711 |
+
)
|
| 712 |
+
save_draft(
|
| 713 |
+
st.session_state["annotator"],
|
| 714 |
+
{
|
| 715 |
+
"current_item_id": item["item_id"],
|
| 716 |
+
"labels": labels,
|
| 717 |
+
"notes": notes,
|
| 718 |
+
"saved_at": now_iso(),
|
| 719 |
+
},
|
| 720 |
+
)
|
| 721 |
+
st.success(f"Submitted to Hugging Face as {path_in_repo}")
|
| 722 |
+
q = queue_df()
|
| 723 |
+
if not q.empty:
|
| 724 |
+
set_current_item_id(q.iloc[0]["item_id"])
|
| 725 |
+
st.rerun()
|
| 726 |
+
except Exception as e:
|
| 727 |
+
st.error(f"Upload failed. Saved locally only. Error: {e}")
|
| 728 |
+
save_draft(
|
| 729 |
+
st.session_state["annotator"],
|
| 730 |
+
{
|
| 731 |
+
"current_item_id": item["item_id"],
|
| 732 |
+
"labels": labels,
|
| 733 |
+
"notes": notes,
|
| 734 |
+
"saved_at": now_iso(),
|
| 735 |
+
},
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
st.caption("Each submission is a separate file in the annotation dataset repo, so multiple annotators can work in parallel without write conflicts.")
|
| 739 |
+
|
| 740 |
+
elif page == "Review":
|
| 741 |
+
st.subheader("Inter-rater review")
|
| 742 |
+
multi = (
|
| 743 |
+
anns_df.groupby("item_id")["annotator"].nunique().reset_index(name="n_annotators")
|
| 744 |
+
if not anns_df.empty
|
| 745 |
+
else pd.DataFrame(columns=["item_id", "n_annotators"])
|
| 746 |
+
)
|
| 747 |
+
multi = multi[multi["n_annotators"] >= 2] if not multi.empty else multi
|
| 748 |
+
|
| 749 |
+
if multi.empty:
|
| 750 |
+
st.info("No items with at least two annotations yet.")
|
| 751 |
+
else:
|
| 752 |
+
selected_item = st.selectbox("Item with multiple annotations", multi["item_id"].tolist())
|
| 753 |
+
row = items_df[items_df["item_id"] == selected_item].iloc[0].to_dict()
|
| 754 |
+
sample = samples_df[samples_df["sample_id"] == row["sample_id"]].iloc[0].to_dict()
|
| 755 |
+
row.update(sample)
|
| 756 |
+
|
| 757 |
+
st.markdown("### Context")
|
| 758 |
+
st.code(row["system_instruction"], language="text")
|
| 759 |
+
st.code(row["bot_turn"] or "", language="text")
|
| 760 |
+
st.code(row["distractor_text"] or "", language="text")
|
| 761 |
+
|
| 762 |
+
st.markdown("### Annotations")
|
| 763 |
+
sub = anns_df[anns_df["item_id"] == selected_item].copy()
|
| 764 |
+
cols = st.columns(min(len(sub), 3)) if len(sub) > 0 else st.columns(1)
|
| 765 |
+
for idx, (_, ann) in enumerate(sub.iterrows()):
|
| 766 |
+
with cols[idx % len(cols)]:
|
| 767 |
+
st.write(f"**{ann['annotator']}**")
|
| 768 |
+
st.caption(f"{ann['status']} · {ann['created_at']}")
|
| 769 |
+
st.json(ann["labels"])
|
| 770 |
+
if ann.get("notes"):
|
| 771 |
+
st.caption(ann["notes"])
|
| 772 |
+
|
| 773 |
+
agreement = compute_agreement(sub, label_key="assistant_behavior")
|
| 774 |
+
c1, c2, c3 = st.columns(3)
|
| 775 |
+
c1.metric("Paired items", agreement["paired_items"])
|
| 776 |
+
c2.metric("Raw agreement", f"{agreement['raw_agreement']:.2%}" if agreement["raw_agreement"] is not None else "n/a")
|
| 777 |
+
c3.metric("Cohen's κ", f"{agreement['cohen_kappa']:.3f}" if agreement["cohen_kappa"] is not None else "n/a")
|
| 778 |
+
|
| 779 |
+
elif page == "Dashboard":
|
| 780 |
+
st.subheader("Dashboard")
|
| 781 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 782 |
+
c1.metric("Source samples", len(samples_df))
|
| 783 |
+
c2.metric("Source items", len(items_df))
|
| 784 |
+
c3.metric("Annotation files", len(anns_df))
|
| 785 |
+
c4.metric("My queue", len(queue_df()))
|
| 786 |
+
|
| 787 |
+
st.markdown("### Progress by annotator")
|
| 788 |
+
if anns_df.empty:
|
| 789 |
+
st.info("No annotations yet.")
|
| 790 |
+
else:
|
| 791 |
+
by_ann = anns_df.groupby("annotator")["item_id"].nunique().reset_index(name="annotated_items").sort_values("annotated_items", ascending=False)
|
| 792 |
+
st.dataframe(by_ann, use_container_width=True, hide_index=True)
|
| 793 |
+
|
| 794 |
+
st.markdown("### Progress by domain")
|
| 795 |
+
joined = anns_df.merge(items_df[["item_id", "domain"]], on="item_id", how="left")
|
| 796 |
+
by_domain = joined.groupby("domain")["item_id"].nunique().reset_index(name="annotated_items").sort_values("annotated_items", ascending=False)
|
| 797 |
+
st.dataframe(by_domain, use_container_width=True, hide_index=True)
|
| 798 |
+
|
| 799 |
+
st.markdown("### Agreement snapshot")
|
| 800 |
+
metric = compute_agreement(anns_df, label_key="assistant_behavior")
|
| 801 |
+
st.write(metric)
|
| 802 |
+
|
| 803 |
+
st.markdown("### Recent annotation previews")
|
| 804 |
+
recent = anns_df.sort_values("created_at", ascending=False).head(20).copy()
|
| 805 |
+
if "labels" in recent.columns:
|
| 806 |
+
recent["assistant_behavior"] = recent["labels"].apply(lambda x: x.get("assistant_behavior") if isinstance(x, dict) else None)
|
| 807 |
+
recent["distractor_kind"] = recent["labels"].apply(lambda x: x.get("distractor_kind") if isinstance(x, dict) else None)
|
| 808 |
+
st.dataframe(
|
| 809 |
+
recent[["annotator", "item_id", "status", "created_at", "assistant_behavior", "distractor_kind", "notes"]],
|
| 810 |
+
use_container_width=True,
|
| 811 |
+
hide_index=True,
|
| 812 |
+
)
|
| 813 |
+
|
| 814 |
+
else:
|
| 815 |
+
st.subheader("Export")
|
| 816 |
+
st.write("Export the merged dataset for downstream analysis or model training.")
|
| 817 |
+
|
| 818 |
+
merged = items_df.merge(samples_df, on="sample_id", how="left")
|
| 819 |
+
if not anns_df.empty:
|
| 820 |
+
export_df = merged.merge(anns_df[["item_id", "annotator", "labels", "notes", "status", "created_at"]], on="item_id", how="left")
|
| 821 |
+
else:
|
| 822 |
+
export_df = merged.copy()
|
| 823 |
+
export_df["annotator"] = None
|
| 824 |
+
export_df["labels"] = None
|
| 825 |
+
export_df["notes"] = None
|
| 826 |
+
export_df["status"] = None
|
| 827 |
+
export_df["created_at"] = None
|
| 828 |
+
|
| 829 |
+
c1, c2 = st.columns(2)
|
| 830 |
+
with c1:
|
| 831 |
+
jsonl = LOCAL_EXPORT_DIR / "annotations_export.jsonl"
|
| 832 |
+
if st.button("Generate JSONL export", use_container_width=True):
|
| 833 |
+
with jsonl.open("w", encoding="utf-8") as f:
|
| 834 |
+
for _, r in export_df.iterrows():
|
| 835 |
+
f.write(json.dumps(r.where(pd.notna(r), None).to_dict(), ensure_ascii=False) + "\n")
|
| 836 |
+
st.success(f"Wrote {jsonl}")
|
| 837 |
+
st.download_button("Download JSONL", jsonl.read_text(encoding="utf-8"), file_name=jsonl.name, mime="application/json")
|
| 838 |
+
with c2:
|
| 839 |
+
csv = LOCAL_EXPORT_DIR / "annotations_export.csv"
|
| 840 |
+
if st.button("Generate CSV export", use_container_width=True):
|
| 841 |
+
export_df.to_csv(csv, index=False)
|
| 842 |
+
st.success(f"Wrote {csv}")
|
| 843 |
+
st.download_button("Download CSV", csv.read_text(encoding="utf-8"), file_name=csv.name, mime="text/csv")
|
| 844 |
+
|
| 845 |
+
st.markdown("### Repository handoff")
|
| 846 |
+
st.code(
|
| 847 |
+
f"Source repo: {source_repo}\nAnnotation repo: {annotation_repo}\nSplit: {source_split}\nAnnotator: {st.session_state['annotator']}",
|
| 848 |
+
language="text",
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
if __name__ == "__main__":
|
| 853 |
+
main()
|
hf-space/hf-space/.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
hf-space/hf-space/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Llm Annotation Platform
|
| 3 |
+
emoji: 🦀
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
hf-space/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.37
|
| 2 |
+
pandas>=2.2
|
| 3 |
+
datasets>=2.21
|
| 4 |
+
huggingface_hub>=0.24
|
| 5 |
+
scikit-learn>=1.5
|
hf-space/scripts/seed.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
|
| 5 |
+
from datasets import load_dataset
|
| 6 |
+
|
| 7 |
+
from app import DEFAULT_ANNOTATION_REPO, DEFAULT_SOURCE_DATASET, DEFAULT_SOURCE_SPLIT
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main() -> None:
|
| 11 |
+
parser = argparse.ArgumentParser()
|
| 12 |
+
parser.add_argument("--source", default=DEFAULT_SOURCE_DATASET)
|
| 13 |
+
parser.add_argument("--split", default=DEFAULT_SOURCE_SPLIT)
|
| 14 |
+
parser.add_argument("--annotation-repo", default=DEFAULT_ANNOTATION_REPO)
|
| 15 |
+
parser.add_argument("--limit", type=int, default=0)
|
| 16 |
+
args = parser.parse_args()
|
| 17 |
+
|
| 18 |
+
records = load_dataset(args.source, split=args.split)
|
| 19 |
+
if args.limit:
|
| 20 |
+
records = records.select(range(min(len(records), args.limit)))
|
| 21 |
+
|
| 22 |
+
print(f"Loaded {len(records)} source records from {args.source}/{args.split}")
|
| 23 |
+
print(f"Annotation repo: {args.annotation_repo}")
|
| 24 |
+
print("Open the Streamlit app and submit annotations there.")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
main()
|