박정기
박정기
🗡️ AI 레전드
🎖️ 마스터 파트너
🚀 SNS 챌린지 달성자

"라마의 모든것" 노션 데이터로 스터디장의 업무를 자동화!

배경 및 목적

노션에 라마 스터디에 필요한 모든 데이터를 넣었어요. 이 데이터를 가지고 저를 대신해서 답변이 가능한 챗봇을 Llamaindex를 활용하여 구축하려고 했습니다.

참고 자료

https://colab.research.google.com/github/run-llama/llama_index/blob/main/docs/docs/examples/data_connectors/NotionDemo.ipynb
-> 코랩에서 직접 라마인덱스를 활용해서 RAG를 구축해보는 코드를 볼 수 있고, 실행도 해볼 수 있었어요

https://docs.llamaindex.ai/en/stable/examples/data_connectors/NotionDemo/?h=notion

-> 라마 인덱스 공식 문서 안에 있는 example 중 하나에요!

https://developers.notion.com/docs/authorization

-> 노션 데이터를 활용하기 위해 노션 DOC를 참고했어요


https://colab.research.google.com/drive/1iDLah_yoVKmx6EOqi14RWOVAJ7Tt-EwK?usp=sharing
-> 제가 만든 코랩이에요. 참고하시면 노션 데이터를 바탕으로 RAG POC구축이 가능합니다.

활용 툴

POC : 코랩

추후 진행될 툴 : VSCODE에서 Streamlit으로 로컬에서 채팅이 가능하도록 구현

- Python, Llamaindex, Notion DB, Vscode, Streamlit

실행 과정

먼저 라마인덱스 DOC을 처음부터 차근차근 읽었어요. 그리고 Example 항목에서 지금 내가 가장 빠르게 구현할 수 있는 방법을 알려주는 자료를 찾았어요.

https://docs.llamaindex.ai/en/stable/examples/

코랩 코드를 학습하고, 코랩 코드를 바탕으로 "클로드 3.5 소넷" 에게 질문을 해가면서 RAG 프롬프트도 "빅라마"에 맞게 변경해주고, 채팅 형태의 구현이 가능하도록 코드를 함께 짜고 디버깅 했습니다!

    custom_prompt_template = PromptTemplate(
        "당신은 이름은 빅라마 입니다. 라마인덱스 스터디의 스터디장이고, "
        "항상 친절하고 공손한 어조로 질문에 마크다운 형식으로 답변합니다."
        "스터디에 열심히 참여하도록 스터디원들을 응원합니다."
        "다음 정보를 바탕으로 질문에 답변해주세요:\n"
        "만약 답을 모른다면 \"빅라마가 답변하기 힘든 질문입니다. [email protected] 로 연락을 주시면 빠른 답변을 드릴게요! \" 라고 해줘 ."
        "---------------------\n"
        "{context_str}\n"
        "---------------------\n"
        "질문: {query_str}\n"
        "답변: "
    )


1. 코랩으로 RAG 구축 가능 여부 확인하기
https://colab.research.google.com/drive/1iDLah_yoVKmx6EOqi14RWOVAJ7Tt-EwK?usp=sharing

검은 화면에 한국어 텍스트 스크린샷
한국사이트 스크린샷


2. Vscode 환경에서 구축하기

"""
Notion Reader Script

This script demonstrates the use of Notion data connector with LlamaIndex.
It retrieves data from a Notion page, creates an index, and saves documents to files.
"""

import os
import json
import logging
from typing import List

from llama_index.core import SummaryIndex, Document
from llama_index.readers.notion import NotionPageReader
from llama_index.core.prompts import PromptTemplate
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core import Settings

# Configure logging
logging.basicConfig(level=logging.DEBUG)  # Changed to DEBUG for more detailed logs
logger = logging.getLogger(__name__)

# Set up environment variables
os.environ['OPENAI_API_KEY'] = '키를 입력해주세요'
NOTION_INTEGRATION_TOKEN = "키를 입력해주세요"
PAGE_IDS = ["페이지 아이디1", "페이지 아이디2"]

def load_notion_documents(integration_token: str, page_ids: List[str]) -> List[Document]:
    """Load documents from Notion pages."""
    reader = NotionPageReader(integration_token=integration_token)
    documents = reader.load_data(page_ids=page_ids)
    logger.debug(f"Loaded {len(documents)} documents from Notion")
    return documents

def create_index(documents: List[Document]) -> SummaryIndex:
    """Create a SummaryIndex from documents."""
    return SummaryIndex.from_documents(documents)

def create_query_engine(index: SummaryIndex) -> RetrieverQueryEngine:
    """Create a custom query engine with a defined prompt template."""
    custom_prompt_template = PromptTemplate(
        "당신은 이름은 빅라마 입니다. 라마인덱스 스터디의 스터디장이고, "
        "항상 친절하고 공손한 어조로 질문에 답변합니다. "
        "스터디에 열심히 참여하도록 스터디원들을 응원합니다."
        "다음 정보를 바탕으로 질문에 답변해주세요:\n"
        "---------------------\n"
        "{context_str}\n"
        "---------------------\n"
        "질문: {query_str}\n"
        "답변: "
    )
    
    retriever = index.as_retriever()
    Settings.llm = Settings.llm or Settings.llm_defaults
    return RetrieverQueryEngine.from_args(
        retriever,
        text_qa_template=custom_prompt_template
    )

def perform_query(query_engine: RetrieverQueryEngine, query: str) -> str:
    """Perform a query and return the response."""
    response = query_engine.query(query)
    return response

def save_documents(documents: List[Document]):
    """Save each document to a separate file and its metadata to a JSON file."""
    for i, doc in enumerate(documents, 1):
        # Save document content
        content_filename = f"document_{i}_content.txt"
        try:
            with open(content_filename, "w", encoding="utf-8") as f:
                f.write(doc.text)
            logger.info(f"Saved document content to {content_filename}")
            logger.debug(f"Document {i} length: {len(doc.text)} characters")
        except Exception as e:
            logger.error(f"Error saving document {i} content: {str(e)}")

        # Save document metadata
        metadata_filename = f"document_{i}_metadata.json"
        try:
            with open(metadata_filename, "w", encoding="utf-8") as f:
                json.dump(doc.metadata, f, ensure_ascii=False, indent=2)
            logger.info(f"Saved document metadata to {metadata_filename}")
        except Exception as e:
            logger.error(f"Error saving document {i} metadata: {str(e)}")

def main():
    """Main function to run the Notion Reader script."""
    documents = load_notion_documents(NOTION_INTEGRATION_TOKEN, PAGE_IDS)
    
    # Log document details before saving
    for i, doc in enumerate(documents, 1):
        logger.debug(f"Document {i} details:")
        logger.debug(f"  Length: {len(doc.text)} characters")
        logger.debug(f"  Metadata: {doc.metadata}")
    
    # Save the loaded documents
    save_documents(documents)
    
    # Verify saved content
    for i, doc in enumerate(documents, 1):
        content_filename = f"document_{i}_content.txt"
        try:
            with open(content_filename, "r", encoding="utf-8") as f:
                saved_content = f.read()
            if len(saved_content) != len(doc.text):
                logger.warning(f"Document {i} content length mismatch: "
                               f"Original {len(doc.text)}, Saved {len(saved_content)}")
        except Exception as e:
            logger.error(f"Error verifying saved content for document {i}: {str(e)}")
    
    index = create_index(documents)
    query_engine = create_query_engine(index)

    queries = [
        "라마 스터디가 뭐야?",
        "스터디 일정을 알려줘!",
        "스터디 주차별 진행사항을 알려줘!",
        "라마 인덱스 공식 문서 링크는 어떻게 돼?"
    ]

    # Uncomment the following lines to perform queries
    for query in queries:
        response = perform_query(query_engine, query)
        print(f"질문: {query}")
        print(f"답변: {response}\n")

if __name__ == "__main__":
    main()

-> VScode 환경에서도 돌아가게 만듬

3. Streamlit 코드 구현하기

import streamlit as st
import os
import json
import logging
from typing import List

from llama_index.core import SummaryIndex, Document
from llama_index.readers.notion import NotionPageReader
from llama_index.core.prompts import PromptTemplate
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core import Settings

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Set up environment variables
os.environ['OPENAI_API_KEY'] = '키를 입력해주세요'
NOTION_INTEGRATION_TOKEN = "키를 입력해주세요"
PAGE_IDS = ["페이지 아이디 1", "페이지 아이디 2", "페이지 아이디 3"]

@st.cache_resource
def load_notion_documents():
    """Load documents from Notion pages."""
    reader = NotionPageReader(integration_token=NOTION_INTEGRATION_TOKEN)
    documents = reader.load_data(page_ids=PAGE_IDS)
    logger.info(f"Loaded {len(documents)} documents from Notion")
    return documents

@st.cache_resource
def create_index_and_query_engine():
    """Create a SummaryIndex from documents and a query engine."""
    documents = load_notion_documents()
    index = SummaryIndex.from_documents(documents)
    
    custom_prompt_template = PromptTemplate(
        "당신은 이름은 빅라마 입니다. 라마인덱스 스터디의 스터디장이고, "
        "항상 친절하고 공손한 어조로 질문에 답변합니다."
        "스터디에 열심히 참여하도록 스터디원들을 응원합니다."
        "다음 정보를 바탕으로 질문에 답변해주세요:\n"
        "만약 답을 모른다면 \"빅라마가 답변하기 힘든 질문입니다. [email protected] 로 연락을 주시면 빠른 답변을 드릴게요! \" 라고 해줘 ."
        "---------------------\n"
        "{context_str}\n"
        "---------------------\n"
        "질문: {query_str}\n"
        "답변: "
    )
    
    retriever = index.as_retriever()
    Settings.llm = Settings.llm or Settings.llm_defaults
    query_engine = RetrieverQueryEngine.from_args(
        retriever,
        text_qa_template=custom_prompt_template
    )
    
    return query_engine

def perform_query(query_engine: RetrieverQueryEngine, query: str) -> str:
    """Perform a query and return the response."""
    response = query_engine.query(query)
    return str(response)

def main():
    st.title("라마인덱스 스터디 채팅봇")

    # Initialize session state for chat history
    if "messages" not in st.session_state:
        st.session_state.messages = []

    # Load documents and create query engine
    query_engine = create_index_and_query_engine()

    # Display chat messages
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])

    # Chat input
    if prompt := st.chat_input("무엇이든 물어보세요!"):
        st.session_state.messages.append({"role": "user", "content": prompt})
        with st.chat_message("user"):
            st.markdown(prompt)

        with st.chat_message("assistant"):
            response = perform_query(query_engine, prompt)
            st.markdown(response)
        st.session_state.messages.append({"role": "assistant", "content": response})

if __name__ == "__main__":
    main()결과 및 인사이트

한국사이트 스크린샷

-> 저를 대신할 챗봇이 생겼습니다.

한국어 텍스트가 있는 검은 화면

-> 문서에 없는 내용을 물어보면 제 메일로 연락을 달라고 하도록 수정했습니다.

노션DB를 활용해서 저를 대신할 챗봇을 구현할 수 있었어요. 라마인덱스 스터디에 관심이 있거나, 라마인덱스 스터디 진행중에 나왔던 참고할만한 데이터들을 한 곳에 모아두고 질문시 답변할 수 있도록 구축했어요.

추후 진행할 내용

  1. RAG에서 일어나고 있는 것들을 tracing

https://docs.llamaindex.ai/en/stable/module_guides/observability/

라마클라우드-트레이싱 코랩 예제 돌려보고 적용해보기

https://github.com/run-llama/llamacloud-demo/blob/main/examples/tracing/llamacloud_tracing_phoenix.ipynb

3
2개의 답글

👉 이 게시글도 읽어보세요