Skip to content

Commit 4864897

Browse files
authored
Merge pull request #6 from Amore-Innovation/feature/rag-db-v4
feat: 랭그래프 에이전트 상태 관리 및 브랜드 톤 노드 추가
2 parents c902e04 + 84ed653 commit 4864897

17 files changed

Lines changed: 548 additions & 228 deletions

app/agent/graph.py

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import operator
12
from typing import TypedDict, Optional, Any, List
23
from langgraph.graph import StateGraph, END
34
from app.agent.nodes.retrieval import retrieve
45
from app.agent.nodes.query_transform import transform_query
56
from app.agent.nodes.generation import generate
67
from app.agent.nodes.grading import grade_message, grade_documents
8+
from app.agent.nodes.brand_mapping import brand_mapping
79
from langchain_core.documents import Document
810

911
class AgentState(TypedDict):
@@ -14,6 +16,9 @@ class AgentState(TypedDict):
1416
trigger_context: Optional[dict]
1517
trigger_data: Optional[dict] # 트리거 데이터 청크
1618
status: Optional[str]
19+
product_contexts: List[dict]
20+
review_contexts: List[dict]
21+
grade_style_loop_count: int
1722

1823
question: str
1924
generation: str # 생성된 메시지
@@ -25,61 +30,60 @@ class AgentState(TypedDict):
2530
grade_style_loop_count: int
2631
feedback: str
2732

28-
def build_graph():
2933

30-
# Conditional Edge for Document Grading
31-
def decide_document(state):
32-
"""문서 검색 로직
33-
1. 통과 -> 메시지 생성
34-
2. 탈락 -> 쿼리 수정 및 문서 재검색
35-
3. 탈락 2회 이상 -> 메시지 생성 (무한루프 방지)"""
36-
37-
grade = state.get("relevance_grade")
38-
grade_document_loop_count = state.get("grade_document_loop_count", 0)
34+
# --- 1. 문서 검색 조건부 로직 (최대 2회) ---
35+
def decide_document(state):
36+
"""문서 검색 로직
37+
1. 통과 -> 메시지 생성
38+
2. 탈락 -> 쿼리 수정 및 문서 재검색
39+
3. 탈락 2회 이상 -> 메시지 생성 (무한루프 방지)"""
40+
41+
grade = state.get("relevance_grade")
42+
grade_document_loop_count = state.get("grade_document_loop_count", 0)
43+
44+
if grade == 'yes':
45+
print("검수 통과 -> 메시지 생성으로 넘어갑니다.")
46+
return "generate"
3947

40-
if grade == 'yes':
41-
print("검수 통과 -> 메시지 생성으로 넘어갑니다.")
42-
return "generate"
48+
if grade_document_loop_count >= 2:
49+
print("재탐색 한계 도달 -> 문서 재검색 없이 메시지 생성으로 넘어갑니다.")
50+
return "generate"
4351

44-
if grade_document_loop_count >= 2:
45-
print("재탐색 한계 도달 -> 문서 재검색 없이 메시지 생성으로 넘어갑니다.")
46-
return "generate"
52+
print("검수 탈락 -> 쿼리 재작성 및 문서 재검색")
53+
return "transform_query"
4754

48-
print("검수 탈락 -> 쿼리 재작성 및 문서 재검색")
49-
return "modify_query"
55+
# --- 2. 메시지 생성 조건부 로직 (최대 2회) ---
56+
def decide_generation(state):
57+
"""메시지 생성 로직
58+
1. 통과 -> 최종 메시지 출력
59+
2. 탈락 -> 피드백 반영해 메시지 재생성
60+
3. 탈락 2회 이상 -> 최종 메시지 출력 (무한루프 방지)"""
5061

51-
52-
53-
# Conditional Edge for Message Grading
54-
def decide_generation(state):
55-
"""메시지 생성 로직
56-
1. 통과 -> 최종 메시지 출력
57-
2. 탈락 -> 피드백 반영해 메시지 재생성
58-
3. 탈락 2회 이상 -> 최종 메시지 출력 (무한루프 방지)"""
62+
grade = state.get("style_grade")
63+
grade_style_loop_count = state.get("grade_style_loop_count", 0)
5964

60-
grade = state.get("style_grade")
61-
grade_style_loop_count = state.get("grade_style_loop_count", 0)
65+
if grade == 'yes':
66+
print("검수 통과 -> 메시지 생성 완료.")
67+
return "brand_mapping"
6268

63-
if grade == 'yes':
64-
print("검수 통과 -> 메시지 생성 완료.")
65-
return END
69+
if grade_style_loop_count >= 2:
70+
print("재생성 한계 도달 -> 메시지 재생성 없이 출력.")
71+
return "brand_mapping"
6672

67-
if grade_style_loop_count >= 2:
68-
print("재생성 한계 도달 -> 메시지 재생성 없이 출력.")
69-
return END
70-
71-
print("검수 탈락 -> 피드백 반영하여 메시지 재생성.")
72-
return "generate"
73+
print("검수 탈락 -> 피드백 반영하여 메시지 재생성.")
74+
return "generate"
7375

7476

75-
77+
# --- 3. 그래프 빌더 ---
78+
def build_graph():
7679
workflow = StateGraph(AgentState)
7780

7881
workflow.add_node("retrieve", retrieve)
7982
workflow.add_node("grade_documents", grade_documents)
8083
workflow.add_node("transform_query", transform_query)
8184
workflow.add_node("generate", generate)
8285
workflow.add_node("grade_message", grade_message)
86+
workflow.add_node("brand_mapping", brand_mapping)
8387

8488
# Set entry point
8589
workflow.set_entry_point("retrieve")
@@ -88,22 +92,23 @@ def decide_generation(state):
8892
"grade_documents",
8993
decide_document,
9094
{
91-
"modify_query": "transform_query",
95+
"transform_query": "transform_query",
9296
"generate": "generate"
9397
}
9498
)
9599
workflow.add_edge("transform_query", "retrieve")
96100
workflow.add_edge("generate", "grade_message")
101+
97102
workflow.add_conditional_edges(
98103
"grade_message",
99104
decide_generation,
100105
{
101106
"generate": "generate",
102-
END: END
107+
"brand_mapping": "brand_mapping"
103108
}
104109
)
105110

111+
workflow.add_edge("brand_mapping", END)
106112
return workflow.compile()
107113

108-
109-
agent_executor = build_graph()
114+
agent_executor = build_graph()

app/agent/nodes/brand_mapping.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import json
2+
from typing import Dict, Any
3+
from langchain_openai import ChatOpenAI
4+
from langchain_core.prompts import ChatPromptTemplate
5+
from langchain_core.output_parsers import StrOutputParser
6+
from app.core.config import settings
7+
from app.agent.prompts import (
8+
SULWHASOO_PROMPT, ETUDE_PROMPT, AESTURA_PROMPT,
9+
INNISFREE_PROMPT, BREADY_PROMPT
10+
)
11+
12+
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
13+
14+
async def brand_mapping(state: Dict[str, Any]):
15+
print("===== [Final Step] 브랜드 톤 매핑 =====")
16+
17+
# 1. 이전 노드에서 넘어온 데이터 확보
18+
current_msg = state.get("generation", "")
19+
product_contexts = state.get("product_contexts", [])
20+
21+
print(f"DEBUG - 넘어온 상품 컨텍스트 개수: {len(product_contexts)}")
22+
if product_contexts:
23+
print(f"DEBUG - 첫 번째 상품 데이터: {product_contexts[0]}")
24+
25+
# 2. Context 분석하여 브랜드 식별
26+
sample_product = product_contexts[0] if product_contexts else {}
27+
target_text = str(sample_product.get("브랜드", "")) + str(sample_product.get("제품명", ""))
28+
29+
# 3. 적절한 프롬프트 매칭
30+
selected_prompt = INNISFREE_PROMPT # 기본값
31+
brand_key = "COMMON"
32+
33+
if "설화수" in target_text:
34+
selected_prompt = SULWHASOO_PROMPT
35+
brand_key = "SULWHASOO"
36+
elif "에뛰드" in target_text:
37+
selected_prompt = ETUDE_PROMPT
38+
brand_key = "ETUDE"
39+
elif "에스트라" in target_text:
40+
selected_prompt = AESTURA_PROMPT
41+
brand_key = "AESTURA"
42+
elif "비레디" in target_text:
43+
selected_prompt = BREADY_PROMPT
44+
brand_key = "B.READY"
45+
elif "이니스프리" in target_text:
46+
selected_prompt = INNISFREE_PROMPT
47+
brand_key = "INNISFREE"
48+
49+
print(f"CRM 메시지 브랜드: {brand_key}")
50+
51+
# 4. 브랜드 전용 프롬프트로 최종 보정
52+
refine_template = """
53+
{brand_guideline}
54+
55+
위 브랜드 가이드라인에 맞춰 아래 [초안 메시지]를 최종 수정해줘.
56+
메시지의 핵심 내용은 유지하되, 브랜드의 목소리와 규칙(이모지, 어미 등)을 완벽하게 적용해야 해.
57+
58+
[초안 메시지]:
59+
{draft_message}
60+
"""
61+
62+
prompt = ChatPromptTemplate.from_template(refine_template)
63+
chain = prompt | llm | StrOutputParser()
64+
65+
final_msg = await chain.ainvoke({
66+
"brand_guideline": selected_prompt,
67+
"draft_message": current_msg
68+
})
69+
70+
print(f"브랜드 톤 적용 완료 ({brand_key})")
71+
72+
return {
73+
"generation": final_msg,
74+
"product_contexts": state.get("product_contexts", []),
75+
"review_contexts": state.get("review_contexts", []),
76+
"persona_data": state.get("persona_data"),
77+
"persona_id": state.get("persona_id")
78+
}

app/agent/nodes/generation.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ async def generate(state: Dict[str, Any]):
2020

2121
# retrieval 노드에서 넘겨준 documents 리스트
2222
documents = state.get("documents", [])
23+
p_contexts = state.get("product_contexts", [])
24+
r_contexts = state.get("review_contexts", [])
2325

2426
feedback = state.get("feedback", None)
2527
context = "\n".join([d.page_content for d in documents])
@@ -35,13 +37,17 @@ async def generate(state: Dict[str, Any]):
3537
chain = prompt | llm | StrOutputParser()
3638

3739
# chain.invoke는 동기 함수지만, 전체 노드는 async로 구성하는 것이 좋습니다.
38-
# invoke()가 블로킹되지 않도록 주의하거나 ainvoke 사용 권장
3940
msg = await chain.ainvoke({
4041
"user_json": json.dumps(user, ensure_ascii=False, default=str),
4142
"context": context,
4243
"feedback_note": feedback_instruction
4344
})
4445

4546
print(f"\n\n[생성된 메시지]\n{msg}\n=====")
47+
print(f"DEBUG [GradeDocs] 최종 필터링된 상품 수: {len(p_contexts)}")
4648

47-
return {"generation": msg}
49+
return {"generation": msg,
50+
"product_contexts": state.get("product_contexts", []),
51+
"review_contexts": state.get("review_contexts", []),
52+
"documents": state.get("documents", [])
53+
}

0 commit comments

Comments
 (0)