Skip to content

Commit ab893d1

Browse files
committed
fix: 관세 예측 기능 버그 수정
1 parent dd42be2 commit ab893d1

5 files changed

Lines changed: 304 additions & 45 deletions

File tree

app/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1-
from flask import Flask
1+
from flask import Flask, jsonify
22
from flasgger import Swagger
33
from .routes import api_blueprint
44

55
def create_app():
66
app = Flask(__name__)
77
Swagger(app)
88
app.register_blueprint(api_blueprint)
9+
10+
@app.route('/')
11+
def index():
12+
return jsonify({
13+
"message": "관세청 챗봇 API 서버",
14+
"endpoints": {
15+
"/predict": "POST - 관세 예측 및 통관 관련 질문 처리",
16+
"/apidocs": "GET - API 문서 (Swagger)"
17+
},
18+
"example": {
19+
"question": "미국에서 150만원에 노트북을 샀는데 관세가 얼마나 나올까요?"
20+
}
21+
})
22+
923
return app

core/graphs/final_agent.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# - 답변은 마크다운, 끝에 책임 한계 안내문구 필수
55

66
import re
7+
from typing import List
78
from core.shared.states.states import CustomsAgentState
89
from core.shared.utils.llm import get_llm
910
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
@@ -22,15 +23,17 @@
2223
- 통관·관세와 무관한 질문(예: 음식, 연애, 일상 상담 등)은 정중히 거절합니다.
2324
2425
[질문 분류 규칙]
25-
- 다음 키워드가 포함되면 ‘통관 관련’으로 간주합니다: 관세, 통관, HS 코드, 과세가격, 원산지, 관세법, AEO, 수입신고, 세율, 관세환급 등.
26-
- 키워드가 없더라도 질문 의미상 통관 절차·세금·신고와 직접 연관되면 ‘통관 관련’으로 처리합니다.
27-
- 위 조건에 해당하지 않으면 ‘무관한 질문’으로 간주하고 정중히 거절합니다.
26+
- 다음 키워드가 포함되면 '통관 관련'으로 간주합니다: 관세, 통관, HS 코드, 과세가격, 원산지, 관세법, AEO, 수입신고, 세율, 관세환급 등.
27+
- 키워드가 없더라도 질문 의미상 통관 절차·세금·신고와 직접 연관되면 '통관 관련'으로 처리합니다.
28+
- **tariff_prediction 에이전트에서 온 응답은 항상 통관 관련으로 간주합니다.**
29+
- 위 조건에 해당하지 않으면 '무관한 질문'으로 간주하고 정중히 거절합니다.
2830
2931
[출력 규칙]
3032
1. 전체 출력은 **마크다운(Markdown) 형식**으로 작성합니다.
3133
2. 다음 원칙을 따릅니다:
3234
- 질문이 통관/관세 관련이 **아닌 경우**: 정중히 거절하는 짧은 메시지만 출력합니다.
3335
- 질문이 통관/관세 관련인 경우: 주어진 근거 자료(qna_agent 결과)만 활용하여 요약된 정보를 제공합니다.
36+
- **tariff_prediction 에이전트에서 온 경우**: 해당 응답을 그대로 사용합니다.
3437
- 답변 이후 활용한 근거 및 조항에 대해 출처를 반드시 밝혀야 합니다.
3538
3. 답변 말미에 반드시 아래의 **책임 한계 안내 문구**를 포함해야 합니다.
3639
4. Chain-of-Thought(COT)는 내부 추론 과정에서만 활용하며, 최종 출력에는 포함하지 않습니다.
@@ -43,14 +46,39 @@ def final_agent(state: CustomsAgentState) -> CustomsAgentState:
4346
llm = get_llm()
4447
query = state.get("query", "")
4548
prev_reply = state.get("final_response", "")
49+
intent = state.get("intent", "")
50+
messages = state.get("messages", [])
51+
52+
# tariff_prediction 에이전트에서 온 경우 해당 응답을 그대로 사용
53+
if intent == "tariff_prediction" and prev_reply:
54+
return state
55+
4656
try:
47-
messages = [
48-
SystemMessage(content=SYSTEM_PROMPT),
49-
HumanMessage(content=query),
50-
AIMessage(content=prev_reply)
51-
]
52-
result = llm.invoke(messages)
57+
# 대화 히스토리를 포함한 메시지 구성
58+
from langchain_core.messages import BaseMessage
59+
llm_messages: List[BaseMessage] = [SystemMessage(content=SYSTEM_PROMPT)]
60+
61+
# 기존 대화 히스토리 추가 (최근 10개 메시지로 제한)
62+
if messages:
63+
recent_messages = messages[-10:] # 최근 10개 메시지만 사용
64+
llm_messages.extend(recent_messages)
65+
66+
# 현재 쿼리 추가
67+
llm_messages.append(HumanMessage(content=query))
68+
69+
# 이전 응답이 있으면 추가
70+
if prev_reply:
71+
llm_messages.append(AIMessage(content=prev_reply))
72+
73+
result = llm.invoke(llm_messages)
5374
state["final_response"] = str(result.content) if hasattr(result, "content") else str(result)
75+
76+
# 대화 히스토리에 현재 대화 추가
77+
if "messages" not in state:
78+
state["messages"] = []
79+
state["messages"].append(HumanMessage(content=query))
80+
state["messages"].append(AIMessage(content=state["final_response"]))
81+
5482
except Exception as e:
5583
# LLM 실패시 기존 답변 유지
5684
pass

core/shared/router/intent_router.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,65 @@
55

66
def intent_router(state: CustomsAgentState) -> CustomsAgentState:
77
"""사용자 쿼리의 의도를 분류합니다."""
8+
9+
# 이전 대화에서 관세 예측 중인지 확인
10+
messages = state.get("messages", [])
11+
is_in_tariff_session = False
12+
13+
# 최근 메시지들을 확인하여 관세 예측 세션 중인지 판단
14+
for msg in messages[-5:]: # 최근 5개 메시지 확인
15+
if hasattr(msg, 'content') and isinstance(msg.content, str):
16+
content = msg.content.lower()
17+
# 관세 예측 관련 키워드가 있거나 HS 코드 선택 메시지가 있으면 관세 예측 세션으로 판단
18+
if any(keyword in content for keyword in [
19+
'hs6 코드 후보', 'hs10 코드 후보', '번호를 선택', '관세 계산', '관세 예측',
20+
'상품묘사', '구매 국가', '상품 가격', '시나리오 선택', 'tariff_prediction'
21+
]):
22+
is_in_tariff_session = True
23+
break
24+
25+
# 이전 의도가 tariff_prediction이었는지도 확인
26+
if messages and len(messages) > 0:
27+
last_msg = messages[-1]
28+
if hasattr(last_msg, 'content') and isinstance(last_msg.content, str):
29+
if 'tariff_prediction' in last_msg.content:
30+
is_in_tariff_session = True
31+
32+
# 현재 쿼리가 숫자 선택인지 확인
33+
current_query = state["query"].strip()
34+
is_number_selection = False
35+
36+
# 숫자 선택 패턴 확인 (1번, 2번, 3번, 1, 2, 3 등)
37+
import re
38+
number_patterns = [
39+
r'^\d+번?$', # 1번, 2번, 3번
40+
r'^\d+$', # 1, 2, 3
41+
r'^\d+\.?\d*$', # 8471.60, 8517.70 등 HS 코드
42+
r'^\d{4,6}$', # 8471, 851770 등 HS 코드
43+
r'^\d{4}\.\d{2}$', # 8471.60 등 HS 코드
44+
]
45+
46+
for pattern in number_patterns:
47+
if re.match(pattern, current_query):
48+
is_number_selection = True
49+
break
50+
51+
# 관세 예측 세션 중이고 숫자 선택이면 무조건 tariff_prediction
52+
if is_in_tariff_session and is_number_selection:
53+
state["intent"] = "tariff_prediction" # type: ignore
54+
state["messages"].append(AIMessage(content=f"의도 분류 완료: {state['intent']} (세션 연속성 유지)"))
55+
print(state)
56+
return state
57+
58+
# 숫자 선택이지만 관세 예측 세션이 아닌 경우에도 tariff_prediction으로 분류
59+
# (HS 코드 직접 입력 등의 경우)
60+
if is_number_selection:
61+
state["intent"] = "tariff_prediction" # type: ignore
62+
state["messages"].append(AIMessage(content=f"의도 분류 완료: {state['intent']} (숫자 선택 감지)"))
63+
print(state)
64+
return state
65+
66+
# 일반적인 의도 분류
867
llm = get_llm()
968

1069
classification_prompt = """
@@ -40,8 +99,8 @@ def intent_router(state: CustomsAgentState) -> CustomsAgentState:
4099
intent = str(result.content).strip()
41100
if intent not in ["customs_tracking", "tariff_prediction", "qna"]:
42101
intent = "qna" # 기본값
43-
# 타입 힌트에 맞게 Literal로 제한
102+
44103
state["intent"] = intent # type: ignore
45104
state["messages"].append(AIMessage(content=f"의도 분류 완료: {intent}"))
46-
print(state) # type: ignore
105+
print(state)
47106
return state

0 commit comments

Comments
 (0)