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 |
|