eodi-mcp / src /auth /asset_parser.py
lovelymango's picture
Upload 25 files
978996e verified
"""
Asset Text Parser
==================
์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํ…์ŠคํŠธ์—์„œ ํฌ์ธํŠธ/๋งˆ์ผ๋ฆฌ์ง€ ์ •๋ณด๋ฅผ ์ž๋™ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
์˜ˆ์‹œ ์ž…๋ ฅ:
- "๋Œ€ํ•œํ•ญ๊ณต 45,000 ๋งˆ์ผ"
- "AMEX MR 50000์ "
- "์ฒด์ด์Šค UR 30,000"
"""
import re
from typing import Dict, Any, List, Optional
from datetime import datetime
from .config import POINT_VALUATIONS
# =============================================================================
# ํ”„๋กœ๊ทธ๋žจ ๋ณ„์นญ ๋งคํ•‘
# =============================================================================
PROGRAM_ALIASES = {
# ํ•œ๊ตญ ํ•ญ๊ณต
"๋Œ€ํ•œํ•ญ๊ณต": "KOREAN_AIR",
"๋Œ€ํ•œ": "KOREAN_AIR",
"kal": "KOREAN_AIR",
"korean air": "KOREAN_AIR",
"์Šค์นด์ดํŒจ์Šค": "KOREAN_AIR",
"์•„์‹œ์•„๋‚˜": "ASIANA",
"oz": "ASIANA",
"asiana": "ASIANA",
# ๋ฏธ๊ตญ ํ•ญ๊ณต
"delta": "DELTA_SKYMILES",
"๋ธํƒ€": "DELTA_SKYMILES",
"skymiles": "DELTA_SKYMILES",
"united": "UNITED_MILEAGEPLUS",
"์œ ๋‚˜์ดํ‹ฐ๋“œ": "UNITED_MILEAGEPLUS",
"mileageplus": "UNITED_MILEAGEPLUS",
"american": "AA_AADVANTAGE",
"aa": "AA_AADVANTAGE",
"์•„๋ฉ”๋ฆฌ์นธ": "AA_AADVANTAGE",
"aadvantage": "AA_AADVANTAGE",
# ์‹ ์šฉ์นด๋“œ ํฌ์ธํŠธ
"amex": "AMEX_MR",
"์•„๋ฉ•์Šค": "AMEX_MR",
"mr": "AMEX_MR",
"membership rewards": "AMEX_MR",
"chase": "CHASE_UR",
"์ฒด์ด์Šค": "CHASE_UR",
"ur": "CHASE_UR",
"ultimate rewards": "CHASE_UR",
"citi": "CITI_TYP",
"์‹œํ‹ฐ": "CITI_TYP",
"typ": "CITI_TYP",
"thankyou": "CITI_TYP",
"capital one": "CAPITAL_ONE",
"์บํ”ผํƒˆ์›": "CAPITAL_ONE",
# ํ˜ธํ…” ํฌ์ธํŠธ
"marriott": "MARRIOTT_BONVOY",
"๋ฉ”๋ฆฌ์–ดํŠธ": "MARRIOTT_BONVOY",
"๋ณธ๋ณด์ด": "MARRIOTT_BONVOY",
"bonvoy": "MARRIOTT_BONVOY",
"hilton": "HILTON_HONORS",
"ํžํŠผ": "HILTON_HONORS",
"honors": "HILTON_HONORS",
"ihg": "IHG_REWARDS",
"hyatt": "HYATT_WOH",
"ํ•˜์–ํŠธ": "HYATT_WOH",
"woh": "HYATT_WOH",
"world of hyatt": "HYATT_WOH",
}
# =============================================================================
# ํŒŒ์‹ฑ ํ•จ์ˆ˜
# =============================================================================
def parse_asset_text(text: str) -> List[Dict[str, Any]]:
"""
ํ…์ŠคํŠธ์—์„œ ํฌ์ธํŠธ/๋งˆ์ผ๋ฆฌ์ง€ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
Args:
text: ์‚ฌ์šฉ์ž ์ž…๋ ฅ ํ…์ŠคํŠธ
Returns:
์ถ”์ถœ๋œ ์ž์‚ฐ ๋ชฉ๋ก [{"program": "...", "amount": ...}, ...]
"""
assets = []
# ์ค„ ๋‹จ์œ„ + ์‰ผํ‘œ ๋‹จ์œ„๋กœ ๋ถ„๋ฆฌ
lines = re.split(r'[\n,]', text)
for line in lines:
line = line.strip()
if not line:
continue
result = _parse_single_line(line)
if result:
# ์ค‘๋ณต ์ œ๊ฑฐ (๊ฐ™์€ ํ”„๋กœ๊ทธ๋žจ์ด๋ฉด ํ•ฉ์‚ฐ)
existing = next(
(a for a in assets if a["program"] == result["program"]),
None
)
if existing:
existing["amount"] += result["amount"]
else:
assets.append(result)
return assets
def _parse_single_line(line: str) -> Optional[Dict[str, Any]]:
"""๋‹จ์ผ ๋ผ์ธ์—์„œ ํ”„๋กœ๊ทธ๋žจ๊ณผ ์ˆ˜๋Ÿ‰ ์ถ”์ถœ."""
line_lower = line.lower()
# ์ˆซ์ž ์ถ”์ถœ (์‰ผํ‘œ ์ œ๊ฑฐ)
numbers = re.findall(r'[\d,]+', line)
if not numbers:
return None
# ๊ฐ€์žฅ ํฐ ์ˆซ์ž๋ฅผ amount๋กœ ์‚ฌ์šฉ
amount = max(int(n.replace(',', '')) for n in numbers if n.replace(',', '').isdigit())
if amount == 0:
return None
# ํ”„๋กœ๊ทธ๋žจ ์‹๋ณ„
program = None
for alias, prog_id in PROGRAM_ALIASES.items():
if alias in line_lower:
program = prog_id
break
if not program:
# ์ˆซ์ž๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ - ๋ฌธ๋งฅ์œผ๋กœ ์ถ”๋ก  ๋ถˆ๊ฐ€
return None
return {"program": program, "amount": amount}
def normalize_program_name(name: str) -> Optional[str]:
"""ํ”„๋กœ๊ทธ๋žจ ๋ณ„์นญ์„ ์ •๊ทœํ™”ํ•ฉ๋‹ˆ๋‹ค."""
name_lower = name.lower().strip()
return PROGRAM_ALIASES.get(name_lower)
# =============================================================================
# MCP Tool ํ•ธ๋“ค๋Ÿฌ
# =============================================================================
async def handle_parse_asset_text(
arguments: Dict[str, Any],
user_id: str
) -> Dict[str, Any]:
"""
user_parse_asset_text Tool ํ•ธ๋“ค๋Ÿฌ.
์‚ฌ์šฉ์ž ์ž…๋ ฅ ํ…์ŠคํŠธ์—์„œ ํฌ์ธํŠธ/๋งˆ์ผ๋ฆฌ์ง€๋ฅผ ์ž๋™ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
"""
text = arguments.get("text", "")
save_to_profile = arguments.get("save_to_profile", False)
if not text:
return {
"success": False,
"error": "ํŒŒ์‹ฑํ•  ํ…์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.",
"usage": "์˜ˆ: '๋Œ€ํ•œํ•ญ๊ณต 45000 ๋งˆ์ผ, AMEX MR 50000์ '"
}
# ํŒŒ์‹ฑ
assets = parse_asset_text(text)
if not assets:
return {
"success": False,
"error": "์ธ์‹ ๊ฐ€๋Šฅํ•œ ํฌ์ธํŠธ/๋งˆ์ผ๋ฆฌ์ง€ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.",
"tip": "ํ”„๋กœ๊ทธ๋žจ๋ช…๊ณผ ์ˆซ์ž๋ฅผ ํ•จ๊ป˜ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ์˜ˆ: '๋Œ€ํ•œํ•ญ๊ณต 45000'",
"supported_programs": list(POINT_VALUATIONS.keys())
}
# ํ”„๋กœ๊ทธ๋žจ ์ด๋ฆ„ ๋ณด๊ฐ•
for asset in assets:
prog_info = POINT_VALUATIONS.get(asset["program"], {})
asset["program_name"] = prog_info.get("name", asset["program"])
asset["currency"] = prog_info.get("currency", "USD")
result = {
"success": True,
"parsed_assets": assets,
"count": len(assets),
"parsed_at": datetime.now().isoformat(),
"tip": "user_get_asset_valuation์„ ํ˜ธ์ถœํ•˜์—ฌ ์ด ๊ฐ€์น˜๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
}
# ํ–ฅํ›„: save_to_profile์ด True๋ฉด DB์— ์ €์žฅ
if save_to_profile:
result["save_status"] = "not_implemented"
result["save_note"] = "์ž์‚ฐ ์ €์žฅ ๊ธฐ๋Šฅ์€ ํ–ฅํ›„ ๊ตฌํ˜„ ์˜ˆ์ •"
return result