Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -18,6 +18,7 @@ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "0421")
|
|
| 18 |
client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
|
| 19 |
|
| 20 |
# --- 外部CSVとして管理する passage データ ---
|
|
|
|
| 21 |
passages_df = pd.read_csv("passage.csv")
|
| 22 |
|
| 23 |
levels = [1, 2, 3, 4, 5]
|
|
@@ -64,6 +65,8 @@ def _new_session_state():
|
|
| 64 |
"question_count": 0, # ✅ 採点済み問題数
|
| 65 |
"user_id": None,
|
| 66 |
"action_log": [], # ✅ 1問中の選択変更履歴(submit時にまとめて保存)
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
# --- ログ追記(CSVのみ・固定カラムで統一) ---
|
|
@@ -95,52 +98,61 @@ def log_event(state: dict, **kwargs):
|
|
| 95 |
entry = {"time": now, "user_id": state.get("user_id"), **kwargs}
|
| 96 |
log_to_csv(entry)
|
| 97 |
|
| 98 |
-
#
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
{
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
""
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
# --- 自動難易度調整 ---
|
| 146 |
def adaptive_test(prev_level, prev_correct):
|
|
@@ -151,15 +163,29 @@ def adaptive_test(prev_level, prev_correct):
|
|
| 151 |
return levels[idx - 1]
|
| 152 |
return prev_level
|
| 153 |
|
| 154 |
-
# --- passage取得 ---
|
| 155 |
def get_passage(level, used_passages):
|
| 156 |
subset = passages_df[passages_df["level"] == level]
|
| 157 |
available = [pid for pid in subset["passage_id"] if pid not in used_passages]
|
| 158 |
if not available:
|
| 159 |
available = list(subset["passage_id"])
|
|
|
|
| 160 |
passage_id = random.choice(available)
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
# --- 開始ボタン動作(✅ start時だけログ) ---
|
| 165 |
def start_test(student_id, state):
|
|
@@ -176,10 +202,12 @@ def start_test(student_id, state):
|
|
| 176 |
state["user_id"] = student_id.strip()
|
| 177 |
|
| 178 |
level = 3
|
| 179 |
-
passage_id, text = get_passage(level, state["used_passages"])
|
| 180 |
state["used_passages"].add(passage_id)
|
| 181 |
|
| 182 |
-
|
|
|
|
|
|
|
| 183 |
displayed_time = datetime.utcnow() + timedelta(hours=9)
|
| 184 |
|
| 185 |
# ✅ startログ:actions列に "start test pushed" を入れる
|
|
@@ -196,7 +224,7 @@ def start_test(student_id, state):
|
|
| 196 |
|
| 197 |
return (
|
| 198 |
state,
|
| 199 |
-
text,
|
| 200 |
"", True, displayed_time.isoformat(), 1, state["user_id"]
|
| 201 |
)
|
| 202 |
|
|
@@ -226,7 +254,10 @@ def next_step(prev_level, user_answer, question_text, passage_text,
|
|
| 226 |
submit_time = datetime.utcnow() + timedelta(hours=9)
|
| 227 |
state["question_count"] += 1
|
| 228 |
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
| 230 |
new_level = adaptive_test(prev_level, correct)
|
| 231 |
|
| 232 |
# ✅ submitログ(ここだけ記録)
|
|
@@ -252,10 +283,12 @@ def next_step(prev_level, user_answer, question_text, passage_text,
|
|
| 252 |
)
|
| 253 |
|
| 254 |
# --- 次の問題へ ---
|
| 255 |
-
next_passage_id, next_text = get_passage(new_level, state["used_passages"])
|
| 256 |
state["used_passages"].add(next_passage_id)
|
| 257 |
|
| 258 |
-
|
|
|
|
|
|
|
| 259 |
next_display_time = datetime.utcnow() + timedelta(hours=9)
|
| 260 |
|
| 261 |
# ✅ 次の問題のために選択変更履歴をリセット
|
|
@@ -266,7 +299,7 @@ def next_step(prev_level, user_answer, question_text, passage_text,
|
|
| 266 |
return (
|
| 267 |
state,
|
| 268 |
feedback + "\n➡️ Loading next question…",
|
| 269 |
-
next_text,
|
| 270 |
None, "", True, next_display_time.isoformat(), state["question_count"] + 1, user_id, next_passage_id
|
| 271 |
)
|
| 272 |
|
|
|
|
| 18 |
client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
|
| 19 |
|
| 20 |
# --- 外部CSVとして管理する passage データ ---
|
| 21 |
+
# passage.csv: passage_id,level,text,question,choices,answer の順
|
| 22 |
passages_df = pd.read_csv("passage.csv")
|
| 23 |
|
| 24 |
levels = [1, 2, 3, 4, 5]
|
|
|
|
| 65 |
"question_count": 0, # ✅ 採点済み問題数
|
| 66 |
"user_id": None,
|
| 67 |
"action_log": [], # ✅ 1問中の選択変更履歴(submit時にまとめて保存)
|
| 68 |
+
# ✅ CSVから読んだ正解(A/B/C/D)を保持
|
| 69 |
+
"current_answer": None,
|
| 70 |
}
|
| 71 |
|
| 72 |
# --- ログ追記(CSVのみ・固定カラムで統一) ---
|
|
|
|
| 98 |
entry = {"time": now, "user_id": state.get("user_id"), **kwargs}
|
| 99 |
log_to_csv(entry)
|
| 100 |
|
| 101 |
+
# =========================
|
| 102 |
+
# ✅ choices列を柔軟にパース
|
| 103 |
+
# - JSON配列: ["...", "...", "...", "..."]
|
| 104 |
+
# - 改行区切り / | / || / ; などにも対応(できるだけ)
|
| 105 |
+
# =========================
|
| 106 |
+
def _parse_choices(raw):
|
| 107 |
+
if raw is None:
|
| 108 |
+
return []
|
| 109 |
+
s = str(raw).strip()
|
| 110 |
+
if s == "":
|
| 111 |
+
return []
|
| 112 |
+
# JSONっぽい場合
|
| 113 |
+
if (s.startswith("[") and s.endswith("]")) or (s.startswith("{") and s.endswith("}")):
|
| 114 |
+
try:
|
| 115 |
+
obj = json.loads(s)
|
| 116 |
+
if isinstance(obj, list):
|
| 117 |
+
return [str(x).strip() for x in obj]
|
| 118 |
+
except Exception:
|
| 119 |
+
pass
|
| 120 |
+
|
| 121 |
+
# それ以外は区切り推定
|
| 122 |
+
# 優先: 改行 -> || -> | -> ; -> ,
|
| 123 |
+
if "\n" in s:
|
| 124 |
+
parts = [p.strip() for p in s.split("\n") if p.strip()]
|
| 125 |
+
return parts
|
| 126 |
+
if "||" in s:
|
| 127 |
+
parts = [p.strip() for p in s.split("||") if p.strip()]
|
| 128 |
+
return parts
|
| 129 |
+
if "|" in s:
|
| 130 |
+
parts = [p.strip() for p in s.split("|") if p.strip()]
|
| 131 |
+
return parts
|
| 132 |
+
if ";" in s:
|
| 133 |
+
parts = [p.strip() for p in s.split(";") if p.strip()]
|
| 134 |
+
return parts
|
| 135 |
+
if "," in s:
|
| 136 |
+
parts = [p.strip() for p in s.split(",") if p.strip()]
|
| 137 |
+
return parts
|
| 138 |
+
|
| 139 |
+
return [s]
|
| 140 |
+
|
| 141 |
+
def _format_question_block(question, choices_list):
|
| 142 |
+
# choices を A-D に整形して question_display に入れる
|
| 143 |
+
labels = ["A", "B", "C", "D"]
|
| 144 |
+
# 4つ未満/超過でも崩れにくくする
|
| 145 |
+
choices = list(choices_list)[:4]
|
| 146 |
+
while len(choices) < 4:
|
| 147 |
+
choices.append("")
|
| 148 |
+
|
| 149 |
+
lines = []
|
| 150 |
+
q = "" if question is None else str(question).strip()
|
| 151 |
+
lines.append(f"Q: {q}")
|
| 152 |
+
for lab, opt in zip(labels, choices):
|
| 153 |
+
opt_s = "" if opt is None else str(opt).strip()
|
| 154 |
+
lines.append(f"{lab}. {opt_s}")
|
| 155 |
+
return "\n".join(lines)
|
| 156 |
|
| 157 |
# --- 自動難易度調整 ---
|
| 158 |
def adaptive_test(prev_level, prev_correct):
|
|
|
|
| 163 |
return levels[idx - 1]
|
| 164 |
return prev_level
|
| 165 |
|
| 166 |
+
# --- passage取得(✅ CSVから question/choices/answer も返す) ---
|
| 167 |
def get_passage(level, used_passages):
|
| 168 |
subset = passages_df[passages_df["level"] == level]
|
| 169 |
available = [pid for pid in subset["passage_id"] if pid not in used_passages]
|
| 170 |
if not available:
|
| 171 |
available = list(subset["passage_id"])
|
| 172 |
+
|
| 173 |
passage_id = random.choice(available)
|
| 174 |
+
row = subset[subset["passage_id"] == passage_id].iloc[0]
|
| 175 |
+
|
| 176 |
+
text = row["text"]
|
| 177 |
+
question = row["question"]
|
| 178 |
+
choices_raw = row["choices"]
|
| 179 |
+
answer = row["answer"]
|
| 180 |
+
|
| 181 |
+
choices_list = _parse_choices(choices_raw)
|
| 182 |
+
question_block = _format_question_block(question, choices_list)
|
| 183 |
+
|
| 184 |
+
# answer は A/B/C/D を想定(念のため整形)
|
| 185 |
+
ans = "" if answer is None else str(answer).strip().upper()
|
| 186 |
+
ans = ans[:1] # "A" など1文字に寄せる
|
| 187 |
+
|
| 188 |
+
return passage_id, text, question_block, ans
|
| 189 |
|
| 190 |
# --- 開始ボタン動作(✅ start時だけログ) ---
|
| 191 |
def start_test(student_id, state):
|
|
|
|
| 202 |
state["user_id"] = student_id.strip()
|
| 203 |
|
| 204 |
level = 3
|
| 205 |
+
passage_id, text, question_block, ans = get_passage(level, state["used_passages"])
|
| 206 |
state["used_passages"].add(passage_id)
|
| 207 |
|
| 208 |
+
# ✅ この問題の正解を state に保持
|
| 209 |
+
state["current_answer"] = ans
|
| 210 |
+
|
| 211 |
displayed_time = datetime.utcnow() + timedelta(hours=9)
|
| 212 |
|
| 213 |
# ✅ startログ:actions列に "start test pushed" を入れる
|
|
|
|
| 224 |
|
| 225 |
return (
|
| 226 |
state,
|
| 227 |
+
text, question_block, level, passage_id, None,
|
| 228 |
"", True, displayed_time.isoformat(), 1, state["user_id"]
|
| 229 |
)
|
| 230 |
|
|
|
|
| 254 |
submit_time = datetime.utcnow() + timedelta(hours=9)
|
| 255 |
state["question_count"] += 1
|
| 256 |
|
| 257 |
+
# ✅ CSVから読んだ正解(A/B/C/D)と比較(AI採点はしない)
|
| 258 |
+
correct_answer = (state.get("current_answer") or "").strip().upper()
|
| 259 |
+
correct = (str(user_answer).strip().upper() == correct_answer)
|
| 260 |
+
|
| 261 |
new_level = adaptive_test(prev_level, correct)
|
| 262 |
|
| 263 |
# ✅ submitログ(ここだけ記録)
|
|
|
|
| 283 |
)
|
| 284 |
|
| 285 |
# --- 次の問題へ ---
|
| 286 |
+
next_passage_id, next_text, next_question_block, next_ans = get_passage(new_level, state["used_passages"])
|
| 287 |
state["used_passages"].add(next_passage_id)
|
| 288 |
|
| 289 |
+
# ✅ 次の問題の正解を state に保持
|
| 290 |
+
state["current_answer"] = next_ans
|
| 291 |
+
|
| 292 |
next_display_time = datetime.utcnow() + timedelta(hours=9)
|
| 293 |
|
| 294 |
# ✅ 次の問題のために選択変更履歴をリセット
|
|
|
|
| 299 |
return (
|
| 300 |
state,
|
| 301 |
feedback + "\n➡️ Loading next question…",
|
| 302 |
+
next_text, next_question_block, new_level,
|
| 303 |
None, "", True, next_display_time.isoformat(), state["question_count"] + 1, user_id, next_passage_id
|
| 304 |
)
|
| 305 |
|