Toya0421 commited on
Commit
025a5e0
·
verified ·
1 Parent(s): 6e7eeb8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -56
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
- # --- AIで問題生成 ---
99
- def generate_question(text):
100
- prompt = f"""
101
- Read the following passage and create ONE multiple-choice question with 4 options (A–D).
102
- Only output the question and options.
103
- Format:
104
- Q: <question text>
105
- A. <option>
106
- B. <option>
107
- C. <option>
108
- D. <option>
109
- Passage:
110
- {text}
111
- """
112
- response = client.chat.completions.create(
113
- model="google/gemma-3-27b-it:free",
114
- messages=[{"role": "user", "content": prompt}],
115
- max_tokens=250,
116
- temperature=0.7,
117
- )
118
- return response.choices[0].message.content.strip()
119
-
120
- # --- 正誤判定 ---
121
- def check_answer_with_ai(text, question, user_answer):
122
- prompt = f"""
123
- Read the passage and question below. Decide if the user's answer is correct.
124
- Passage:
125
- {text}
126
- Question:
127
- {question}
128
- User Answer: {user_answer}
129
- Respond with one word: "Correct" or "Incorrect".
130
- """
131
- response = client.chat.completions.create(
132
- model="google/gemma-3-27b-it:free",
133
- messages=[{"role": "user", "content": prompt}],
134
- max_tokens=10,
135
- temperature=0,
136
- )
137
- result = response.choices[0].message.content.strip().lower()
138
-
139
- if re.search(r"\bincorrect\b", result):
140
- return False
141
- if re.search(r"\bcorrect\b", result):
142
- return True
143
- return False
 
 
 
 
 
 
 
 
 
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
- text = subset[subset["passage_id"] == passage_id]["text"].iloc[0]
162
- return passage_id, text
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- question = generate_question(text)
 
 
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, question, level, passage_id, None,
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
- correct = check_answer_with_ai(passage_text, question_text, user_answer)
 
 
 
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
- next_question = generate_question(next_text)
 
 
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, next_question, new_level,
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