Skip to content

Commit eccfb29

Browse files
authored
Merge branch 'dev' into feat/text-news-mcp-server
2 parents 252f2a9 + ab129e2 commit eccfb29

8 files changed

Lines changed: 170 additions & 3 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ LANGGRAPH_PORT=2024
1313
# You can get it from the OpenAI website (https://platform.openai.com/).
1414
OPENAI_API_KEY=sk...
1515

16+
1617
# Google API Key (예시에는 없지만, 필요하다면 주석 추가)
1718
GOOGLE_API_KEY=...
1819

@@ -22,3 +23,7 @@ UPSTAGE_API_KEY=...
2223
# News API Key - required to use the news API.
2324
# You can get it from the News API website (https://newsapi.org/).
2425
NEWS_API_KEY=...
26+
27+
# Groq API Key - used to access Groq LLMs such as Mixtral or LLaMA models.
28+
# Sign up and get your key from https://console.groq.com/keys
29+
GROQ_API_KEY=grq...

agents/text/modules/chains.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,24 @@
55
66
"""
77

8+
89
from langchain_core.output_parsers import StrOutputParser
910
from langchain_core.runnables import (
1011
RunnableLambda,
12+
RunnableMap,
1113
RunnablePassthrough,
1214
RunnableSerializable,
1315
)
1416

1517
from agents.text.mcp.mcp_client import scrape_news
16-
from agents.text.modules.models import get_openai_model
18+
from agents.text.modules.models import get_groq_model, get_openai_model
1719
from agents.text.modules.persona import PERSONA
1820
from agents.text.modules.prompts import (
1921
get_extraction_prompt,
2022
get_instagram_text_prompt,
2123
get_news_scraping_query_prompt,
2224
get_topic_from_news_prompt,
25+
get_persona_match_prompt,
2326
)
2427

2528

@@ -119,3 +122,89 @@ def set_instagram_text_chain() -> RunnableSerializable:
119122
| model # LLM 모델 호출
120123
| StrOutputParser() # 결과를 문자열로 변환
121124
)
125+
126+
127+
def set_instagram_text_format_check_chain() -> RunnableLambda:
128+
"""
129+
인스타그램 포맷(2200자 이하) 검사를 위한 체인을 반환합니다.
130+
131+
Returns:
132+
RunnableLambda: 텍스트 길이를 검사하는 실행 체인
133+
"""
134+
return RunnableLambda(lambda x: len(x["text"]) <= 2200)
135+
136+
137+
def set_sensitive_text_check_chain() -> RunnableLambda:
138+
def is_text_safe(x):
139+
model = get_groq_model("meta-llama/llama-guard-4-12b")
140+
try:
141+
response = model.invoke(x["text"])
142+
return "safe" in response.content.lower()
143+
except Exception as e:
144+
print(f"[ERROR] llama-guard request failed: {e}")
145+
return False
146+
147+
return RunnableLambda(is_text_safe)
148+
149+
150+
def set_text_persona_match_check_chain() -> RunnableLambda:
151+
def check_persona_match(x):
152+
model = get_openai_model()
153+
154+
text = x["text"]
155+
persona = x.get("persona", {})
156+
157+
# 다양한 타입의 persona_description 처리: dict, str, list
158+
if isinstance(persona, dict):
159+
persona_description = "\n".join([f"{k}: {v}" for k, v in persona.items()])
160+
elif isinstance(persona, list):
161+
persona_description = "\n".join([str(p) for p in persona])
162+
else:
163+
persona_description = str(persona)
164+
165+
prompt_template = get_persona_match_prompt()
166+
prompt = prompt_template.format(
167+
persona_description=persona_description, text=text
168+
)
169+
170+
try:
171+
response = model.invoke(prompt).content.strip().upper()
172+
return "YES" in response
173+
except Exception as e:
174+
print(f"[ERROR] Persona check failed: {e}")
175+
return False
176+
177+
return RunnableLambda(check_persona_match)
178+
179+
180+
def set_text_content_check_chain() -> RunnableSerializable:
181+
return (
182+
RunnablePassthrough.assign(
183+
text=lambda x: x if isinstance(x, str) else x.get("instagram_text", ""),
184+
persona=lambda x: (
185+
x.get("persona_extracted", {}) if isinstance(x, dict) else {}
186+
),
187+
)
188+
| RunnableMap(
189+
{
190+
"format_check_passed": set_instagram_text_format_check_chain(),
191+
"safety_check_passed": set_sensitive_text_check_chain(),
192+
"persona_check_passed": set_text_persona_match_check_chain(),
193+
}
194+
)
195+
| RunnableLambda(
196+
lambda results: {
197+
"text_content_checker_result": {
198+
"success": all(results.values()),
199+
"reason": [k for k, v in results.items() if not v],
200+
"content_check_passed": all(results.values()),
201+
**results,
202+
"message": (
203+
"Text content is valid."
204+
if all(results.values())
205+
else "Text content failed validation checks."
206+
),
207+
}
208+
}
209+
)
210+
)

agents/text/modules/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
기본적으로 사용할 모델 인스턴스를 설정하고 생성하고 반환시킵니다.
44
"""
55

6+
from langchain_groq import ChatGroq
67
from langchain_openai import ChatOpenAI
78

89

@@ -17,3 +18,15 @@ def get_openai_model(temperature=0.7, top_p=0.9):
1718
"""
1819
# OpenAI 모델 초기화 및 반환
1920
return ChatOpenAI(model="gpt-4o-mini", temperature=temperature, top_p=top_p)
21+
22+
23+
def get_groq_model(model_name="llama3-8b-8192", temperature=0.7, top_p=0.9):
24+
"""
25+
Groq API를 사용하는 Llama3 기반 모델을 LangChain에서 가져옵니다.
26+
사용 가능한 모델 예: "llama3-8b-8192", "llama3-70b-8192", "mixtral-8x7b-32768"
27+
"""
28+
return ChatGroq(
29+
model_name=model_name,
30+
temperature=temperature,
31+
top_p=top_p,
32+
)

agents/text/modules/nodes.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
set_extraction_chain,
1212
set_instagram_text_chain,
1313
set_topic_generation_news_chain,
14+
set_text_content_check_chain,
1415
)
1516
from agents.text.modules.persona import PERSONA
1617
from agents.text.modules.state import TextState
@@ -89,3 +90,36 @@ def execute(self, state: TextState) -> dict:
8990
return {"response": result}
9091
except Exception as e:
9192
return {"response": f"뉴스 검색 중 오류가 발생했습니다: {str(e)}"}
93+
94+
95+
class TextContentCheckNode(BaseNode):
96+
def __init__(self, **kwargs):
97+
super().__init__(**kwargs)
98+
self.chain = set_text_content_check_chain()
99+
100+
def execute(self, state: TextState) -> dict:
101+
instagram_text = state.get("instagram_text", "")
102+
103+
# instagram_text가 빈 칸(또는 공백)일 때는 모든 체크를 스킵하고 성공 결과 리턴
104+
if not instagram_text or not instagram_text.strip():
105+
result = {
106+
"text_content_checker_result": {
107+
"success": True,
108+
"reason": [],
109+
"content_check_passed": True,
110+
"format_check_passed": True,
111+
"safety_check_passed": True,
112+
"persona_check_passed": True,
113+
"message": "Skipped checks because text_content is empty.",
114+
}
115+
}
116+
state.update(result)
117+
return result
118+
119+
input_data = {
120+
"response": state.get("response", [""]),
121+
"persona_extracted": state.get("persona_extracted", {}),
122+
}
123+
result = self.chain.invoke(input_data)
124+
state.update(result)
125+
return result

agents/text/modules/prompts.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,22 @@ def get_topic_from_news_prompt():
139139
return PromptTemplate(
140140
template=prompt_template,
141141
input_variables=["news_article", "persona_details"],
142+
143+
144+
145+
def get_persona_match_prompt() -> PromptTemplate:
146+
"""
147+
Returns a prompt template to evaluate if a given text aligns with a provided persona.
148+
149+
The model must respond only with "YES" or "NO".
150+
"""
151+
template = (
152+
"The following is an Instagram text content. Please determine whether it aligns with the provided persona. "
153+
"Reply only with 'YES' if it matches well, or 'NO' if it doesn't.\n\n"
154+
"[Persona]\n{persona_description}\n\n"
155+
"[Text]\n{text}"
156+
)
157+
158+
return PromptTemplate(
159+
template=template, input_variables=["persona_description", "text"]
142160
)

agents/text/modules/state.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ class TextState(TypedDict):
2424
response: Optional[Annotated[list, add_messages]] = (
2525
None # 응답 메시지 목록 (add_messages로 주석되어 메시지 추가 기능 제공)
2626
)
27+
text_content_checker_result: (dict) # 텍스트 컨텐츠 검사 결과 전체를 담는 구조화된 필드

agents/text/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ readme = "README.md"
1111
requires-python = ">=3.13"
1212
dependencies = [
1313
"langchain-mcp-adapters>=0.0.9",
14+
"langchain-groq>=0.3.2",
1415
"langchain-openai>=0.3.12",
1516
"mcp>=1.6.0",
1617
"newsapi-python>=0.2.7",

agents/text/workflow.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
GenTextNode,
66
PersonaExtractionNode,
77
TopicFromNewsNode,
8+
TextContentCheckNode,
89
)
910
from agents.text.modules.state import TextState
1011

@@ -40,13 +41,18 @@ def build(self):
4041
# 텍스트 생성 노드 추가
4142
builder.add_node("text_generation", GenTextNode())
4243

44+
# 텍스트 컨텐츠 체커 노드 추가
45+
builder.add_node("text_content_check", TextContentCheckNode())
46+
4347
# 시작 노드에서 페르소나 추출 노드로 연결
4448
builder.add_edge("__start__", "topic_from_news")
4549
builder.add_edge("topic_from_news", "persona_extraction")
4650
# 페르소나 추출 노드에서 텍스트 생성 노드로 연결
4751
builder.add_edge("persona_extraction", "text_generation")
48-
# 텍스트 생성 노드에서 종료 노드로 연결
49-
builder.add_edge("text_generation", "__end__")
52+
# 텍스트 생성 노드에서 텍스트 컨텐츠 체커 노드로 연결
53+
builder.add_edge("text_generation", "text_content_check")
54+
# 텍스트 컨텐츠 체커 노드에서 종료 노드로 연결
55+
builder.add_edge("text_content_check", "__end__")
5056

5157
# 조건부 에지 추가 예시
5258
# builder.add_conditional_edges(

0 commit comments

Comments
 (0)