Skip to content

Commit 1351f63

Browse files
authored
Merge pull request #20 from hanjuhn/main
chore: 부가세 계산 기능 수정
2 parents f918c55 + e6def55 commit 1351f63

6 files changed

Lines changed: 52 additions & 100 deletions

File tree

core/shared/constants/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
# 배송 추적 관련 키워드
2929
CUSTOMS_TRACKING_KEYWORDS = [
30-
'운송장', '배송', '통관', '조회', '추적', '배송상태', '위치', '도착', '출고', '통관번호', '운송장번호'
30+
'운송장', '통관', '조회', '추적', '배송상태', '위치', '도착', '출고', '통관번호', '운송장번호'
3131
]
3232

3333
# 의도 분류 프롬프트

core/shared/router/intent_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def intent_router(state: CustomsAgentState) -> CustomsAgentState:
7676
print(state)
7777
return state
7878

79-
# 운송장/배송 관련 키워드가 있으면 customs_tracking으로 분류
79+
# 운송장/배송 관련 키워드가 있으면 customs_tracking으로 분류 (배송은 제외)
8080
if any(keyword in current_query_lower for keyword in CUSTOMS_TRACKING_KEYWORDS):
8181
state["intent"] = "customs_tracking" # type: ignore
8282
state["messages"].append(AIMessage(content=f"의도 분류 완료: {state['intent']} (배송 추적 키워드 감지)"))

core/tariff_prediction/agent/tariff_prediction_agent.py

Lines changed: 27 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def __init__(self):
5151
'hs10_code': None,
5252
'current_step': 'scenario_selection',
5353
'session_active': False,
54-
'responses': []
54+
'responses': [],
55+
'predicted_scenario': None, # 예측된 시나리오 저장
56+
'last_user_input': None # 마지막 사용자 입력 저장
5557
}
5658

5759
# 환율 지원 국가 목록
@@ -72,7 +74,9 @@ def reset_session(self):
7274
'hs10_code': None,
7375
'current_step': 'scenario_selection',
7476
'session_active': False,
75-
'responses': []
77+
'responses': [],
78+
'predicted_scenario': None,
79+
'last_user_input': None
7680
}
7781

7882
def is_supported_country(self, country: str) -> bool:
@@ -177,6 +181,7 @@ def handle_scenario_selection(self, user_input: str) -> str:
177181
response = (
178182
"구매하신 상품 정보를 입력해 주세요!\n\n"
179183
"💡 **상품 묘사의 정확도가 높을수록 정확한 관세 예측이 가능합니다!**\n\n"
184+
"💡 **가격은 배송비를 제외하고 입력해 주세요!**\n\n"
180185
"예시:\n"
181186
"• \"아랫창은 고무로 되어있고 하얀색 운동화를 80000원에 독일에서 샀어요\"\n"
182187
"• \"인텔 i7 노트북을 150만원에 미국에서 구매했어요\"\n"
@@ -194,6 +199,7 @@ def handle_scenario_selection(self, user_input: str) -> str:
194199
response = (
195200
"구매하신 상품 정보를 입력해 주세요!\n\n"
196201
"💡 **상품 묘사의 정확도가 높을수록 정확한 관세 예측이 가능합니다!**\n\n"
202+
"💡 **가격은 배송비를 제외하고 입력해 주세요!**\n\n"
197203
"예시:\n"
198204
"• \"아랫창은 고무로 되어있고 하얀색 운동화를 80000원에 독일에서 샀어요\"\n"
199205
"• \"인텔 i7 노트북을 150만원에 미국에서 구매했어요\"\n"
@@ -213,33 +219,35 @@ def handle_scenario_selection(self, user_input: str) -> str:
213219
self.state['responses'].append(response)
214220
return response
215221

222+
def josa_으로(self, word: str) -> str:
223+
if not word:
224+
return ""
225+
last_char = word[-1]
226+
if (ord(last_char) - 44032) % 28 == 0:
227+
return word + "로"
228+
else:
229+
return word + "으로"
230+
216231
def handle_input_collection(self, user_input: str) -> str:
217-
# 컨텍스트에서 추가 정보 추출 시도
232+
# 최초 진입: 시나리오 예측
233+
if not self.state.get('scenario'):
234+
predicted = self.detect_scenario_from_input(user_input)
235+
if predicted:
236+
self.state['scenario'] = predicted
218237
enhanced_input = user_input
219-
220-
# 이전 대화에서 상품 정보가 누락된 경우 컨텍스트에서 찾기
221238
if "이전 대화:" in user_input:
222239
context_part = user_input.split("현재 질문:")[0].replace("이전 대화:", "").strip()
223240
current_part = user_input.split("현재 질문:")[1].strip() if "현재 질문:" in user_input else user_input
224-
225-
# 컨텍스트에서 상품 정보 추출 시도
226241
context_info = extract_info_from_context(context_part)
227242
if context_info:
228-
# 현재 입력에 누락된 정보를 컨텍스트에서 보완
229243
enhanced_input = merge_context_with_current(context_info, current_part)
230-
231244
parsed = self.parse_user_input(enhanced_input)
232-
233-
# 상품명이 없으면 입력 전체를 상품명으로 사용
234245
if 'product_name' not in parsed or not parsed['product_name']:
235-
# 입력에서 불필요한 키워드 제거 후 상품명으로 사용
236246
cleaned_input = user_input.strip()
237247
for keyword in ['관세', '예측', '계산', '해줘', '알려줘', '어떻게', '해주세요']:
238248
cleaned_input = cleaned_input.replace(keyword, '').strip()
239249
if cleaned_input:
240250
parsed['product_name'] = cleaned_input
241-
242-
# 필수 정보 확인
243251
missing_info = []
244252
if 'product_name' not in parsed or not parsed['product_name']:
245253
missing_info.append("상품명")
@@ -248,7 +256,6 @@ def handle_input_collection(self, user_input: str) -> str:
248256
if 'price' not in parsed or not parsed['price']:
249257
missing_info.append("상품 가격")
250258
if missing_info:
251-
# 이미 입력된 정보는 보여주고, 누락된 정보만 안내
252259
info_lines = []
253260
if 'product_name' in parsed and parsed['product_name']:
254261
info_lines.append(f"상품명: {parsed['product_name']}")
@@ -272,11 +279,8 @@ def handle_input_collection(self, user_input: str) -> str:
272279
)
273280
self.state['responses'].append(response)
274281
return response
275-
# 환율 변환 처리
276282
price = parsed['price']
277283
price_unit = parsed.get('price_unit', '원')
278-
279-
# 원화가 아닌 경우 환율 변환
280284
if price_unit != '원':
281285
try:
282286
from core.tariff_prediction.tools.get_exchange_rate_info import get_exchange_rate_api
@@ -285,23 +289,16 @@ def handle_input_collection(self, user_input: str) -> str:
285289
price = price * exchange_rate
286290
price_unit = '원'
287291
else:
288-
# 환율 조회 실패 시 기본 환율 사용
289292
if price_unit in DEFAULT_EXCHANGE_RATES:
290293
price = price * DEFAULT_EXCHANGE_RATES[price_unit]
291294
price_unit = '원'
292-
except Exception as e:
293-
print(f"[DEBUG] 환율 변환 오류: {e}")
294-
# 오류 시 기본 환율 사용
295+
except Exception:
295296
if price_unit in DEFAULT_EXCHANGE_RATES:
296297
price = price * DEFAULT_EXCHANGE_RATES[price_unit]
297298
price_unit = '원'
298-
299-
# 상태 업데이트
300299
self.state.update(parsed)
301300
self.state['price'] = price
302301
self.state['price_unit'] = price_unit
303-
304-
# step_api.py 활용
305302
req = TariffPredictionRequest(
306303
step="input",
307304
product_description=parsed['product_name'],
@@ -318,15 +315,12 @@ def handle_input_collection(self, user_input: str) -> str:
318315
self.state['hs6_candidates'] = resp.hs6_candidates
319316
self.state['current_step'] = 'hs6_selection'
320317
scenario_str = self.state.get('scenario', '')
321-
scenario_guide = f"{scenario_str}로 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
322-
323-
# 가격 표시 (원화 변환된 경우)
318+
scenario_guide = f"{self.josa_으로(scenario_str)} 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
324319
price_display = f"{price:,.0f}원"
325320
if price_unit != '원' and parsed.get('price_unit') != '원':
326321
original_price = parsed.get('price', price)
327322
original_unit = parsed.get('price_unit', price_unit)
328323
price_display = f"{original_price} {original_unit} (약 {price:,.0f}원)"
329-
330324
response = scenario_guide + f"상품묘사: {parsed['product_name']}\n국가: {parsed['country']}\n가격: {price_display}\n수량: {parsed.get('quantity', 1)}\n\nHS 코드 예측 모델로부터 HS6 코드 후보를 찾았습니다. 번호를 선택해 주세요:\n" + '\n'.join([
331325
f"{i+1}. {c['description']} (신뢰도: {c['confidence']:.1%})" for i, c in enumerate(resp.hs6_candidates or [])
332326
]) + f"\n\n💡 **위 후보 중 하나를 선택해 주세요.**\n예시: \"1번\", \"2번\", \"3번\" 등"
@@ -415,8 +409,7 @@ def handle_hs6_selection(self, user_input: str) -> str:
415409
self.state['responses'].append(response)
416410
return response
417411

418-
except Exception as e:
419-
print(f"[DEBUG] handle_hs6_selection intent detection error: {e}")
412+
except Exception:
420413
# 예외 발생 시 안내 메시지로 graceful 처리
421414
response = f"입력 처리 중 오류가 발생했습니다. 숫자를 입력하거나, 재예측을 원하시면 '다시', '재예측' 등으로 입력해 주세요."
422415
self.state['responses'].append(response)
@@ -487,68 +480,41 @@ def handle_hs10_selection(self, user_input: str) -> str:
487480

488481

489482
def _perform_hs6_reprediction(self, user_input: str) -> str:
490-
"""HS6 코드 재예측을 수행합니다."""
491483
from core.tariff_prediction.tools.parse_hs_results import parse_hs6_result
492484
from core.shared.utils.llm import get_llm
493-
494485
product_name = self.state.get('product_name')
495486
if not product_name or not isinstance(product_name, str) or not product_name.strip():
496487
response = "상품명을 알 수 없어 HS 코드 예측을 다시 시도할 수 없습니다. 처음부터 다시 입력해 주세요."
497488
self.state['responses'].append(response)
498489
return response
499-
500490
try:
501-
# 재예측을 위한 명확한 프롬프트
502-
reprediction_prompt = f"""아래 상품명과 사용자의 추가 의견을 참고하여 HS 코드 후보를 예측해주세요.
503-
504-
상품명: {product_name}
505-
사용자 추가 의견: {user_input}
506-
507-
다음 형식으로 HS 코드 후보 3개 이내를 반환하세요:
508-
1. [6자리 HS코드] (확률: [확률]%)
509-
2. [6자리 HS코드] (확률: [확률]%)
510-
3. [6자리 HS코드] (확률: [확률]%)
511-
512-
예시:
513-
1. 851770 (확률: 85.5%)
514-
2. 851712 (확률: 12.3%)
515-
3. 851713 (확률: 2.2%)"""
516-
491+
reprediction_prompt = f"""아래 상품명과 사용자의 추가 의견을 참고하여 HS 코드 후보를 예측해주세요.\n\n상품명: {product_name}\n사용자 추가 의견: {user_input}\n\n다음 형식으로 HS 코드 후보 3개 이내를 반환하세요:\n1. [6자리 HS코드] (확률: [확률]%)\n2. [6자리 HS코드] (확률: [확률]%)\n3. [6자리 HS코드] (확률: [확률]%)\n\n예시:\n1. 851770 (확률: 85.5%)\n2. 851712 (확률: 12.3%)\n3. 851713 (확률: 2.2%)"""
517492
llm = get_llm()
518493
hs6_response = llm.invoke([{"role": "user", "content": reprediction_prompt}])
519494
hs6_result = extract_llm_response(hs6_response)
520-
521-
# LLM 응답이 비어있거나 잘못된 경우 처리
522495
if not hs6_result or len(hs6_result.strip()) < 10:
523496
response = "HS 코드 예측에 실패했습니다. 상품명을 더 구체적으로 입력해 주세요."
524497
self.state['responses'].append(response)
525498
return response
526-
527-
# parse_hs6_result 함수 호출 시 예외 처리
528499
try:
529500
hs6_candidates = parse_hs6_result(hs6_result)
530501
except Exception as parse_error:
531502
print(f"[DEBUG] parse_hs6_result error: {parse_error}")
532503
response = "HS 코드 예측 결과를 처리하는 중 오류가 발생했습니다. 다시 시도해 주세요."
533504
self.state['responses'].append(response)
534505
return response
535-
536506
if not hs6_candidates:
537507
response = "HS 코드 예측에 다시 실패했습니다. 상품명을 더 구체적으로 입력해 주세요."
538508
self.state['responses'].append(response)
539509
return response
540-
541510
self.state['hs6_candidates'] = hs6_candidates
542511
scenario_str = self.state.get('scenario', '')
543-
scenario_guide = f"{scenario_str}로 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
544-
512+
scenario_guide = f"{self.josa_으로(scenario_str)} 예상하고 안내를 도와드릴게요.\n\n" if scenario_str else ""
545513
response = scenario_guide + f"상품묘사: {product_name}\n국가: {self.state.get('country','')}\n가격: {self.state.get('price',0):,}\n수량: {self.state.get('quantity',1)}\n\nHS 코드 재예측 결과입니다. 번호를 선택해 주세요:\n" + '\n'.join([
546514
f"{i+1}. {c['description']} (신뢰도: {c['confidence']:.1%})" for i, c in enumerate(hs6_candidates)
547515
]) + f"\n\n💡 **위 후보 중 하나를 선택해 주세요.**\n예시: \"1번\", \"2번\", \"3번\" 등"
548-
549516
self.state['responses'].append(response)
550517
return response
551-
552518
except Exception as e:
553519
print(f"[DEBUG] _perform_hs6_reprediction error: {e}")
554520
response = "HS 코드 재예측 중 오류가 발생했습니다. 다시 시도해 주세요."

core/tariff_prediction/tools/calculate_tariff_amount.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,16 @@ def calculate_tariff_amount(product_code: str, value: float, origin_country: str
166166

167167
# 관세 계산
168168
tax_info = calculate_tax_amount(value, item_count, shipping_cost, float(tariff_info['관세율']), krw_rate, situation)
169-
170-
# 부가가치세 계산
169+
170+
# 부가가치세 계산 (미국 $200, 기타 $150 초과 시 무조건 부과)
171171
VAT = 0
172-
if situation == '해외직구' and tax_info['tax_amount'] != 0:
172+
# USD 환율 계산
173+
total_price_krw = tax_info['total_price']
174+
total_price_usd = total_price_krw / krw_rate if krw_rate else 0
175+
vat_threshold = 200 if origin_country == '미국' else 150
176+
if total_price_usd > vat_threshold:
173177
VAT = (tax_info['total_price'] + tax_info['tax_amount']) * 0.1
174-
178+
175179
# 최종 결과
176180
result = {
177181
'HS코드': product_code,

core/tariff_prediction/tools/detect_scenario.py

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,28 @@
55

66
@tool
77
def detect_scenario_from_input(user_input: str) -> str | None:
8-
"""사용자 입력에서 시나리오를 자동 감지합니다."""
8+
"""
9+
사용자 입력에서 관세 예측 시나리오(해외직구, 해외체류 중 구매, 해외배송)를 자동으로 감지합니다.
10+
키워드 기반 우선 매칭 후, 실패 시 LLM을 사용해 감지합니다.
11+
"""
912
try:
13+
lowered = user_input.lower()
14+
# 키워드 기반 우선 매칭
15+
if any(word in lowered for word in ["여행", "직접", "휴대", "체류"]):
16+
return "해외체류 중 구매"
17+
if any(word in lowered for word in ["온라인", "쇼핑", "직구"]):
18+
return "해외직구"
19+
if any(word in lowered for word in ["배송", "택배", "운송"]):
20+
return "해외배송"
21+
# 키워드 매칭 실패 시 LLM fallback
1022
llm = get_llm()
11-
prompt = f"""다음은 관세 계산을 위한 사용자 입력입니다. 이 입력이 어떤 시나리오에 해당하는지 판단해주세요.
12-
13-
시나리오 종류:
14-
1. 해외직구: 온라인 쇼핑몰에서 해외 상품을 구매하는 경우
15-
2. 해외체류 중 구매: 해외 여행 중에 직접 구매하여 휴대품으로 가져오는 경우
16-
3. 해외배송: 해외에서 한국으로 택배나 운송을 통해 배송받는 경우
17-
18-
사용자 입력: "{user_input}"
19-
20-
위 입력을 분석하여 다음 중 하나로 답변해주세요:
21-
- "해외직구"
22-
- "해외체류 중 구매"
23-
- "해외배송"
24-
25-
답변:"""
23+
prompt = f"""다음은 관세 계산을 위한 사용자 입력입니다. 이 입력이 어떤 시나리오에 해당하는지 판단해주세요.\n\n시나리오 종류:\n1. 해외직구: 온라인 쇼핑몰에서 해외 상품을 구매하는 경우\n2. 해외체류 중 구매: 해외 여행 중에 직접 구매하여 휴대품으로 가져오는 경우 \n3. 해외배송: 해외에서 한국으로 택배나 운송을 통해 배송받는 경우\n\n사용자 입력: \"{user_input}\"\n\n위 입력을 분석하여 다음 중 하나로 답변해주세요:\n- \"해외직구\"\n- \"해외체류 중 구매\" \n- \"해외배송\"\n\n답변:"""
2624
response = llm.invoke([HumanMessage(content=prompt)])
2725
result = str(response.content) if hasattr(response, 'content') else str(response)
28-
result = result.strip()
29-
30-
# 응답에서 시나리오 추출 (따옴표나 "답변:" 등의 접두사 제거)
31-
32-
# 직접 매칭 시도
33-
if result in VALID_SCENARIOS:
34-
return result
35-
36-
# 응답에서 시나리오 추출 시도
26+
result = result.strip().replace("답변:", "").replace("입니다", "").replace("에 해당합니다", "").replace(".", "").replace("\"", "").replace("'", "").strip()
3727
for scenario in VALID_SCENARIOS:
3828
if scenario in result:
3929
return scenario
40-
41-
# "답변:" 접두사 제거 후 시도
42-
if result.startswith('답변:'):
43-
clean_result = result.replace('답변:', '').strip().strip('"').strip("'")
44-
if clean_result in VALID_SCENARIOS:
45-
return clean_result
46-
4730
return None
4831
except Exception:
4932
return None

core/tariff_prediction/tools/parse_tariff_result.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ def parse_tariff_result(tariff_result: str) -> Dict[str, Any]:
8282
| **원산지** | {parsed['origin_country']} |
8383
| **상품 가격** | {formatted_price} |
8484
| **수량** | {parsed['quantity']}개 |
85-
| **배송비** | {formatted_shipping} |
8685
8786
### 💰 세금 정보
8887
| 항목 | 금액 |

0 commit comments

Comments
 (0)