Skip to content

Commit 9237f0a

Browse files
authored
Merge pull request #14 from hanjuhn/main
chore: parser 수정
2 parents de83fe2 + 104cb3f commit 9237f0a

6 files changed

Lines changed: 115 additions & 72 deletions

File tree

core/shared/router/intent_router.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,25 @@ def intent_router(state: CustomsAgentState) -> CustomsAgentState:
88
llm = get_llm()
99

1010
classification_prompt = """
11-
다음 사용자 쿼리를 분석하여 정확히 하나의 카테고리로 분류해주세요:
11+
다음 사용자 쿼리를 아래 세 카테고리 중 하나로 분류하세요.
1212
13-
1. customs_tracking: 통관 조회, 운송장 추적, 배송 상태 관련
14-
2. tariff_prediction: 관세 계산, 세율 문의, 관세 예측 관련
15-
3. qna: 일반적인 관세/통관 관련 질문, 법령 문의, 절차 안내
13+
1. customs_tracking: 운송장, 배송, 통관, 조회, 추적, 배송상태, 위치, 도착, 출고, 통관번호, 운송장번호 등
14+
2. tariff_prediction: 관세, 세금, 세율, 관세 계산, 관세 예측, 세금 얼마, 관세 얼마, 관세 계산해줘, 관세 예측해줘, 세금 예측, 예상 관세, 뭘 샀어, 뭐 샀어, 샀어, 구매 등
15+
3. qna: 관세청 정보, 전화번호, 법령, 수입/수출 절차, 일반 안내, 기타 FAQ
1616
1717
# 예시
1818
- "관세 예측해줘" → tariff_prediction
1919
- "관세 계산해줘" → tariff_prediction
20-
- "관세 알려줘" → tariff_prediction
2120
- "이 물건의 세금이 얼마나 나올까?" → tariff_prediction
21+
- "노트북 샀어" → tariff_prediction
22+
- "미국에서 뭐 샀어" → tariff_prediction
2223
- "운송장 번호 123456 어디쯤이야?" → customs_tracking
24+
- "배송이 어디까지 왔어?" → customs_tracking
2325
- "통관 진행 상황 알려줘" → customs_tracking
2426
- "관세청 전화번호 알려줘" → qna
2527
- "수입 절차가 궁금해" → qna
28+
- "관세청 홈페이지 알려줘" → qna
29+
- "관세법 제12조가 뭐야?" → qna
2630
2731
사용자 쿼리: {query}
2832

core/tariff_prediction/agent/step_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from core.shared.utils.llm import get_llm
77

88
def tariff_prediction_step_api(req: TariffPredictionRequest) -> TariffPredictionResponse:
9-
step = req.stepㄴ
9+
step = req.step
1010
# Step 자동 분류: step이 'auto'이거나 비어 있으면 LLM으로 분류
1111
if not step or step == 'auto':
1212
llm = get_llm()
@@ -36,7 +36,7 @@ def tariff_prediction_step_api(req: TariffPredictionRequest) -> TariffPrediction
3636
return TariffPredictionResponse(
3737
step="hs10_select",
3838
hs10_candidates=hs10_candidates,
39-
message="해당하는 HS10 코드를 선택해 주세요."
39+
message="HS10 코드 후보를 선택해 주세요."
4040
)
4141
elif step == "hs10_select":
4242
# HS10 코드, 국가, 가격 등 입력받아 관세 계산
@@ -63,6 +63,6 @@ def tariff_prediction_step_api(req: TariffPredictionRequest) -> TariffPrediction
6363
)
6464
else:
6565
return TariffPredictionResponse(
66-
step="input",
66+
step="hs6_select",
6767
message="잘못된 요청입니다. 상품 설명을 입력해 주세요."
6868
)

core/tariff_prediction/tools/calculate_tariff_amount.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def calculate_tariff_amount(product_code: str, value: float, origin_country: str
130130
if cur_unit is None:
131131
return f"환율 정보를 찾을 수 없습니다. 국가: {origin_country}"
132132
usd_rate = get_exchange_rate_api(cur_unit, situation)
133+
# 환율이 None이면 더미값(1300.0)으로 대체
134+
if usd_rate is None:
135+
usd_rate = 1300.0
133136

134137
# 관세 계산
135138
tax_info = calculate_tax_amount(value, item_count, shipping_cost, float(tariff_info['관세율']), usd_rate, situation)

core/tariff_prediction/tools/parse_hs_results.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,20 @@ def parse_hs6_result(hs6_result: str) -> List[Dict]:
1313
lines = hs6_result.strip().split('\n')
1414
for line in lines:
1515
if line.strip() and any(char.isdigit() for char in line):
16-
# "1. 8471.30.00 (확률: 85%)" 형태 파싱
17-
match = re.search(r'(\d+)\.\s*([0-9]{4}\.[0-9]{2}\.[0-9]{2})\s*\(확률:\s*(\d+)%\)', line)
16+
# "1. 851770 (확률: 98.02%)" 형태 파싱
17+
match = re.search(r'(\d+)\.\s*([0-9]{6})\s*\(확률:\s*([0-9.]+)%\)', line)
1818
if match:
1919
rank = int(match.group(1))
2020
code = match.group(2)
21-
confidence = int(match.group(3)) / 100.0
22-
23-
# HS6 코드로 변환 (첫 6자리)
24-
hs6_code = '.'.join(code.split('.')[:2])
25-
21+
confidence = float(match.group(3)) / 100.0
22+
hs6_code = code[:4] + '.' + code[4:]
2623
candidates.append({
2724
'code': hs6_code,
2825
'description': f'HS6 코드 {hs6_code}',
2926
'confidence': confidence,
3027
'full_code': code
3128
})
32-
33-
# 파싱 실패 시 더미 데이터 반환
34-
if not candidates:
35-
candidates = [
36-
{'code': '8471.30', 'description': '노트북 컴퓨터', 'confidence': 0.95},
37-
{'code': '8471.40', 'description': '데스크톱 컴퓨터', 'confidence': 0.85},
38-
{'code': '8471.50', 'description': '서버 컴퓨터', 'confidence': 0.75}
39-
]
40-
29+
print(f"[DEBUG] parse_hs6_result candidates: {candidates}")
4130
return candidates
4231

4332
@tool
Lines changed: 95 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,104 @@
11
from typing import Dict, Any
22
import re
33
from langchain_core.tools import tool
4-
4+
from core.shared.utils.llm import get_llm
5+
import json
56
from core.tariff_prediction.constants import SUPPORTED_COUNTRIES, REMOVE_KEYWORDS, PRICE_PATTERNS, QUANTITY_PATTERNS
67

7-
@tool
8-
def parse_user_input(user_input: str) -> Dict[str, Any]:
9-
"""자연어 입력을 파싱하여 상품 정보를 추출합니다."""
8+
def parse_user_input_rule(user_input: str) -> Dict[str, Any]:
109
parsed = {}
11-
12-
# 가격 정보 추출 (숫자 + 원/달러/엔/위안 등)
10+
# 가격 정보 추출 (만원, 천원, 원, 달러, 엔, 위안 등)
11+
price = None
1312
for pattern in PRICE_PATTERNS:
14-
matches = re.findall(pattern, user_input)
15-
if matches:
16-
price_str = matches[0].replace(',', '')
17-
if '만원' in user_input:
18-
parsed['price'] = float(price_str) * 10000
19-
elif '천원' in user_input:
20-
parsed['price'] = float(price_str) * 1000
21-
else:
22-
parsed['price'] = float(price_str)
13+
match = re.search(pattern, user_input)
14+
if match:
15+
price_str = match.group(1).replace(',', '')
16+
unit = match.group(2) if len(match.groups()) > 1 else ''
17+
try:
18+
price = float(price_str)
19+
if '만' in unit:
20+
price *= 10000
21+
elif '천' in unit:
22+
price *= 1000
23+
parsed['price'] = price
24+
break
25+
except Exception:
26+
continue
27+
# 수량 정보 추출 (숫자+개, 한 개, 두 개 등)
28+
quantity = None
29+
for pattern in QUANTITY_PATTERNS + [r'([한두세네]) ?개']:
30+
match = re.search(pattern, user_input)
31+
if match:
32+
try:
33+
if match.group(1).isdigit():
34+
quantity = int(match.group(1))
35+
else:
36+
h2n = {'한':1, '두':2, '세':3, '네':4}
37+
quantity = h2n.get(match.group(1), 1)
38+
parsed['quantity'] = quantity
39+
break
40+
except Exception:
41+
continue
42+
# 국가 정보 추출 (미국에서, 일본에서 등 조사 포함)
43+
country = None
44+
for c in SUPPORTED_COUNTRIES.keys():
45+
if c in user_input:
46+
country = c
47+
parsed['country'] = c
2348
break
24-
25-
# 수량 정보 추출
26-
for pattern in QUANTITY_PATTERNS:
27-
matches = re.findall(pattern, user_input)
28-
if matches:
29-
parsed['quantity'] = int(matches[0])
49+
elif c + '에서' in user_input:
50+
country = c
51+
parsed['country'] = c
3052
break
31-
32-
# 국가 정보 추출
33-
countries = list(SUPPORTED_COUNTRIES.keys())
34-
for country in countries:
35-
if country in user_input:
36-
parsed['country'] = country
37-
break
38-
39-
# 상품 묘사 추출 (가격, 수량, 국가 정보를 제외한 나머지 부분)
40-
# 먼저 가격, 수량, 국가 관련 키워드를 제거
41-
cleaned_input = user_input
42-
for pattern in PRICE_PATTERNS + QUANTITY_PATTERNS:
43-
cleaned_input = re.sub(pattern, '', cleaned_input)
44-
45-
for country in countries:
46-
cleaned_input = cleaned_input.replace(country, '')
47-
48-
# 일반적인 키워드 제거
49-
for keyword in REMOVE_KEYWORDS:
50-
cleaned_input = cleaned_input.replace(keyword, '')
51-
52-
# 상품 묘사로 사용할 부분 추출
53-
cleaned_input = cleaned_input.strip()
54-
if cleaned_input and len(cleaned_input) > 2: # 의미있는 길이인 경우만
55-
parsed['product_name'] = cleaned_input
56-
57-
return parsed
53+
# 상품명/묘사 추출
54+
cleaned = user_input
55+
for pattern in PRICE_PATTERNS + QUANTITY_PATTERNS + [r'([한두세네]) ?개']:
56+
cleaned = re.sub(pattern, '', cleaned)
57+
if country:
58+
cleaned = cleaned.replace(country, '')
59+
cleaned = cleaned.replace(country + '에서', '')
60+
for keyword in REMOVE_KEYWORDS + ['샀어요', '구매', '예측해줘', '관세', '예측', '해줘']:
61+
cleaned = cleaned.replace(keyword, '')
62+
cleaned = cleaned.strip()
63+
if cleaned and len(cleaned) > 1:
64+
parsed['product_name'] = cleaned
65+
return parsed
66+
67+
@tool
68+
def parse_user_input(user_input: str) -> Dict[str, Any]:
69+
"""자연어 입력을 LLM으로 파싱하여 상품 정보를 추출합니다. 실패 시 rule 기반 파싱을 fallback으로 사용합니다."""
70+
prompt = f"""
71+
아래는 관세 예측을 위한 사용자 입력입니다. 입력에서 다음 정보를 추출해 JSON으로 반환하세요.
72+
- product_name: 상품명 또는 상품 설명 (예: 노트북, 운동화, 블루투스 이어폰)
73+
- country: 구매 국가 (예: 미국, 일본, 독일 등)
74+
- price: 상품 가격(숫자만, 단위는 원)
75+
- quantity: 수량(숫자, 없으면 1)
76+
77+
입력: "{user_input}"
78+
79+
반환 예시:
80+
{{
81+
"product_name": "노트북",
82+
"country": "미국",
83+
"price": 1500000,
84+
"quantity": 1
85+
}}
86+
87+
반드시 위와 같은 JSON만 반환하세요.
88+
"""
89+
try:
90+
llm = get_llm()
91+
response = llm.invoke([{"role": "user", "content": prompt}])
92+
json_str = response.content if hasattr(response, 'content') else str(response)
93+
if not isinstance(json_str, str):
94+
raise ValueError('LLM 응답이 문자열이 아님')
95+
json_start = json_str.find('{')
96+
json_end = json_str.rfind('}') + 1
97+
parsed = json.loads(json_str[json_start:json_end])
98+
# 값이 하나라도 있으면 반환
99+
if parsed and (parsed.get('product_name') or parsed.get('country') or parsed.get('price')):
100+
return parsed
101+
except Exception:
102+
pass
103+
# 실패 시 rule 기반 파싱
104+
return parse_user_input_rule(user_input)

0 commit comments

Comments
 (0)