LlamaIndex + streamlit으로 문서 기반 챗봇 구현 - starter.py 코드 베이스 (작성중)

배경 및 목적

  • LlamaIndex 기본 코드(start.py)를 토대로 streamlit으로 chatbot 만들기

참고 자료

활용 툴

  • Visual studio code

  • streamlit

실행 과정

1. LlamIndex 기본 코드

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")
print(response)
  • "data" 폴더 안에 있는 파일을 load

  • load 된 파일을 indexing
    > embedding model : OpenAIEmbedding / text-embedding-ada-002

  • "query_engine" 생성

  • 질문(query)에 답변을 생성.
    > model : OpenAI / gpt-3.5-turbo(temperature 0.1)

  1. 업그레이드 버전 코드

    1. openai_api_key : ".env" 파일 저장 / 로드(load_dotenv)

    2. 인덱싱 결과 저장(Query your data)

      1. 처음 인덱싱 처리 후 결과를 "storage" 폴더에 저장

      2. 다음부터는 storage에 저장된 인덱스 결과를 활용

    3. embedding model : text-embedding-3-small

    4. openai model : gpt-4o-mini, temperature(0)

    5. query_engine : similarity-top-k = 4

    6. streaming 방식 출력

from dotenv import load_dotenv
from llama_index.core import (
    VectorStoreIndex,
    SimpleDirectoryReader,
    StorageContext,
    load_index_from_storage,
)
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core import Settings
import os.path

# 환경 변수 로드(OPENAI_API_KEY)
load_dotenv()

# OpenAI LLM 모델 설정
Settings.llm = OpenAI(model="gpt-4o-mini", temperature=0)

# Embeded Model 설정
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")

# check if storage alrealy exists:
PERSIST_DIR = "./storage"

if not os.path.exists(PERSIST_DIR):
    # documents 로드
    documents = SimpleDirectoryReader("data").load_data()

    # indexing
    index = VectorStoreIndex.from_documents(documents)

    # store it for later
    index.storage_context.persist(persist_dir=PERSIST_DIR)

else:
    # load the existing index
    storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
    index = load_index_from_storage(storage_context)

# Query Engine 생성
query_engine = index.as_query_engine(similarity_top_k=4, streaming=True)

# 질문에 대한 응답 생성
question = "What did the author do growing up?"
response = query_engine.query(question)
response.print_response_stream()

  1. streamlit chatbot 구현

    1. as_chat_engine() 사용

    2. openai_api_key를 직접 입력 기능

    3. 인덱싱 대상 문서 선택 / 업로드 기능

      1. 업로드 파일명의 폴더를 생성하여 인덱싱 결과 저장

    4. error 발생 시 메시지 표시.

[streamlit chatbot 코드] : 시연을 했던 최종 버전

import streamlit as st
from llama_index.core import (
    VectorStoreIndex,
    SimpleDirectoryReader,
    StorageContext,
    load_index_from_storage,
)
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core import Settings
import os.path
import logging, traceback

# 로깅 레벨을 ERROR로 설정(Error, Critical만 출력됨.)
logging.getLogger().setLevel(logging.ERROR)

st.title("LlamaIndex 버전 RAG Chatbot 💬")

# 처음 1번만 실행하기 위한 코드
if "messages" not in st.session_state:
    # 대화기록을 저장하기 위한 용도로 생성한다.
    st.session_state["messages"] = []

# 캐시 디렉토리 생성
if not os.path.exists("./cache"):
    os.mkdir("./cache")

# 파일 업로드 전용 폴더
if not os.path.exists("./cache/data"):
    os.mkdir("./cache/data")


# 초기화 버튼 클릭 시 실행.
def clear_chat():
    try:
        st.session_state["messages"] = []
        if "chat_engine" in st.session_state:
            del st.session_state["chat_engine"]

        if "show_warning" in st.session_state:
            del st.session_state["show_warning"]

        # cache/data 폴더에 업로드된 파일 삭제
        cache_data_dir = "./cache/data"
        if os.path.exists(cache_data_dir):
            for file_name in os.listdir(cache_data_dir):
                file_path = os.path.join(cache_data_dir, file_name)
                if os.path.isfile(file_path):
                    os.remove(file_path)

        st.session_state["chatbot_api_key"] = ""

    except Exception as e:
        st.error(f"초기화 중 오류가 발생했습니다: {e}")


# 사이드바 생성
with st.sidebar:
    # 초기화 버튼 생성
    clear_btn = st.button("대화 초기화", on_click=clear_chat)

    # LLM 선택
    selected_model = st.selectbox(
        "모델을 선택해 주세요.", ("gpt-4o-mini", "gpt-4o"), index=0
    )

    # OpneAI API Key
    openai_api_key = st.text_input(
        "OpenAI API Key", key="chatbot_api_key", type="password"
    )

    # 파일 업로드
    uploaded_file = st.file_uploader(
        "파일 업로드",
        type=["txt", "pdf", "md", "ppt", "doc", "hwp", "csv", "pptx"],
        key="uploaded_file",
    )


# 이전 대화를 출력
def print_messages():
    for chat_message in st.session_state["messages"]:
        # print(chat_message)
        st.chat_message(chat_message["role"]).write(chat_message["content"])


# 새로운 메시지를 추가
def add_to_message(role, content):
    message = {"role": role, "content": str(content)}
    st.session_state["messages"].append(message)  # Add response to message history


# 업로드 파일을 캐시 폴더에 저장
@st.cache_resource(show_spinner="업로드한 파일을 처리 중입니다...")
def load_index_data(file):

    try:

        # 업로드한 파일을 캐시 디렉토리에 저장.
        # file_content = file.read()
        file_path = f"./cache/data/{file.name}"
        with open(file_path, "wb") as f:
            f.write(file.getvalue())

        # documents 로드
        documents = SimpleDirectoryReader("./cache/data").load_data()

        return VectorStoreIndex.from_documents(documents)
    except Exception as e:
        st.error(f"파일 처리 중 오류가 발생했습니다: {str(e)}")
        logging.error(f"파일 처리 오류: {str(e)}")
        logging.error(traceback.format_exc())
        return None


if openai_api_key:
    # OpenAI LLM 모델 설정
    Settings.llm = OpenAI(model=selected_model, temperature=0, api_key=openai_api_key)

    # Embeded Model 설정
    Settings.embed_model = OpenAIEmbedding(
        mode="similarity", model="text-embedding-3-small", api_key=openai_api_key
    )
else:
    st.warning("OpenAI API 키를 입력해 주세요.")

# 이전 대화 기록 출력
print_messages()

# 사용자의 입력
user_input = st.chat_input("궁금한 내용을 물어보세요!")

# 경고 메시지를 띄우기 위한 빈 영역
warning_msg = st.empty()

if uploaded_file is not None:

    if not openai_api_key:
        st.info("Please add your OpenAI API key to continue.")
        st.stop()

    try:
        # check if storage alrealy exists:
        file_name_without_ext = os.path.splitext(uploaded_file.name)[0]
        PERSIST_DIR = f"./cache/storage/{file_name_without_ext}"

        if not os.path.exists(PERSIST_DIR):
            # indexing
            index = load_index_data(uploaded_file)
            # store it for later
            index.storage_context.persist(persist_dir=PERSIST_DIR)
        else:
            # load the existing index
            storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
            index = load_index_from_storage(storage_context)

        if "chat_engine" not in st.session_state:  # Initialize the query engine
            st.session_state["chat_engine"] = index.as_chat_engine(
                chat_mode="context", verbose=True
            )
    except Exception as e:
        st.error(f"인덱스 생성 중 오류가 발생했습니다: {str(e)}")
        logging.error(f"인덱스 생성 오류: {str(e)}")
        logging.error(traceback.format_exc())
else:
    if "show_warning" not in st.session_state:
        st.session_state["show_warning"] = True  # 경고 메시지 표시 플래그
    if st.session_state["show_warning"]:
        st.warning("파일을 업로드하세요.")

# 사용자 입력을 받아서 처리하는 부분.
if user_input:

    add_to_message("user", user_input)

    # 사용자의 입력을 출력함.
    st.chat_message("user").write(user_input)

    # 스트리밍 호출
    with st.chat_message("assistant"):
        response_str = ""
        response_container = st.empty()

        try:
            # container에 토큰을 스트리밍 출력
            response = st.session_state["chat_engine"].stream_chat(user_input)

            for token in response.response_gen:
                response_str += token
                response_container.markdown(response_str)
        except Exception as e:
            response_str = f"에러가 발생했습니다. 첨부 파일이 업로드되었는지를 확인하세요.\n [error] {e}"
            response_container.markdown(response_str.replace("\n", "  \n"))
            logging.error(f"응답 생성 오류: {str(e)}")
            logging.error(traceback.format_exc())
            st.session_state["show_warning"] = False

        add_to_message("assistant", response_str)

    # Save the state of the generator
#     st.session_state["response_gen"] = response.response_gen
  • 첨부 파일을 업로드 하면, "./cache/data" 폴더에 첨부 파일 임시 저장

  • "./cache/storage" 폴더 내에 첨부파일 명의 폴더를 생성하고 indexing 파일들 저장

  • "대화 초기화" 클릭 시 "./cache/data" 폴더 내 임시 저장 파일 삭제

  • 다음에 동일한 첨부 파일 업로드 할 경우, 이전에 생성된 indexing 파일 저장 폴더 존재 여부 확인 -> 있으면 저장된 indexing 결과 재사용

실행 화면

limadex rag chatbot이라는 단어가 포함된 페이지의 스크린샷

결과 및 인사이트

  • LLamaIndex 기본 코드(starter.py)를 업그레이드 해 나가면서 LlamaIndex에 대한 조금은 이해가 된 듯 하지만, 아직 명확하게 파악이 된 것은 아닌 듯 합니다.

  • 현재의 버전은 starter.py 코드를 베이스로 한 것이라, Component Guides와 여러 가지 examples를 참고해서 앞으로 더 고도화를 할 예정입니다.

  • as_chat_engine()에는 기본적으로 memory 기능이 적용되어 있는 듯 함.

  • txt, pdf, md, pptx 같은 텍스트 형식의 파일은 로딩이 되는 것을 확인하였으나, 이외의 형식에 대해서는 별도 확인 필요.

  • pptx 파일을 로드할 경우 다음을 설치해야 함. ("PptxReader" class 사용)

    pip install torch transformers python-pptx Pillow
  • streamlit에서 200M 이상의 파일을 uploader할 경우 에러 발생하는데, config.toml 파일을 다음의 위치에 생성하면 됨.

your_project/
├── .streamlit/
│   └── config.toml
├── your_app.py
└── ...
# config.toml
[server]
maxUploadSize = 1000
3
3개의 답글

👉 이 게시글도 읽어보세요