feat(rectification): add adaptive question selector
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts.active_rectification_questions import build_questionnaire
|
||||
from scripts.active_rectification_selector import select_next_questions
|
||||
|
||||
|
||||
def test_selector_asks_one_question_and_ranks_by_separation() -> None:
|
||||
questionnaire = build_questionnaire("2001-02-03 10:20", uncertainty_minutes=30)
|
||||
|
||||
selection = questionnaire["selection"]
|
||||
|
||||
assert len(selection["selected_questions"]) == 1
|
||||
assert selection["selected_questions"][0]["id"] == questionnaire["questions"][0]["id"]
|
||||
assert selection["selected_questions"][0]["why_asked"]
|
||||
assert selection["selected_questions"][0]["candidate_ids_distinguished"]
|
||||
assert selection["selected_questions"][0]["technique_routes"]
|
||||
assert selection["ranking"]
|
||||
|
||||
|
||||
def test_selector_skips_non_discriminating_questions() -> None:
|
||||
questionnaire = {
|
||||
"question_bank": [
|
||||
{
|
||||
"id": "neutral_only",
|
||||
"domain": "fine_timing",
|
||||
"prompt": "几乎同时吗?",
|
||||
"sensitivity": ["KP_cusp"],
|
||||
"scoring_map": {
|
||||
"A": {"cluster": "neutral", "points": 0},
|
||||
"B": {"cluster": "neutral", "points": 0},
|
||||
"C": {"cluster": "neutral", "points": 0},
|
||||
"D": {"cluster": "neutral", "points": 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "usable_question",
|
||||
"domain": "career",
|
||||
"prompt": "工作是否变动?",
|
||||
"sensitivity": ["D10"],
|
||||
"scoring_map": {
|
||||
"A": {"cluster": "career_up", "points": 2},
|
||||
"B": {"cluster": "career_up", "points": 1},
|
||||
"C": {"cluster": "career_down", "points": -2},
|
||||
"D": {"cluster": "neutral", "points": 0},
|
||||
},
|
||||
},
|
||||
],
|
||||
"candidate_scan": {"candidate_count": 61},
|
||||
}
|
||||
|
||||
selection = select_next_questions(questionnaire, {}, limit=1)
|
||||
|
||||
assert selection["selected_questions"][0]["id"] == "usable_question"
|
||||
assert any(item["question_id"] == "neutral_only" and item["skipped"] for item in selection["ranking"])
|
||||
|
||||
|
||||
def test_selector_changes_domain_after_uncertainty() -> None:
|
||||
questionnaire = build_questionnaire("2001-02-03 10:20", uncertainty_minutes=30)
|
||||
first = questionnaire["questions"][0]["id"]
|
||||
|
||||
selection = select_next_questions(
|
||||
{
|
||||
"question_bank": questionnaire["questions"],
|
||||
"candidate_scan": questionnaire["candidate_scan"],
|
||||
},
|
||||
{first: "D"},
|
||||
limit=1,
|
||||
)
|
||||
|
||||
assert selection["selected_questions"]
|
||||
assert selection["selected_questions"][0]["id"] != first
|
||||
assert selection["selected_questions"][0]["domain"] != questionnaire["questions"][0]["domain"]
|
||||
|
||||
|
||||
def test_selector_stops_when_no_answer_can_improve_separation() -> None:
|
||||
questionnaire = {
|
||||
"question_bank": [
|
||||
{
|
||||
"id": "fine_only",
|
||||
"domain": "fine_timing",
|
||||
"prompt": "先内后外吗?",
|
||||
"sensitivity": ["KP_cusp"],
|
||||
"scoring_map": {
|
||||
"A": {"cluster": "neutral", "points": 0},
|
||||
"B": {"cluster": "neutral", "points": 0},
|
||||
"C": {"cluster": "neutral", "points": 0},
|
||||
"D": {"cluster": "neutral", "points": 0},
|
||||
},
|
||||
}
|
||||
],
|
||||
"candidate_scan": {"candidate_count": 61},
|
||||
}
|
||||
|
||||
selection = select_next_questions(questionnaire, {}, limit=1)
|
||||
|
||||
assert selection["selected_questions"] == []
|
||||
assert selection["stop"] is True
|
||||
assert selection["stop_reason"] == "no_answer_can_improve_separation"
|
||||
|
||||
|
||||
def _minute_question(question_id: str, domain: str, layer: str) -> dict[str, object]:
|
||||
return {
|
||||
"id": question_id,
|
||||
"domain": domain,
|
||||
"prompt": "这个虚构事件是否发生?",
|
||||
"sensitivity": [layer],
|
||||
"positive_cluster": f"{question_id}_yes",
|
||||
"negative_cluster": f"{question_id}_no",
|
||||
"factual_reliability": 0.9,
|
||||
"domain_priority": 1,
|
||||
"scoring_map": {
|
||||
"A": {"cluster": f"{question_id}_yes", "points": 2},
|
||||
"B": {"cluster": f"{question_id}_yes", "points": 1},
|
||||
"C": {"cluster": f"{question_id}_no", "points": -2},
|
||||
"D": {"cluster": "neutral", "points": 0},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_selector_changes_with_remaining_candidate_window() -> None:
|
||||
questions = [
|
||||
_minute_question("relationship_split", "relationship", "D9"),
|
||||
_minute_question("career_split", "career", "D10"),
|
||||
]
|
||||
relationship_window = {
|
||||
"candidate_count": 3,
|
||||
"transitions": [{"between": ["10:19", "10:20"]}],
|
||||
"rows": [
|
||||
{"divisional_ascendants": {"D9": {"sign": "Aries", "degree": 1}, "D10": {"sign": "Leo"}}},
|
||||
{"divisional_ascendants": {"D9": {"sign": "Taurus", "degree": 2}, "D10": {"sign": "Leo"}}},
|
||||
{"divisional_ascendants": {"D9": {"sign": "Gemini", "degree": 3}, "D10": {"sign": "Leo"}}},
|
||||
],
|
||||
}
|
||||
career_window = {
|
||||
"candidate_count": 3,
|
||||
"transitions": [{"between": ["10:20", "10:21"]}],
|
||||
"rows": [
|
||||
{"divisional_ascendants": {"D9": {"sign": "Aries"}, "D10": {"sign": "Leo", "degree": 1}}},
|
||||
{"divisional_ascendants": {"D9": {"sign": "Aries"}, "D10": {"sign": "Virgo", "degree": 2}}},
|
||||
{"divisional_ascendants": {"D9": {"sign": "Aries"}, "D10": {"sign": "Libra", "degree": 3}}},
|
||||
],
|
||||
}
|
||||
|
||||
first = select_next_questions(
|
||||
{"question_bank": questions, "candidate_scan": {"candidate_count": 3, "minute_scan": relationship_window}},
|
||||
{},
|
||||
)
|
||||
second = select_next_questions(
|
||||
{"question_bank": questions, "candidate_scan": {"candidate_count": 3, "minute_scan": career_window}},
|
||||
{},
|
||||
)
|
||||
|
||||
assert first["selected_questions"][0]["id"] == "relationship_split"
|
||||
assert second["selected_questions"][0]["id"] == "career_split"
|
||||
assert first["selected_questions"][0]["minute_relevance"] > 0
|
||||
assert second["selected_questions"][0]["minute_relevance"] > 0
|
||||
Reference in New Issue
Block a user