Skip to content

Commit 0b1cf64

Browse files
authored
Merge pull request #18 from hanjuhn/main
chore: 입출력 디테일 수정
2 parents 7cd6e12 + c28c929 commit 0b1cf64

5 files changed

Lines changed: 186 additions & 47 deletions

File tree

core/tariff_prediction/agent/step_api.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from core.tariff_prediction.tools.get_hs_classification import get_hs_classification
44
from core.tariff_prediction.tools.parse_hs_results import parse_hs6_result, generate_hs10_candidates
55
from core.tariff_prediction.tools.calculate_tariff_amount import calculate_tariff_amount
6+
from core.tariff_prediction.tools.parse_tariff_result import parse_tariff_result
67
from core.shared.utils.llm import get_llm
78

89
def tariff_prediction_step_api(req: TariffPredictionRequest) -> TariffPredictionResponse:
@@ -48,18 +49,27 @@ def tariff_prediction_step_api(req: TariffPredictionRequest) -> TariffPrediction
4849
"shipping_cost": req.shipping_cost,
4950
"situation": req.scenario
5051
})
51-
if isinstance(result, str):
52+
53+
# 결과를 문자열로 변환
54+
result_str = str(result)
55+
56+
# 에러 메시지인지 확인 (에러 메시지는 보통 짧고 특정 키워드를 포함)
57+
if result_str.startswith("오류") or result_str.startswith("Error") or "실패" in result_str or "오류" in result_str:
5258
# 에러 메시지
5359
return TariffPredictionResponse(
5460
step="result",
5561
calculation_result=None,
56-
message=result
62+
message=result_str
5763
)
5864
else:
65+
# 성공적인 결과 - 예쁘게 포맷팅
66+
parsed_result = parse_tariff_result(result_str)
67+
formatted_result = parsed_result['formatted_result']
68+
5969
return TariffPredictionResponse(
6070
step="result",
61-
calculation_result=result,
62-
message="관세 계산 결과입니다."
71+
calculation_result=parsed_result, # 딕셔너리 형태로 전달
72+
message=formatted_result # 포맷팅된 결과를 message에 전달
6373
)
6474
else:
6575
return TariffPredictionResponse(

core/tariff_prediction/agent/tariff_prediction_agent.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,16 @@ def handle_input_collection(self, user_input: str) -> str:
229229
enhanced_input = merge_context_with_current(context_info, current_part)
230230

231231
parsed = self.parse_user_input(enhanced_input)
232+
233+
# 상품명이 없으면 입력 전체를 상품명으로 사용
234+
if 'product_name' not in parsed or not parsed['product_name']:
235+
# 입력에서 불필요한 키워드 제거 후 상품명으로 사용
236+
cleaned_input = user_input.strip()
237+
for keyword in ['관세', '예측', '계산', '해줘', '알려줘', '어떻게', '해주세요']:
238+
cleaned_input = cleaned_input.replace(keyword, '').strip()
239+
if cleaned_input:
240+
parsed['product_name'] = cleaned_input
241+
232242
# 필수 정보 확인
233243
missing_info = []
234244
if 'product_name' not in parsed or not parsed['product_name']:
@@ -450,8 +460,14 @@ def handle_hs10_selection(self, user_input: str) -> str:
450460
)
451461
resp: TariffPredictionResponse = tariff_prediction_step_api(req)
452462
self.reset_session()
453-
if resp.calculation_result:
454-
response = f"# 🎯 관세 계산 결과\n{resp.calculation_result}\n\n{resp.message or ''}"
463+
if resp.message and "📊 관세 계산 결과" in resp.message:
464+
# 포맷팅된 결과가 message에 있음
465+
response = resp.message
466+
self.state['responses'].append(response)
467+
return response
468+
elif resp.calculation_result:
469+
# 딕셔너리 형태의 결과가 있으면 포맷팅
470+
response = resp.calculation_result.get('formatted_result', str(resp.calculation_result))
455471
self.state['responses'].append(response)
456472
return response
457473
else:
@@ -677,13 +693,11 @@ def tariff_prediction_agent(state: CustomsAgentState) -> CustomsAgentState:
677693

678694
# 컨텍스트가 있으면 쿼리와 결합
679695
if enhanced_context:
680-
enhanced_query = f"이전 대화 및 LLM 응답: {enhanced_context}\n\n현재 질문: {state['query']}"
681-
print(f"[DEBUG] Enhanced query with LLM context: {enhanced_query}")
696+
enhanced_query = f"{enhanced_context}\n\n{state['query']}"
682697
response = workflow.process_user_input(enhanced_query)
683698
else:
684699
response = workflow.process_user_input(state["query"])
685700

686-
print(f"[DEBUG] tariff_prediction_agent response: {response}")
687701

688702
state["final_response"] = response
689703
return state

core/tariff_prediction/constants/constants.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,7 @@
123123
'prediction_failed': "HS 코드 예측에 실패했습니다. 상품명을 더 구체적으로 입력해 주세요.",
124124
'reprediction_failed': "HS 코드 예측에 다시 실패했습니다. 상품명을 더 구체적으로 입력해 주세요.",
125125
'reprediction_error': "HS 코드 재예측 중 오류가 발생했습니다. 다시 시도해 주세요.",
126-
'no_hs6_code': "HS6 코드가 없어 HS10 코드 예측을 다시 시도할 수 없습니다. HS6 코드부터 다시 선택해 주세요.",
127-
'hs10_prediction_failed': "HS10 코드 예측에 실패했습니다. HS6 코드를 다시 선택해 주세요.",
128-
'hs10_reprediction_error': "HS10 코드 재예측 중 오류가 발생했습니다. 다시 시도해 주세요.",
126+
129127
'input_processing_error': "입력 처리 중 오류가 발생했습니다. 숫자를 입력하거나, 재예측을 원하시면 '다시', '재예측' 등으로 입력해 주세요.",
130128
'unknown_state': "죄송합니다. 현재 상태를 인식할 수 없습니다. 처음부터 다시 시작하겠습니다.",
131129
'calculation_failed': "계산 결과를 가져오지 못했습니다.",
Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
from typing import Dict, Any
22
from langchain_core.tools import tool
33

4+
def format_price(price_str: str) -> str:
5+
"""가격을 깔끔하게 포맷팅합니다."""
6+
try:
7+
# 숫자 부분만 추출
8+
price_str = price_str.replace('원', '').replace(',', '').strip()
9+
price = float(price_str)
10+
11+
# 정수인 경우 정수로, 소수인 경우 소수점 2자리까지
12+
if price.is_integer():
13+
return f"{int(price):,}원"
14+
else:
15+
return f"{price:,.2f}원"
16+
except:
17+
return price_str
18+
419
@tool
520
def parse_tariff_result(tariff_result: str) -> Dict[str, Any]:
621
"""관세 계산 결과를 파싱하고 포맷팅합니다."""
722
parsed = {
23+
'hs_code': '',
24+
'origin_country': '',
25+
'product_price': '',
26+
'quantity': '',
27+
'shipping_cost': '',
828
'tariff_rate': '0%',
929
'tariff_amount': '0원',
1030
'vat_amount': '0원',
1131
'total_tax': '0원',
32+
'tariff_rule': '',
1233
'fta_applied': 'No',
34+
'note': '',
1335
'formatted_result': tariff_result
1436
}
1537

@@ -18,38 +40,74 @@ def parse_tariff_result(tariff_result: str) -> Dict[str, Any]:
1840
lines = tariff_result.split('\n')
1941
for line in lines:
2042
line = line.strip()
21-
if '관세율:' in line:
43+
if 'HS코드:' in line:
44+
parsed['hs_code'] = line.split(':')[-1].strip()
45+
elif '원산지:' in line:
46+
parsed['origin_country'] = line.split(':')[-1].strip()
47+
elif '상품가격:' in line:
48+
parsed['product_price'] = line.split(':')[-1].strip()
49+
elif '수량:' in line:
50+
parsed['quantity'] = line.split(':')[-1].strip()
51+
elif '배송비:' in line:
52+
parsed['shipping_cost'] = line.split(':')[-1].strip()
53+
elif '관세율:' in line:
2254
parsed['tariff_rate'] = line.split(':')[-1].strip()
2355
elif '관세금액:' in line:
2456
parsed['tariff_amount'] = line.split(':')[-1].strip()
2557
elif '부가가치세:' in line:
2658
parsed['vat_amount'] = line.split(':')[-1].strip()
2759
elif '총 세금:' in line:
2860
parsed['total_tax'] = line.split(':')[-1].strip()
61+
elif '적용 관세 규칙:' in line:
62+
parsed['tariff_rule'] = line.split(':')[-1].strip()
2963
elif 'FTA 적용:' in line:
3064
parsed['fta_applied'] = line.split(':')[-1].strip()
65+
elif '비고:' in line:
66+
parsed['note'] = line.split(':')[-1].strip()
67+
68+
# 가격 포맷팅
69+
formatted_price = format_price(parsed['product_price'])
70+
formatted_shipping = format_price(parsed['shipping_cost'])
71+
formatted_tariff = format_price(parsed['tariff_amount'])
72+
formatted_vat = format_price(parsed['vat_amount'])
73+
formatted_total = format_price(parsed['total_tax'])
3174

3275
# 마크다운 형식의 결과 포맷팅
33-
if parsed['tariff_amount'] != '0원':
34-
formatted_result = f"""| 항목 | 금액 |
76+
formatted_result = f"""## 📊 관세 계산 결과
77+
78+
### 📦 상품 정보
79+
| 항목 | 내용 |
80+
|------|------|
81+
| **HS 코드** | `{parsed['hs_code']}` |
82+
| **원산지** | {parsed['origin_country']} |
83+
| **상품 가격** | {formatted_price} |
84+
| **수량** | {parsed['quantity']}개 |
85+
| **배송비** | {formatted_shipping} |
86+
87+
### 💰 세금 정보
88+
| 항목 | 금액 |
3589
|------|------|
3690
| **관세율** | {parsed['tariff_rate']} |
37-
| **관세금액** | {parsed['tariff_amount']} |
38-
| **부가가치세** | {parsed['vat_amount']} |
39-
| **총 세금** | {parsed['total_tax']} |
40-
| **FTA 적용** | {parsed['fta_applied']} |"""
41-
else:
42-
formatted_result = f"""| 항목 | 금액 |
91+
| **관세금액** | {formatted_tariff} |
92+
| **부가가치세** | {formatted_vat} |
93+
| **총 세금** | **{formatted_total}** |
94+
95+
### 📋 추가 정보
96+
| 항목 | 내용 |
4397
|------|------|
44-
| **관세금액** | {parsed['tariff_amount']} (면세) |
45-
| **부가가치세** | {parsed['vat_amount']} |
46-
| **총 세금** | {parsed['total_tax']} |
47-
| **FTA 적용** | {parsed['fta_applied']} |"""
98+
| **적용 관세 규칙** | {parsed['tariff_rule']} |
99+
| **FTA 적용** | {parsed['fta_applied']} |
100+
| **비고** | {parsed['note']} |"""
48101

49102
parsed['formatted_result'] = formatted_result
50103

51104
except Exception as e:
52-
# 파싱 실패 시 원본 결과 사용
53-
parsed['formatted_result'] = f"```\n{tariff_result}\n```"
105+
# 파싱 실패 시 원본 결과를 예쁘게 포맷팅
106+
formatted_result = f"""## 📊 관세 계산 결과
107+
108+
```
109+
{tariff_result}
110+
```"""
111+
parsed['formatted_result'] = formatted_result
54112

55113
return parsed

core/tariff_prediction/tools/parse_user_input.py

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
def parse_user_input_rule(user_input: str) -> Dict[str, Any]:
99
parsed = {}
10+
11+
# 상품명을 먼저 추출 (가장 중요한 정보)
12+
product_name = extract_product_name(user_input)
13+
if product_name:
14+
parsed['product_name'] = product_name
15+
1016
# 가격 정보 추출 (만원, 천원, 원, 달러, 엔, 위안 등)
1117
price = None
1218
for pattern in PRICE_PATTERNS:
@@ -24,6 +30,7 @@ def parse_user_input_rule(user_input: str) -> Dict[str, Any]:
2430
break
2531
except Exception:
2632
continue
33+
2734
# 수량 정보 추출 (숫자+개, 한 개, 두 개 등)
2835
quantity = None
2936
for pattern in QUANTITY_PATTERNS + [r'([한두세네]) ?개']:
@@ -39,6 +46,7 @@ def parse_user_input_rule(user_input: str) -> Dict[str, Any]:
3946
break
4047
except Exception:
4148
continue
49+
4250
# 국가 정보 추출 (미국에서, 일본에서 등 조사 포함)
4351
country = None
4452
for c in SUPPORTED_COUNTRIES.keys():
@@ -50,49 +58,93 @@ def parse_user_input_rule(user_input: str) -> Dict[str, Any]:
5058
country = c
5159
parsed['country'] = c
5260
break
53-
# 상품명/묘사 추출
61+
62+
return parsed
63+
64+
def extract_product_name(user_input: str) -> str:
65+
"""상품명을 추출하는 전용 함수"""
66+
# 간단한 상품명 패턴 (단일 단어 또는 짧은 구문)
67+
simple_patterns = [
68+
r'^([가-힣a-zA-Z0-9]+)$', # 단일 단어 (커피, 노트북 등)
69+
r'^([가-힣a-zA-Z0-9\s]+)$', # 단일 단어 + 공백
70+
r'([가-힣a-zA-Z0-9]+)\s*(?:을|를|이|가|의)', # 조사 앞의 단어
71+
r'(?:이|가|을|를)\s*([가-힣a-zA-Z0-9]+)', # 조사 뒤의 단어
72+
]
73+
74+
for pattern in simple_patterns:
75+
match = re.search(pattern, user_input.strip())
76+
if match:
77+
product = match.group(1).strip()
78+
if product and len(product) >= 2: # 최소 2글자 이상
79+
return product
80+
81+
# 기존 방식으로 정제
5482
cleaned = user_input
5583
for pattern in PRICE_PATTERNS + QUANTITY_PATTERNS + [r'([한두세네]) ?개']:
5684
cleaned = re.sub(pattern, '', cleaned)
57-
if country:
58-
cleaned = cleaned.replace(country, '')
59-
cleaned = cleaned.replace(country + '에서', '')
60-
for keyword in REMOVE_KEYWORDS + ['샀어요', '구매', '예측해줘', '관세', '예측', '해줘']:
85+
86+
# 국가명 제거
87+
for c in SUPPORTED_COUNTRIES.keys():
88+
cleaned = cleaned.replace(c, '')
89+
cleaned = cleaned.replace(c + '에서', '')
90+
91+
# 불필요한 키워드 제거
92+
for keyword in REMOVE_KEYWORDS + ['샀어요', '구매', '예측해줘', '관세', '예측', '해줘', '어떻게', '알려줘', '계산', '해주세요']:
6193
cleaned = cleaned.replace(keyword, '')
94+
6295
cleaned = cleaned.strip()
63-
if cleaned and len(cleaned) > 1:
64-
parsed['product_name'] = cleaned
65-
return parsed
96+
97+
# 정제된 결과가 있으면 반환
98+
if cleaned and len(cleaned) >= 2:
99+
return cleaned
100+
101+
# 마지막 수단: 입력 전체를 상품명으로 사용 (단, 너무 길지 않은 경우)
102+
if len(user_input.strip()) <= 20 and len(user_input.strip()) >= 2:
103+
return user_input.strip()
104+
105+
return ""
66106

67107
@tool
68108
def parse_user_input(user_input: str) -> Dict[str, Any]:
69109
"""자연어 입력을 LLM으로 파싱하여 상품 정보를 추출합니다. 실패 시 rule 기반 파싱을 fallback으로 사용합니다."""
110+
111+
# 간단한 입력의 경우 rule 기반 파싱을 우선 사용
112+
if len(user_input.strip()) <= 10:
113+
rule_result = parse_user_input_rule(user_input)
114+
if rule_result.get('product_name'):
115+
return rule_result
116+
70117
prompt = f"""
71118
아래는 관세 예측을 위한 사용자 입력입니다. 입력에서 다음 정보를 추출해 JSON으로 반환하세요.
72-
- product_name: 상품명 또는 상품 설명 (예: 노트북, 운동화, 블루투스 이어폰)
119+
- product_name: 상품명 또는 상품 설명 (가장 중요한 정보, 반드시 추출해야 함)
73120
- country: 구매 국가 (예: 미국, 일본, 독일 등)
74121
- price: 상품 가격(원화가 아닌 경우 원래 통화 단위 그대로 유지, 숫자만)
75122
- price_unit: 가격 단위 (원, 달러, 엔, 위안, 유로 등)
76123
- quantity: 수량(숫자, 없으면 1)
77124
78125
입력: "{user_input}"
79126
127+
주의사항:
128+
1. product_name은 반드시 추출해야 합니다. 입력이 "커피"라면 product_name은 "커피"여야 합니다.
129+
2. 입력이 단순한 상품명만 있는 경우에도 product_name을 추출하세요.
130+
3. 가격이나 국가 정보가 없어도 상품명은 반드시 추출하세요.
131+
80132
반환 예시:
81133
{{
82-
"product_name": "노트북",
83-
"country": "미국",
84-
"price": 150,
85-
"price_unit": "달러",
134+
"product_name": "커피",
135+
"country": null,
136+
"price": null,
137+
"price_unit": null,
86138
"quantity": 1
87139
}}
88140
89141
또는
90142
91143
{{
92-
"product_name": "운동화",
93-
"country": "독일",
94-
"price": 80,
95-
"price_unit": "유로",
144+
"product_name": "노트북",
145+
"country": "미국",
146+
"price": 150,
147+
"price_unit": "달러",
96148
"quantity": 1
97149
}}
98150
@@ -107,10 +159,17 @@ def parse_user_input(user_input: str) -> Dict[str, Any]:
107159
json_start = json_str.find('{')
108160
json_end = json_str.rfind('}') + 1
109161
parsed = json.loads(json_str[json_start:json_end])
110-
# 값이 하나라도 있으면 반환
111-
if parsed and (parsed.get('product_name') or parsed.get('country') or parsed.get('price')):
162+
# product_name이 있으면 반환 (가장 중요한 정보)
163+
if parsed and parsed.get('product_name'):
112164
return parsed
113165
except Exception:
114166
pass
167+
115168
# 실패 시 rule 기반 파싱
116-
return parse_user_input_rule(user_input)
169+
rule_result = parse_user_input_rule(user_input)
170+
171+
# rule 기반 파싱에서도 product_name이 없으면 입력 전체를 상품명으로 사용
172+
if not rule_result.get('product_name') and user_input.strip():
173+
rule_result['product_name'] = user_input.strip()
174+
175+
return rule_result

0 commit comments

Comments
 (0)