pearmut 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pearmut/app.py +50 -8
- pearmut/assignment.py +135 -11
- pearmut/cli.py +1 -1
- pearmut/static/assets/style.css +7 -0
- pearmut/static/dashboard.bundle.js +1 -1
- pearmut/static/dashboard.html +1 -1
- pearmut/static/listwise.bundle.js +1 -1
- pearmut/static/listwise.html +2 -2
- pearmut/static/pointwise.bundle.js +1 -1
- pearmut/static/pointwise.html +1 -1
- pearmut/utils.py +55 -2
- {pearmut-0.1.2.dist-info → pearmut-0.2.0.dist-info}/METADATA +85 -10
- pearmut-0.2.0.dist-info/RECORD +19 -0
- pearmut-0.1.2.dist-info/RECORD +0 -19
- {pearmut-0.1.2.dist-info → pearmut-0.2.0.dist-info}/WHEEL +0 -0
- {pearmut-0.1.2.dist-info → pearmut-0.2.0.dist-info}/entry_points.txt +0 -0
- {pearmut-0.1.2.dist-info → pearmut-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pearmut-0.1.2.dist-info → pearmut-0.2.0.dist-info}/top_level.txt +0 -0
pearmut/app.py
CHANGED
|
@@ -8,8 +8,8 @@ from fastapi.responses import JSONResponse
|
|
|
8
8
|
from fastapi.staticfiles import StaticFiles
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
|
-
from .assignment import get_next_item, reset_task, update_progress
|
|
12
|
-
from .utils import ROOT, load_progress_data, save_progress_data
|
|
11
|
+
from .assignment import get_i_item, get_next_item, reset_task, update_progress
|
|
12
|
+
from .utils import ROOT, load_progress_data, save_db_payload, save_progress_data
|
|
13
13
|
|
|
14
14
|
os.makedirs(f"{ROOT}/data/outputs", exist_ok=True)
|
|
15
15
|
|
|
@@ -36,7 +36,7 @@ class LogResponseRequest(BaseModel):
|
|
|
36
36
|
campaign_id: str
|
|
37
37
|
user_id: str
|
|
38
38
|
item_i: int
|
|
39
|
-
payload: Any
|
|
39
|
+
payload: dict[str, Any]
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
@app.post("/log-response")
|
|
@@ -45,6 +45,7 @@ async def _log_response(request: LogResponseRequest):
|
|
|
45
45
|
|
|
46
46
|
campaign_id = request.campaign_id
|
|
47
47
|
user_id = request.user_id
|
|
48
|
+
item_i = request.item_i
|
|
48
49
|
|
|
49
50
|
if campaign_id not in progress_data:
|
|
50
51
|
return JSONResponse(content={"error": "Unknown campaign ID"}, status_code=400)
|
|
@@ -52,8 +53,8 @@ async def _log_response(request: LogResponseRequest):
|
|
|
52
53
|
return JSONResponse(content={"error": "Unknown user ID"}, status_code=400)
|
|
53
54
|
|
|
54
55
|
# append response to the output log
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
save_db_payload(campaign_id, request.payload | {
|
|
57
|
+
"user_id": user_id, "item_i": item_i})
|
|
57
58
|
|
|
58
59
|
# if actions were submitted, we can log time data
|
|
59
60
|
if "actions" in request.payload:
|
|
@@ -68,7 +69,16 @@ async def _log_response(request: LogResponseRequest):
|
|
|
68
69
|
for a, b in zip(times, times[1:])
|
|
69
70
|
])
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
# Initialize validation_checks if it doesn't exist
|
|
73
|
+
print(request.payload.keys())
|
|
74
|
+
if "validations" in request.payload:
|
|
75
|
+
if "validations" not in progress_data[campaign_id][user_id]:
|
|
76
|
+
progress_data[campaign_id][user_id]["validations"] = {}
|
|
77
|
+
|
|
78
|
+
progress_data[campaign_id][user_id]["validations"][request.item_i] = request.payload["validations"]
|
|
79
|
+
|
|
80
|
+
update_progress(campaign_id, user_id, tasks_data,
|
|
81
|
+
progress_data, request.item_i, request.payload)
|
|
72
82
|
save_progress_data(progress_data)
|
|
73
83
|
|
|
74
84
|
return JSONResponse(content={"status": "ok"}, status_code=200)
|
|
@@ -97,6 +107,32 @@ async def _get_next_item(request: NextItemRequest):
|
|
|
97
107
|
)
|
|
98
108
|
|
|
99
109
|
|
|
110
|
+
class GetItemRequest(BaseModel):
|
|
111
|
+
campaign_id: str
|
|
112
|
+
user_id: str
|
|
113
|
+
item_i: int
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.post("/get-i-item")
|
|
117
|
+
async def _get_i_item(request: GetItemRequest):
|
|
118
|
+
campaign_id = request.campaign_id
|
|
119
|
+
user_id = request.user_id
|
|
120
|
+
item_i = request.item_i
|
|
121
|
+
|
|
122
|
+
if campaign_id not in progress_data:
|
|
123
|
+
return JSONResponse(content={"error": "Unknown campaign ID"}, status_code=400)
|
|
124
|
+
if user_id not in progress_data[campaign_id]:
|
|
125
|
+
return JSONResponse(content={"error": "Unknown user ID"}, status_code=400)
|
|
126
|
+
|
|
127
|
+
return get_i_item(
|
|
128
|
+
campaign_id,
|
|
129
|
+
user_id,
|
|
130
|
+
tasks_data,
|
|
131
|
+
progress_data,
|
|
132
|
+
item_i,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
100
136
|
class DashboardDataRequest(BaseModel):
|
|
101
137
|
campaign_id: str
|
|
102
138
|
token: str | None = None
|
|
@@ -119,6 +155,11 @@ async def _dashboard_data(request: DashboardDataRequest):
|
|
|
119
155
|
for user_id, user_val in progress_data[campaign_id].items():
|
|
120
156
|
# shallow copy
|
|
121
157
|
entry = dict(user_val)
|
|
158
|
+
entry["validations"] = [
|
|
159
|
+
all(v)
|
|
160
|
+
for v in list(entry.get("validations", {}).values())
|
|
161
|
+
]
|
|
162
|
+
|
|
122
163
|
|
|
123
164
|
if not is_privileged:
|
|
124
165
|
entry["token_correct"] = None
|
|
@@ -203,10 +244,11 @@ async def _download_progress(
|
|
|
203
244
|
|
|
204
245
|
static_dir = f"{os.path.dirname(os.path.abspath(__file__))}/static/"
|
|
205
246
|
if not os.path.exists(static_dir + "index.html"):
|
|
206
|
-
raise FileNotFoundError(
|
|
247
|
+
raise FileNotFoundError(
|
|
248
|
+
"Static directory not found. Please build the frontend first.")
|
|
207
249
|
|
|
208
250
|
app.mount(
|
|
209
251
|
"/",
|
|
210
252
|
StaticFiles(directory=static_dir, html=True, follow_symlink=True),
|
|
211
253
|
name="static",
|
|
212
|
-
)
|
|
254
|
+
)
|
pearmut/assignment.py
CHANGED
|
@@ -3,6 +3,8 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from fastapi.responses import JSONResponse
|
|
5
5
|
|
|
6
|
+
from .utils import get_db_log_item
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def _completed_response(
|
|
8
10
|
progress_data: dict,
|
|
@@ -37,13 +39,117 @@ def get_next_item(
|
|
|
37
39
|
if assignment == "task-based":
|
|
38
40
|
return get_next_item_taskbased(campaign_id, user_id, tasks_data, progress_data)
|
|
39
41
|
elif assignment == "single-stream":
|
|
40
|
-
return
|
|
42
|
+
return get_next_item_singlestream(campaign_id, user_id, tasks_data, progress_data)
|
|
41
43
|
elif assignment == "dynamic":
|
|
42
44
|
return get_next_item_dynamic(campaign_id, user_id, tasks_data, progress_data)
|
|
43
45
|
else:
|
|
44
46
|
return JSONResponse(content={"error": "Unknown campaign assignment type"}, status_code=400)
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
def get_i_item(
|
|
50
|
+
campaign_id: str,
|
|
51
|
+
user_id: str,
|
|
52
|
+
tasks_data: dict,
|
|
53
|
+
progress_data: dict,
|
|
54
|
+
item_i: int,
|
|
55
|
+
) -> JSONResponse:
|
|
56
|
+
"""
|
|
57
|
+
Get a specific item by index for the user in the specified campaign.
|
|
58
|
+
"""
|
|
59
|
+
assignment = tasks_data[campaign_id]["info"]["assignment"]
|
|
60
|
+
if assignment == "task-based":
|
|
61
|
+
return get_i_item_taskbased(campaign_id, user_id, tasks_data, progress_data, item_i)
|
|
62
|
+
elif assignment == "single-stream":
|
|
63
|
+
return get_i_item_singlestream(campaign_id, user_id, tasks_data, progress_data, item_i)
|
|
64
|
+
else:
|
|
65
|
+
return JSONResponse(content={"error": "Get item not supported for this assignment type"}, status_code=400)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_i_item_taskbased(
|
|
69
|
+
campaign_id: str,
|
|
70
|
+
user_id: str,
|
|
71
|
+
data_all: dict,
|
|
72
|
+
progress_data: dict,
|
|
73
|
+
item_i: int,
|
|
74
|
+
) -> JSONResponse:
|
|
75
|
+
"""
|
|
76
|
+
Get specific item for task-based protocol.
|
|
77
|
+
"""
|
|
78
|
+
user_progress = progress_data[campaign_id][user_id]
|
|
79
|
+
|
|
80
|
+
# try to get existing annotations if any
|
|
81
|
+
items_existing = get_db_log_item(campaign_id, user_id, item_i)
|
|
82
|
+
if items_existing:
|
|
83
|
+
# get the latest ones
|
|
84
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
85
|
+
|
|
86
|
+
if item_i < 0 or item_i >= len(data_all[campaign_id]["data"][user_id]):
|
|
87
|
+
return JSONResponse(
|
|
88
|
+
content={"status": "error", "message": "Item index out of range"},
|
|
89
|
+
status_code=400
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return JSONResponse(
|
|
93
|
+
content={
|
|
94
|
+
"status": "ok",
|
|
95
|
+
"progress": user_progress["progress"],
|
|
96
|
+
"time": user_progress["time"],
|
|
97
|
+
"info": {
|
|
98
|
+
"item_i": item_i,
|
|
99
|
+
} | {
|
|
100
|
+
k: v
|
|
101
|
+
for k, v in data_all[campaign_id]["info"].items()
|
|
102
|
+
if k.startswith("protocol")
|
|
103
|
+
},
|
|
104
|
+
"payload": data_all[campaign_id]["data"][user_id][item_i]
|
|
105
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
106
|
+
status_code=200
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_i_item_singlestream(
|
|
111
|
+
campaign_id: str,
|
|
112
|
+
user_id: str,
|
|
113
|
+
data_all: dict,
|
|
114
|
+
progress_data: dict,
|
|
115
|
+
item_i: int,
|
|
116
|
+
) -> JSONResponse:
|
|
117
|
+
"""
|
|
118
|
+
Get specific item for single-stream assignment.
|
|
119
|
+
"""
|
|
120
|
+
user_progress = progress_data[campaign_id][user_id]
|
|
121
|
+
|
|
122
|
+
# try to get existing annotations if any
|
|
123
|
+
# note the None user_id since it is shared
|
|
124
|
+
items_existing = get_db_log_item(campaign_id, None, item_i)
|
|
125
|
+
if items_existing:
|
|
126
|
+
# get the latest ones
|
|
127
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
128
|
+
|
|
129
|
+
if item_i < 0 or item_i >= len(data_all[campaign_id]["data"]):
|
|
130
|
+
return JSONResponse(
|
|
131
|
+
content={"status": "error", "message": "Item index out of range"},
|
|
132
|
+
status_code=400
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return JSONResponse(
|
|
136
|
+
content={
|
|
137
|
+
"status": "ok",
|
|
138
|
+
"progress": user_progress["progress"],
|
|
139
|
+
"time": user_progress["time"],
|
|
140
|
+
"info": {
|
|
141
|
+
"item_i": item_i,
|
|
142
|
+
} | {
|
|
143
|
+
k: v
|
|
144
|
+
for k, v in data_all[campaign_id]["info"].items()
|
|
145
|
+
if k.startswith("protocol")
|
|
146
|
+
},
|
|
147
|
+
"payload": data_all[campaign_id]["data"][item_i]
|
|
148
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
149
|
+
status_code=200
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
47
153
|
def get_next_item_taskbased(
|
|
48
154
|
campaign_id: str,
|
|
49
155
|
user_id: str,
|
|
@@ -51,7 +157,7 @@ def get_next_item_taskbased(
|
|
|
51
157
|
progress_data: dict,
|
|
52
158
|
) -> JSONResponse:
|
|
53
159
|
"""
|
|
54
|
-
Get the next item for task-based
|
|
160
|
+
Get the next item for task-based assignment.
|
|
55
161
|
"""
|
|
56
162
|
user_progress = progress_data[campaign_id][user_id]
|
|
57
163
|
if all(user_progress["progress"]):
|
|
@@ -59,6 +165,13 @@ def get_next_item_taskbased(
|
|
|
59
165
|
|
|
60
166
|
# find first incomplete item
|
|
61
167
|
item_i = min([i for i, v in enumerate(user_progress["progress"]) if not v])
|
|
168
|
+
|
|
169
|
+
# try to get existing annotations if any
|
|
170
|
+
items_existing = get_db_log_item(campaign_id, user_id, item_i)
|
|
171
|
+
if items_existing:
|
|
172
|
+
# get the latest ones
|
|
173
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
174
|
+
|
|
62
175
|
return JSONResponse(
|
|
63
176
|
content={
|
|
64
177
|
"status": "ok",
|
|
@@ -71,23 +184,20 @@ def get_next_item_taskbased(
|
|
|
71
184
|
for k, v in data_all[campaign_id]["info"].items()
|
|
72
185
|
if k.startswith("protocol")
|
|
73
186
|
},
|
|
74
|
-
"payload": data_all[campaign_id]["data"][user_id][item_i]
|
|
187
|
+
"payload": data_all[campaign_id]["data"][user_id][item_i]
|
|
188
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
75
189
|
status_code=200
|
|
76
190
|
)
|
|
77
191
|
|
|
78
192
|
|
|
79
|
-
def
|
|
80
|
-
raise NotImplementedError("Dynamic protocol is not implemented yet.")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_next_item_single_stream(
|
|
193
|
+
def get_next_item_singlestream(
|
|
84
194
|
campaign_id: str,
|
|
85
195
|
user_id: str,
|
|
86
196
|
data_all: dict,
|
|
87
197
|
progress_data: dict,
|
|
88
198
|
) -> JSONResponse:
|
|
89
199
|
"""
|
|
90
|
-
Get the next item for single-stream
|
|
200
|
+
Get the next item for single-stream assignment.
|
|
91
201
|
In this mode, all users share the same pool of items.
|
|
92
202
|
Items are randomly selected from unfinished items.
|
|
93
203
|
|
|
@@ -104,6 +214,13 @@ def get_next_item_single_stream(
|
|
|
104
214
|
incomplete_indices = [i for i, v in enumerate(progress) if not v]
|
|
105
215
|
item_i = random.choice(incomplete_indices)
|
|
106
216
|
|
|
217
|
+
# try to get existing annotations if any
|
|
218
|
+
# note the None user_id since it is shared
|
|
219
|
+
items_existing = get_db_log_item(campaign_id, None, item_i)
|
|
220
|
+
if items_existing:
|
|
221
|
+
# get the latest ones
|
|
222
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
223
|
+
|
|
107
224
|
return JSONResponse(
|
|
108
225
|
content={
|
|
109
226
|
"status": "ok",
|
|
@@ -116,16 +233,24 @@ def get_next_item_single_stream(
|
|
|
116
233
|
for k, v in data_all[campaign_id]["info"].items()
|
|
117
234
|
if k.startswith("protocol")
|
|
118
235
|
},
|
|
119
|
-
"payload": data_all[campaign_id]["data"][item_i]
|
|
236
|
+
"payload": data_all[campaign_id]["data"][item_i]
|
|
237
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
120
238
|
status_code=200
|
|
121
239
|
)
|
|
122
240
|
|
|
123
241
|
|
|
242
|
+
|
|
243
|
+
def get_next_item_dynamic(campaign_data: dict, user_id: str, progress_data: dict, data_all: dict):
|
|
244
|
+
raise NotImplementedError("Dynamic protocol is not implemented yet.")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
124
248
|
def _reset_user_time(progress_data: dict, campaign_id: str, user_id: str) -> None:
|
|
125
249
|
"""Reset time tracking fields for a user."""
|
|
126
250
|
progress_data[campaign_id][user_id]["time"] = 0.0
|
|
127
251
|
progress_data[campaign_id][user_id]["time_start"] = None
|
|
128
252
|
progress_data[campaign_id][user_id]["time_end"] = None
|
|
253
|
+
progress_data[campaign_id][user_id]["validations"] = {}
|
|
129
254
|
|
|
130
255
|
|
|
131
256
|
def reset_task(
|
|
@@ -171,7 +296,6 @@ def update_progress(
|
|
|
171
296
|
if assignment == "task-based":
|
|
172
297
|
# even if it's already set it should be fine
|
|
173
298
|
progress_data[campaign_id][user_id]["progress"][item_i] = True
|
|
174
|
-
# TODO: log attention checks/quality?
|
|
175
299
|
return JSONResponse(content={"status": "ok"}, status_code=200)
|
|
176
300
|
elif assignment == "single-stream":
|
|
177
301
|
# progress all users
|
pearmut/cli.py
CHANGED
|
@@ -214,7 +214,7 @@ def main():
|
|
|
214
214
|
import shutil
|
|
215
215
|
|
|
216
216
|
confirm = input(
|
|
217
|
-
"Are you sure you want to purge all campaign data? This action cannot be undone. [y/n]"
|
|
217
|
+
"Are you sure you want to purge all campaign data? This action cannot be undone. [y/n] "
|
|
218
218
|
)
|
|
219
219
|
if confirm.lower() == 'y':
|
|
220
220
|
shutil.rmtree(f"{ROOT}/data/tasks", ignore_errors=True)
|
pearmut/static/assets/style.css
CHANGED
|
@@ -225,4 +225,11 @@ input[type="button"].error_delete:hover {
|
|
|
225
225
|
|
|
226
226
|
#progress span.progress_incomplete:hover {
|
|
227
227
|
background: #aaa;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Validation warning indicator */
|
|
231
|
+
.validation_warning {
|
|
232
|
+
margin-right: 5px;
|
|
233
|
+
position: relative;
|
|
234
|
+
top: -5px;
|
|
228
235
|
}
|