LangChain with Streamlit 논문요약 예제

Page content

개요

  • LangChain의 기본 개념에 대해 살펴본다.
  • LangChain을 활용하며 간단한 웹앱을 구현한다.
  • 각 사용자가 본인의 API를 입력하면 해당 기능을 사용할 수 있도록 구현한다.

LangChain의 기본개념

  • LangChain은 대규모 언어 모델(LLM)을 활용한 애플리케이션 개발을 위한 프레임워크입니다.
  • 주요 특징과 장점은 다음과 같다.
    • 모듈성
      • 다양한 LLM과 도구들을 쉽게 통합하고 교체할 수 있다.
      • 재사용 가능한 컴포넌트를 제공한다.
    • 체이닝(Chaining)
      • 여러 컴포넌트를 연결하여 복잡한 워크플로우를 구성할 수 있다.
      • 프롬프트, LLM 호출, 출력 파싱 등을 순차적으로 처리한다
    • 메모리 관리
      • 대화 기록을 저장하고 관리할 수 있다.
      • 문맥을 유지하면서 대화형 애플리케이션을 만들 수 있다.
    • 다양한 통합
      • 여러 AI 모델(OpenAI, Anthropic 등)을 지원한다.
      • 데이터베이스, 검색 엔진 등 외부 도구와 연동이 가능하다.
    • 개발 편의성
      • 파이썬 기반의 직관적인 API를 제공한다.
      • 풍부한 문서와 예제가 제공된다.

LangChain을 활용한 프로젝트 구조 만들기

  • API 키를 입력하면 해당 기능을 사용할 수 있도록 구현한다.
  • 구현 프로세스
    • API 키 입력 화면 구현
    • 입력된 API 키를 활용한 기능 구현
    • 웹앱 레이아웃 구성
    • 사용자 친화적인 인터페이스 제공
  • PDF논문 요약 기능 구현
    • 논문 PDF 파일 업로드
    • 논문 요약 기능 구현
    • 요약 결과 표시 (각 챕터별)
  • 웹앱 레이아웃 구성
    • 사용자 친화적인 인터페이스 제공
    • 논문 요약 기능 구현
    • 요약 결과 표시
  • 먼저 테스트성으로 jupyter notebook으로 구현
    • jupyter notebook에서는 .env 파일에 있는 API 키를 불러오는 형태로 구현한다.
    • 논문은 files 폴더에 있는 논문 1개를 사용한다.

참고 논문

jupyter notebook 테스트

  • 코드는 아래와 같다.
# 필요한 라이브러리 임포트
import os
from langchain.chat_models import ChatOpenAI  # OpenAI의 ChatGPT 모델을 사용하기 위한 클래스
from langchain_core.prompts import ChatPromptTemplate  # 채팅 프롬프트 템플릿 생성을 위한 클래스
from langchain_core.output_parsers import StrOutputParser  # 출력을 문자열로 파싱하기 위한 클래스
from langchain_community.document_loaders import PyPDFLoader  # PDF 파일을 로드하기 위한 클래스
from langchain.text_splitter import RecursiveCharacterTextSplitter  # 텍스트를 청크로 분할하기 위한 클래스
from docx import Document  # Word 문서 생성을 위한 클래스
from dotenv import load_dotenv  # 환경 변수 로드를 위한 모듈
from datetime import datetime  # 날짜/시간 처리를 위한 클래스

# .env 파일에서 환경 변수 로드
load_dotenv()

# 환경 변수에서 OpenAI API 키 가져오기
openai_api_key = os.getenv("OPENAI_API_KEY")

# API 키가 존재하는지 확인
if not openai_api_key:
    raise ValueError("OPENAI_API_KEY 환경 변수를 찾을 수 없습니다. .env 파일을 확인해주세요.")

# files 디렉토리에서 PDF 파일 찾기
pdf_files = [f for f in os.listdir("files") if f.endswith('.pdf')]
if not pdf_files:
    raise ValueError("files 디렉토리에서 PDF 파일을 찾을 수 없습니다")

# 첫 번째 PDF 파일의 전체 경로 생성
pdf_path = os.path.join(os.getcwd(), "files", pdf_files[0])

# PDF 파일 로드 및 페이지 분할
loader = PyPDFLoader(pdf_path)
pages = loader.load_and_split()

# 텍스트 분할기 설정 - 큰 청크 사이즈로 설정하여 컨텍스트 유지
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=4000,  # 각 청크의 최대 문자 수
    chunk_overlap=400,  # 청크 간 중복되는 문자 수
    length_function=len
)

# 페이지들을 청크로 분할
texts = text_splitter.split_documents(pages)

# 텍스트를 전반부와 후반부 두 개의 파트로 병합
if len(texts) > 2:
    # 전반부 텍스트 병합
    first_half = " ".join([t.page_content for t in texts[:len(texts)//2]])
    # 후반부 텍스트 병합
    second_half = " ".join([t.page_content for t in texts[len(texts)//2:]])
    texts = [
        {"content": first_half, "part": "전반부"},
        {"content": second_half, "part": "후반부"}
    ]

# LangChain의 ChatOpenAI 모델 설정
llm = ChatOpenAI(
    openai_api_key=openai_api_key,
    model="gpt-3.5-turbo",  # GPT-3.5 모델 사용
    temperature=0.1,  # 낮은 temperature로 일관된 출력 생성
    max_tokens=1000  # 충분한 길이의 요약을 위한 토큰 수 설정
)

# 요약을 위한 프롬프트 템플릿 설정
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 전문 학술 연구원입니다. 다음 학술 논문 파트를 주요 포인트와 핵심 발견사항을 중심으로 한글로 포괄적이고 상세하게 요약해주세요."),
    ("user", "다음 텍스트를 한글로 요약해주세요. 이 텍스트는 논문의 {part}입니다:\n\n{text}\n\n반드시 한글로 요약해주시고, 영어 전문용어가 있다면 한글 설명을 덧붙여주세요.")
])

# 프롬프트, LLM, 출력 파서를 연결하는 체인 생성
chain = prompt | llm | StrOutputParser()

# Word 문서 객체 생성
doc = Document()
doc.add_heading('논문 요약', 0)

# 현재 시간을 파일명에 포함시키기 위한 형식 지정
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")

# 각 파트별로 요약 실행 및 Word 문서에 추가
for i, text in enumerate(texts):
    print(f"파트 {i+1}/2 요약 중...")
    # 체인을 실행하여 요약 생성
    summary = chain.invoke({"text": text["content"], "part": text["part"]})
    
    # Word 문서에 요약 내용 추가
    doc.add_heading(f'{text["part"]} 요약', level=1)
    doc.add_paragraph(summary)
    doc.add_paragraph('-' * 40)  # 구분선 추가
    
    # 콘솔에 요약 출력
    print(f"\n{text['part']} 요약:")
    print(summary)
    print("-" * 80)

# 생성된 Word 문서를 저장
output_path = f"논문_요약_{current_time}.docx"
doc.save(output_path)
print(f"\n한글 요약본이 {output_path}에 저장되었습니다")
  • 위 코드를 실제 실행하면 다음과 같은 결과가 나온다.

Screenshot 2025-02-09 at 7.36.19 PM.png

  • 위 결과 내용은 워드에서 동일하게 나오는 것을 확인 할 수 있다.

Screenshot 2025-02-09 at 7.38.22 PM.png

Streamlit App

수행결과 화면

  • 앱 실행 결과 화면은 아래와 같다.

Screenshot 2025-02-09 at 7.54.14 PM.png

Screenshot 2025-02-09 at 7.54.52 PM.png

  • Download Summary 버튼을 클릭하면 워드 파일을 확인할 수 있다.

Screenshot 2025-02-09 at 7.59.26 PM.png

실습코드

  • 실습 코드는 아래와 같다.
import streamlit as st
import os
from langchain.chat_models import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from docx import Document
from datetime import datetime
from pathlib import Path

def create_summary_chain(api_key, language="ko"):
    """LangChain 요약 체인을 생성하는 함수
    
    Args:
        api_key (str): OpenAI API 키
        language (str): 요약 언어 선택 ('ko' 또는 'en')
        
    Returns:
        chain: 프롬프트, LLM, 출력 파서가 연결된 LangChain 체인
    """
    llm = ChatOpenAI(
        openai_api_key=api_key,
        model="gpt-3.5-turbo",  # GPT-3.5 모델 사용
        temperature=0.1,        # 낮은 temperature로 일관된 출력 생성
        max_tokens=1000         # 최대 토큰 수 제한
    )
    
    # 언어에 따른 프롬프트 설정
    if language == "ko":
        system_message = "당신은 전문 학술 연구원입니다. 다음 학술 논문 파트를 주요 포인트와 핵심 발견사항을 중심으로 한글로 포괄적이고 상세하게 요약해주세요."
        user_message = "다음 텍스트를 한글로 요약해주세요. 이 텍스트는 논문의 {part}입니다:\n\n{text}\n\n반드시 한글로 요약해주시고, 영어 전문용어가 있다면 한글 설명을 덧붙여주세요."
    else:
        system_message = "You are a professional academic researcher. Please provide a comprehensive and detailed summary of the following academic paper part, focusing on key points and findings."
        user_message = "Please summarize the following text in English. This is the {part} of the paper:\n\n{text}\n\nPlease provide the summary in English, including explanations of technical terms."

    prompt = ChatPromptTemplate.from_messages([
        ("system", system_message),
        ("user", user_message)
    ])
    
    return prompt | llm | StrOutputParser()

def process_pdf(pdf_file, api_key, language):
    """PDF 파일을 처리하고 요약하는 함수
    
    Args:
        pdf_file: Streamlit 파일 업로더로 받은 PDF 파일
        api_key (str): OpenAI API 키
        language (str): 요약 언어 선택
        
    Returns:
        tuple: (생성된 파일명, 파일 경로)
    """
    # 임시 파일로 PDF 저장
    temp_pdf_path = "temp_pdf_file.pdf"
    with open(temp_pdf_path, 'wb') as f:
        f.write(pdf_file.getvalue())

    try:
        # PDF 로드 및 페이지 분할
        loader = PyPDFLoader(temp_pdf_path)
        pages = loader.load_and_split()

        # 텍스트 분할 설정
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=4000,        # 각 청크의 최대 문자 수
            chunk_overlap=400,      # 청크 간 중복되는 문자 수
            length_function=len     # 길이 측정 함수
        )

        texts = text_splitter.split_documents(pages)

        # 텍스트를 전반부와 후반부로 나누기
        first_half = " ".join([t.page_content for t in texts[:len(texts)//2]])
        second_half = " ".join([t.page_content for t in texts[len(texts)//2:]])
        texts = [
            {"content": first_half, "part": "First Half"},
            {"content": second_half, "part": "Second Half"}
        ]

        # 요약 체인 생성
        chain = create_summary_chain(api_key, language)

        # Word 문서 생성 및 제목 추가
        doc = Document()
        doc.add_heading('Paper Summary', 0)

        summaries = []
        for i, text in enumerate(texts, 1):
            with st.spinner(f'Processing part {i}/2...'):
                # 각 부분 요약 생성
                summary = chain.invoke({"text": text["content"], "part": text["part"]})
                summaries.append(summary)
                
                # Word 문서에 요약 내용 추가
                doc.add_heading(f'{text["part"]} Summary', level=1)
                doc.add_paragraph(summary)
                doc.add_paragraph('-' * 40)  # 구분선 추가

                # Streamlit 웹 페이지에 요약 표시
                st.subheader(f'{text["part"]} Summary')
                st.write(summary)
                st.markdown("---")

        # 현재 시간을 포함한 파일명 생성
        current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"paper_summary_{current_time}.docx"
        
        # 사용자의 바탕화면 경로 가져오기
        desktop = str(Path.home() / "Desktop")
        filepath = os.path.join(desktop, filename)
        
        # Word 문서 저장
        doc.save(filepath)
        
        return filename, filepath

    finally:
        # 임시 PDF 파일 정리
        if os.path.exists(temp_pdf_path):
            os.remove(temp_pdf_path)

def main():
    """메인 애플리케이션 함수"""
    st.title("PDF Paper Summarizer")
    
    # 사이드바에 OpenAI API 키 입력 필드 추가
    api_key = st.sidebar.text_input("Enter your OpenAI API Key", type="password")
    
    # 요약 언어 선택 옵션
    language = st.sidebar.selectbox(
        "Select summary language",
        ["ko", "en"],
        format_func=lambda x: "Korean" if x == "ko" else "English"
    )
    
    # PDF 파일 업로드 위젯
    uploaded_file = st.file_uploader("Upload your PDF paper", type="pdf")
    
    if uploaded_file and api_key:
        if st.button("Generate Summary"):
            try:
                # PDF 처리 및 요약 생성
                filename, filepath = process_pdf(uploaded_file, api_key, language)
                st.success(f"Summary has been saved to your desktop as: {filename}")
                
                # Word 문서 다운로드 버튼 생성
                with open(filepath, "rb") as file:
                    st.download_button(
                        label="Download Summary",
                        data=file,
                        file_name=filename,
                        mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
                    )
                    
            except Exception as e:
                st.error(f"An error occurred: {str(e)}")
    
    elif not api_key:
        st.warning("Please enter your OpenAI API key in the sidebar.")
    elif not uploaded_file:
        st.info("Please upload a PDF file to begin.")

if __name__ == "__main__":
    main()