Panel로 PDF와 채팅하기

Panel로 PDF와 채팅하기 (Mistral)

panel을 사용해 채팅과 PDF 읽기 기능을 갖춘 챗봇을 만드는 기본기를 소개하는 가이드예요. Mistral로 채팅 인터페이스를 만들고, PDF 텍스트를 추출해 간단한 RAG로 문서와 대화할 수 있게 합니다.

출처: 문서

본문

간단한 채팅 인터페이스를 먼저 구현합니다. 이를 위해 panel과 mistralai 라이브러리가 필요해요.

pip install panel mistralai

이 데모는 panel===1.4.4과 mistralai===0.4.0을 사용합니다.

import panel as pn
from mistralai.client import MistralClient
from mistralai.models.chat_completion import ChatMessage

진행 전에 pn.extension()을 실행해 panel을 올바르게 구성해야 합니다.

pn.extension()

Mistral API 키로 MistralClient 인스턴스를 만듭니다.

mistral_api_key = "your_api_key"
cli = MistralClient(api_key = mistral_api_key)

기본 채팅 인터페이스 (Basic Chat Interface)

클라이언트가 준비됐으니 인터페이스를 만들 차례입니다. panel의 ChatInterface를 사용할게요.

async def callback(contents: str, user: str, instance: pn.chat.ChatInterface):
    messages = [ChatMessage(role = "user", content = contents)]

    response = cli.chat_stream(model = "open-mistral-7b", messages = messages, max_tokens = 512)
    message = ""
    for chunk in response:
        message += chunk.choices[0].delta.content
        yield message

chat_interface = pn.chat.ChatInterface(callback = callback, callback_user = "Mistral")
chat_interface.servable()

이 코드에서는 사용자가 메시지를 보낼 때마다 호출되는 콜백 함수를 정의합니다. 이 함수는 Mistral의 모델로 응답을 생성해요. 실행하려면 콘솔에서 panel serve basic_chat.py를 입력합니다.

채팅 기록 (Chat History)

지금은 모델이 가장 최근 메시지에만 접근할 수 있고 전체 대화를 알지 못합니다. 이를 해결하려면 전체 채팅을 추적해 모델에 제공해야 하는데, 다행히 panel이 이를 대신해 줍니다!

async def callback(contents: str, user: str, instance: pn.chat.ChatInterface):
    messages_objects = [w for w in instance.objects]
    messages = [ChatMessage(
        role = "user" if w.user == "User" else "assistant",
        content = w.object
    ) for w in messages_objects]

    response = cli.chat_stream(model = "open-mistral-7b", messages = messages, max_tokens = 512)
    message = ""
    for chunk in response:
        message += chunk.choices[0].delta.content
        yield message

그 김에 사용자에게 환영 메시지를 추가해 봅니다. 콜백에서 이 메시지는 무시해야 해요.

async def callback(contents: str, user: str, instance: pn.chat.ChatInterface):
    messages_objects = [w for w in instance.objects if w.user != "System"]
    messages = [ChatMessage(
        role="user" if w.user == "User" else "assistant",
        content=w.object
    ) for w in messages_objects]

    response = cli.chat_stream(model = "open-mistral-7b", messages = messages, max_tokens = 512)
    message = ""
    for chunk in response:
        message += chunk.choices[0].delta.content
        yield message

chat_interface = pn.chat.ChatInterface(callback = callback, callback_user = "Mistral")
chat_interface.send("Chat with Mistral!", user = "System", respond = False)
chat_interface.servable()

이제 Mistral과 전체 대화를 할 수 있어요: panel serve chat_history.py

PDF와 채팅하기 (Chatting with PDFs)

모델이 PDF를 읽게 하려면 내용을 변환·텍스트 추출하고, Mistral의 임베딩 모델로 문서 청크를 검색해 모델에 공급해야 해요. 간단한 RAG를 구현해야 합니다. 이 작업에는 faiss, PyPDF2와 기타 라이브러리가 필요해요.

pip install io numpy PyPDF2 faiss

CPU 전용이라면 faiss-cpu를 설치하세요. 이 데모는 numpy===1.26.4, PyPDF2===0.4.0, faiss-cpu===1.8.0을 사용합니다.

import io
from mistralai.client import MistralClient
from mistralai.models.chat_completion import ChatMessage
import numpy as np
import panel as pn
import PyPDF2
import faiss

먼저 파일 업로드 옵션을 추가합니다. ChatInterface의 가능한 입력을 다음과 같이 지정해요.

chat_interface = pn.chat.ChatInterface(widgets = [pn.widgets.TextInput(),pn.widgets.FileInput(accept = ".pdf")], callback = callback, callback_user = "Mistral")
chat_interface.send("Chat with Mistral and talk to your PDFs!", user = "System", respond = False)
chat_interface.servable()

이제 사용자가 채팅을 하면서 PDF도 업로드할 수 있어요. 이 새로운 가능성을 콜백에서 처리합니다.

async def callback(contents: str, user: str, instance: pn.chat.ChatInterface):
    if type(contents) is str:
        messages_objects = [w for w in instance.objects if w.user != "System" and type(w.object) is not pn.chat.message._FileInputMessage]
        messages = [ChatMessage(
            role = "user" if w.user == "User" else "assistant",
            content = w.object
        ) for w in messages_objects]

        pdf_objects = [w for w in instance.objects if w.user != "System" and w not in messages_objects]

        response = cli.chat_stream(model = "open-mistral-7b", messages = messages, max_tokens = 1024)
        message = ""
        for chunk in response:
            message += chunk.choices[0].delta.content
            yield message

pdf_objects에는 이전에 업로드된 모든 PDF가 담기며, 이들이 RAG의 대상 문서가 됩니다. RAG를 전부 처리하는 함수를 정의해 볼게요. PDF와 사용자 질문을 입력으로 받아 검색된 청크를 하나의 문자열로 이어서 반환합니다.

def rag_pdf(pdfs: list, question: str) -> str:
    chunk_size = 2048
    chunks = []

    for pdf in pdfs:
        chunks += [pdf[i:i + chunk_size] for i in range(0, len(pdf), chunk_size)]

계속하기 전에 모든 청크의 임베딩을 얻어야 합니다. 텍스트를 임베딩으로 변환하는 함수를 빠르게 만들게요.

def get_text_embedding(input_text: str):
    embeddings_batch_response = cli.embeddings(
        model = "mistral-embed",
        input = input_text
    )
    return embeddings_batch_response.data[0].embedding

이 임베딩을 모든 청크에 적용하고, faiss로 가장 관련성 높은 청크를 검색하는 벡터 스토어를 만듭니다. 여기서는 최고 4개 청크를 가져옵니다.

def rag_pdf(pdfs: list, question: str) -> str:
    chunk_size = 4096
    chunks = []

    for pdf in pdfs:
        chunks += [pdf[i:i + chunk_size] for i in range(0, len(pdf), chunk_size)]

    text_embeddings = np.array([get_text_embedding(chunk) for chunk in chunks])

    d = text_embeddings.shape[1]
    index = faiss.IndexFlatL2(d)
    index.add(text_embeddings)

    question_embeddings = np.array([get_text_embedding(question)])
    D, I = index.search(question_embeddings, k = 4)
    retrieved_chunk = [chunks[i] for i in I.tolist()[0]]
    text_retrieved = "\n\n".join(retrieved_chunk)

    return text_retrieved

RAG 함수가 준비됐으니 채팅 인터페이스에 구현합니다. PyPDF2로 PDF를 읽고 rag_pdf로 핵심 텍스트를 검색해요.

async def callback(contents: str, user: str, instance: pn.chat.ChatInterface):
    if type(contents) is str:
        messages_objects = [w for w in instance.objects if w.user != "System" and type(w.object) is not pn.chat.message._FileInputMessage]
        messages = [ChatMessage(
            role = "user" if w.user == "User" else "assistant",
            content = w.object
        ) for w in messages_objects]

        pdf_objects = [w for w in instance.objects if w.user != "System" and w not in messages_objects]

        if pdf_objects:
            pdfs = []
            for w in pdf_objects:
                reader = PyPDF2.PdfReader(io.BytesIO(w.object.contents))
                txt = ""
                for page in reader.pages:
                    txt += page.extract_text()
                pdfs.append(txt)
            messages[-1].content = rag_pdf(pdfs, contents) + "\n\n" + contents

        response = cli.chat_stream(model = "open-mistral-7b", messages = messages, max_tokens = 1024)
        message = ""
        for chunk in response:
            message += chunk.choices[0].delta.content
            yield message

채팅에 PDF가 있으면 그것을 읽고 필요한 정보를 검색해 원래 사용자 메시지에 이어 붙입니다. 이제 Mistral과 PDF에 대해 완전히 대화할 수 있어요: panel serve chat_with_pdfs.py

더 알아보기 (Learn more)

  • Panel 공식 문서 — Python 웹 대시보드·앱 프레임워크
  • pn.chat.ChatInterface — 채팅 UI 컴포넌트
  • faiss.IndexFlatL2 — 유사도 검색용 FAISS 인덱스
  • 모델: open-mistral-7b, mistral-embed