Files
exAichatbot_agent/scripts/quick_inquiry_nlu_test.py
2026-08-18 09:26:35 +09:00

627 lines
28 KiB
Python

#!/usr/bin/env python3
"""간편조회 기능 다양 질문 테스트 → test.csv
- domain-query: 간편조회 UI와 동일 경로 (다수 케이스, 빠름)
- /ask: 실제 고객 자연어 질문 (기능당 소수)
사용:
PYTHONUNBUFFERED=1 python3 scripts/quick_inquiry_nlu_test.py \\
--base http://127.0.0.1:8083/web --out test.csv
"""
from __future__ import annotations
import argparse
import csv
import json
import re
import ssl
import time
import urllib.error
import urllib.request
from collections import Counter
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
# 운영 HTTPS 등에서 로컬 CA 이슈 시 사용
_SSL_CTX = ssl.create_default_context()
def one_line(text: str, limit: int = 280) -> str:
t = re.sub(r"\s+", " ", (text or "").strip())
return (t[: limit - 1] + "…") if len(t) > limit else t
def post_json(url: str, body: Dict[str, Any], timeout: float) -> Tuple[Optional[int], Any, float]:
t0 = time.time()
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
url, data=data, headers={"Content-Type": "application/json; charset=utf-8"}
)
try:
with urllib.request.urlopen(req, timeout=timeout, context=_SSL_CTX) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return resp.status, json.loads(raw), time.time() - t0
except json.JSONDecodeError:
return resp.status, raw, time.time() - t0
except urllib.error.HTTPError as e:
try:
payload = e.read().decode("utf-8", errors="replace")
try:
return e.code, json.loads(payload), time.time() - t0
except Exception:
return e.code, payload, time.time() - t0
except Exception:
return e.code, str(e), time.time() - t0
except Exception as e:
return None, str(e), time.time() - t0
def get_json(url: str, timeout: float) -> Tuple[Optional[int], Any, float]:
t0 = time.time()
try:
with urllib.request.urlopen(url, timeout=timeout, context=_SSL_CTX) as resp:
return resp.status, json.loads(resp.read().decode("utf-8", errors="replace")), time.time() - t0
except Exception as e:
return None, str(e), time.time() - t0
def domain_cases_prod_light() -> List[Dict[str, Any]]:
"""운영용 — 기능당 1~2건만 (부하 최소화)."""
light: List[Tuple[str, str, str, Dict[str, str]]] = [
("미납조회", "FARE_UNPAID", "야 내 차 12가3456인데 미납 있어?", {"carNo": "12가3456"}),
("환불조회", "FARE_REFUND", "잘못 낸 톨비 환불받을 수 있어? 12가3456", {"carNo": "12가3456"}),
("구간통행요금", "FARE_SEARCH", "서울에서 부산까지 톨비 얼마 나와?", {"fromIc": "서울", "toIc": "부산"}),
("구간통행요금", "FARE_SEARCH", "판교에서 신갈 가면 요금이 어떻게 돼?", {"fromIc": "판교", "toIc": "신갈"}),
("구간통행시간", "TROAD_TRAVEL_TIME", "서울에서 부산까지 지금 가면 얼마나 걸려?",
{"fromIc": "서울", "toIc": "부산", "carType": "1"}),
("구간통행시간", "TROAD_TRAVEL_TIME", "판교에서 신갈 지금 막혀? 몇 분 걸려?",
{"fromIc": "판교", "toIc": "신갈", "carType": "1"}),
("차단계획", "CONSTRUCTION_PLAN", "경부선 오늘 공사해서 막히는 데 있어?", {"routeName": "경부선"}),
("차단계획", "CONSTRUCTION_PLAN", "영동선 차로 막는 일정 있나?", {"routeName": "영동선"}),
("영업소정보", "IC_TEL", "판교 톨게이트 전화번호 좀", {"icName": "판교"}),
("영업소정보", "IC_TEL", "기흥 톨게이트 주소랑 전화 좀", {"icName": "기흥"}),
("긴급구조기관", "EMERGENCY_HELP_ORG", "경부선에서 사고 나면 어디로 연락해?",
{"listMode": "true", "routeName": "경부선"}),
("휴게소음식", "REST_AREA_FOOD_LIST", "죽전휴게소에서 뭐 먹을 거 있어?", {"restAreaName": "죽전"}),
("휴게소브랜드매장", "BRAND_SHOP", "죽전에 편의점이나 카페 있어?", {"restAreaName": "죽전"}),
("휴게소시설", "REST_AREA_FACILITY", "죽전휴게소에 수유실 있나?", {"restAreaName": "죽전"}),
("휴게소주유", "GASSTATION", "죽전휴게소 기름값 얼마야?", {"restAreaName": "죽전"}),
("휴게소주유", "GASSTATION", "경부선에서 기름 제일 싼 데 어디야?",
{"compareMode": "cheapest", "fuelType": "휘발유", "routeName": "경부선"}),
("휴게소충전소", "REST_AREA_CHARGING", "죽전에서 전기차 충전돼?",
{"restAreaName": "죽전", "chargingType": "all"}),
("휴게소충전소", "REST_AREA_CHARGING", "강릉휴게소에 충전소 있어?",
{"restAreaName": "강릉", "chargingType": "all"}),
("휴게소청년창업매장", "REST_AREA_YOUTH_SHOP", "죽전휴게소에 청년들 장사하는 가게 있어?",
{"restAreaName": "죽전"}),
("가까운휴게소", "REST_AREA_NEAREST", "판교 근처 휴게소 어디 있어?", {"icName": "판교"}),
("노선혼잡", "TROAD_CONGESTION", "경부선 지금 막히는 구간 있어?", {"routeName": "경부선"}),
]
return [
{
"mode": "domain-query",
"feature": f,
"expected_intent": intent,
"question": q,
"params": params,
"note": "운영 경량 domain-query",
}
for f, intent, q, params in light
]
def extract_answer(body: Any) -> str:
if not isinstance(body, dict):
return one_line(str(body))
for key in ("answer", "message", "text", "statusMsg", "llmSummary", "formattedAnswer"):
val = body.get(key)
if isinstance(val, str) and val.strip():
return one_line(val)
for nest_key in ("domainData", "data", "rawData"):
nest = body.get(nest_key)
if isinstance(nest, dict):
for key in ("llmSummary", "statusMsg", "answer", "message"):
val = nest.get(key)
if isinstance(val, str) and val.strip():
return one_line(val)
# list summary
lst = nest.get("list") or nest.get("items")
if isinstance(lst, list) and lst:
return one_line(f"건수={len(lst)} 예시={json.dumps(lst[0], ensure_ascii=False)}")
if body.get("needsRestAreaForm") or body.get("needsIcTelForm"):
return one_line(str(body.get("statusMsg") or "폼 유도 응답"))
return one_line(json.dumps(body, ensure_ascii=False))
def intent_of(body: Any, fallback: str = "") -> str:
if not isinstance(body, dict):
return fallback
return str(body.get("intentType") or fallback)
def status_of(body: Any) -> str:
if not isinstance(body, dict):
return ""
if "status" in body:
return str(body.get("status"))
dd = body.get("domainData")
if isinstance(dd, dict) and "status" in dd:
return str(dd.get("status"))
return ""
INTENT_OK = {
"FARE_UNPAID": {"FARE_UNPAID"},
"FARE_REFUND": {"FARE_REFUND"},
"FARE_SEARCH": {"FARE_SEARCH"},
"TROAD_TRAVEL_TIME": {"TROAD_TRAVEL_TIME"},
"TROAD_BREAKING_NEWS": {"TROAD_BREAKING_NEWS", "TRAFFIC_NEWS"},
"TRAFFIC_NEWS": {"TRAFFIC_NEWS", "TROAD_BREAKING_NEWS"},
"CONSTRUCTION_PLAN": {"CONSTRUCTION_PLAN"},
"IC_TEL": {"IC_TEL"},
"EMERGENCY_HELP_ORG": {"EMERGENCY_HELP_ORG"},
"REST_AREA_FOOD_LIST": {"REST_AREA_FOOD_LIST"},
"BRAND_SHOP": {"BRAND_SHOP"},
"REST_AREA_FACILITY": {"REST_AREA_FACILITY"},
"GASSTATION": {"GASSTATION"},
"REST_AREA_CHARGING": {"REST_AREA_CHARGING"},
"REST_AREA_YOUTH_SHOP": {"REST_AREA_YOUTH_SHOP"},
"REST_AREA_NEAREST": {"REST_AREA_NEAREST"},
"TROAD_CONGESTION": {"TROAD_CONGESTION"},
}
def judge(expected: str, actual: str, status: str, answer: str, http_st: Optional[int]) -> Tuple[str, str]:
if http_st != 200:
return "FAIL", f"HTTP {http_st}"
natural = len(answer or "") >= 6 and "Traceback" not in (answer or "")
intent_ok = (actual or "") in INTENT_OK.get(expected, {expected}) or actual == expected
st_true = status in ("True", "true", "1")
infra = any(
h in (answer or "")
for h in ("일시적인 장애", "Failed to obtain", "Listener refused", "연결")
)
if intent_ok and st_true and natural:
return "PASS", "의도·응답 정상"
if intent_ok and natural and infra:
return "PARTIAL", "의도 일치, 인프라/DB 제약 안내"
if intent_ok and natural:
return "PARTIAL", "의도 일치, 데이터없음/안내성 응답"
if natural and actual:
return "PARTIAL", f"의도상이 expected={expected} actual={actual}"
if natural:
return "PARTIAL", "응답만 존재"
return "FAIL", f"의도/응답 불량 expected={expected} actual={actual or '-'}"
# ---- case builders ----
def domain_cases() -> List[Dict[str, Any]]:
"""간편조회 domain-query — 질문은 구어체, 호출은 폼/직조회 params."""
rows: List[Dict[str, Any]] = []
def add(feature: str, intent: str, question: str, params: Dict[str, str], note: str = "") -> None:
rows.append(
{
"mode": "domain-query",
"feature": feature,
"expected_intent": intent,
"question": question,
"params": params,
"note": note,
}
)
# 미납/환불
add("미납조회", "FARE_UNPAID", "야 내 차 12가3456인데 미납 있어?", {"carNo": "12가3456"}, "Oracle 미연결 시 PARTIAL 가능")
add("미납조회", "FARE_UNPAID", "하이패스 미납금 좀 봐줘 서울12가3456", {"carNo": "서울12가3456"}, "Oracle 미연결 시 PARTIAL 가능")
add("미납조회", "FARE_UNPAID", "톨비 밀린 거 있어요? 차량번호 123가4567요", {"carNo": "123가4567"}, "Oracle 미연결 시 PARTIAL 가능")
add("미납조회", "FARE_UNPAID", "98나1234 미납통행료 나와요?", {"carNo": "98나1234"}, "Oracle 미연결 시 PARTIAL 가능")
add("환불조회", "FARE_REFUND", "12가3456으로 환불받을 돈 있어요?", {"carNo": "12가3456"}, "Oracle 미연결 시 PARTIAL 가능")
add("환불조회", "FARE_REFUND", "잘못 낸 톨비 환불 내역 좀 확인해줘 서울12가3456", {"carNo": "서울12가3456"}, "Oracle 미연결 시 PARTIAL 가능")
add("환불조회", "FARE_REFUND", "과오납 환불금 나왔나? 123가4567", {"carNo": "123가4567"}, "Oracle 미연결 시 PARTIAL 가능")
add("환불조회", "FARE_REFUND", "하이패스 환불받을 거 있는지 봐줘 98나1234", {"carNo": "98나1234"}, "Oracle 미연결 시 PARTIAL 가능")
# 통행요금
fare_qs = [
("서울에서 부산까지 톨비 얼마 나와?", "서울", "부산"),
("판교에서 신갈 가면 요금이 어떻게 돼?", "판교", "신갈"),
("판교IC에서 신갈IC까지 하이패스 얼마야?", "판교IC", "신갈IC"),
("판교 영업소에서 수원까지 통행료 좀", "판교영업소", "수원"),
("대전에서 대구 가는 데 톨비 얼마쯤 해?", "대전", "대구"),
("천안에서 남이IC까지 요금 알려줄래?", "천안", "남이"),
("서울 톨게이트에서 기흥까지 얼마야?", "서울영업소", "기흥"),
("김포에서 인천 쪽 고속도로 요금 궁금해", "김포", "인천"),
]
for q, a, b in fare_qs:
add("구간통행요금", "FARE_SEARCH", q, {"fromIc": a, "toIc": b})
# 통행시간
time_qs = [
("서울에서 부산까지 지금 가면 얼마나 걸려?", "서울", "부산"),
("판교에서 신갈 지금 막혀? 몇 분 걸려?", "판교", "신갈"),
("대전에서 대구까지 지금 소요시간 어때?", "대전", "대구"),
("서울IC에서 부산IC 지금 가면 몇 시간이야?", "서울IC", "부산IC"),
("기흥에서 천안까지 지금 얼마나 걸릴까?", "기흥", "천안"),
]
for q, a, b in time_qs:
add("구간통행시간", "TROAD_TRAVEL_TIME", q, {"fromIc": a, "toIc": b, "carType": "1"})
# 차단계획
for q, route in [
("경부선 오늘 공사해서 막히는 데 있어?", "경부선"),
("영동선 차로 막는 일정 있나?", "영동선"),
("서해안선 공사구간 좀 알려줘", "서해안선"),
("호남선 이번 주 차단계획 있어?", "호남선"),
("경부고속도로 공사한다던데 어디야?", "경부고속도로"),
("중부내륙선 차로통제 있어?", "중부내륙선"),
]:
add("차단계획", "CONSTRUCTION_PLAN", q, {"routeName": route})
# 영업소
for q, ic in [
("판교 톨게이트 전화번호 좀", "판교"),
("판교IC 전화 어떻게 걸어?", "판교IC"),
("판교 영업소 연락처 알려줘", "판교영업소"),
("서울 영업소 번호 뭐야?", "서울"),
("기흥 톨게이트 주소랑 전화 좀", "기흥"),
("부산IC 영업소 정보 있어?", "부산IC"),
("신갈 영업소 전화 받나?", "신갈"),
("경부선 판교 쪽 영업소 번호 알려줘", "경부선판교"),
]:
add("영업소정보", "IC_TEL", q, {"icName": ic})
# 긴급구조
for q, route in [
("경부선에서 사고 나면 어디로 연락해?", "경부선"),
("영동선 긴급구조 가능한 데 어디야?", "영동선"),
("서해안선 긴급구조기관 좀 알려줘", "서해안선"),
]:
add("긴급구조기관", "EMERGENCY_HELP_ORG", q, {"listMode": "true", "routeName": route})
add("긴급구조기관", "EMERGENCY_HELP_ORG", "판교 근처 고속도로 긴급구조대 있어?", {"icName": "판교"})
# 휴게소
rest_sets = [
("죽전", "죽전휴게소에서 뭐 먹을 거 있어?", "죽전에 편의점이나 카페 있어?",
"죽전휴게소에 수유실 있나?", "죽전휴게소 기름값 얼마야?",
"죽전에서 전기차 충전돼?", "죽전휴게소에 청년들 장사하는 가게 있어?"),
("안성", "안성휴게소 밥 뭐 파나?", "안성휴게소에 브랜드 매장 뭐 있어?",
"안성휴게소 샤워실 쓸 수 있어?", "안성 휘발유 얼마야?",
"안성휴게소 전기차 충전소 있어?", "안성에 청년창업 매장 있어?"),
("김천", "김천휴게소에서 뭐 먹지?", "김천휴게소 입점 매장 뭐 있어?",
"김천휴게소 편의시설 뭐 있어?", "김천 경유값 어때?",
"김천휴게소에 수소차 충전해?", "김천에 청년몰 있어?"),
("금강", "금강휴게소 메뉴 추천해줘", "금강휴게소에 카페 있어?",
"금강휴게소 시설 뭐 있어?", "금강휴게소 주유소 있어?",
"금강에서 전기차 충전 가능해?", "금강휴게소 청년창업매장 있나?"),
("죽전휴게소", "죽전 휴게소 식당 뭐 있어?", "죽전 휴게소에 쇼핑할 데 있어?",
"죽전 휴게소에 애완동물 시설 있어?", "죽전 휴게소 기름 넣어?",
"죽전 휴게소 EV 충전돼?", "죽전 휴게소 청년가게 있어?"),
("강릉", "강릉휴게소에서 뭐 먹을까?", "강릉휴게소 매장 뭐 있어?",
"강릉휴게소 편의시설 알려줘", "강릉휴게소 주유소 유가 궁금해",
"강릉휴게소 충전소 있어?", "강릉에 청년창업 매장 있어?"),
]
for name, q_food, q_brand, q_fac, q_gas, q_chg, q_youth in rest_sets:
add("휴게소음식", "REST_AREA_FOOD_LIST", q_food, {"restAreaName": name})
add("휴게소브랜드매장", "BRAND_SHOP", q_brand, {"restAreaName": name})
add("휴게소시설", "REST_AREA_FACILITY", q_fac, {"restAreaName": name})
add("휴게소주유", "GASSTATION", q_gas, {"restAreaName": name})
add("휴게소충전소", "REST_AREA_CHARGING", q_chg, {"restAreaName": name, "chargingType": "all"})
add("휴게소청년창업매장", "REST_AREA_YOUTH_SHOP", q_youth, {"restAreaName": name})
add("휴게소주유", "GASSTATION", "경부선에서 기름 제일 싼 데 어디야?",
{"compareMode": "cheapest", "fuelType": "휘발유", "routeName": "경부선"})
add("휴게소주유", "GASSTATION", "고속도로에서 경유 제일 싼 곳 알려줘",
{"compareMode": "cheapest", "fuelType": "경유"})
add("휴게소충전소", "REST_AREA_CHARGING", "경부선에 전기차 충전되는 휴게소 어디 있어?",
{"listMode": "true", "routeName": "경부선", "chargingType": "electric"})
add("휴게소충전소", "REST_AREA_CHARGING", "강릉휴게소충전소 있어?",
{"restAreaName": "강릉휴게소충전소", "chargingType": "all"})
for q, ic in [
("판교 근처 휴게소 어디 있어?", "판교"),
("서울 톨게이트 지나서 바로 나오는 휴게소 어디야?", "서울"),
("기흥IC 근처에 쉴 데 있어?", "기흥IC"),
]:
add("가까운휴게소", "REST_AREA_NEAREST", q, {"icName": ic})
for q, route in [
("경부선 지금 막히는 구간 있어?", "경부선"),
("영동선 어디가 막혀?", "영동선"),
("서해안선 정체 심한 데 알려줘", "서해안선"),
]:
add("노선혼잡", "TROAD_CONGESTION", q, {"routeName": route})
return rows
def ask_cases() -> List[Dict[str, Any]]:
"""자연어 /ask — 일반 이용자 구어체 질문."""
pairs = [
("미납조회", "FARE_UNPAID", [
"야 내 차 12가3456인데 미납 있어?",
"하이패스 미납금 나왔나 봐줘 서울12가3456",
"톨비 밀린 거 있어요? 45다6789요",
]),
("환불조회", "FARE_REFUND", [
"잘못 낸 톨비 환불받을 수 있어? 12가3456",
"하이패스 환불금 들어왔나? 서울33하1122",
]),
("구간통행요금", "FARE_SEARCH", [
"서울에서 부산까지 톨비 얼마 나와?",
"판교에서 신갈 가면 요금이 어떻게 돼?",
"판교IC에서 신갈IC까지 하이패스 얼마야?",
"대전에서 대구 가는 데 톨비 얼마쯤 해?",
]),
("구간통행시간", "TROAD_TRAVEL_TIME", [
"서울에서 부산까지 지금 가면 얼마나 걸려?",
"판교에서 신갈 지금 막혀? 몇 분 걸려?",
"지금 대전에서 대구까지 가면 몇 시간이야?",
]),
("노선별교통속보", "TROAD_BREAKING_NEWS", [
"고속도로에 사고 났다는데 어디야?",
"경부선에 사고 났어?",
"영동선 지금 교통상황 어때?",
]),
("차단계획", "CONSTRUCTION_PLAN", [
"경부선 오늘 공사해서 막히는 데 있어?",
"영동선 차로 막는 일정 있나?",
"서해안선 공사구간 좀 알려줘",
]),
("영업소정보", "IC_TEL", [
"판교 톨게이트 전화번호 좀",
"기흥 톨게이트 주소랑 전화 좀",
"판교 영업소 연락처 알려줘",
]),
("긴급구조기관", "EMERGENCY_HELP_ORG", [
"경부선에서 사고 나면 어디로 연락해?",
"고속도로에서 사고 났는데 가까운 구조대 어디야?",
]),
("휴게소음식", "REST_AREA_FOOD_LIST", [
"죽전휴게소에서 뭐 먹을 거 있어?",
"안성휴게소 밥 뭐 파나?",
]),
("휴게소브랜드매장", "BRAND_SHOP", [
"죽전에 편의점이나 카페 있어?",
"김천휴게소에 브랜드 매장 뭐 있어?",
]),
("휴게소시설", "REST_AREA_FACILITY", [
"죽전휴게소에 수유실 있나?",
"안성휴게소 샤워실 쓸 수 있어?",
]),
("휴게소주유", "GASSTATION", [
"죽전휴게소 기름값 얼마야?",
"경부선에서 기름 제일 싼 데 어디야?",
"고속도로에서 휘발유 제일 싼 곳 알려줘",
]),
("휴게소충전소", "REST_AREA_CHARGING", [
"죽전에서 전기차 충전돼?",
"강릉휴게소에 충전소 있어?",
"김천휴게소에서 수소차 충전해?",
]),
("휴게소청년창업매장", "REST_AREA_YOUTH_SHOP", [
"죽전휴게소에 청년들 장사하는 가게 있어?",
"고속도로에 청년창업 매장 있는 휴게소 어디야?",
]),
("가까운휴게소", "REST_AREA_NEAREST", [
"판교 근처 휴게소 어디 있어?",
"서울 톨게이트 지나서 바로 나오는 휴게소 어디야?",
]),
("노선혼잡", "TROAD_CONGESTION", [
"경부선 지금 막히는 구간 있어?",
"영동선 어디가 막혀?",
]),
]
rows: List[Dict[str, Any]] = []
for feature, intent, qs in pairs:
for q in qs:
rows.append(
{
"mode": "ask",
"feature": feature,
"expected_intent": intent,
"question": q,
"params": {},
"note": "구어체 자연어 채팅",
}
)
return rows
FIELDNAMES = [
"tested_at",
"mode",
"feature",
"expected_intent",
"question",
"params",
"http_status",
"elapsed_sec",
"actual_intent",
"api_status",
"answer",
"result",
"reason",
"route_type",
"note",
]
def append_row(path: str, row: Dict[str, str], write_header: bool) -> None:
with open(path, "a", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=FIELDNAMES)
if write_header:
w.writeheader()
w.writerow(row)
f.flush()
def main() -> int:
global _SSL_CTX
ap = argparse.ArgumentParser()
ap.add_argument("--base", default="http://127.0.0.1:8083/web")
ap.add_argument("--out", default="test.csv")
ap.add_argument("--timeout-domain", type=float, default=45.0)
ap.add_argument("--timeout-ask", type=float, default=90.0)
ap.add_argument("--sleep", type=float, default=0.15)
ap.add_argument("--sleep-ask", type=float, default=0.0, help="ask 전용 sleep(0이면 --sleep)")
ap.add_argument("--skip-ask", action="store_true")
ap.add_argument("--skip-domain", action="store_true")
ap.add_argument("--limit", type=int, default=0)
ap.add_argument(
"--profile",
choices=["full", "prod"],
default="full",
help="prod=기능당 소수 + 구어체(운영 부하 최소화)",
)
ap.add_argument("--insecure", action="store_true", help="SSL 검증 비활성화")
args = ap.parse_args()
if args.insecure:
_SSL_CTX = ssl._create_unverified_context()
base = args.base.rstrip("/")
domain_url = base + "/domain-query"
ask_url = base + "/ask"
traffic_url = base + "/traffic-news"
started = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sleep_ask = args.sleep_ask if args.sleep_ask > 0 else max(args.sleep, 1.5)
# reset out
open(args.out, "w", encoding="utf-8-sig").close()
header_needed = True
results: List[str] = []
def emit(row: Dict[str, str]) -> None:
nonlocal header_needed
append_row(args.out, row, header_needed)
header_needed = False
results.append(row["result"])
print(
f"{row['result']:7} [{row['mode']:12}] {row['feature']:14} "
f"{row['expected_intent']:22} → {row['actual_intent'] or '-':22} | {row['question'][:42]}",
flush=True,
)
# traffic-news GET
http_st, body, elapsed = get_json(traffic_url, args.timeout_domain)
ans = extract_answer(body) if isinstance(body, (dict, list)) else one_line(str(body))
if isinstance(body, list):
ans = one_line(f"list={len(body)}")
ok = http_st == 200
else:
ok = http_st == 200 and (
(
isinstance(body, dict)
and (
body.get("status") is True
or "list" in body
or "items" in body
or "routes" in body
)
)
or isinstance(body, list)
)
emit(
{
"tested_at": started,
"mode": "domain-api",
"feature": "노선별교통속보",
"expected_intent": "TRAFFIC_NEWS",
"question": "오늘 고속도로에 사고나 통제 소식 있어?",
"params": "",
"http_status": str(http_st or ""),
"elapsed_sec": f"{elapsed:.2f}",
"actual_intent": "TRAFFIC_NEWS",
"api_status": "True" if ok else "False",
"answer": ans,
"result": "PASS" if ok else "FAIL",
"reason": "GET /traffic-news" if ok else "GET 실패",
"route_type": "GET",
"note": "간편조회 UI 전용",
}
)
suite: List[Dict[str, Any]] = []
if not args.skip_domain:
suite.extend(domain_cases_prod_light() if args.profile == "prod" else domain_cases())
if not args.skip_ask:
# prod: 기능당 질문 최대 2개로 축소
ask = ask_cases()
if args.profile == "prod":
by_feat: Dict[str, int] = {}
trimmed: List[Dict[str, Any]] = []
for c in ask:
n = by_feat.get(c["feature"], 0)
if n < 2:
trimmed.append(c)
by_feat[c["feature"]] = n + 1
ask = trimmed
suite.extend(ask)
if args.limit > 0:
suite = suite[: args.limit]
total = len(suite)
for i, case in enumerate(suite, 1):
mode = case["mode"]
print(f"({i}/{total}) …", flush=True)
if mode == "domain-query":
http_st, body, elapsed = post_json(
domain_url,
{
"intentType": case["expected_intent"],
"params": case["params"],
"botId": f"qi-dom-{i}",
},
args.timeout_domain,
)
actual = intent_of(body, case["expected_intent"])
# domain-query often echoes intent in request; keep expected if missing
if not actual:
actual = case["expected_intent"]
else:
http_st, body, elapsed = post_json(
ask_url,
{"query": case["question"], "botId": f"qi-ask-{i}"},
args.timeout_ask,
)
actual = intent_of(body)
status = status_of(body)
answer = extract_answer(body)
route_type = ""
if isinstance(body, dict):
route_type = str(body.get("routeType") or mode)
result, reason = judge(case["expected_intent"], actual, status, answer, http_st)
emit(
{
"tested_at": started,
"mode": mode,
"feature": case["feature"],
"expected_intent": case["expected_intent"],
"question": case["question"],
"params": json.dumps(case.get("params") or {}, ensure_ascii=False),
"http_status": str(http_st or ""),
"elapsed_sec": f"{elapsed:.2f}",
"actual_intent": actual,
"api_status": status,
"answer": answer,
"result": result,
"reason": reason,
"route_type": route_type,
"note": case.get("note") or "",
}
)
time.sleep(sleep_ask if mode == "ask" else args.sleep)
c = Counter(results)
print("---", flush=True)
print(f"saved={args.out} total={len(results)} {dict(c)}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())