#!/usr/bin/env python3
"""Jev の応答を受け取る分岐の例。通常実行は模擬応答で、API を呼ばない。

python3 jev-ticket-routing.py
TYPESAFE_API_KEY=... python3 jev-ticket-routing.py --live

--live も架空の問い合わせを1回評価し、候補を表示するだけ。
返金、口座変更、チケット更新、利用者への返信は行わない。
0.9 / 0.1 は分岐説明用の仮値で、本番の推奨値ではない。
confidence は分布の指標であり、正答率として扱わない。
API 仕様: https://docs.typesafe.ai/api (確認日: 2026-09-25)
"""

import argparse
import http.client
import json
import math
import os
import urllib.error
import urllib.request

MODEL = "jev-1.13.0"
MIN_DEPARTMENT_PROBABILITY = 0.9
ACCOUNT_ACTION_REVIEW_THRESHOLD = 0.1
QUESTIONS = {
    "department": {
        "type": "choice",
        "instructions": "この問い合わせの主な担当部門はどれですか。",
        "criteria": {
            "billing": "料金、請求書、支払いに関する問い合わせ。",
            "technical": "不具合、障害、API連携に関する問い合わせ。",
            "other": "上記以外、複数部門で決められない、または情報不足。",
        },
    },
    "needs_account_action": {
        "type": "noul",
        "instructions": "口座・契約・請求などの利用者記録を変更する操作を求めていますか。",
        "criteria": {
            "true": "口座変更、契約変更、返金などの操作を依頼している。",
            "false": "説明や不具合調査のみを求め、記録の変更は依頼していない。",
        },
    },
}


def request_body(state):
    return {"model": MODEL, "state": state, "questions": QUESTIONS}


def probability(value):
    if type(value) not in (int, float) or not 0 <= value <= 1:
        raise ValueError("確率の型または範囲が不正")
    return value


def policy(response):
    """応答の構造を確認し、表示用候補と理由を返す。意味の正しさは検証できない。"""
    answers = response["answers"]
    department, action = answers["department"], answers["needs_account_action"]
    if department["type"] != "choice" or action["type"] != "noul":
        raise ValueError("応答型が不正")
    choice, probabilities = department["choice"], department["probabilities"]
    if not isinstance(probabilities, dict) or set(probabilities) != set(QUESTIONS["department"]["criteria"]):
        raise ValueError("選択肢が不正")
    if not isinstance(choice, str) or choice not in probabilities:
        raise ValueError("選択された値が不正")
    values = [probability(value) for value in probabilities.values()]
    if not math.isclose(sum(values), 1.0, abs_tol=1e-6) or probabilities[choice] != max(values):
        raise ValueError("確率の総和または選択が不正")
    probability(department["confidence"])  # 存在と範囲だけ確認。正答率ではない。
    action_probability = probability(action["noul"])
    if choice == "other":
        return "human_review", "対象外・情報不足・担当を特定できない"
    if action_probability >= ACCOUNT_ACTION_REVIEW_THRESHOLD:
        return "human_review", "記録変更の要求、またはその可能性を除外できない"
    if probabilities[choice] < MIN_DEPARTMENT_PROBABILITY:
        return "human_review", "担当部門の確率が閾値未満"
    return choice, "担当候補のみ（操作は実行しない）"


def evaluate(fetch):
    try:
        return policy(fetch())
    except (KeyError, TypeError, ValueError, OSError, http.client.HTTPException, urllib.error.URLError):
        return "human_review", "API失敗または不正応答"


def live(state, key):
    request = urllib.request.Request(
        "https://api.typesafe.ai/v1/systemone",
        data=json.dumps(request_body(state), ensure_ascii=False).encode("utf-8"),
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=20) as response:
        return json.load(response)  # この例では再試行しない。


def fixture(choice="billing", distribution=(0.98, 0.01, 0.01), action=0.02):
    return {"answers": {
        "department": {"type": "choice", "choice": choice,
                       "probabilities": dict(zip(("billing", "technical", "other"), distribution)),
                       "confidence": 0.8},  # 模擬値。分岐には使わない。
        "needs_account_action": {"type": "noul", "noul": action},
    }}


def offline():
    print("OFFLINE: すべて手作りの模擬応答。Jevの精度・速度・日本語能力の実測ではありません。")
    cases = [
        ("明確な請求", "請求書の消費税欄の意味を教えてください。", fixture(), "billing"),
        ("明確な不具合", "APIが500エラーを返します。原因を調べてください。",
         fixture("technical", (0.01, 0.98, 0.01)), "technical"),
        ("曖昧", "支払い画面が進みません。", fixture(distribution=(0.51, 0.48, 0.01)), "human_review"),
        ("情報不足・other", "困っています。", fixture("other", (0.01, 0.01, 0.98)), "human_review"),
        ("口座変更要求", "振込先口座を変更してください。", fixture(action=0.99), "human_review"),
        ("変更要求が不確か", "先月の支払いをどうにかしてほしい。", fixture(action=0.5), "human_review"),
        ("変更要求の閾値ちょうど", "支払いについて相談があります。", fixture(action=0.1), "human_review"),
        ("高確率の意味的誤答", "APIが500エラーを返します。", fixture(), "billing"),
        ("応答欠落", "請求書の説明をお願いします。", {"answers": {}}, "human_review"),
        ("不正な確率", "請求書の説明をお願いします。", fixture(action=float("nan")), "human_review"),
        ("通信失敗", "請求書の説明をお願いします。", OSError("模擬通信失敗"), "human_review"),
    ]
    for name, state, response, expected in cases:
        def fetch():
            if isinstance(response, Exception):
                raise response
            return response
        candidate, reason = evaluate(fetch)
        assert candidate == expected, (name, candidate, expected)
        print(f"{name}: {state} => {candidate} / {reason}")
    print(f"分岐確認: {len(cases)}/{len(cases)} 一致。高確率の意味的誤答もbillingとして通過しました。")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--live", action="store_true", help="架空問い合わせを実APIへ1回送信")
    args = parser.parse_args()
    if not args.live:
        return offline()
    key = os.environ.get("TYPESAFE_API_KEY", "").strip()
    if not key:
        parser.error("TYPESAFE_API_KEY が未設定です。送信していません。")
    state = "今月の請求書にある消費税欄の意味を教えてください。"
    print("LIVE: 架空問い合わせを実APIに1回送信します。表示結果は担当候補のみです。")
    print(evaluate(lambda: live(state, key)))


if __name__ == "__main__":
    main()
