insurance-chatbot / app /tools /customer_db.py
κΉ€λ―Όκ²½
fix: customer_db/db_setup μŠ€ν‚€λ§ˆ λ³€κ²½ λˆ„λ½ 컀밋
5e0068d
"""κ³„μ•½μž/계약 DB 쑰회 도ꡬ β€” SQLite3 기반."""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from app.tools.db_setup import get_connection, ensure_db_ready
from app.tools.data import PRODUCTS, _json
_STATUS_MAP = {
"active": "μœ μ§€",
"terminated": "ν•΄μ§€",
"lapsed": "μ‹€νš¨",
"expired": "만기",
}
_GENDER_MAP = {"M": "남성", "F": "μ—¬μ„±"}
# ── Input Schemas ─────────────────────────────────────────────────────────────
class ContractLookupInput(BaseModel):
customer_id: str = Field(default="", description="κ³„μ•½μž ID (예: C001)")
customer_name: str = Field(default="", description="κ³„μ•½μž 이름 (λΆ€λΆ„ 일치 검색)")
class DuplicateCheckInput(BaseModel):
customer_id: str = Field(..., description="κ³„μ•½μž ID (예: C001)")
product_code: str = Field(..., description="κ°€μž… 확인할 μƒν’ˆ μ½”λ“œ (예: B00115023)")
class CustomerSearchInput(BaseModel):
name: str = Field(default="", description="κ³„μ•½μž 이름 (λΆ€λΆ„ 일치)")
age_min: int = Field(default=0, ge=0, le=120, description="μ΅œμ†Œ λ‚˜μ΄")
age_max: int = Field(default=200, ge=0, le=200, description="μ΅œλŒ€ λ‚˜μ΄ (200이면 μ œν•œ μ—†μŒ)")
gender: str = Field(default="", description="성별 ν•„ν„° (M/F, 빈 값이면 전체)")
# ── Helpers ───────────────────────────────────────────────────────────────────
def _rows_to_dicts(rows) -> list[dict[str, Any]]:
return [dict(r) for r in rows]
def _safe_customer(d: dict[str, Any]) -> dict[str, Any]:
"""κ°œμΈμ •λ³΄(phone λ“±)λ₯Ό μ œμ™Έν•œ λ³΄ν—˜ μš©μ–΄ 기반 κ³„μ•½μž μ •λ³΄λ§Œ λ°˜ν™˜."""
return {
"κ³„μ•½μžID": d.get("customer_id", ""),
"이름": d.get("name", ""),
"λ‚˜μ΄": d.get("age", ""),
"성별": _GENDER_MAP.get(d.get("gender", ""), d.get("gender", "")),
}
def _safe_contract(d: dict[str, Any]) -> dict[str, Any]:
"""계약 정보λ₯Ό λ³΄ν—˜ μš©μ–΄ ν•„λ“œλͺ…μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ λ°˜ν™˜."""
status = d.get("status", "")
result: dict[str, Any] = {
"μƒν’ˆλͺ…": d.get("product_name", ""),
"ν”Όλ³΄ν—˜μž": d.get("insured_name", ""),
"κ³„μ•½μƒνƒœ": _STATUS_MAP.get(status, status),
"계약일": d.get("start_date", ""),
"채널": d.get("channel", ""),
}
if d.get("end_date"):
result["만료일"] = d["end_date"]
if d.get("terminated_date"):
result["해지일"] = d["terminated_date"]
if d.get("renewal_date"):
result["갱신일"] = d["renewal_date"]
return result
# ── Tools ─────────────────────────────────────────────────────────────────────
@tool(args_schema=ContractLookupInput)
def customer_contract_lookup(customer_id: str = "", customer_name: str = "") -> str:
"""κ³„μ•½μž ID λ˜λŠ” μ΄λ¦„μœΌλ‘œ κΈ°μ‘΄ 계약 λͺ©λ‘μ„ μ‘°νšŒν•©λ‹ˆλ‹€."""
ensure_db_ready()
conn = get_connection()
if not customer_id and not customer_name:
return _json({"error": "customer_id λ˜λŠ” customer_name 쀑 ν•˜λ‚˜λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”."})
if customer_id:
cust = conn.execute("SELECT * FROM customers WHERE customer_id = ?", (customer_id,)).fetchone()
else:
cust = conn.execute("SELECT * FROM customers WHERE name LIKE ?", (f"%{customer_name}%",)).fetchone()
if not cust:
return _json({"error": f"κ³„μ•½μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. (검색: {customer_id or customer_name})"})
cust_dict = dict(cust)
cid = cust_dict["customer_id"]
raw_contracts = [dict(c) for c in conn.execute(
"SELECT * FROM contracts WHERE customer_id = ? ORDER BY start_date DESC", (cid,)
).fetchall()]
contract_list = [_safe_contract(c) for c in raw_contracts]
active_count = sum(1 for c in raw_contracts if c["status"] == "active")
return _json({
"κ³„μ•½μž": _safe_customer(cust_dict),
"계약λͺ©λ‘": contract_list,
"μ΄κ³„μ•½μˆ˜": len(contract_list),
"μœ μ§€κ³„μ•½μˆ˜": active_count,
})
@tool(args_schema=DuplicateCheckInput)
def duplicate_enrollment_check(customer_id: str, product_code: str) -> str:
"""κ³„μ•½μžμ˜ κΈ°μ‘΄ 계약을 기반으둜 νŠΉμ • μƒν’ˆ μΆ”κ°€ κ°€μž… κ°€λŠ₯ μ—¬λΆ€λ₯Ό νŒλ‹¨ν•©λ‹ˆλ‹€."""
ensure_db_ready()
conn = get_connection()
cust = conn.execute("SELECT * FROM customers WHERE customer_id = ?", (customer_id,)).fetchone()
if not cust:
return _json({"error": f"κ³„μ•½μž '{customer_id}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."})
product = PRODUCTS.get(product_code)
if not product:
return _json({"error": f"μƒν’ˆ '{product_code}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."})
active_contracts = _rows_to_dicts(conn.execute("SELECT * FROM contracts WHERE customer_id = ? AND status = 'active'", (customer_id,)).fetchall())
rules = _rows_to_dicts(conn.execute("SELECT * FROM enrollment_rules WHERE product_code = ?", (product_code,)).fetchall())
blockers, warnings = [], []
cust_age = dict(cust)["age"]
min_age, max_age = product.get("min_age", 0), product.get("max_age", 999)
if not (min_age <= cust_age <= max_age):
blockers.append(f"λ‚˜μ΄ μ œν•œ: {min_age}~{max_age}μ„Έ κ°€μž… κ°€λŠ₯, κ³„μ•½μž λ‚˜μ΄ {cust_age}μ„Έ")
for rule in rules:
rtype, rval = rule["rule_type"], rule["rule_value"]
if rtype == "max_concurrent":
same = [c for c in active_contracts if c["product_code"] == product_code]
limit = int(rval) if rval else 1
if len(same) >= limit:
blockers.append(f"동일 μƒν’ˆ 쀑볡 κ°€μž… λΆˆκ°€: ν˜„μž¬ {len(same)}건 (μ΅œλŒ€ {limit}건)")
elif rtype == "same_category_limit":
cat_key = product.get("category", "").split("/")[0]
same_cat = [c for c in active_contracts if cat_key in PRODUCTS.get(c["product_code"], {}).get("category", "")]
limit = int(rval) if rval else 1
if len(same_cat) >= limit:
blockers.append(f"동일 μΉ΄ν…Œκ³ λ¦¬({cat_key}) ν•œλ„ 초과: ν˜„μž¬ {len(same_cat)}건 (μ΅œλŒ€ {limit}건)")
elif same_cat:
warnings.append(f"동일 μΉ΄ν…Œκ³ λ¦¬({cat_key}) {len(same_cat)}건 μœ μ§€ 쀑 (ν•œλ„ {limit}건)")
elif rtype == "prerequisite":
if rval and rval.startswith("requires_active:"):
req_code = rval.split(":")[1]
has = any(c["product_code"] == req_code and c["status"] == "active" for c in active_contracts)
if not has:
blockers.append(f"μ „μ œμ‘°κ±΄ λ―ΈμΆ©μ‘±: {rule['rule_description']}")
eligible = len(blockers) == 0
return _json({
"κ³„μ•½μž": _safe_customer(dict(cust)),
"μƒν’ˆ": {"μƒν’ˆλͺ…": product["name"]},
"κ°€μž…κ°€λŠ₯": eligible,
"μ œν•œμ‚¬μœ ": blockers,
"μ°Έκ³ ": warnings,
"μš”μ•½": "κ°€μž… κ°€λŠ₯ν•©λ‹ˆλ‹€." if eligible else f"κ°€μž… λΆˆκ°€: {'; '.join(blockers)}",
})
@tool(args_schema=CustomerSearchInput)
def customer_search(name: str = "", age_min: int = 0, age_max: int = 200, gender: str = "") -> str:
"""쑰건(이름, λ‚˜μ΄, 성별)으둜 κ³„μ•½μžλ₯Ό κ²€μƒ‰ν•©λ‹ˆλ‹€."""
ensure_db_ready()
conn = get_connection()
conditions, params = ["1=1"], []
if name:
conditions.append("name LIKE ?")
params.append(f"%{name}%")
if age_min:
conditions.append("age >= ?")
params.append(age_min)
if age_max and age_max < 200:
conditions.append("age <= ?")
params.append(age_max)
if gender:
conditions.append("gender = ?")
params.append(gender.upper())
query = f"SELECT * FROM customers WHERE {' AND '.join(conditions)}"
rows = conn.execute(query, params).fetchall()
return _json({"κ³„μ•½μžλͺ©λ‘": [_safe_customer(dict(r)) for r in rows], "총인원": len(rows)})
TOOLS = [
customer_contract_lookup, duplicate_enrollment_check, customer_search,
]