helmo Codex commited on
Commit
a7e2b39
·
1 Parent(s): bfcae03

[interview] Restore persecution categories + fix CheckboxGroup reveal lag

Browse files

- Re-added the persecution-grounds question (Political / Ethnic / Religious /
Gender-based / Sexual orientation / Climate displacement / Other) as a
multi-select, in addition to the free-text "what happened" narrative; options
translated into all 10 languages. Shown in the assessment facts + review.
- Fixed "options only appear after pressing Continue": each transition now
renders in two passes (question with controls hidden, then the control), which
makes the CheckboxGroup appear immediately.

64 fast tests pass.

Co-authored-by: Codex <noreply@openai.com>

app/interview_script.py CHANGED
@@ -34,6 +34,9 @@ QUESTIONS: list[Question] = [
34
  Question("q_current", "current_country", State.SITUATION, "country"),
35
  Question("q_origin", "origin_country", State.SITUATION, "country"),
36
  Question("q_reason", "free_text_history", State.SITUATION, "text", alt_sid="q_reason_instill"),
 
 
 
37
  Question("q_danger", "immediate_danger", State.SITUATION, "yesno", kind="bool"),
38
  Question("q_duration", "displacement_duration", State.HISTORY, "text", skip_if_in_origin=True),
39
  Question("q_documents", "documents_available", State.HISTORY, "choice", kind="list",
@@ -62,6 +65,10 @@ TR: dict[str, dict[str, str]] = {
62
  "opt_yes": "Yes", "opt_no": "No",
63
  "opt_passport": "Passport", "opt_id": "National ID", "opt_birth": "Birth certificate",
64
  "opt_none": "None", "opt_other": "Other",
 
 
 
 
65
  "ui_answer": "Your answer", "ui_continue": "Continue",
66
  },
67
  "French": {
@@ -285,6 +292,39 @@ _EXTRA = {
285
  for _lang, _d in _EXTRA.items():
286
  TR.setdefault(_lang, {}).update(_d)
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  def t(language: str | None, sid: str) -> str:
290
  """Translation lookup with English fallback."""
 
34
  Question("q_current", "current_country", State.SITUATION, "country"),
35
  Question("q_origin", "origin_country", State.SITUATION, "country"),
36
  Question("q_reason", "free_text_history", State.SITUATION, "text", alt_sid="q_reason_instill"),
37
+ Question("q_grounds", "persecution_types", State.SITUATION, "choice", kind="list",
38
+ options=["opt_political", "opt_ethnic", "opt_religious", "opt_gender",
39
+ "opt_orientation", "opt_climate", "opt_other"]),
40
  Question("q_danger", "immediate_danger", State.SITUATION, "yesno", kind="bool"),
41
  Question("q_duration", "displacement_duration", State.HISTORY, "text", skip_if_in_origin=True),
42
  Question("q_documents", "documents_available", State.HISTORY, "choice", kind="list",
 
65
  "opt_yes": "Yes", "opt_no": "No",
66
  "opt_passport": "Passport", "opt_id": "National ID", "opt_birth": "Birth certificate",
67
  "opt_none": "None", "opt_other": "Other",
68
+ "q_grounds": "Which of these best describe your situation? (Choose any that apply.)",
69
+ "opt_political": "Political", "opt_ethnic": "Ethnic", "opt_religious": "Religious",
70
+ "opt_gender": "Gender-based", "opt_orientation": "Sexual orientation",
71
+ "opt_climate": "Climate displacement",
72
  "ui_answer": "Your answer", "ui_continue": "Continue",
73
  },
74
  "French": {
 
292
  for _lang, _d in _EXTRA.items():
293
  TR.setdefault(_lang, {}).update(_d)
294
 
295
+ # Persecution-grounds question + category labels (multi-select).
296
+ _GROUNDS = {
297
+ "French": {"q_grounds": "Parmi ces situations, laquelle correspond à la vôtre ? (Choisissez celles qui s'appliquent.)",
298
+ "opt_political": "Politique", "opt_ethnic": "Ethnique", "opt_religious": "Religieux",
299
+ "opt_gender": "Lié au genre", "opt_orientation": "Orientation sexuelle", "opt_climate": "Déplacement climatique"},
300
+ "Spanish": {"q_grounds": "¿Cuál de estas situaciones describe la suya? (Elige las que correspondan.)",
301
+ "opt_political": "Político", "opt_ethnic": "Étnico", "opt_religious": "Religioso",
302
+ "opt_gender": "De género", "opt_orientation": "Orientación sexual", "opt_climate": "Desplazamiento climático"},
303
+ "Portuguese": {"q_grounds": "Qual destas situações descreve a sua? (Escolha as que se aplicam.)",
304
+ "opt_political": "Político", "opt_ethnic": "Étnico", "opt_religious": "Religioso",
305
+ "opt_gender": "De gênero", "opt_orientation": "Orientação sexual", "opt_climate": "Deslocamento climático"},
306
+ "Arabic": {"q_grounds": "أيٌّ من هذه الحالات تنطبق عليك؟ (اختر كل ما ينطبق.)",
307
+ "opt_political": "سياسي", "opt_ethnic": "عِرقي", "opt_religious": "ديني",
308
+ "opt_gender": "قائم على النوع الاجتماعي", "opt_orientation": "الميل الجنسي", "opt_climate": "النزوح المناخي"},
309
+ "Hindi": {"q_grounds": "इनमें से कौन-सी स्थिति आप पर लागू होती है? (जो भी लागू हों चुनें।)",
310
+ "opt_political": "राजनीतिक", "opt_ethnic": "जातीय", "opt_religious": "धार्मिक",
311
+ "opt_gender": "लिंग आधारित", "opt_orientation": "यौन रुझान", "opt_climate": "जलवायु विस्थापन"},
312
+ "Chinese": {"q_grounds": "以下哪些情况符合您的处境?(可多选)",
313
+ "opt_political": "政治", "opt_ethnic": "族裔", "opt_religious": "宗教",
314
+ "opt_gender": "基于性别", "opt_orientation": "性取向", "opt_climate": "气候流离失所"},
315
+ "Japanese": {"q_grounds": "次のうち、あなたの状況に当てはまるものはどれですか?(複数選択可)",
316
+ "opt_political": "政治的", "opt_ethnic": "民族的", "opt_religious": "宗教的",
317
+ "opt_gender": "ジェンダーに基づく", "opt_orientation": "性的指向", "opt_climate": "気候による避難"},
318
+ "Korean": {"q_grounds": "다음 중 귀하의 상황에 해당하는 것은 무엇인가요? (해당되는 것을 모두 선택하세요.)",
319
+ "opt_political": "정치적", "opt_ethnic": "민족적", "opt_religious": "종교적",
320
+ "opt_gender": "성별 기반", "opt_orientation": "성적 지향", "opt_climate": "기후 이재민"},
321
+ "Russian": {"q_grounds": "Что из перечисленного описывает вашу ситуацию? (Выберите все подходящее.)",
322
+ "opt_political": "Политическое", "opt_ethnic": "Этническое", "opt_religious": "Религиозное",
323
+ "opt_gender": "По признаку пола", "opt_orientation": "Сексуальная ориентация", "opt_climate": "Климатическое перемещение"},
324
+ }
325
+ for _lang, _d in _GROUNDS.items():
326
+ TR.setdefault(_lang, {}).update(_d)
327
+
328
 
329
  def t(language: str | None, sid: str) -> str:
330
  """Translation lookup with English fallback."""
app/phases/assessment.py CHANGED
@@ -33,6 +33,7 @@ FACT_FIELDS = [
33
  ("Country of origin", "origin_country", "text"),
34
  ("Current location", "current_country", "text"),
35
  ("What happened", "free_text_history", "text"),
 
36
  ("Immediate danger", "immediate_danger", "bool"),
37
  ("Time displaced", "displacement_duration", "text"),
38
  ("Documents", "documents_available", "list"),
 
33
  ("Country of origin", "origin_country", "text"),
34
  ("Current location", "current_country", "text"),
35
  ("What happened", "free_text_history", "text"),
36
+ ("Reason", "persecution_types", "chips"),
37
  ("Immediate danger", "immediate_danger", "bool"),
38
  ("Time displaced", "displacement_duration", "text"),
39
  ("Documents", "documents_available", "list"),
app/phases/interview.py CHANGED
@@ -242,6 +242,7 @@ def _labeled_facts(session: SessionState) -> str:
242
  ("Country of origin", iv.origin_country),
243
  ("Current country", iv.current_country),
244
  ("What happened", iv.free_text_history),
 
245
  ("Immediate danger", None if iv.immediate_danger is None else ("yes" if iv.immediate_danger else "no")),
246
  ("Time since leaving", iv.displacement_duration),
247
  ("Documents", ", ".join(iv.documents_available) if iv.documents_available else None),
@@ -383,19 +384,34 @@ def build(visible: bool = True, session_st=None, loop_st=None, slot_idx_st=None)
383
  return (render_chat(session.messages), render_rail(session.state),
384
  *control_updates(session, idx), session, idx)
385
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  async def start(session, loop, slot_idx=0):
387
  if session is None:
388
  session = SessionState(); session.transition_to(State.INTAKE)
389
  loop = loop or create_loop()
390
- out = await _present(session, loop, 0)
391
- yield (out[0], out[1], out[2], out[3], out[4], out[5], out[6], loop, out[7])
 
392
 
393
  async def on_continue(radio_v, multi_v, country_v, text_v, session, loop, idx):
394
  if session is None:
395
  session = SessionState(); session.transition_to(State.INTAKE)
396
  loop = loop or create_loop()
397
  o = await _present(session, loop, 0)
398
- return (o[0], o[1], o[2], o[3], o[4], o[5], o[6], loop, o[7])
 
 
399
  loop = loop or create_loop()
400
  lang = session.language
401
 
@@ -403,32 +419,40 @@ def build(visible: bool = True, session_st=None, loop_st=None, slot_idx_st=None)
403
  if idx == CORRECT_INDEX:
404
  correction = (text_v or "").strip()
405
  if not correction:
406
- return (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
 
407
  session.messages = list(session.messages) + [{"role": "user", "content": correction}]
408
  await _apply_correction(session, loop, correction)
409
- o = await _present(session, loop, REVIEW_INDEX) # show the corrected summary
410
- return (o[0], o[1], o[2], o[3], o[4], o[5], o[6], loop, o[7])
 
 
411
 
412
  # Review step
413
  if idx >= REVIEW_INDEX:
414
  if not radio_v:
415
- return (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
 
416
  session.messages = list(session.messages) + [{"role": "user", "content": radio_v}]
417
  if radio_v == t(lang, "review_yes"):
418
  advance_to(session, State.ASSESSMENT) # triggers assessment via .then()
419
- return (render_chat(session.messages), render_rail(session.state),
420
- gr.update(visible=False), gr.update(visible=False),
421
- gr.update(visible=False), gr.update(visible=False), session, loop, idx)
422
- o = await _present(session, loop, CORRECT_INDEX) # ask what to change (free text)
423
- return (o[0], o[1], o[2], o[3], o[4], o[5], o[6], loop, o[7])
 
 
424
 
425
  q = QUESTIONS[idx]
426
  display = _store(session, q, radio_v, multi_v, country_v, text_v)
427
  if display is None:
428
- return (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
 
429
  session.messages = list(session.messages) + [{"role": "user", "content": display}]
430
  o = await _present(session, loop, _next_index(session, idx))
431
- return (o[0], o[1], o[2], o[3], o[4], o[5], o[6], loop, o[7])
 
432
 
433
  continue_event = cont.click(
434
  on_continue,
 
242
  ("Country of origin", iv.origin_country),
243
  ("Current country", iv.current_country),
244
  ("What happened", iv.free_text_history),
245
+ ("Reason", ", ".join(iv.persecution_types) if iv.persecution_types else None),
246
  ("Immediate danger", None if iv.immediate_danger is None else ("yes" if iv.immediate_danger else "no")),
247
  ("Time since leaving", iv.displacement_duration),
248
  ("Documents", ", ".join(iv.documents_available) if iv.documents_available else None),
 
384
  return (render_chat(session.messages), render_rail(session.state),
385
  *control_updates(session, idx), session, idx)
386
 
387
+ _HIDE4 = (gr.update(visible=False), gr.update(visible=False),
388
+ gr.update(visible=False), gr.update(visible=False))
389
+
390
+ def _frames(o, loop):
391
+ """Two render passes: show the new question with controls hidden, then
392
+ reveal the control. Splitting the transition makes Gradio render the
393
+ choice controls immediately instead of one Continue-press late."""
394
+ return [
395
+ (o[0], o[1], *_HIDE4, o[6], loop, o[7]),
396
+ (o[0], o[1], o[2], o[3], o[4], o[5], o[6], loop, o[7]),
397
+ ]
398
+
399
  async def start(session, loop, slot_idx=0):
400
  if session is None:
401
  session = SessionState(); session.transition_to(State.INTAKE)
402
  loop = loop or create_loop()
403
+ o = await _present(session, loop, 0)
404
+ for frame in _frames(o, loop):
405
+ yield frame
406
 
407
  async def on_continue(radio_v, multi_v, country_v, text_v, session, loop, idx):
408
  if session is None:
409
  session = SessionState(); session.transition_to(State.INTAKE)
410
  loop = loop or create_loop()
411
  o = await _present(session, loop, 0)
412
+ for frame in _frames(o, loop):
413
+ yield frame
414
+ return
415
  loop = loop or create_loop()
416
  lang = session.language
417
 
 
419
  if idx == CORRECT_INDEX:
420
  correction = (text_v or "").strip()
421
  if not correction:
422
+ yield (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
423
+ return
424
  session.messages = list(session.messages) + [{"role": "user", "content": correction}]
425
  await _apply_correction(session, loop, correction)
426
+ o = await _present(session, loop, REVIEW_INDEX)
427
+ for frame in _frames(o, loop):
428
+ yield frame
429
+ return
430
 
431
  # Review step
432
  if idx >= REVIEW_INDEX:
433
  if not radio_v:
434
+ yield (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
435
+ return
436
  session.messages = list(session.messages) + [{"role": "user", "content": radio_v}]
437
  if radio_v == t(lang, "review_yes"):
438
  advance_to(session, State.ASSESSMENT) # triggers assessment via .then()
439
+ yield (render_chat(session.messages), render_rail(session.state),
440
+ *_HIDE4, session, loop, idx)
441
+ return
442
+ o = await _present(session, loop, CORRECT_INDEX) # ask what to change
443
+ for frame in _frames(o, loop):
444
+ yield frame
445
+ return
446
 
447
  q = QUESTIONS[idx]
448
  display = _store(session, q, radio_v, multi_v, country_v, text_v)
449
  if display is None:
450
+ yield (gr.update(), gr.update(), *control_updates(session, idx), session, loop, idx)
451
+ return
452
  session.messages = list(session.messages) + [{"role": "user", "content": display}]
453
  o = await _present(session, loop, _next_index(session, idx))
454
+ for frame in _frames(o, loop):
455
+ yield frame
456
 
457
  continue_event = cont.click(
458
  on_continue,