Jun-Min Lee commited on
Commit
7b45c3b
Β·
1 Parent(s): c5ecf03
Files changed (7) hide show
  1. .gitignore +3 -0
  2. README.md +39 -6
  3. app.py +198 -0
  4. requirements.txt +3 -0
  5. utils/__init__.py +0 -0
  6. utils/common.py +48 -0
  7. utils/filesys_utils.py +7 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ .env
3
+ .vscode/*
README.md CHANGED
@@ -1,10 +1,43 @@
1
  ---
2
- title: SQL Eval
3
- emoji: 🐒
4
- colorFrom: green
5
- colorTo: indigo
6
- sdk: static
 
 
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SQL-Eval-Arena
3
+ emoji: 🩺
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 6.10.0
8
+ app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
+ short_description: 'Human labeling of SQL result correctness'
12
  ---
13
 
14
+ # sql-eval-arena
15
+
16
+ Human-labeling tool for judging whether an agent's SQL query + result
17
+ (attached to each `ANS`/`SANS` turn in `results/chat_pp/all_raw_diag.json`)
18
+ actually answers the user's final intent, including anything resolved via
19
+ clarification.
20
+
21
+ ## Setup
22
+
23
+ 1. `pip install -r requirements.txt`
24
+ 2. Regenerate `data/sessions.json` whenever `all_raw_diag.json` changes:
25
+ `python3 ../results/chat_pp/build_sql_eval_data.py` (run from repo root)
26
+ 3. Create a `.env` with:
27
+ ```
28
+ GITHUB_TOKEN=...
29
+ GITHUB_REPO=your-org/sql-eval-human-labeling
30
+ ```
31
+ The target repo must already exist; submitted labels are written to its
32
+ `labels/{pid}_{sid}.json` files (one per completed session).
33
+ 4. `python3 app.py`
34
+
35
+ ## How assignment works
36
+
37
+ Each "new session" click hands out one whole conversation session (all its
38
+ judgable turns at once) to whoever clicked. A session counts as done the
39
+ moment its label file is created in the GitHub repo -- from then on it's
40
+ excluded from everyone's pool, so no two people label the same session.
41
+ Assignment itself (which session goes to which click) is tracked in-memory
42
+ per running process, so it only works correctly as a single app instance
43
+ (don't scale this to multiple replicas without adding shared claim state).
app.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import datetime
3
+ import gradio as gr
4
+
5
+ from utils.filesys_utils import json_load
6
+ from utils.common import list_completed_keys, upload_label
7
+
8
+
9
+ ERROR_CATEGORIES = [
10
+ "λŒ€μƒκ΅°(μ½”ν˜ΈνŠΈ) 해석 였λ₯˜",
11
+ "μ§€ν‘œ/계산식 해석 였λ₯˜",
12
+ "SQL ꡬ문/둜직 였λ₯˜",
13
+ "κ²°κ³Όβ†’μ„€λͺ… 뢈일치",
14
+ "기타",
15
+ ]
16
+
17
+ MAX_TURNS = 15 # actual max across the dataset is 13; a little headroom
18
+
19
+ SESSIONS = json_load("data/sessions.json")
20
+ SESSION_BY_KEY = {f"{s['pid']}_{s['sid']}": s for s in SESSIONS}
21
+
22
+ # In-memory only, shared across all concurrent users of this single process.
23
+ # Best-effort de-duplication of who's currently working on what; the durable
24
+ # "is this session actually done" truth lives in the GitHub labels/ dir.
25
+ CLAIMED: set = set()
26
+
27
+
28
+ def render_dialogue_md(sess: dict) -> str:
29
+ role_label = {"user": "πŸ§‘ μ‚¬μš©μž", "assistant": "πŸ€– μ—μ΄μ „νŠΈ"}
30
+ judgable_set = set(sess["judgable_turns"])
31
+ lines = [f"### μ°Έκ°€μž `{sess['pid']}` / μ„Έμ…˜ `{sess['sid']}` (μ „λž΅: {sess['strategy']})", ""]
32
+ for i, t in enumerate(sess["dialogue"]):
33
+ marker = " **β¬… 라벨링 λŒ€μƒ (μ•„λž˜μ—μ„œ νŒλ‹¨)**" if i in judgable_set else ""
34
+ lines.append(f"**#{i} [{t['type']}] {role_label[t['role']]}**{marker}")
35
+ lines.append(t["content"])
36
+ lines.append("")
37
+ return "\n".join(lines)
38
+
39
+
40
+ def render_turn_detail_md(sess: dict, idx: int) -> str:
41
+ t = sess["dialogue"][idx]
42
+ lines = [f"#### νŒλ‹¨ λŒ€μƒ: ν„΄ #{idx} ({t['type']})"]
43
+ for j, s in enumerate(t.get("sql", [])):
44
+ lines.append(f"**쿼리 {j + 1}:**")
45
+ lines.append(f"```sql\n{s['query']}\n```")
46
+ lines.append(f"**μ‹€ν–‰ κ²°κ³Ό:** `{s['result']}`")
47
+ return "\n".join(lines)
48
+
49
+
50
+ def _reset_turn_updates():
51
+ """One (visible=False, empty text, cleared radio, cleared checkboxes,
52
+ cleared textbox) group per pre-allocated turn slot."""
53
+ updates = []
54
+ for _ in range(MAX_TURNS):
55
+ updates += [gr.update(visible=False), "", gr.update(value=None), gr.update(value=[]), gr.update(value="")]
56
+ return updates
57
+
58
+
59
+ def get_new_session():
60
+ completed = list_completed_keys()
61
+ available = [k for k in SESSION_BY_KEY if k not in completed and k not in CLAIMED]
62
+
63
+ if not available:
64
+ return (
65
+ [None, ""]
66
+ + _reset_turn_updates()
67
+ + [gr.update(visible=False), "πŸŽ‰ λͺ¨λ“  μ„Έμ…˜μ΄ λΌλ²¨λ§λ˜μ—ˆμŠ΅λ‹ˆλ‹€! κ°μ‚¬ν•©λ‹ˆλ‹€."]
68
+ )
69
+
70
+ key = random.choice(available)
71
+ CLAIMED.add(key)
72
+ sess = SESSION_BY_KEY[key]
73
+ judgable = sess["judgable_turns"]
74
+
75
+ updates = []
76
+ for i in range(MAX_TURNS):
77
+ if i < len(judgable):
78
+ updates += [
79
+ gr.update(visible=True),
80
+ render_turn_detail_md(sess, judgable[i]),
81
+ gr.update(value=None),
82
+ gr.update(value=[]),
83
+ gr.update(value=""),
84
+ ]
85
+ else:
86
+ updates += [gr.update(visible=False), "", gr.update(value=None), gr.update(value=[]), gr.update(value="")]
87
+
88
+ status = f"μ„Έμ…˜ `{key}` 배정됨 -- {len(judgable)}개 턴을 λΌλ²¨λ§ν•΄μ£Όμ„Έμš”."
89
+ return [key, render_dialogue_md(sess)] + updates + [gr.update(visible=True), status]
90
+
91
+
92
+ def submit_labels(key, *flat):
93
+ # session_key_state is a gr.State -- it needs its actual (unchanged) value
94
+ # passed through, not a bare gr.update(), which is only meaningful for
95
+ # ordinary display/interactive components.
96
+ def unchanged(msg):
97
+ return [key, gr.update()] + [gr.update()] * (MAX_TURNS * 5) + [gr.update(), msg]
98
+
99
+ if not key:
100
+ return unchanged("λ¨Όμ € 'μƒˆ μ„Έμ…˜ λ°›κΈ°'λ₯Ό λˆŒλŸ¬μ£Όμ„Έμš”.")
101
+
102
+ sess = SESSION_BY_KEY.get(key)
103
+ if sess is None:
104
+ return unchanged("μ„Έμ…˜ 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
105
+
106
+ judgable = sess["judgable_turns"]
107
+ n = len(judgable)
108
+ verdicts = list(flat[0::3])[:n]
109
+ cats_list = list(flat[1::3])[:n]
110
+ reasons = list(flat[2::3])[:n]
111
+
112
+ missing = [i + 1 for i, v in enumerate(verdicts) if not v]
113
+ if missing:
114
+ msg = f"❌ 아직 νŒλ‹¨ν•˜μ§€ μ•Šμ€ ν•­λͺ©μ΄ μžˆμŠ΅λ‹ˆλ‹€: #{', '.join(map(str, missing))}. λͺ¨λ‘ μ²΄ν¬ν•΄μ£Όμ„Έμš”."
115
+ return unchanged(msg)
116
+
117
+ turn_labels = [
118
+ {"turn_index": idx, "verdict": v, "error_categories": c or [], "reason": r or ""}
119
+ for idx, v, c, r in zip(judgable, verdicts, cats_list, reasons)
120
+ ]
121
+ payload = {
122
+ "pid": sess["pid"],
123
+ "sid": sess["sid"],
124
+ "strategy": sess.get("strategy"),
125
+ "source": sess.get("source"),
126
+ "labeled_at": datetime.datetime.now().isoformat(),
127
+ "turns": turn_labels,
128
+ }
129
+
130
+ ok, msg = upload_label(key, payload)
131
+ if not ok:
132
+ return unchanged(msg)
133
+
134
+ CLAIMED.discard(key)
135
+ # auto-advance to the next session on success
136
+ next_state = get_new_session()
137
+ next_state[-1] = f"{msg} λ‹€μŒ μ„Έμ…˜μ„ λ°°μ •ν–ˆμŠ΅λ‹ˆλ‹€.\n\n{next_state[-1]}"
138
+ return next_state
139
+
140
+
141
+ css = """
142
+ .dialog-box {
143
+ max-height: 500px;
144
+ overflow-y: auto;
145
+ border: 1px solid var(--border-color-primary);
146
+ border-radius: 8px;
147
+ padding: 12px;
148
+ }
149
+ """
150
+
151
+ with gr.Blocks(title="SQL Result Evaluation", css=css) as demo:
152
+ gr.Markdown("# 🩺 SQL κ²°κ³Ό 적합성 라벨링")
153
+ gr.Markdown(
154
+ "μ•„λž˜ 전체 λŒ€ν™”λ₯Ό λκΉŒμ§€ 읽고, **'라벨링 λŒ€μƒ'**으둜 ν‘œμ‹œλœ 각 μ—μ΄μ „νŠΈ 응닡이 "
155
+ "μ‹€μ œλ‘œ μ‹€ν–‰λœ SQLκ³Ό κ·Έ κ²°κ³Όλ₯Ό λ°”νƒ•μœΌλ‘œ μ‚¬μš©μžμ˜ μ΅œμ’… μ˜λ„"
156
+ "(쀑간에 λͺ…ν™•ν™”(clarification)κ°€ μžˆμ—ˆλ‹€λ©΄ κ·Έκ²ƒκΉŒμ§€ λ°˜μ˜ν•œ μ˜λ„)에 λ§žλŠ” 닡을 ν–ˆλŠ”μ§€ νŒλ‹¨ν•΄μ£Όμ„Έμš”."
157
+ )
158
+ gr.Markdown(
159
+ "* ν•œ μ„Έμ…˜μ—λŠ” 라벨링 λŒ€μƒμ΄ μ—¬λŸ¬ 개 μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€ -- μ„Έμ…˜μ˜ λͺ¨λ“  λŒ€μƒμ„ μ±„μ›Œμ•Ό μ œμΆœλ©λ‹ˆλ‹€.\n"
160
+ "* μ œμΆœν•˜λ©΄ κ·Έ μ„Έμ…˜μ€ μ™„λ£Œ μ²˜λ¦¬λ˜μ–΄ λ‹€λ₯Έ μ‚¬λžŒμ—κ²Œ λ‹€μ‹œ λ°°μ •λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.\n"
161
+ "* 'ν‹€λ¦Ό'을 μ„ νƒν•œ 경우 원인 μΉ΄ν…Œκ³ λ¦¬λ₯Ό μ²΄ν¬ν•˜κ³ , κ°€λŠ₯ν•˜λ©΄ 자유 μ„œμˆ λ‘œ μ΄μœ λ„ λ‚¨κ²¨μ£Όμ„Έμš”."
162
+ )
163
+
164
+ session_key_state = gr.State(None)
165
+ btn_new = gr.Button("🎲 μƒˆ μ„Έμ…˜ λ°›κΈ°")
166
+ status_msg = gr.Markdown("")
167
+ dialogue_md = gr.Markdown(elem_classes="dialog-box")
168
+
169
+ turn_groups, detail_mds, verdict_radios, category_checks, reason_boxes = [], [], [], [], []
170
+ for i in range(MAX_TURNS):
171
+ with gr.Group(visible=False) as grp:
172
+ detail = gr.Markdown()
173
+ verdict = gr.Radio(["맞음", "ν‹€λ¦Ό"], label=f"νŒλ‹¨ #{i + 1}: 이 응닡은 μ‚¬μš©μžμ˜ μ΅œμ’… μ˜λ„μ— λ§žλ‚˜μš”?")
174
+ cats = gr.CheckboxGroup(ERROR_CATEGORIES, label="였λ₯˜ μœ ν˜• (틀렸을 λ•Œλ§Œ ν•΄λ‹Ήν•˜λŠ” ν•­λͺ© 체크)")
175
+ reason = gr.Textbox(label="자유 μ„œμˆ  이유", lines=2)
176
+ turn_groups.append(grp)
177
+ detail_mds.append(detail)
178
+ verdict_radios.append(verdict)
179
+ category_checks.append(cats)
180
+ reason_boxes.append(reason)
181
+
182
+ with gr.Row(visible=False) as submit_row:
183
+ submit_btn = gr.Button("πŸ“€ μ œμΆœν•˜κΈ°")
184
+
185
+ all_outputs = [session_key_state, dialogue_md]
186
+ for grp, detail, verdict, cats, reason in zip(turn_groups, detail_mds, verdict_radios, category_checks, reason_boxes):
187
+ all_outputs += [grp, detail, verdict, cats, reason]
188
+ all_outputs += [submit_row, status_msg]
189
+
190
+ btn_new.click(fn=get_new_session, inputs=[], outputs=all_outputs)
191
+
192
+ submit_inputs = [session_key_state]
193
+ for verdict, cats, reason in zip(verdict_radios, category_checks, reason_boxes):
194
+ submit_inputs += [verdict, cats, reason]
195
+ submit_btn.click(fn=submit_labels, inputs=submit_inputs, outputs=all_outputs)
196
+
197
+ if __name__ == "__main__":
198
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio==5.49.1
2
+ dotenv==0.9.9
3
+ PyGithub==2.8.1
utils/__init__.py ADDED
File without changes
utils/common.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from github import Github
4
+ from dotenv import load_dotenv
5
+
6
+
7
+ LABELS_DIR = "labels"
8
+
9
+
10
+ def _get_repo():
11
+ load_dotenv(override=True)
12
+ repo_name = os.getenv("GITHUB_REPO")
13
+ if not repo_name:
14
+ raise RuntimeError("GITHUB_REPO env var is not set")
15
+ g = Github(os.getenv("GITHUB_TOKEN"))
16
+ return g.get_repo(repo_name)
17
+
18
+
19
+ def list_completed_keys() -> set:
20
+ """Session keys ("{pid}_{sid}") that already have a submitted label file
21
+ in the GitHub repo's labels/ directory. Returns an empty set (rather than
22
+ raising) if the repo/labels dir isn't reachable yet, so the app still
23
+ works before it's configured or if the directory doesn't exist yet."""
24
+ try:
25
+ repo = _get_repo()
26
+ contents = repo.get_contents(LABELS_DIR, ref="main")
27
+ return {os.path.splitext(c.name)[0] for c in contents}
28
+ except Exception:
29
+ return set()
30
+
31
+
32
+ def upload_label(key: str, payload: dict):
33
+ """Create labels/{key}.json in the GitHub repo. Uses create (not update)
34
+ so a second submission for an already-completed session fails loudly
35
+ instead of silently overwriting someone else's labels."""
36
+ path = f"{LABELS_DIR}/{key}.json"
37
+ content = json.dumps(payload, ensure_ascii=False, indent=2)
38
+ try:
39
+ repo = _get_repo()
40
+ try:
41
+ repo.get_contents(path, ref="main")
42
+ return False, "⚠️ 이미 λ‹€λ₯Έ μ‚¬λžŒμ΄ 이 μ„Έμ…˜μ„ μ™„λ£Œν–ˆμŠ΅λ‹ˆλ‹€. μƒˆ μ„Έμ…˜μ„ λ°›μ•„μ£Όμ„Έμš”."
43
+ except Exception:
44
+ pass
45
+ repo.create_file(path, f"feat: add labels for {key}", content, branch="main")
46
+ return True, "βœ… 제좜 μ™„λ£Œ! κ°μ‚¬ν•©λ‹ˆλ‹€."
47
+ except Exception as e:
48
+ return False, f"❌ μ—…λ‘œλ“œ μ‹€νŒ¨: {e}"
utils/filesys_utils.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Any
3
+
4
+
5
+ def json_load(path: str) -> Any:
6
+ with open(path, "r", encoding="utf-8") as f:
7
+ return json.load(f)