pearmut 0.0.5__py3-none-any.whl → 0.1.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/{run.py → app.py} +4 -13
- pearmut/cli.py +71 -23
- pearmut/protocols.py +15 -2
- pearmut/static/assets/style.css +12 -8
- pearmut/static/dashboard.bundle.js +1 -1
- pearmut/static/dashboard.html +4 -3
- pearmut/static/pointwise.bundle.js +1 -1
- pearmut/static/pointwise.html +87 -23
- {pearmut-0.0.5.dist-info → pearmut-0.1.0.dist-info}/METADATA +56 -26
- pearmut-0.1.0.dist-info/RECORD +17 -0
- pearmut/model.py +0 -61
- pearmut-0.0.5.dist-info/RECORD +0 -18
- {pearmut-0.0.5.dist-info → pearmut-0.1.0.dist-info}/WHEEL +0 -0
- {pearmut-0.0.5.dist-info → pearmut-0.1.0.dist-info}/entry_points.txt +0 -0
- {pearmut-0.0.5.dist-info → pearmut-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {pearmut-0.0.5.dist-info → pearmut-0.1.0.dist-info}/top_level.txt +0 -0
pearmut/{run.py → app.py}
RENAMED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
import urllib
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
6
5
|
from fastapi import FastAPI, Query
|
|
@@ -9,7 +8,7 @@ from fastapi.responses import JSONResponse
|
|
|
9
8
|
from fastapi.staticfiles import StaticFiles
|
|
10
9
|
from pydantic import BaseModel
|
|
11
10
|
|
|
12
|
-
from .protocols import get_next_item,
|
|
11
|
+
from .protocols import get_next_item, reset_task, update_progress
|
|
13
12
|
from .utils import ROOT, load_progress_data, save_progress_data
|
|
14
13
|
|
|
15
14
|
os.makedirs(f"{ROOT}/data/outputs", exist_ok=True)
|
|
@@ -32,15 +31,6 @@ for campaign_id in progress_data.keys():
|
|
|
32
31
|
with open(f"{ROOT}/data/tasks/{campaign_id}.json", "r") as f:
|
|
33
32
|
tasks_data[campaign_id] = json.load(f)
|
|
34
33
|
|
|
35
|
-
if tasks_data:
|
|
36
|
-
# print access dashboard URL for all campaigns
|
|
37
|
-
print(
|
|
38
|
-
list(tasks_data.values())[0]["info"]["url"] + "/dashboard.html?" + "&".join([
|
|
39
|
-
f"campaign_id={urllib.parse.quote_plus(campaign_id)}&token={campaign_data["token"]}"
|
|
40
|
-
for campaign_id, campaign_data in tasks_data.items()
|
|
41
|
-
])
|
|
42
|
-
)
|
|
43
|
-
|
|
44
34
|
|
|
45
35
|
class LogResponseRequest(BaseModel):
|
|
46
36
|
campaign_id: str
|
|
@@ -61,6 +51,7 @@ async def _log_response(request: LogResponseRequest):
|
|
|
61
51
|
if user_id not in progress_data[campaign_id]:
|
|
62
52
|
return JSONResponse(content={"error": "Unknown user ID"}, status_code=400)
|
|
63
53
|
|
|
54
|
+
# append response to the output log
|
|
64
55
|
with open(f"{ROOT}/data/outputs/{campaign_id}.jsonl", "a") as log_file:
|
|
65
56
|
log_file.write(json.dumps(request.payload, ensure_ascii=False) + "\n")
|
|
66
57
|
|
|
@@ -77,7 +68,7 @@ async def _log_response(request: LogResponseRequest):
|
|
|
77
68
|
for a, b in zip(times, times[1:])
|
|
78
69
|
])
|
|
79
70
|
|
|
80
|
-
|
|
71
|
+
update_progress(campaign_id, user_id, tasks_data, progress_data, request.item_i, request.payload)
|
|
81
72
|
save_progress_data(progress_data)
|
|
82
73
|
|
|
83
74
|
return JSONResponse(content={"status": "ok"}, status_code=200)
|
|
@@ -214,4 +205,4 @@ app.mount(
|
|
|
214
205
|
"/",
|
|
215
206
|
StaticFiles(directory=f"{os.path.dirname(os.path.abspath(__file__))}/static/" , html=True, follow_symlink=True),
|
|
216
207
|
name="static",
|
|
217
|
-
)
|
|
208
|
+
)
|
pearmut/cli.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for managing and running the Pearmut server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
import argparse
|
|
2
6
|
import hashlib
|
|
3
7
|
import json
|
|
4
8
|
import os
|
|
5
9
|
import urllib.parse
|
|
6
10
|
|
|
11
|
+
import psutil
|
|
12
|
+
|
|
7
13
|
from .utils import ROOT, load_progress_data
|
|
8
14
|
|
|
9
15
|
os.makedirs(f"{ROOT}/data/tasks", exist_ok=True)
|
|
@@ -13,39 +19,58 @@ load_progress_data(warn=None)
|
|
|
13
19
|
def _run(args_unknown):
|
|
14
20
|
import uvicorn
|
|
15
21
|
|
|
22
|
+
from .app import app, tasks_data
|
|
23
|
+
|
|
16
24
|
args = argparse.ArgumentParser()
|
|
17
|
-
args.add_argument(
|
|
25
|
+
args.add_argument(
|
|
26
|
+
"--port", type=int, default=8001,
|
|
27
|
+
help="Port to run the server on"
|
|
28
|
+
)
|
|
29
|
+
args.add_argument(
|
|
30
|
+
"--server", default="http://localhost:8001",
|
|
31
|
+
help="Prefix server URL for protocol links"
|
|
32
|
+
)
|
|
18
33
|
args = args.parse_args(args_unknown)
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
# print access dashboard URL for all campaigns
|
|
36
|
+
if tasks_data:
|
|
37
|
+
print(
|
|
38
|
+
args.server + "/dashboard.html?" + "&".join([
|
|
39
|
+
f"campaign_id={urllib.parse.quote_plus(campaign_id)}&token={campaign_data["token"]}"
|
|
40
|
+
for campaign_id, campaign_data in tasks_data.items()
|
|
41
|
+
])
|
|
42
|
+
)
|
|
26
43
|
|
|
27
|
-
from .run import app
|
|
28
44
|
uvicorn.run(
|
|
29
45
|
app,
|
|
30
46
|
host="127.0.0.1",
|
|
31
|
-
port=
|
|
32
|
-
|
|
47
|
+
port=args.port,
|
|
48
|
+
reload=False,
|
|
33
49
|
# log_level="info",
|
|
34
|
-
# app_dir="src",
|
|
35
|
-
# factory=False # factory=False means it expects 'app' to be a variable
|
|
36
50
|
)
|
|
37
51
|
|
|
38
52
|
|
|
39
53
|
def _add_campaign(args_unknown):
|
|
54
|
+
"""
|
|
55
|
+
Add a new campaign from a JSON data file.
|
|
56
|
+
"""
|
|
40
57
|
import random
|
|
41
58
|
|
|
42
59
|
import wonderwords
|
|
43
60
|
|
|
44
61
|
args = argparse.ArgumentParser()
|
|
45
|
-
args.add_argument(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
args.add_argument(
|
|
63
|
+
'data_file', type=str,
|
|
64
|
+
help='Path to the campaign data file'
|
|
65
|
+
)
|
|
66
|
+
args.add_argument(
|
|
67
|
+
"-o", "--overwrite", action="store_true",
|
|
68
|
+
help="Overwrite existing campaign if it exists"
|
|
69
|
+
)
|
|
70
|
+
args.add_argument(
|
|
71
|
+
"--server", default="http://localhost:8001",
|
|
72
|
+
help="Prefix server URL for protocol links"
|
|
73
|
+
)
|
|
49
74
|
args = args.parse_args(args_unknown)
|
|
50
75
|
|
|
51
76
|
with open(args.data_file, 'r') as f:
|
|
@@ -61,13 +86,30 @@ def _add_campaign(args_unknown):
|
|
|
61
86
|
)
|
|
62
87
|
exit(1)
|
|
63
88
|
|
|
89
|
+
if "info" not in campaign_data:
|
|
90
|
+
raise ValueError("Campaign data must contain 'info' field.")
|
|
91
|
+
if "data" not in campaign_data:
|
|
92
|
+
raise ValueError("Campaign data must contain 'data' field.")
|
|
93
|
+
if "type" not in campaign_data["info"]:
|
|
94
|
+
raise ValueError("Campaign 'info' must contain 'type' field.")
|
|
95
|
+
if "template" not in campaign_data["info"]:
|
|
96
|
+
raise ValueError("Campaign 'info' must contain 'template' field.")
|
|
97
|
+
|
|
64
98
|
# use random words for identifying users
|
|
65
99
|
rng = random.Random(campaign_data["campaign_id"])
|
|
66
100
|
rword = wonderwords.RandomWord(rng=rng)
|
|
67
101
|
if campaign_data["info"]["type"] == "task-based":
|
|
68
102
|
tasks = campaign_data["data"]
|
|
103
|
+
if not isinstance(tasks, list):
|
|
104
|
+
raise ValueError("Task-based campaign 'data' must be a list of tasks.")
|
|
105
|
+
if not all(isinstance(task, list) for task in tasks):
|
|
106
|
+
raise ValueError("Each task in task-based campaign 'data' must be a list of items.")
|
|
69
107
|
amount = len(tasks)
|
|
70
108
|
elif campaign_data["info"]["type"] == "dynamic":
|
|
109
|
+
if "num_users" not in campaign_data:
|
|
110
|
+
raise ValueError("Dynamic campaigns must specify 'num_users'.")
|
|
111
|
+
if not isinstance(campaign_data["data"], list):
|
|
112
|
+
raise ValueError("Dynamic campaign 'data' must be a list of items.")
|
|
71
113
|
amount = campaign_data["num_users"]
|
|
72
114
|
else:
|
|
73
115
|
raise ValueError(
|
|
@@ -75,6 +117,7 @@ def _add_campaign(args_unknown):
|
|
|
75
117
|
|
|
76
118
|
user_ids = []
|
|
77
119
|
while len(user_ids) < amount:
|
|
120
|
+
# generate random user IDs
|
|
78
121
|
new_id = f"{rword.random_words(amount=1, include_parts_of_speech=['adjective'])[0]}-{rword.random_words(amount=1, include_parts_of_speech=['noun'])[0]}"
|
|
79
122
|
if new_id not in user_ids:
|
|
80
123
|
user_ids.append(new_id)
|
|
@@ -83,11 +126,6 @@ def _add_campaign(args_unknown):
|
|
|
83
126
|
for user_id in user_ids
|
|
84
127
|
]
|
|
85
128
|
|
|
86
|
-
server_url = campaign_data["info"].get(
|
|
87
|
-
"url",
|
|
88
|
-
"127.0.0.1:8001", # by default local server
|
|
89
|
-
).removesuffix("/")
|
|
90
|
-
|
|
91
129
|
campaign_data["data"] = {
|
|
92
130
|
user_id: task
|
|
93
131
|
for user_id, task in zip(user_ids, tasks)
|
|
@@ -106,7 +144,7 @@ def _add_campaign(args_unknown):
|
|
|
106
144
|
"time_end": None,
|
|
107
145
|
"time": 0,
|
|
108
146
|
"url": (
|
|
109
|
-
f"{
|
|
147
|
+
f"{args.server}/{campaign_data["info"]["template"]}.html"
|
|
110
148
|
f"?campaign_id={urllib.parse.quote_plus(campaign_data['campaign_id'])}"
|
|
111
149
|
f"&user_id={user_id}"
|
|
112
150
|
),
|
|
@@ -125,7 +163,7 @@ def _add_campaign(args_unknown):
|
|
|
125
163
|
json.dump(progress_data, f, indent=2, ensure_ascii=False)
|
|
126
164
|
|
|
127
165
|
print(
|
|
128
|
-
f"{
|
|
166
|
+
f"{args.server}/dashboard.html"
|
|
129
167
|
f"?campaign_id={urllib.parse.quote_plus(campaign_data['campaign_id'])}"
|
|
130
168
|
f"&token={campaign_data['token']}"
|
|
131
169
|
)
|
|
@@ -136,10 +174,20 @@ def _add_campaign(args_unknown):
|
|
|
136
174
|
|
|
137
175
|
|
|
138
176
|
def main():
|
|
177
|
+
"""
|
|
178
|
+
Main entry point for the CLI.
|
|
179
|
+
"""
|
|
139
180
|
args = argparse.ArgumentParser()
|
|
140
181
|
args.add_argument('command', type=str, choices=['run', 'add', 'purge'])
|
|
141
182
|
args, args_unknown = args.parse_known_args()
|
|
142
183
|
|
|
184
|
+
# enforce that only one pearmut process is running
|
|
185
|
+
for p in psutil.process_iter():
|
|
186
|
+
if "pearmut" == p.name() and p.pid != os.getpid():
|
|
187
|
+
print("Exit all running pearmut processes before running more commands.")
|
|
188
|
+
print(p)
|
|
189
|
+
exit(1)
|
|
190
|
+
|
|
143
191
|
if args.command == 'run':
|
|
144
192
|
_run(args_unknown)
|
|
145
193
|
elif args.command == 'add':
|
pearmut/protocols.py
CHANGED
|
@@ -9,6 +9,9 @@ def get_next_item(
|
|
|
9
9
|
tasks_data: dict,
|
|
10
10
|
progress_data: dict,
|
|
11
11
|
) -> JSONResponse:
|
|
12
|
+
"""
|
|
13
|
+
Get the next item for the user in the specified campaign.
|
|
14
|
+
"""
|
|
12
15
|
if tasks_data[campaign_id]["info"]["type"] == "task-based":
|
|
13
16
|
return get_next_item_taskbased(campaign_id, user_id, tasks_data, progress_data)
|
|
14
17
|
elif tasks_data[campaign_id]["info"]["type"] == "dynamic":
|
|
@@ -23,6 +26,9 @@ def get_next_item_taskbased(
|
|
|
23
26
|
data_all: dict,
|
|
24
27
|
progress_data: dict,
|
|
25
28
|
) -> JSONResponse:
|
|
29
|
+
"""
|
|
30
|
+
Get the next item for task-based protocol.
|
|
31
|
+
"""
|
|
26
32
|
if all(progress_data[campaign_id][user_id]["progress"]):
|
|
27
33
|
# all items completed
|
|
28
34
|
# TODO: add check for data quality
|
|
@@ -51,7 +57,7 @@ def get_next_item_taskbased(
|
|
|
51
57
|
"total": len(data_all[campaign_id]["data"][user_id]),
|
|
52
58
|
},
|
|
53
59
|
"info": {
|
|
54
|
-
"
|
|
60
|
+
"instructions": data_all[campaign_id]["info"].get("instructions", ""),
|
|
55
61
|
"item_i": item_i,
|
|
56
62
|
} | {
|
|
57
63
|
k: v
|
|
@@ -74,6 +80,9 @@ def reset_task(
|
|
|
74
80
|
tasks_data: dict,
|
|
75
81
|
progress_data: dict,
|
|
76
82
|
) -> JSONResponse:
|
|
83
|
+
"""
|
|
84
|
+
Reset the task progress for the user in the specified campaign.
|
|
85
|
+
"""
|
|
77
86
|
if tasks_data[campaign_id]["info"]["type"] == "task-based":
|
|
78
87
|
progress_data[campaign_id][user_id]["progress"] = [False]*len(tasks_data[campaign_id]["data"][user_id])
|
|
79
88
|
progress_data[campaign_id][user_id]["time"] = 0.0
|
|
@@ -89,7 +98,7 @@ def reset_task(
|
|
|
89
98
|
|
|
90
99
|
|
|
91
100
|
|
|
92
|
-
def
|
|
101
|
+
def update_progress(
|
|
93
102
|
campaign_id: str,
|
|
94
103
|
user_id: str,
|
|
95
104
|
tasks_data: dict,
|
|
@@ -97,9 +106,13 @@ def log_response(
|
|
|
97
106
|
item_i: int,
|
|
98
107
|
payload: Any,
|
|
99
108
|
) -> JSONResponse:
|
|
109
|
+
"""
|
|
110
|
+
Log the user's response for the specified item in the campaign.
|
|
111
|
+
"""
|
|
100
112
|
if tasks_data[campaign_id]["info"]["type"] == "task-based":
|
|
101
113
|
# even if it's already set it should be fine
|
|
102
114
|
progress_data[campaign_id][user_id]["progress"][item_i] = True
|
|
115
|
+
# TODO: log attention checks/quality?
|
|
103
116
|
return JSONResponse(content={"status": "ok"}, status_code=200)
|
|
104
117
|
elif tasks_data[campaign_id]["info"]["type"] == "dynamic":
|
|
105
118
|
return JSONResponse(content={"status": "error", "message": "Dynamic protocol logging not implemented yet."}, status_code=400)
|
pearmut/static/assets/style.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
body {
|
|
2
2
|
margin: 0;
|
|
3
3
|
padding: 0;
|
|
4
|
-
background: linear-gradient(135deg, #b9e2a1 0%, #
|
|
4
|
+
background: linear-gradient(135deg, #b9e2a1 0%, #e7e2cf 100%);
|
|
5
5
|
background-attachment: fixed;
|
|
6
6
|
}
|
|
7
7
|
|
|
@@ -14,7 +14,7 @@ body {
|
|
|
14
14
|
width: 30%;
|
|
15
15
|
background-color: #fffc;
|
|
16
16
|
padding: 10px;
|
|
17
|
-
border-radius:
|
|
17
|
+
border-radius: 8px;
|
|
18
18
|
vertical-align: top;
|
|
19
19
|
margin-left: 5px;
|
|
20
20
|
}
|
|
@@ -32,25 +32,29 @@ body {
|
|
|
32
32
|
input[type="button"] {
|
|
33
33
|
background: #fff;
|
|
34
34
|
border: none;
|
|
35
|
-
border-radius:
|
|
35
|
+
border-radius: 8px;
|
|
36
36
|
font-size: large;
|
|
37
|
+
box-shadow: 0 2px 4px #0001;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
.button_navigation.button_selected {
|
|
40
|
-
background: #8db3ec !important;
|
|
41
|
-
}
|
|
42
40
|
|
|
43
41
|
input[type="button"]:hover:not(:disabled) {
|
|
44
|
-
background: #
|
|
42
|
+
background: #ffd;
|
|
45
43
|
cursor: pointer;
|
|
46
44
|
}
|
|
47
45
|
|
|
46
|
+
input[type="button"]:disabled {
|
|
47
|
+
background: #bbb;
|
|
48
|
+
cursor: not-allowed;
|
|
49
|
+
}
|
|
50
|
+
|
|
48
51
|
label {
|
|
49
52
|
user-select: none;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
.white-box {
|
|
53
|
-
border-radius:
|
|
56
|
+
border-radius: 8px;
|
|
54
57
|
background: #fff;
|
|
55
58
|
padding: 15pt;
|
|
59
|
+
box-shadow: 0 4px 6px #0000001a
|
|
56
60
|
}
|