StreamlitとLangchainで作る会話型PDFアシスタント


## 初めに
この記事では、**Streamlit**と**Langchain**を使用して開発した会話型PDFアシスタントについて紹介します。StreamlitとLangchainを学ぶために簡易的に作成したものです。これから、Streamlit、Langchainを使って簡単にチャットボットを作成してみたい！という方におすすめです。


## 概要
PDFデータを読み込んで、PDFの内容を回答してくれるチャットボット。


## Streamlitとは
[Streamlit](https://streamlit.io/)は、PythonでWebアプリケーションを作成するためのフレームワークで、特にデータの可視化や分析を行うアプリケーションに適しています。Streamlitを用いることで、フロントエンドの技術がなくてもWebアプリケーションを作成することができます。

## Langchainとは
[Langchain](https://www.langchain.com/)は、GPTなどの大規模言語モデル（Large Language Model：LLM）を活用してサービスを開発する際に役立つライブラリです。このライブラリは、LLMと外部リソース（データソース、言語処理系）を組み合わせて、より高度なアプリケーションやサービスの開発をサポートすることができます。



## 開発環境の詳細とバージョン情報

| ツール / ライブラリ | バージョン      |
|-----------------|---------------|
| Python          |3.11.3        |
| Streamlit       | 1.29.0        |
| Langchain       | 0.0.350       |


## ライブラリ

```terminal
pip install streamlit streamlit_chat langchain openai faiss-cpu tiktoken pypdf
```

## ディレクトリ構成

```bash
├── ChatBot-env/       # 仮想環境フォルダ
│   ├── bin/
│   ├── include/
│   ├── lib/
│   └── ...
│
├── app.py             # Streamlitアプリケーション
├── chatbot.py         # チャットボットのロジック
├── pdf_loader.py      # PDF読み込み用のスクリプト
```

### app.py
```python
import os
import streamlit as st
from streamlit_chat import message
from chatbot import conversational_chat, setup_chain
from pdf_loader import load_pdf_data

# UI設定
st.set_page_config(page_title="DocTalk", page_icon=":speech_balloon:")

# タイトルと説明文を追加
st.title("DocTalk: PDF会話型アシスタント")
st.markdown("""
    PDFドキュメントから情報を引き出すためのチャットボットです。
    OpenAI APIキーを入力し、PDFファイルをアップロードしてください。
""")

# セッション状態の初期化
def initialize_state():
    if 'history' not in st.session_state:
        st.session_state['history'] = []
    if 'generated' not in st.session_state:
        st.session_state['generated'] = ["会話を始めるには、PDFファイルをアップロードしてください。"]
    if 'past' not in st.session_state:
        st.session_state['past'] = ["やあ！"]
initialize_state()

# サイドバー設定
st.sidebar.title("設定")
user_api_key = st.sidebar.text_input("OpenAI APIキー", type="password")
uploaded_file = st.sidebar.file_uploader("PDFファイルをアップロード", type="pdf")

# APIキーとファイルの確認
if user_api_key:
    os.environ['OPENAI_API_KEY'] = user_api_key
    if uploaded_file:
        data = load_pdf_data(uploaded_file)
        setup_chain(data)

# チャット履歴の表示
if st.session_state['generated']:
    for i in range(len(st.session_state['generated'])):
        message(st.session_state["past"][i], is_user=True, key=str(i) + '_user')
        message(st.session_state["generated"][i], key=str(i))

# チャット入力と送信ボタンの設定
user_input = st.text_input("質問：", placeholder="PDFデータについてここで話しましょう :)", key='input')
submit_button = st.button('送信')

# ユーザー入力の処理
if submit_button and user_input:
    if user_api_key and uploaded_file is not None:
        output = conversational_chat(user_input)
        st.session_state['past'].append(user_input)
        st.session_state['generated'].append(output)
    else:
        st.warning("APIキーとPDFファイルの両方が必要です。")
```

### chatbot.py
```python
import streamlit as st
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
from langchain.vectorstores import FAISS

chain = None

def setup_chain(data):
    global chain
    embeddings = OpenAIEmbeddings()
    vectors = FAISS.from_documents(data, embeddings)
    chain = ConversationalRetrievalChain.from_llm(
        llm=ChatOpenAI(temperature=0.0, model_name='gpt-3.5-turbo-16k'),
        retriever=vectors.as_retriever())

def conversational_chat(query):
    if chain is None:
        return "Chain is not initialized."

    result = chain({"question": query, "chat_history": st.session_state['history']})
    st.session_state['history'].append((query, result["answer"]))
    return result["answer"]
```
### pdf_loader.py
```python
import tempfile
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

def load_pdf_data(uploaded_file):
    with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
        tmp_file.write(uploaded_file.getvalue())
        tmp_file_path = tmp_file.name

    loader = PyPDFLoader(file_path=tmp_file_path)
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=2000,
        chunk_overlap=100,
        length_function=len)
    
    return loader.load_and_split(text_splitter)
```

## 今回使ったPDF資料
[Gamma](https://gamma.app/)を使って、架空の会社のメンバー自己紹介スライドを作成しました。
Gammaは、ビジュアルよく簡単にスライドが作成できるのでおすすめです！

![スクリーンショット 2023-12-19 23.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3630469/fba879e5-d2d2-1c94-1e45-b0e1dc4671cd.png)
![スクリーンショット 2023-12-19 23.56.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3630469/c28d360c-3d39-f611-dacc-ef7006e57e8f.png)
![スクリーンショット 2023-12-20 0.03.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3630469/ca1b14b8-c12d-40db-960b-a88ba0659215.png)


## 完成した会話型PDFアシスタント

![Chatbot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3630469/e2e0a5ce-7f7f-d6b7-5a55-dd018788864c.gif)



## まとめ
Langchain、Streamlitを使って、自分自身で手軽にアプリケーションを作成してみたいと思い、今回作成してみました。今回、作成してみて、Streamlitの簡単な操作性に驚きました。直感的で使いやすいUI構築機能になっていて、まだ試していないUIの機能もあるので、これから試していきたいです。

## 参考資料
https://liginc.co.jp/631680

https://zenn.dev/aidemy/articles/a189fb2d3794e7