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