From d830d20ee301859872c42cfdfc86372fde5c9c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:16:27 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20openai=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/models.py | 39 ++++++++++++++++-------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/agents/marketing/modules/models.py b/agents/marketing/modules/models.py index 4e8842d..b2a9af6 100644 --- a/agents/marketing/modules/models.py +++ b/agents/marketing/modules/models.py @@ -3,23 +3,26 @@ 이 모듈은 LangChain에서 사용할 LLM 모델을 설정하고 반환하는 함수들을 포함합니다. """ -# from langchain_openai import ChatOpenAI +import os -# 예시 함수들입니다. 참고용으로 남겨둡니다. +from langchain_openai import ChatOpenAI -# def get_openai_model() -> ChatOpenAI: -# """ -# OpenAI 모델 인스턴스를 생성하고 반환합니다. -# -# 이 함수는 교육 마케팅 관련 작업에 적합한 설정으로 -# OpenAI의 ChatGPT 모델을 초기화합니다. -# -# Returns: -# ChatOpenAI: 초기화된 OpenAI 모델 인스턴스 -# """ -# # OpenAI 모델 초기화 및 반환 -# return ChatOpenAI( -# model_name="gpt-4", # 모델 이름 (gpt-4 사용) -# temperature=0.7, # 창의성 조절 (0.7은 균형 잡힌 창의성) -# max_tokens=1500, # 최대 토큰 수 -# ) + +def get_openai_model( + model_name: str = "gpt-3.5-turbo", temperature: float = 0.7, max_tokens: int = 4000 +) -> ChatOpenAI: + """ + OpenAI 모델 인스턴스를 생성하고 반환합니다. + + 이 함수는 교육 마케팅 관련 작업에 적합한 설정으로 + OpenAI의 ChatGPT 모델을 초기화합니다. + + Returns: + ChatOpenAI: 초기화된 OpenAI 모델 인스턴스 + """ + return ChatOpenAI( + api_key=os.environ["OPENAI_API_KEY"], + model_name=model_name, # 모델 이름 (gpt-4 사용) + temperature=temperature, # 창의성 조절 (0.7은 균형 잡힌 창의성) + max_tokens=max_tokens, # 최대 토큰 수 + ) From 3bafa2d37148a12533da88a5d9badb2687d87556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:17:28 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20contents=20state=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/state.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/agents/marketing/modules/state.py b/agents/marketing/modules/state.py index 307afd3..c90b3d0 100644 --- a/agents/marketing/modules/state.py +++ b/agents/marketing/modules/state.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Annotated, List, TypedDict +from typing import Annotated, List, Optional, TypedDict from langchain_core.messages import AIMessage, HumanMessage from langgraph.graph.message import add_messages @@ -28,3 +28,12 @@ class MarketingState(TypedDict): messages: Annotated[List[HumanMessage | AIMessage], add_messages] # 메시지 목록 # Annotated는 Python 타입 어노테이션으로, add_messages는 LangGraph에서 사용하는 특별한 함수입니다. # add_messages는 메시지 목록에 새로운 메시지를 추가할 때 이전 메시지를 유지하면서 추가하는 기능을 자동으로 처리합니다. + + +@dataclass +class ContentState(TypedDict): + input_file: str + interview_summary: Optional[str] + content: Optional[str] + notion_payload: Optional[dict] + result: Optional[str] From 61b8f1887d13c244c922e263a0fbeef0938e3ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:18:28 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20pdf=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EB=85=B8=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/chains.py | 38 ++++++++++++++++++++---- agents/marketing/modules/nodes.py | 46 +++++++++++++++++++++++++++-- agents/marketing/modules/prompts.py | 11 ++++++- agents/marketing/modules/utils.py | 31 +++++++++++++++++++ 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/agents/marketing/modules/chains.py b/agents/marketing/modules/chains.py index 7c32d31..4219fe5 100644 --- a/agents/marketing/modules/chains.py +++ b/agents/marketing/modules/chains.py @@ -4,15 +4,15 @@ 기본적으로 modules.prompt 템플릿과 modules.models 모듈을 사용하여 LangChain 체인을 생성합니다. """ -# from langchain.schema.runnable import RunnablePassthrough, RunnableSerializable +from langchain.chains.summarize import load_summarize_chain +from langchain.schema.runnable import RunnableSerializable # , RunnablePassthrough + +from agents.marketing.modules.models import get_openai_model +from agents.marketing.modules.prompts import summary_doc_prompt + # from langchain_core.output_parsers import JsonOutputParser, StrOutputParser # from langchain_core.pydantic_v1 import BaseModel, Field -# from agents.marketing.modules.models import get_openai_model -# from agents.marketing.modules.prompts import ( -# get_campaign_generation_prompt, -# get_content_creation_prompt, -# ) # 예시 함수들입니다. 참고용으로 남겨둡니다. @@ -95,3 +95,29 @@ # | model # LLM 모델 호출 # | StrOutputParser() # 결과를 문자열로 변환 # ) + + +def map_reduce_summary_chain() -> RunnableSerializable: + """ + Map Reduce 방식으로 문서를 나누어서 각각을 요약한 뒤 모두 합쳐서 한번 더 요약합니다. + """ + model = get_openai_model() + prompt = summary_doc_prompt() + chain = load_summarize_chain( + llm=model, chain_type="map_reduce", map_prompt=prompt, combine_prompt=prompt + ) + return chain + + +def stuff_summary_chain() -> RunnableSerializable: + """ + 문서를 한번에 보고 요약합니다. + """ + model = get_openai_model() + prompt = summary_doc_prompt() + chain = load_summarize_chain( + llm=model, + chain_type="stuff", + prompt=prompt, + ) + return chain diff --git a/agents/marketing/modules/nodes.py b/agents/marketing/modules/nodes.py index 2987c17..742c3f7 100644 --- a/agents/marketing/modules/nodes.py +++ b/agents/marketing/modules/nodes.py @@ -6,9 +6,15 @@ 각 노드는 execute 메서드를 구현하여 상태(state)를 입력받아 처리하고, 처리 결과를 새로운 상태 업데이트로 반환합니다. """ -# from agents.base_node import BaseNode +from langchain_core.documents import Document -# from agents.marketing.modules.chains import set_campaign_generation_chain, set_content_creation_chain +from agents.base_node import BaseNode +from agents.marketing.modules.chains import ( + map_reduce_summary_chain, + stuff_summary_chain, +) +from agents.marketing.modules.state import ContentState +from agents.marketing.modules.utils import load_pdf_documents # 아래는 구현 예정인 노드 클래스들입니다. 실제 구현 시 주석을 해제하고 사용하면 됩니다. @@ -86,3 +92,39 @@ # # # 생성된 마케팅 콘텐츠를 새로운 상태 업데이트로 반환 # return {"messages": marketing_content} + + +class DocSummarizationNode(BaseNode): + """ + 문서를 요약해주는 노드 + + 필요에따라 map reduce 방식, stuff 방식 혹은 둘 모두를 사용하여 문서를 요약하여 state에 추가합니다. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.map_reduce_summary_chain = map_reduce_summary_chain() + self.stuff_summary_chain = stuff_summary_chain() + + def load_pdf(input_file: str) -> list[Document]: + return load_pdf_documents(input_file) + + def execute(self, state: ContentState) -> dict: + """ + 주어진 상태(state)에 포함된 문서명(str)을 가지고 문서를 열람한 뒤 요약합니다. + + llm의 버전에 따라 다르지만 gpt기준 토큰 수 제한이 있어 map reduce를 먼저 수행한 뒤 stuff로 한번 더 요약합니다. + + Args: + state (ContentState): Workflow의 현재 상태. input_file 정보를 포함. + + Returns: + dict: 생성된 블로그 콘텐츠를 포함한 상태 업데이트 + """ + # TODO: pdf 외의 다른 입력도 고려하여 분기를 만들지? + + interview_doc = self.load_pdf(state.get("input_file")) + + interview_summary = self.map_reduce_summary_chain.invoke(interview_doc) + interview_summary = self.stuff_summary_chain.invoke(interview_summary) + return {"interview_summary": interview_summary} diff --git a/agents/marketing/modules/prompts.py b/agents/marketing/modules/prompts.py index 6d2b119..a62145c 100644 --- a/agents/marketing/modules/prompts.py +++ b/agents/marketing/modules/prompts.py @@ -4,7 +4,7 @@ 각 함수는 특정 작업에 맞는 프롬프트 템플릿을 생성합니다. """ -# from langchain_core.prompts import PromptTemplate +from langchain_core.prompts import PromptTemplate # 예시 함수들입니다. 참고용으로 남겨둡니다. @@ -101,3 +101,12 @@ # ... # """ # return PromptTemplate.from_template(template) + + +def summary_doc_prompt() -> PromptTemplate: + template = """ + 다음 문서를 확인하고 정리해줘. + 문서의 핵심 요약과, 주요 질문사항을 포함하여 정리해줘. + {text} + """ + return PromptTemplate(template=template, input_variables=["text"]) diff --git a/agents/marketing/modules/utils.py b/agents/marketing/modules/utils.py index f3ce638..0ead21a 100644 --- a/agents/marketing/modules/utils.py +++ b/agents/marketing/modules/utils.py @@ -13,6 +13,37 @@ # import json # from datetime import datetime +from langchain.document_loaders import PyPDFLoader +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_core.documents import Document + + +def load_pdf_documents( + file_path: str, + chunk_size: int = 5000, + chunk_overlap: int = 100, +) -> list[Document]: + """ + PDF 파일을 로드하고, 지정된 크기로 텍스트를 분할한 후 Document 객체 리스트로 반환합니다. + + RecursiveCharacterTextSplitter를 통해 긴 텍스트를 여러 chunk로 나눕니다. + 각 chunk는 downstream LLM 처리에 적합한 길이로 유지됩니다. + + Args: + file_path (str): 분할할 PDF 파일의 경로. + chunk_size (int, optional): 각 chunk의 최대 문자 수. + chunk_overlap (int, optional): 인접한 chunk 간 중복 문자 수. + + Returns: + List[Document]: 분할된 문서 조각들의 리스트. 각 Document는 page_content와 metadata를 포함합니다. + """ + loader = PyPDFLoader(file_path) + documents = loader.load() + splitter = RecursiveCharacterTextSplitter( + chunk_size=chunk_size, chunk_overlap=chunk_overlap + ) + return splitter.split_documents(documents) + # def get_message_text(msg: BaseMessage) -> str: # """메시지의 텍스트 내용을 가져옵니다.""" From a9446fb189b3cbf7deb6583fc3ce3e52b90e1173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:28:11 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20pdf=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EB=85=B8=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/chains.py | 14 +++++++++++-- agents/marketing/modules/nodes.py | 32 ++++++++++++++++++++++++++++- agents/marketing/modules/prompts.py | 13 ++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/agents/marketing/modules/chains.py b/agents/marketing/modules/chains.py index 4219fe5..5d0d4c7 100644 --- a/agents/marketing/modules/chains.py +++ b/agents/marketing/modules/chains.py @@ -6,11 +6,14 @@ from langchain.chains.summarize import load_summarize_chain from langchain.schema.runnable import RunnableSerializable # , RunnablePassthrough +from langchain_core.output_parsers import StrOutputParser from agents.marketing.modules.models import get_openai_model -from agents.marketing.modules.prompts import summary_doc_prompt +from agents.marketing.modules.prompts import ( + get_write_contents_prompt, + summary_doc_prompt, +) -# from langchain_core.output_parsers import JsonOutputParser, StrOutputParser # from langchain_core.pydantic_v1 import BaseModel, Field @@ -121,3 +124,10 @@ def stuff_summary_chain() -> RunnableSerializable: prompt=prompt, ) return chain + + +def write_blog_content_chain() -> RunnableSerializable: + model = get_openai_model() + prompt = get_write_contents_prompt() + + return prompt | model | StrOutputParser() diff --git a/agents/marketing/modules/nodes.py b/agents/marketing/modules/nodes.py index 742c3f7..0df79aa 100644 --- a/agents/marketing/modules/nodes.py +++ b/agents/marketing/modules/nodes.py @@ -12,6 +12,7 @@ from agents.marketing.modules.chains import ( map_reduce_summary_chain, stuff_summary_chain, + write_blog_content_chain, ) from agents.marketing.modules.state import ContentState from agents.marketing.modules.utils import load_pdf_documents @@ -119,7 +120,7 @@ def execute(self, state: ContentState) -> dict: state (ContentState): Workflow의 현재 상태. input_file 정보를 포함. Returns: - dict: 생성된 블로그 콘텐츠를 포함한 상태 업데이트 + dict: 요약된 문서를 포함한 상태 업데이트 """ # TODO: pdf 외의 다른 입력도 고려하여 분기를 만들지? @@ -128,3 +129,32 @@ def execute(self, state: ContentState) -> dict: interview_summary = self.map_reduce_summary_chain.invoke(interview_doc) interview_summary = self.stuff_summary_chain.invoke(interview_summary) return {"interview_summary": interview_summary} + + +class NotionWritingNode(BaseNode): + """ + 노션 컨텐츠 작성 및 업로드 노드 + + 제공받은 문서(또는 요약)을 바탕으로 Notion에 게시할 블로그 컨텐츠를 작성합니다. + 작성한 컨텐츠는 Notion API를 사용해 게시합니다. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.write_content_chain = write_blog_content_chain() + + def execute(self, state: ContentState) -> dict: + """ + 주어진 상태(state)의 문서 요약을 바탕으로 블로그 컨텐츠를 작성합니다. + + Args: + state (ContentState): Workflow의 현재 상태. input_file 정보를 포함. + + Returns: + dict: 생성된 블로그 콘텐츠를 포함한 상태 업데이트 + """ + page_content = self.write_content_chain().invoke( + {"interview_summary": state["interview_summary"]} + ) + + return {"page_content": page_content} diff --git a/agents/marketing/modules/prompts.py b/agents/marketing/modules/prompts.py index a62145c..d0ddae1 100644 --- a/agents/marketing/modules/prompts.py +++ b/agents/marketing/modules/prompts.py @@ -110,3 +110,16 @@ def summary_doc_prompt() -> PromptTemplate: {text} """ return PromptTemplate(template=template, input_variables=["text"]) + + +def get_write_contents_prompt() -> PromptTemplate: + template = """ + 너는 블로그 글을 쓰는 컨텐츠팀에서 근무하고 있어. + 아래 내용을 확인하고 ux research에 관련된 블로그 컨텐츠를 작성해줘 + 입력: {interview_summary} + + 중요 조건: + - 다양한 사람들이 이해할 수 있도록 쉽게 써야 해. + - 노션에 글을 쓸거야. 마크다운 문법과 아이콘을 활용해서 눈에 잘 띄는 컨텐츠를 만들어줘. + """ + return PromptTemplate.from_template(template) From c7e8955ccaf023416b1dfb7516e80617a0b27011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:30:31 +0900 Subject: [PATCH 05/11] =?UTF-8?q?style:=20=EC=86=8D=EC=84=B1=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=EC=A0=9C=EB=84=88=EB=9F=B4=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/nodes.py | 8 ++++---- agents/marketing/modules/prompts.py | 2 +- agents/marketing/modules/state.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/agents/marketing/modules/nodes.py b/agents/marketing/modules/nodes.py index 0df79aa..5e30abf 100644 --- a/agents/marketing/modules/nodes.py +++ b/agents/marketing/modules/nodes.py @@ -126,9 +126,9 @@ def execute(self, state: ContentState) -> dict: interview_doc = self.load_pdf(state.get("input_file")) - interview_summary = self.map_reduce_summary_chain.invoke(interview_doc) - interview_summary = self.stuff_summary_chain.invoke(interview_summary) - return {"interview_summary": interview_summary} + document_summary = self.map_reduce_summary_chain.invoke(interview_doc) + document_summary = self.stuff_summary_chain.invoke(document_summary) + return {"document_summary": document_summary} class NotionWritingNode(BaseNode): @@ -154,7 +154,7 @@ def execute(self, state: ContentState) -> dict: dict: 생성된 블로그 콘텐츠를 포함한 상태 업데이트 """ page_content = self.write_content_chain().invoke( - {"interview_summary": state["interview_summary"]} + {"document_summary": state["document_summary"]} ) return {"page_content": page_content} diff --git a/agents/marketing/modules/prompts.py b/agents/marketing/modules/prompts.py index d0ddae1..66f88e1 100644 --- a/agents/marketing/modules/prompts.py +++ b/agents/marketing/modules/prompts.py @@ -116,7 +116,7 @@ def get_write_contents_prompt() -> PromptTemplate: template = """ 너는 블로그 글을 쓰는 컨텐츠팀에서 근무하고 있어. 아래 내용을 확인하고 ux research에 관련된 블로그 컨텐츠를 작성해줘 - 입력: {interview_summary} + 입력: {document_summary} 중요 조건: - 다양한 사람들이 이해할 수 있도록 쉽게 써야 해. diff --git a/agents/marketing/modules/state.py b/agents/marketing/modules/state.py index c90b3d0..e503879 100644 --- a/agents/marketing/modules/state.py +++ b/agents/marketing/modules/state.py @@ -33,7 +33,7 @@ class MarketingState(TypedDict): @dataclass class ContentState(TypedDict): input_file: str - interview_summary: Optional[str] + document_summary: Optional[str] content: Optional[str] notion_payload: Optional[dict] result: Optional[str] From 49cfa387b2726dfdc7fd5691d41081031cb89a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:48:40 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20notion=20node=EC=97=90=20?= =?UTF-8?q?=EC=BB=A8=ED=85=90=EC=B8=A0=20=EA=B2=8C=EC=8B=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/chains.py | 10 ++++++- agents/marketing/modules/nodes.py | 42 ++++++++++++++++++++++++++- agents/marketing/modules/prompts.py | 45 +++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/agents/marketing/modules/chains.py b/agents/marketing/modules/chains.py index 5d0d4c7..f525bce 100644 --- a/agents/marketing/modules/chains.py +++ b/agents/marketing/modules/chains.py @@ -6,11 +6,12 @@ from langchain.chains.summarize import load_summarize_chain from langchain.schema.runnable import RunnableSerializable # , RunnablePassthrough -from langchain_core.output_parsers import StrOutputParser +from langchain_core.output_parsers import JsonOutputParser, StrOutputParser from agents.marketing.modules.models import get_openai_model from agents.marketing.modules.prompts import ( get_write_contents_prompt, + notion_page_creation_prompt, summary_doc_prompt, ) @@ -131,3 +132,10 @@ def write_blog_content_chain() -> RunnableSerializable: prompt = get_write_contents_prompt() return prompt | model | StrOutputParser() + + +def create_notion_page_chain() -> RunnableSerializable: + model = get_openai_model() + prompt = notion_page_creation_prompt() + + return prompt | model | JsonOutputParser() diff --git a/agents/marketing/modules/nodes.py b/agents/marketing/modules/nodes.py index 5e30abf..99b9bcf 100644 --- a/agents/marketing/modules/nodes.py +++ b/agents/marketing/modules/nodes.py @@ -6,10 +6,15 @@ 각 노드는 execute 메서드를 구현하여 상태(state)를 입력받아 처리하고, 처리 결과를 새로운 상태 업데이트로 반환합니다. """ +import json +import os + +import requests from langchain_core.documents import Document from agents.base_node import BaseNode from agents.marketing.modules.chains import ( + create_notion_page_chain, map_reduce_summary_chain, stuff_summary_chain, write_blog_content_chain, @@ -142,10 +147,34 @@ class NotionWritingNode(BaseNode): def __init__(self, **kwargs): super().__init__(**kwargs) self.write_content_chain = write_blog_content_chain() + self.create_page_chain = create_notion_page_chain() + self.parent_id = os.environ["NOTION_PARENT_ID"] + self.api_key = os.environ["NOTION_API_KEY"] + + def create_page(self, payload: dict) -> str: + # TODO: retry 코드 추가 필요 + + payload["parent"] = {"page_id": self.parent_id} + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "Notion-Version": "2022-06-28", + } + + res = requests.post( + "https://api.notion.com/v1/pages", headers=headers, json=payload + ) + + if res.status_code == 200: + return "success ✅" + else: + return f"failed ❌: {res.status_code}, {res.text}" def execute(self, state: ContentState) -> dict: """ 주어진 상태(state)의 문서 요약을 바탕으로 블로그 컨텐츠를 작성합니다. + 이후 작성한 컨텐츠를 json형태로 바꾸어 notion api를 통해 notion에 게시합니다. Args: state (ContentState): Workflow의 현재 상태. input_file 정보를 포함. @@ -156,5 +185,16 @@ def execute(self, state: ContentState) -> dict: page_content = self.write_content_chain().invoke( {"document_summary": state["document_summary"]} ) + output_str = self.create_page_chain.invoke( + {"page_content": state["page_content"]} + ) + # output_str = re.sub(r"```json|```", "", output_str).strip() # TODO: 필요한지 검증 + + notion_json = json.loads(output_str) + results = self.create_page(notion_json) - return {"page_content": page_content} + return { + "page_content": page_content, + "notion_payload": notion_json, + "results": results, + } diff --git a/agents/marketing/modules/prompts.py b/agents/marketing/modules/prompts.py index 66f88e1..0e68a8a 100644 --- a/agents/marketing/modules/prompts.py +++ b/agents/marketing/modules/prompts.py @@ -123,3 +123,48 @@ def get_write_contents_prompt() -> PromptTemplate: - 노션에 글을 쓸거야. 마크다운 문법과 아이콘을 활용해서 눈에 잘 띄는 컨텐츠를 만들어줘. """ return PromptTemplate.from_template(template) + + +def notion_page_creation_prompt() -> PromptTemplate: + template = """ + 너는 노션 페이지를 만들어야 해. 주어진 입력에 대해서 JSON body를 만들어줘. + 입력: {page_content} + + 중요 조건: + - 절대 간단한 요약 JSON을 출력하지 마. 반드시 Notion API의 완전한 JSON 구조를 따라야 해. + - 결과는 JSON 형태로만 출력해줘. 대신 제일 처음과 끝에 코드 블록(```json)은 절대 붙이지 마.** + - 임의로 내용을 요약하지 마. 있는 내용 그대로를 포함하는 JSON을 만들어야 해. + - children의 paragraph 속성 아래에 'rich_text'를 반드시 넣어야 해. + - 글의 내용은 children 아래에 여러 블록으로 적절하게 나뉘어야 해. + - 아래는 JSON 예시야 사용자 입력을 바탕으로 해당 JSON 내용을 채워줘. JSON의 형태는 엄격하게 검증해줘. + ``` + "parent": {{ "page_id": "부모 페이지 ID" }}, + "properties": {{ + "title": [ + {{ + "type": "text", + "text": {{ + "content": "페이지 제목" + }} + }} + ] + }}, + "children": [ + {{ + "object": "block", + "type": "paragraph", + "paragraph": {{ + "rich_text": [ + {{ + "type": "text", + "text": {{ + "content": "문단 내용" + }} + }} + ] + }} + }} + ] + ``` + """ + return PromptTemplate.from_template(template) From e670d4c744b47f51554fd7b3636c9a4bf353b3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:49:56 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20state=EC=97=90=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=EC=97=86=EB=8A=94=20=EC=86=8D=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/nodes.py | 1 - agents/marketing/modules/state.py | 1 - 2 files changed, 2 deletions(-) diff --git a/agents/marketing/modules/nodes.py b/agents/marketing/modules/nodes.py index 99b9bcf..f2ae0bc 100644 --- a/agents/marketing/modules/nodes.py +++ b/agents/marketing/modules/nodes.py @@ -195,6 +195,5 @@ def execute(self, state: ContentState) -> dict: return { "page_content": page_content, - "notion_payload": notion_json, "results": results, } diff --git a/agents/marketing/modules/state.py b/agents/marketing/modules/state.py index e503879..8b77efe 100644 --- a/agents/marketing/modules/state.py +++ b/agents/marketing/modules/state.py @@ -35,5 +35,4 @@ class ContentState(TypedDict): input_file: str document_summary: Optional[str] content: Optional[str] - notion_payload: Optional[dict] result: Optional[str] From 682144b031de744613205ed426ce56bdae14ed48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Fri, 30 May 2025 15:54:21 +0900 Subject: [PATCH 08/11] =?UTF-8?q?doc:=20env=20example=EC=97=90=20notion=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 00d31fc..445453a 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,6 @@ LANGSMITH_API_KEY=lsv2.... # LangSmith API key (must be replaced with real key) # You can get it from the OpenAI website (https://platform.openai.com/). OPENAI_API_KEY=sk... -# Others... \ No newline at end of file +# Others... +NOTION_PARENT_ID=... +NOTION_API_KEY=ntn_... \ No newline at end of file From 55b1aca1d637cf7ffec8d9380d63090249f271a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Sun, 1 Jun 2025 17:36:17 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/modules/nodes.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/agents/marketing/modules/nodes.py b/agents/marketing/modules/nodes.py index f2ae0bc..2a661a0 100644 --- a/agents/marketing/modules/nodes.py +++ b/agents/marketing/modules/nodes.py @@ -6,7 +6,6 @@ 각 노드는 execute 메서드를 구현하여 상태(state)를 입력받아 처리하고, 처리 결과를 새로운 상태 업데이트로 반환합니다. """ -import json import os import requests @@ -112,7 +111,7 @@ def __init__(self, **kwargs): self.map_reduce_summary_chain = map_reduce_summary_chain() self.stuff_summary_chain = stuff_summary_chain() - def load_pdf(input_file: str) -> list[Document]: + def load_pdf(self, input_file: str) -> list[Document]: return load_pdf_documents(input_file) def execute(self, state: ContentState) -> dict: @@ -182,16 +181,13 @@ def execute(self, state: ContentState) -> dict: Returns: dict: 생성된 블로그 콘텐츠를 포함한 상태 업데이트 """ - page_content = self.write_content_chain().invoke( + page_content = self.write_content_chain.invoke( {"document_summary": state["document_summary"]} ) - output_str = self.create_page_chain.invoke( - {"page_content": state["page_content"]} - ) + output_json = self.create_page_chain.invoke({"page_content": page_content}) # output_str = re.sub(r"```json|```", "", output_str).strip() # TODO: 필요한지 검증 - notion_json = json.loads(output_str) - results = self.create_page(notion_json) + results = self.create_page(output_json) return { "page_content": page_content, From 3d9e1f1ce6d762a784eaf7f466972643657aa838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Sun, 1 Jun 2025 17:37:21 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=EC=98=88=EC=8B=9C=20workflow=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/workflow.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/agents/marketing/workflow.py b/agents/marketing/workflow.py index 03d6533..377e49b 100644 --- a/agents/marketing/workflow.py +++ b/agents/marketing/workflow.py @@ -1,7 +1,8 @@ from langgraph.graph import StateGraph from agents.base_workflow import BaseWorkflow -from agents.marketing.modules.state import MarketingState +from agents.marketing.modules.nodes import DocSummarizationNode, NotionWritingNode +from agents.marketing.modules.state import ContentState # , MarketingState class MarketingWorkflow(BaseWorkflow): @@ -48,7 +49,15 @@ def build(self): # ) # 기본 에지 설정 (임시) - builder.add_edge("__start__", "__end__") + # builder.add_edge("__start__", "__end__") + + # Notion contents writer node and edge + builder.add_node("summarize_doc", DocSummarizationNode()) + builder.add_node("notion_write", NotionWritingNode()) + + builder.add_edge("__start__", "summarize_doc") + builder.add_edge("summarize_doc", "notion_write") + builder.add_edge("notion_write", "__end__") workflow = builder.compile() # 그래프 컴파일 workflow.name = self.name # Workflow 이름 설정 @@ -57,4 +66,11 @@ def build(self): # 마케팅 Workflow 인스턴스 생성 -marketing_workflow = MarketingWorkflow(MarketingState) +marketing_workflow = MarketingWorkflow(ContentState) + + +if __name__ == "__main__": + input_state = {"input_file": "agents/marketing/input_content_001_splitted.pdf"} + + final_state = marketing_workflow().invoke(input_state) + print("📄 결과:", final_state["result"]) From 1b169bd8716430361e97291219e067949500d4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EB=B3=91?= Date: Sun, 1 Jun 2025 17:38:01 +0900 Subject: [PATCH 11/11] =?UTF-8?q?feat:=20=ED=95=84=EC=9A=94=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/marketing/pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agents/marketing/pyproject.toml b/agents/marketing/pyproject.toml index 1d864fc..8849e03 100644 --- a/agents/marketing/pyproject.toml +++ b/agents/marketing/pyproject.toml @@ -9,4 +9,7 @@ version = "0.1.0" description = "교육 콘텐츠 마케팅 전략 및 콘텐츠 생성을 위한 LangGraph Workflow 모듈" readme = "README.md" requires-python = ">=3.13" -dependencies = [] +dependencies = [ + "langchain-openai>=0.3.18", + "pypdf>=5.5.0", +]