"""Jev question definitions for the rectification turn-intent classifier. Offline research only. Does not change production `turn-intent-classifier.ts`. Question JSON matches TypeSafe `primitives/advanced` (Choice / Noul with structured instructions). Combination legality is enforced in code. """ from __future__ import annotations from typing import Any, Mapping, Sequence INTENT_VALUES = ( "answer_current_focus", "provide_new_evidence", "stop_rectification", "ask_about_result", "unclear", ) ANSWER_CLASS_VALUES = ("yes", "weak_yes", "no", "unsure") COLLECT_ANSWER_CLASSES = ("yes", "weak_yes", "no", "unsure") JEV_MODEL = "jev-1.13.0" NOUL_TRUE_THRESHOLD = 0.5 # Literal criteria. Production hedges ("通常", "不要猜", "不要按关键词") are dropped. INTENT_INSTRUCTIONS = { "task": "Classify what the user_message is doing in this rectification turn.", "inputs": ["current_question", "options", "user_message", "case_status"], "rule": "Pick exactly one option. Use only the literal conditions in criteria.", } INTENT_CRITERIA: dict[str, str] = { "answer_current_focus": ( "user_message is answering current_question. " "If options is non-empty, the message matches the meaning of one option's answer_class. " "If options is empty (collect question), the message says the topic happened, did not happen, " "or is not remembered, or answers the collect question with a dated experience." ), "provide_new_evidence": ( "user_message only adds a new experience that includes an approximate year or month, " "and does not answer current_question. " "If current_question is empty, adding a dated experience also maps here." ), "stop_rectification": ( "user_message explicitly asks to stop the entire birth-time rectification. " "Saying the current topic did not happen is not this option." ), "ask_about_result": ( "user_message asks for the rectification result, the current time range, " "candidate minutes, or whether a result is ready. It is not answering current_question." ), "unclear": ( "None of the other four conditions hold, or the message mixes them so no single option applies." ), } ANSWER_CLASS_INSTRUCTIONS = { "task": "If user_message is answering current_question, pick the matching answer_class.", "rule": "Use only the option list in criteria. If the message is not answering current_question, pick none of the content classes; the caller will null this field.", } HAS_NEW_DATED_EVENT_INSTRUCTIONS = ( "这句话里是否出现了一件新的、带大概年或月的经历,且它不是对当前问题的直接回答。" ) HAS_NEW_DATED_EVENT_CRITERIA = { "true": ( "除了回答当前问题之外,还另说了一件带大概年或月的经历。" ), "false": ( "没有另说新的带年月经历。" "单纯否定、单纯记不清、或只用带年月经历来直接回答当前采集题,都不是。" ), } # One row per production prompt sentence that was rewritten or dropped. CRITERION_MAP: tuple[dict[str, str], ...] = ( { "production": "结合当前问题和动态选项判断用户是在回答当前问题、提供新的带时间经历、要求停止整个校正、询问结果,还是语义不清。", "jev": "intent Choice with five literal criteria, including unclear as the residual option.", "lost": "", }, { "production": "若是在回答当前问题,answer_class 必须使用某个选项提供的 answer_class;否则 answer_class 必须为 null。", "jev": "answer_class Choice is built only from the live options; code nulls it when intent != answer_current_focus.", "lost": "", }, { "production": "has_new_dated_event 仅在用户同一句里除了回答当前问题之外,还提供了新的、带大概时间的经历时为 true。", "jev": "has_new_dated_event Noul true/false criteria, same split.", "lost": "", }, { "production": "单纯的否定或单纯的选项回答必须为 false。", "jev": "Noul false: 单纯否定、单纯记不清不算新经历。", "lost": "", }, { "production": "若同一句话既回答了当前问题又补充了新的带时间经历,intent 仍为 answer_current_focus,has_new_dated_event 为 true。", "jev": "intent and Noul are separate questions; code keeps answer_current_focus when both fire.", "lost": "Jev does not itself enforce this pair; code does.", }, { "production": "“当前方面没有、那段时间没有变化”通常是回答当前问题,不是停止整个流程。", "jev": "stop_rectification criterion: only an explicit stop of the whole flow. 当前方面没有 is answer_current_focus.", "lost": "The hedge 通常 is dropped; the stop option is written as an exclusive literal.", }, { "production": "只有用户明确要求停止整个校正时才分类为 stop_rectification。", "jev": "stop_rectification criterion, same literal.", "lost": "", }, { "production": "不要按 A/B/C/D 的位置猜语义,只按选项 label 与 answer_class 判断。", "jev": "answer_class criteria keyed by answer_class, not by A/B/C/D.", "lost": "The prohibition itself is not sent; Jev is not given A/B/C/D as the choice keys.", }, { "production": "「没有、没发生过、这方面没什么」→ intent 为 answer_current_focus,answer_class 为 no。", "jev": "Collect answer_class criterion for no.", "lost": "", }, { "production": "「记不清、不记得、忘了、想不起来、以后再说」→ answer_class 为 unsure。", "jev": "Collect answer_class criterion for unsure.", "lost": "", }, { "production": "用户用带大概年月的经历直接回答当前采集题 → intent 为 answer_current_focus,answer_class 为 yes(程度较弱时为 weak_yes),has_new_dated_event 为 false。", "jev": "Collect yes/weak_yes criteria + Noul false for a dated answer that is the collect answer.", "lost": "", }, { "production": "不要把「没有」或「记不清」标成 yes。", "jev": "Separate no and unsure criteria; yes requires a dated or affirmative answer.", "lost": "The 'do not' wording is dropped.", }, { "production": "若既没有否定、也没有说记不清、也没有给出带年月经历,intent 为 unclear,answer_class 必须为 null。", "jev": "unclear residual on intent Choice; code nulls answer_class.", "lost": "", }, { "production": "若用户只在补充带时间的经历、并没有回答当前采集题,intent 为 provide_new_evidence。", "jev": "provide_new_evidence criterion.", "lost": "", }, { "production": "若同一句话既明确否定当前采集题又补充了新的带时间经历,intent 仍为 answer_current_focus 且 answer_class 为 no,不要改成 provide_new_evidence。", "jev": "Code keeps no + Noul true. Jev intent/Noul are independent.", "lost": "Jev may split this pair; code does not re-vote intent from the Noul.", }, { "production": "不要按关键词表或正则猜测,只根据当前问题与用户这句话的语义分类。", "jev": "Not sent. Jev has no keyword table in the question.", "lost": "The meta-instruction is dropped (Jev answers the written question, not the intended one).", }, ) LOST_SEMANTICS: tuple[str, ...] = tuple( row["lost"] for row in CRITERION_MAP if row["lost"] ) def _option_rows(options: Sequence[Mapping[str, Any]] | None) -> list[dict[str, str]]: rows: list[dict[str, str]] = [] seen: set[str] = set() for item in options or (): answer_class = str(item.get("answer_class") or "") label = str(item.get("label") or "").strip() if answer_class not in ANSWER_CLASS_VALUES or answer_class in seen: continue seen.add(answer_class) rows.append({"answer_class": answer_class, "label": label}) return rows def answer_class_criteria( *, layer: str, options: Sequence[Mapping[str, Any]] | None, ) -> dict[str, str]: """Choice criteria keyed by answer_class. Collect uses the four fixed classes.""" if layer == "collect" and not _option_rows(options): return { "yes": "The message answers the collect question with a dated experience that did happen, stated firmly.", "weak_yes": "The message answers the collect question with a dated or tentative experience, weakly.", "no": "The message says this topic did not happen / there was no change. Close this aspect.", "unsure": "The message says the user does not remember, cannot recall, or wants to skip for now.", } criteria: dict[str, str] = {} for row in _option_rows(options): criteria[row["answer_class"]] = ( f"user_message matches this option's meaning. label={row['label']}" ) if not criteria: criteria = { "yes": "Affirmative answer to current_question.", "weak_yes": "Weak affirmative answer to current_question.", "no": "Negative answer to current_question.", "unsure": "User does not remember the answer to current_question.", } return criteria def build_state(sample: Mapping[str, Any]) -> dict[str, Any]: focus = sample.get("focus") if isinstance(sample.get("focus"), dict) else {} options = list(focus.get("options") or []) return { "current_question": str(focus.get("current_question") or ""), "options": options, "user_message": str(sample.get("user_message") or ""), "case_status": str(focus.get("case_status") or sample.get("case_status") or "collecting_evidence"), } def build_questions(sample: Mapping[str, Any]) -> dict[str, Any]: """Three TypeSafe questions, same request, official fan-out.""" layer = str(sample.get("layer") or "none") focus = sample.get("focus") if isinstance(sample.get("focus"), dict) else {} options = list(focus.get("options") or []) return { "intent": { "type": "choice", "instructions": INTENT_INSTRUCTIONS, "criteria": dict(INTENT_CRITERIA), }, "answer_class": { "type": "choice", "instructions": ANSWER_CLASS_INSTRUCTIONS, "criteria": answer_class_criteria(layer=layer, options=options), }, "has_new_dated_event": { "type": "noul", "instructions": HAS_NEW_DATED_EVENT_INSTRUCTIONS, "criteria": dict(HAS_NEW_DATED_EVENT_CRITERIA), }, } def sdk_questions(sample: Mapping[str, Any]) -> dict[str, Any]: """SDK objects when typesafe_sdk is installed; JSON dicts otherwise.""" payload = build_questions(sample) try: from typesafe_sdk import Choice, Noul except ImportError: return payload return { "intent": Choice( instructions=payload["intent"]["instructions"], criteria=payload["intent"]["criteria"], ), "answer_class": Choice( instructions=payload["answer_class"]["instructions"], criteria=payload["answer_class"]["criteria"], ), "has_new_dated_event": Noul( instructions=payload["has_new_dated_event"]["instructions"], criteria=payload["has_new_dated_event"]["criteria"], ), } def enforce_combo( intent: str | None, answer_class: str | None, has_new_dated_event: bool | None = None, ) -> dict[str, Any]: """Production invariant: answer_class is non-null iff intent is answer_current_focus.""" intent_value = intent if intent in INTENT_VALUES else "unclear" dated = bool(has_new_dated_event) if intent_value != "answer_current_focus": return { "intent": intent_value, "answer_class": None, "has_new_dated_event": dated, } class_value = answer_class if answer_class in ANSWER_CLASS_VALUES else None if class_value is None: return { "intent": "unclear", "answer_class": None, "has_new_dated_event": dated, } return { "intent": intent_value, "answer_class": class_value, "has_new_dated_event": dated, } def parse_jev_answers(answers: Mapping[str, Any]) -> dict[str, Any]: intent_row = answers.get("intent") or {} class_row = answers.get("answer_class") or {} noul_row = answers.get("has_new_dated_event") or {} noul_value = noul_row.get("noul") dated = None if isinstance(noul_value, (int, float)): dated = float(noul_value) >= NOUL_TRUE_THRESHOLD parsed = enforce_combo( str(intent_row.get("choice") or "") or None, str(class_row.get("choice") or "") or None, dated, ) parsed["raw"] = { "intent": { "choice": intent_row.get("choice"), "probabilities": intent_row.get("probabilities"), "confidence": intent_row.get("confidence"), }, "answer_class": { "choice": class_row.get("choice"), "probabilities": class_row.get("probabilities"), "confidence": class_row.get("confidence"), }, "has_new_dated_event": { "noul": noul_value, }, } intent_conf = intent_row.get("confidence") class_conf = class_row.get("confidence") if parsed["intent"] == "answer_current_focus" and isinstance(intent_conf, (int, float)) and isinstance(class_conf, (int, float)): parsed["confidence"] = min(float(intent_conf), float(class_conf)) elif isinstance(intent_conf, (int, float)): parsed["confidence"] = float(intent_conf) else: parsed["confidence"] = None return parsed