Spaces:
Sleeping
Sleeping
Update app.py
#1
by ableman82 - opened
app.py
CHANGED
|
@@ -4,6 +4,29 @@ import pandas as pd
|
|
| 4 |
from sklearn.linear_model import LogisticRegression
|
| 5 |
from xgboost import XGBClassifier
|
| 6 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
# ============================================================
|
| 9 |
# 0. 공통 설정
|
|
@@ -361,89 +384,240 @@ def render_gen4(amount, hour, new_payee_bin, ratio, prob3):
|
|
| 361 |
</div>
|
| 362 |
"""
|
| 363 |
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
dec, bg, fg = decide(prob)
|
| 369 |
-
payee_str = "신규 수취인" if new_payee_bin == 1 else "기존 수취인"
|
| 370 |
-
time_str = "새벽" if (hour <= 6 or hour >= 22) else "일반"
|
| 371 |
|
| 372 |
context_html = (
|
| 373 |
-
f"[SYS] 당신은 FDS 분석가입니다. 거래
|
| 374 |
f"[INPUT] amount={amount:.0f}만, hour={int(hour):02d}, "
|
| 375 |
f"new_payee={'true' if new_payee_bin==1 else 'false'}, ratio={ratio:.1f}×<br>"
|
| 376 |
-
f"[PRIOR] 1-4세대
|
| 377 |
-
f"[TASK]
|
| 378 |
)
|
| 379 |
|
| 380 |
-
cot_rows =
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
f"<td style='text-align:right; padding:6px 4px; font-family:monospace; color:#666;'>0.09</td></tr>"
|
| 393 |
-
)
|
| 394 |
-
|
| 395 |
-
if is_high_risk:
|
| 396 |
-
nl_judgment = (
|
| 397 |
-
f"<b style='color:#633806;'>차단 권고 (의심도 {prob*100:.0f}%)</b><br><br>"
|
| 398 |
-
f"{int(hour):02d}시 {time_str} 시간대에 평소보다 {ratio:.1f}배 급증한 {amount:.0f}만원이 "
|
| 399 |
-
f"<u>{payee_str}</u>에게 이체되는 것은 전형적인 보이스피싱 패턴입니다. "
|
| 400 |
-
f"특히 수취인 계좌가 최근 사기 의심 계좌와 2-hop 거리에 있다는 GNN 신호까지 더해져 위험도가 매우 높습니다.<br><br>"
|
| 401 |
-
f"<b>권고 조치:</b> ① 즉시 거래 보류, ② 등록된 전화번호로 본인 직접 확인, ③ 확인 전 자금 동결 24시간 유지"
|
| 402 |
-
)
|
| 403 |
-
elif prob >= 0.5:
|
| 404 |
-
nl_judgment = (
|
| 405 |
-
f"<b style='color:#633806;'>추가 인증 권고 (의심도 {prob*100:.0f}%)</b><br><br>"
|
| 406 |
-
f"{int(hour):02d}시 거래에서 일부 이상 신호({ratio:.1f}배 금액, {payee_str})가 감지되었으나 "
|
| 407 |
-
f"결정적 위험 패턴은 아닙니다. 차단보다는 추가 인증으로 본인 의사를 확인하는 것이 적절합니다.<br><br>"
|
| 408 |
-
f"<b>권고 조치:</b> ① ARS 또는 OTP 추가 인증, ② 송금 의도 재확인 메시지 발송"
|
| 409 |
-
)
|
| 410 |
-
else:
|
| 411 |
-
nl_judgment = (
|
| 412 |
-
f"<b style='color:#3B6D11;'>통과 권고 (의심도 {prob*100:.0f}%)</b><br><br>"
|
| 413 |
-
f"{int(hour):02d}시 거래의 패턴이 송금인의 평소 행동 범위 내에 있으며, "
|
| 414 |
-
f"1-4세대 모델 모두 위험 신호를 강하게 보내지 않았습니다. 정상 거래로 판단됩니다.<br><br>"
|
| 415 |
-
f"<b>권고 조치:</b> 별도 조치 없이 거래 진행"
|
| 416 |
)
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
return f"""
|
| 419 |
<div style="{CARD_STYLE}">
|
| 420 |
-
{card_header("GEN 5 · FOUNDATION MODEL (LLM)", "초거대 추론 모
|
| 421 |
<p style="font-size:13px; color:#666; margin:4px 0 6px;">컨텍스트 토큰화</p>
|
| 422 |
<div style="background:#f5f5f0; padding:10px 12px; border-radius:6px; font-family:monospace; font-size:11px; line-height:1.7; margin-bottom:12px;">{context_html}</div>
|
| 423 |
<p style="font-size:13px; color:#666; margin:4px 0 6px;">추론 체인 (Chain-of-Thought)</p>
|
| 424 |
<table style="width:100%; font-size:13px; border-collapse:collapse; margin-bottom:12px;">
|
| 425 |
<thead>
|
| 426 |
<tr style="border-bottom:0.5px solid rgba(0,0,0,0.15);">
|
| 427 |
-
<th style="text-align:left; padding:6px 4px; font-weight:500; color:#666; width:
|
| 428 |
-
<th style="text-align:left; padding:6px 4px; font-weight:500; color:#666; width:
|
| 429 |
-
<th style="text-align:right; padding:6px 4px; font-weight:500; color:#666; width:
|
| 430 |
</tr>
|
| 431 |
</thead>
|
| 432 |
<tbody>{cot_rows}</tbody>
|
| 433 |
</table>
|
| 434 |
-
<p style="font-size:13px; color:#666; margin:4px 0 6px;">생성된 자연어 판단
|
| 435 |
-
<div style="background:#FAEEDA; padding:12px 14px; border-radius:6px; font-size:13px; line-height:1.7; color:#412402;">{
|
| 436 |
<details style="margin-top:10px;">
|
| 437 |
<summary style="font-size:12px; color:#666; cursor:pointer;">생성 파라미터 보기</summary>
|
| 438 |
-
<div style="background:#f5f5f0; padding:8px 12px; border-radius:6px; font-family:monospace; font-size:11px; margin-top:6px; line-height:1.6;">
|
| 439 |
-
model: claude-sonnet-4-5 / temperature: 0.2 / top_p: 0.9 / max_tokens: 512<br>
|
| 440 |
-
input_tokens: 142 / output_tokens: 98 / latency: 1.83s (시뮬레이션)
|
| 441 |
-
</div>
|
| 442 |
</details>
|
| 443 |
<p style="font-size:12px; color:#888; margin:10px 0 0; font-style:italic;">강점: 사전 학습된 도메인 지식으로 '왜 사기인지' 자연어 설명 + 권고 조치까지 자동 생성</p>
|
| 444 |
</div>
|
| 445 |
"""
|
| 446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
# ============================================================
|
| 448 |
# 4. 메인 분석 함수
|
| 449 |
# ============================================================
|
|
@@ -495,10 +669,18 @@ def analyze_transaction(amount, hour, new_payee, ratio):
|
|
| 495 |
# 5. Gradio UI
|
| 496 |
# ============================================================
|
| 497 |
with gr.Blocks(theme=gr.themes.Default(), title="FDS 1-5세대 비교 데모") as demo:
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
<div style="text-align:center; padding:10px 0;">
|
| 500 |
<h1 style="margin:0;">🛡️ FDS 1-5세대 비교 데모</h1>
|
| 501 |
<p style="color:#666; margin:6px 0 0;">실무 운영 담당자 시연용 · 판단 수식·가중치·근거 전체 노출 모드</p>
|
|
|
|
| 502 |
</div>
|
| 503 |
""")
|
| 504 |
|
|
|
|
| 4 |
from sklearn.linear_model import LogisticRegression
|
| 5 |
from xgboost import XGBClassifier
|
| 6 |
import time
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
# ============================================================
|
| 12 |
+
# 0-A. Claude API 클라이언트 초기화 (5세대용)
|
| 13 |
+
# ============================================================
|
| 14 |
+
# 환경변수 ANTHROPIC_API_KEY 가 설정되어 있으면 실제 API 호출 모드
|
| 15 |
+
# 없거나 호출 실패 시 자동으로 시뮬레이션 모드로 fallback
|
| 16 |
+
try:
|
| 17 |
+
from anthropic import Anthropic
|
| 18 |
+
_api_key = os.environ.get("ANTHROPIC_API_KEY")
|
| 19 |
+
if _api_key:
|
| 20 |
+
claude_client = Anthropic(api_key=_api_key)
|
| 21 |
+
CLAUDE_AVAILABLE = True
|
| 22 |
+
else:
|
| 23 |
+
claude_client = None
|
| 24 |
+
CLAUDE_AVAILABLE = False
|
| 25 |
+
except ImportError:
|
| 26 |
+
claude_client = None
|
| 27 |
+
CLAUDE_AVAILABLE = False
|
| 28 |
+
|
| 29 |
+
CLAUDE_MODEL = "claude-sonnet-4-6" # 2026년 5월 현재 권장 모델
|
| 30 |
|
| 31 |
# ============================================================
|
| 32 |
# 0. 공통 설정
|
|
|
|
| 384 |
</div>
|
| 385 |
"""
|
| 386 |
|
| 387 |
+
# ============================================================
|
| 388 |
+
# 5세대 — Claude API 실제 호출
|
| 389 |
+
# ============================================================
|
| 390 |
+
def build_claude_prompt(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec):
|
| 391 |
+
"""Claude에게 보낼 시스템 프롬프트와 사용자 메시지 구성"""
|
| 392 |
+
system_prompt = (
|
| 393 |
+
"당신은 한국 은행의 FDS(이상금융거래탐지시스템) 분석 전문가입니다. "
|
| 394 |
+
"주어진 거래 정보와 1-4세대 모델의 사전 판단을 바탕으로 사기 여부를 종합 판정하세요. "
|
| 395 |
+
"특히 부동산 잔금 송금, 사업자 대금 결제 같은 정상 거래 패턴과 보이스피싱·대포통장 패턴을 구분해야 합니다. "
|
| 396 |
+
"반드시 다음 JSON 형식으로만 응답하세요(다른 텍스트 금지):\n"
|
| 397 |
+
'{\n'
|
| 398 |
+
' "risk_score": <0.0~1.0 사이 사기 의심도>,\n'
|
| 399 |
+
' "decision": "차단" | "추가 인증" | "통과",\n'
|
| 400 |
+
' "reasoning_steps": [\n'
|
| 401 |
+
' {"step": "<추론 단계>", "attention": <0.0~1.0>},\n'
|
| 402 |
+
' ... (3~5개)\n'
|
| 403 |
+
' ],\n'
|
| 404 |
+
' "judgment": "<최종 판단 자연어 (왜 그렇게 판단했는지 + 권고 조치)>"\n'
|
| 405 |
+
'}'
|
| 406 |
+
)
|
| 407 |
+
payee_str = "신규" if new_payee_bin == 1 else "기존"
|
| 408 |
+
time_period = "새벽" if (hour <= 6 or hour >= 22) else ("주간" if 9 <= hour <= 18 else "저녁")
|
| 409 |
+
user_message = (
|
| 410 |
+
f"[거래 정보]\n"
|
| 411 |
+
f"- 금액: {amount:.0f}만원\n"
|
| 412 |
+
f"- 거래 시간: {int(hour):02d}시 ({time_period})\n"
|
| 413 |
+
f"- 수취인: {payee_str} 수취인\n"
|
| 414 |
+
f"- 과거 대비 배율: {ratio:.1f}배 (송금인의 평균 거래액 대비)\n\n"
|
| 415 |
+
f"[1-4세대 모델 사전 판단]\n"
|
| 416 |
+
f"- 평균 사기 확률: {prior_avg*100:.1f}%\n"
|
| 417 |
+
f"- 종합 판정: {prior_dec}\n\n"
|
| 418 |
+
f"위 거래에 대해 FDS 전문가 관점에서 종합 판정해주세요."
|
| 419 |
+
)
|
| 420 |
+
return system_prompt, user_message
|
| 421 |
+
|
| 422 |
+
def call_claude_api(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec):
|
| 423 |
+
"""실제 Claude API 호출. 성공 시 (parsed_dict, raw_meta) 반환, 실패 시 (None, error_msg)"""
|
| 424 |
+
if not CLAUDE_AVAILABLE:
|
| 425 |
+
return None, "API 키 미설정 (ANTHROPIC_API_KEY 환경변수 없음)"
|
| 426 |
+
|
| 427 |
+
system_prompt, user_message = build_claude_prompt(
|
| 428 |
+
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec
|
| 429 |
+
)
|
| 430 |
+
try:
|
| 431 |
+
t0 = time.time()
|
| 432 |
+
response = claude_client.messages.create(
|
| 433 |
+
model=CLAUDE_MODEL,
|
| 434 |
+
max_tokens=1024,
|
| 435 |
+
temperature=0.2,
|
| 436 |
+
system=system_prompt,
|
| 437 |
+
messages=[{"role": "user", "content": user_message}]
|
| 438 |
+
)
|
| 439 |
+
latency = time.time() - t0
|
| 440 |
+
|
| 441 |
+
raw_text = response.content[0].text.strip()
|
| 442 |
+
|
| 443 |
+
# JSON 추출: 응답 중 첫 번째 { 부터 마지막 } 까지
|
| 444 |
+
json_match = re.search(r'\{.*\}', raw_text, re.DOTALL)
|
| 445 |
+
if not json_match:
|
| 446 |
+
return None, f"JSON 파싱 실패: {raw_text[:200]}"
|
| 447 |
+
|
| 448 |
+
parsed = json.loads(json_match.group(0))
|
| 449 |
+
|
| 450 |
+
meta = {
|
| 451 |
+
"input_tokens": response.usage.input_tokens,
|
| 452 |
+
"output_tokens": response.usage.output_tokens,
|
| 453 |
+
"latency": latency,
|
| 454 |
+
"model": CLAUDE_MODEL,
|
| 455 |
+
"raw_text": raw_text,
|
| 456 |
+
}
|
| 457 |
+
return parsed, meta
|
| 458 |
+
except Exception as e:
|
| 459 |
+
return None, f"API 호출 실패: {type(e).__name__}: {str(e)[:200]}"
|
| 460 |
+
|
| 461 |
+
def render_gen5_card(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
|
| 462 |
+
prob, decision_text, reasoning_steps, judgment_text,
|
| 463 |
+
meta_html, source_label):
|
| 464 |
+
"""5세대 카드 HTML 렌더링 (API/시뮬레이션 공용)"""
|
| 465 |
dec, bg, fg = decide(prob)
|
|
|
|
|
|
|
| 466 |
|
| 467 |
context_html = (
|
| 468 |
+
f"[SYS] 당신은 한국 은행의 FDS 분석 전문가입니다. 거래 정보와 1-4세대 사전 판단을 토대로 종합 판정하세요.<br>"
|
| 469 |
f"[INPUT] amount={amount:.0f}만, hour={int(hour):02d}, "
|
| 470 |
f"new_payee={'true' if new_payee_bin==1 else 'false'}, ratio={ratio:.1f}×<br>"
|
| 471 |
+
f"[PRIOR] 1-4세대 평균: {prior_avg*100:.1f}% / 종합: {prior_dec}<br>"
|
| 472 |
+
f"[TASK] JSON 형식으로 risk_score, decision, reasoning_steps, judgment 출력"
|
| 473 |
)
|
| 474 |
|
| 475 |
+
cot_rows = ""
|
| 476 |
+
for i, step in enumerate(reasoning_steps, 1):
|
| 477 |
+
step_text = step.get("step", "") if isinstance(step, dict) else str(step)
|
| 478 |
+
attn = step.get("attention", 0.0) if isinstance(step, dict) else 0.0
|
| 479 |
+
try:
|
| 480 |
+
attn = float(attn)
|
| 481 |
+
except (ValueError, TypeError):
|
| 482 |
+
attn = 0.0
|
| 483 |
+
cot_rows += (
|
| 484 |
+
f"<tr><td style='padding:6px 4px;'>{i}</td>"
|
| 485 |
+
f"<td style='padding:6px 4px; color:#666;'>{step_text}</td>"
|
| 486 |
+
f"<td style='text-align:right; padding:6px 4px; font-family:monospace; color:#666;'>{attn:.2f}</td></tr>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
)
|
| 488 |
|
| 489 |
+
judg_color = "#3B6D11" if prob < 0.5 else "#633806"
|
| 490 |
+
judgment_html = (
|
| 491 |
+
f"<b style='color:{judg_color};'>{decision_text} 권고 (의심도 {prob*100:.0f}%)</b><br><br>"
|
| 492 |
+
f"{judgment_text}"
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
return f"""
|
| 496 |
<div style="{CARD_STYLE}">
|
| 497 |
+
{card_header("GEN 5 · FOUNDATION MODEL (LLM)", f"초거대 추론 모��� ({source_label})", dec, bg, fg, f"의심도 {prob*100:.0f}%")}
|
| 498 |
<p style="font-size:13px; color:#666; margin:4px 0 6px;">컨텍스트 토큰화</p>
|
| 499 |
<div style="background:#f5f5f0; padding:10px 12px; border-radius:6px; font-family:monospace; font-size:11px; line-height:1.7; margin-bottom:12px;">{context_html}</div>
|
| 500 |
<p style="font-size:13px; color:#666; margin:4px 0 6px;">추론 체인 (Chain-of-Thought)</p>
|
| 501 |
<table style="width:100%; font-size:13px; border-collapse:collapse; margin-bottom:12px;">
|
| 502 |
<thead>
|
| 503 |
<tr style="border-bottom:0.5px solid rgba(0,0,0,0.15);">
|
| 504 |
+
<th style="text-align:left; padding:6px 4px; font-weight:500; color:#666; width:8%;">단계</th>
|
| 505 |
+
<th style="text-align:left; padding:6px 4px; font-weight:500; color:#666; width:72%;">추론 내용</th>
|
| 506 |
+
<th style="text-align:right; padding:6px 4px; font-weight:500; color:#666; width:20%;">Attention</th>
|
| 507 |
</tr>
|
| 508 |
</thead>
|
| 509 |
<tbody>{cot_rows}</tbody>
|
| 510 |
</table>
|
| 511 |
+
<p style="font-size:13px; color:#666; margin:4px 0 6px;">생성된 자연어 판단</p>
|
| 512 |
+
<div style="background:#FAEEDA; padding:12px 14px; border-radius:6px; font-size:13px; line-height:1.7; color:#412402;">{judgment_html}</div>
|
| 513 |
<details style="margin-top:10px;">
|
| 514 |
<summary style="font-size:12px; color:#666; cursor:pointer;">생성 파라미터 보기</summary>
|
| 515 |
+
<div style="background:#f5f5f0; padding:8px 12px; border-radius:6px; font-family:monospace; font-size:11px; margin-top:6px; line-height:1.6;">{meta_html}</div>
|
|
|
|
|
|
|
|
|
|
| 516 |
</details>
|
| 517 |
<p style="font-size:12px; color:#888; margin:10px 0 0; font-style:italic;">강점: 사전 학습된 도메인 지식으로 '왜 사기인지' 자연어 설명 + 권고 조치까지 자동 생성</p>
|
| 518 |
</div>
|
| 519 |
"""
|
| 520 |
|
| 521 |
+
def render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec):
|
| 522 |
+
"""API 호출 실패/미설정 시 fallback용 시뮬레이션"""
|
| 523 |
+
is_high_risk = (new_payee_bin == 1 and (hour <= 6 or hour >= 22) and amount >= 500)
|
| 524 |
+
prob = 0.95 if is_high_risk else min(prior_avg + 0.02, 0.98)
|
| 525 |
+
payee_str = "신규 수취인" if new_payee_bin == 1 else "기존 수취인"
|
| 526 |
+
time_str = "새벽" if (hour <= 6 or hour >= 22) else "일반"
|
| 527 |
+
|
| 528 |
+
reasoning_steps = [
|
| 529 |
+
{"step": f"시간 {int(hour):02d}시 + {payee_str} → 보이스피싱 정형 패턴 매칭", "attention": 0.42},
|
| 530 |
+
{"step": f"평소 {ratio:.1f}배 금액 → 송금인 평소 행동 이탈도 측정", "attention": 0.28},
|
| 531 |
+
{"step": f"4세대 GNN 신호 + {amount:.0f}만원 고액 → 위험 가중", "attention": 0.21},
|
| 532 |
+
{"step": "최근 보이스피싱 트렌드 사전 지식 활용", "attention": 0.09},
|
| 533 |
+
]
|
| 534 |
+
|
| 535 |
+
if is_high_risk:
|
| 536 |
+
decision_text = "차단"
|
| 537 |
+
judgment_text = (
|
| 538 |
+
f"{int(hour):02d}시 {time_str} 시간대에 평소보다 {ratio:.1f}배 급증한 {amount:.0f}만원이 "
|
| 539 |
+
f"<u>{payee_str}</u>에게 이체되는 것은 전형적인 보이스피싱 패턴입니다. "
|
| 540 |
+
f"수취인 계좌가 사기 의심 계좌와 2-hop 거리에 있다는 GNN 신호까지 더해져 위험도가 매우 높습니다.<br><br>"
|
| 541 |
+
f"<b>권고 조치:</b> ① 즉시 거래 보류, ② 등록된 전화번호로 본인 직접 확인, ③ 확인 전 자금 동결 24시간 유지"
|
| 542 |
+
)
|
| 543 |
+
elif prob >= 0.5:
|
| 544 |
+
decision_text = "추가 인증"
|
| 545 |
+
judgment_text = (
|
| 546 |
+
f"{int(hour):02d}시 거래에서 일부 이상 신호({ratio:.1f}배 금액, {payee_str})가 감지되었으나 "
|
| 547 |
+
f"결정적 위험 패턴은 아닙니다. 차단보다는 추가 인증으로 본인 의사를 확인하는 것이 적절합니다.<br><br>"
|
| 548 |
+
f"<b>권고 조치:</b> ① ARS 또는 OTP 추가 인증, ② 송금 의도 재확인 메시지 발송"
|
| 549 |
+
)
|
| 550 |
+
else:
|
| 551 |
+
decision_text = "통과"
|
| 552 |
+
judgment_text = (
|
| 553 |
+
f"{int(hour):02d}시 거래의 패턴이 송금인의 평소 행동 범위 내에 있으며, "
|
| 554 |
+
f"1-4세대 모델 모두 위험 신호를 강하게 보내지 않았습니다. 정상 거래로 판단됩니다.<br><br>"
|
| 555 |
+
f"<b>권고 조치:</b> 별도 조치 없이 거래 진행"
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
meta_html = (
|
| 559 |
+
"mode: <b>SIMULATION</b> (API 미연결)<br>"
|
| 560 |
+
"이 결과는 if-else 하드코딩으로 생성된 시뮬레이션입니다.<br>"
|
| 561 |
+
"실제 Claude 호출을 활성화하려면 ANTHROPIC_API_KEY 환경변수를 설정하세요."
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
return render_gen5_card(
|
| 565 |
+
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
|
| 566 |
+
prob, decision_text, reasoning_steps, judgment_text,
|
| 567 |
+
meta_html, source_label="시뮬레이션 모드"
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
def render_gen5(amount, hour, new_payee_bin, ratio, prior_avg):
|
| 571 |
+
"""5세대 진입점: API 가능 시 Claude 호출, 실패 시 시뮬레이션 fallback"""
|
| 572 |
+
prior_dec, _, _ = decide(prior_avg)
|
| 573 |
+
|
| 574 |
+
if not CLAUDE_AVAILABLE:
|
| 575 |
+
return render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
|
| 576 |
+
|
| 577 |
+
parsed, meta_or_err = call_claude_api(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
|
| 578 |
+
|
| 579 |
+
if parsed is None:
|
| 580 |
+
fallback_html = render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
|
| 581 |
+
warning = (
|
| 582 |
+
f'<div style="background:#FCEBEB; border-left:3px solid #A32D2D; padding:10px 14px; '
|
| 583 |
+
f'border-radius:6px; margin-bottom:10px; font-size:13px; color:#4A1B0C;">'
|
| 584 |
+
f'⚠️ Claude API 호출 실패. 시뮬레이션 모드로 대체합니다.<br>'
|
| 585 |
+
f'<span style="font-family:monospace; font-size:11px; color:#712B13;">사유: {meta_or_err}</span>'
|
| 586 |
+
f'</div>'
|
| 587 |
+
)
|
| 588 |
+
return warning + fallback_html
|
| 589 |
+
|
| 590 |
+
try:
|
| 591 |
+
prob = float(parsed.get("risk_score", 0.5))
|
| 592 |
+
prob = max(0.0, min(1.0, prob))
|
| 593 |
+
decision_text = parsed.get("decision", "추가 인증")
|
| 594 |
+
reasoning_steps = parsed.get("reasoning_steps", [])
|
| 595 |
+
judgment_text = parsed.get("judgment", "(판단 내용 누락)")
|
| 596 |
+
|
| 597 |
+
meta = meta_or_err
|
| 598 |
+
meta_html = (
|
| 599 |
+
f"model: <b>{meta['model']}</b> / temperature: 0.2 / max_tokens: 1024<br>"
|
| 600 |
+
f"input_tokens: {meta['input_tokens']} / output_tokens: {meta['output_tokens']} / "
|
| 601 |
+
f"latency: {meta['latency']:.2f}s<br>"
|
| 602 |
+
f"mode: <b style='color:#3B6D11;'>LIVE API CALL ✓</b>"
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
return render_gen5_card(
|
| 606 |
+
amount, hour, new_payee_bin, ratio, prior_avg, prior_dec,
|
| 607 |
+
prob, decision_text, reasoning_steps, judgment_text,
|
| 608 |
+
meta_html, source_label=f"실제 {CLAUDE_MODEL}"
|
| 609 |
+
)
|
| 610 |
+
except (KeyError, ValueError, TypeError) as e:
|
| 611 |
+
fallback_html = render_gen5_simulation(amount, hour, new_payee_bin, ratio, prior_avg, prior_dec)
|
| 612 |
+
warning = (
|
| 613 |
+
f'<div style="background:#FCEBEB; border-left:3px solid #A32D2D; padding:10px 14px; '
|
| 614 |
+
f'border-radius:6px; margin-bottom:10px; font-size:13px; color:#4A1B0C;">'
|
| 615 |
+
f'⚠️ Claude 응답 파싱 실패. 시뮬레이션 모드로 대체합니다.<br>'
|
| 616 |
+
f'<span style="font-family:monospace; font-size:11px; color:#712B13;">'
|
| 617 |
+
f'{type(e).__name__}: {str(e)[:150]}</span></div>'
|
| 618 |
+
)
|
| 619 |
+
return warning + fallback_html
|
| 620 |
+
|
| 621 |
# ============================================================
|
| 622 |
# 4. 메인 분석 함수
|
| 623 |
# ============================================================
|
|
|
|
| 669 |
# 5. Gradio UI
|
| 670 |
# ============================================================
|
| 671 |
with gr.Blocks(theme=gr.themes.Default(), title="FDS 1-5세대 비교 데모") as demo:
|
| 672 |
+
_api_badge = (
|
| 673 |
+
f'<span style="background:#EAF3DE; color:#3B6D11; padding:3px 10px; border-radius:8px; font-size:12px; font-weight:500;">'
|
| 674 |
+
f'● Claude API 연결됨 ({CLAUDE_MODEL})</span>'
|
| 675 |
+
if CLAUDE_AVAILABLE else
|
| 676 |
+
'<span style="background:#FAEEDA; color:#854F0B; padding:3px 10px; border-radius:8px; font-size:12px; font-weight:500;">'
|
| 677 |
+
'○ API 미연결 (5세대 시뮬레이션 모드)</span>'
|
| 678 |
+
)
|
| 679 |
+
gr.HTML(f"""
|
| 680 |
<div style="text-align:center; padding:10px 0;">
|
| 681 |
<h1 style="margin:0;">🛡️ FDS 1-5세대 비교 데모</h1>
|
| 682 |
<p style="color:#666; margin:6px 0 0;">실무 운영 담당자 시연용 · 판단 수식·가중치·근거 전체 노출 모드</p>
|
| 683 |
+
<div style="margin-top:8px;">{_api_badge}</div>
|
| 684 |
</div>
|
| 685 |
""")
|
| 686 |
|