Jupyterでipywidgetsを使ってGPTと会話する

# はじめに

LangChainを使ってGPT関連の開発していると、Jupyter中でGPTと会話しながら調整したいことがある。
そこで、Jupyter中でipywidgetsを使ってインタラクティブにchainを使ってGPTと会話できるようにした。

# 本記事の完成品

![gpt.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/63e99b6d-ef29-d8b3-7a33-b4138d250a6a.gif)


# 準備 環境の設定, apiキー, chainの設定

使用した環境のrequirements.txt
```
langchain==0.0.343
openai==0.28.1
jupyterlab==4.0.10
```

JupyterLabでipywidgetを動かすために、以下を実行して再起動した。
```
jupyter labextension install @jupyter-widgets/jupyterlab-manager
```

まずapiキーを環境変数に入れておく
```python
import os

#APIキーをセット
os.environ["OPENAI_API_KEY"] = #############
```

GPTとの会話で使いたいchainを作っておく。記法はLangChain Expression Language (LCEL)を使用した。(関連記事: [LangChainのLCELで | (パイプ文字)を使って処理できる理由](https://qiita.com/MichaelHashimoto/items/75f05394584e5b82bc18))
```python
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.memory import ConversationBufferMemory
from langchain.chat_models import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from operator import itemgetter

# memoryを作成。これによって、会話の履歴を保存し、gptに毎回履歴を渡せるようになる。
memory = ConversationBufferMemory(return_messages=True)

# promptを作成
template = """Answer the question in Japanese:
Question: {question}
"""
prompt = ChatPromptTemplate.from_messages([
    MessagesPlaceholder(variable_name="chat_history"),
    HumanMessagePromptTemplate.from_template(template)
])
# modelを作成
model = ChatOpenAI(model_name="gpt-3.5-turbo-16k",verbose=True)
# parserを作成
output_parser = StrOutputParser()

# chainへの入力に、chat_histryという変数名で、memoryが持っている会話履歴が加わるようにする。
setup = RunnablePassthrough.assign(
    chat_history = RunnableLambda(memory.load_memory_variables)| itemgetter("history")
)

chain = setup | prompt | model | output_parser
```

## ipyywidgetsで会話

```python
from IPython.display import display, HTML, Markdown
import ipywidgets as widgets

# テキストボックスとボタンの作成
text_box = widgets.Text(placeholder='テキストを入力してください')
submit_button = widgets.Button(description='実行')
# 出力用のウィジェット
output = widgets.Output()
display(output)

# 実行ボタンを押したときの関数
def run_function(_):
    # gptの返事を待っている間は、実行ボタンを選択不可にする。
    submit_button.description = "実行中"
    submit_button.disabled = True
    # 入力された値を取得、取得後はクリア
    text = text_box.value
    text_box.value = ""
    with output:
        # 入力されたテキストをアウトプットウィンドウに出力
        input_section_format = """<div>
            <p>入力> {text}</p>
        </div>"""
        input_section = HTML(input_section_format.format(text=text))
        display(input_section)
    
        # gptのchainの実行
        # 入力されたテキストを渡し、chainを実行
        input = {"question":text}
        gpt_output = chain.invoke(input)
        # 入力とgptからの出力をmemoryに保存しておく。
        memory.save_context({"input":template.format(**input)},{"output": gpt_output})
    
        # gptの出力内容をアウトプットウィンドウに出力
        # 出力内容を折り畳めるように、detailsタグを使用する。
        gpt_section = Markdown(f"""<div>
                    <details><summary>GPT</summary><pre>{gpt_output}</pre></details>
                </div>""")
        display(gpt_section)
    
        # 実行ボタンの選択不可を解除。
        submit_button.description = "実行"
        submit_button.disabled = False

submit_button.on_click(run_function)

# テキストボックスと実行ボタンを表示
display(text_box, submit_button)
```

完成。chainの部分を色々変えて試行錯誤しながら使えそう。