【GPT】LangChainで自動で分析してくれる簡易ツールを作った備忘録

# 背景
巷で流行りの生成AIを実験してみたく、csv形式のローデータを入れると自動で分析ステップを踏みながら出力してくれるツールを作りました。
グラフを表示するところなどで未完成な部分はありますが、大枠ができたので備忘録として公開します。

## 完成物
csvデータを入れるとこのように動作します。データはタイタニック号乗客者の生存状況に関するものです。
![62a9d761-6e9a-460a-b330-b0ef6d704544_Trim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/414468/edf7102d-6439-f941-b8a8-0b1329c8d3d4.gif)

# 参考
https://qiita.com/yazoo/items/4549f4b319b6706d46e2

https://www.mattari-benkyo-note.com/2023/04/27/langchain_pandas/


# 使用技術
+ Python
+ LangChain
→LangChainAgentというものを使いました。
+ Streamlit
→PythonベースでWebアプリケーションを作成するためのフレームワーク。簡易的なものであればめちゃくちゃ便利です。

# コード
```
import streamlit as st
from langchain.llms import OpenAI
from langchain.agents import create_pandas_dataframe_agent
from langchain.memory import ConversationBufferMemory
from langchain.callbacks.base import BaseCallbackHandler
from langchain.schema import HumanMessage
from typing import Any, Dict, List
import pandas as pd
import os
import json
import re
from collections import namedtuple
import matplotlib.collections
import matplotlib.pyplot as plt

AgentAction = namedtuple('AgentAction', ['tool', 'tool_input', 'log'])

class SimpleStreamlitCallbackHandler(BaseCallbackHandler):
    def __init__(self) -> None:
        self.tokens_area = st.empty()
        self.tokens_stream = ""
        
    def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
        self.tokens_stream += token
        self.tokens_area.markdown(self.tokens_stream)

def run_agent(df):
    state = {"memory": ConversationBufferMemory(memory_key="chat_history")}
    agent = create_pandas_dataframe_agent(OpenAI(temperature=0), df, memory=state['memory'], verbose=True, return_intermediate_steps=True)
    prompt = """
    あなたはPythonでpandasのdataframeを操作しています。dataframeの名前は`df`です。
    あなたは以下のツールを使って、投げかけられた質問に日本語で答える必要があります：

    python_repl_ast： Pythonのシェルです。python_repl_ast：Pythonのシェルです。入力は有効なpythonコマンドである必要があります。このツールを使用すると、時々出力が省略されます - あなたの答えにそれを使用する前に、それが省略されたように見えないことを確認してください。

    与えられたデータから、以下の分析をステップバイステップで行ってください。

    Step.1 データの要約を説明する

    Step.2 基本統計量を確認する
    ここでは、平均値、中央値、標準偏差、最大値、最小値など、基本的な統計量を確認します。

    Step.3 データ分布の確認
    代表的なカラムを使って、散布図を描画してください。

    Step.4 分析のまとめと提案
    上記の結果を踏まえて、データの分析概要と、そこから得られる提案や仮説があれば提示してください。
    
    
    なお、質問と答えについては、次の形式を使用してください：

    質問：あなたが答えなければならない入力の質問
    思考：何をすべきか常に考えておくこと
    行動：取るべき行動。[python_repl_ast]のいずれかであるべきです。
    Action Input：アクションへの入力
    観察: アクションの結果
    ... （このThought/Action/Action Input/ObservationはN回繰り返すことができます。）
    思考： 最終的な答えがわかった
    最終的な答え：入力された元の質問に対する最終的な答え


    """
    result = agent({"input": prompt})
    return result

st.title('Langchain Agent')

uploaded_file = st.file_uploader("CSVファイルをアップロードしてください。", type=['csv'])
![Something went wrong]()

if uploaded_file is not None:
    df = pd.read_csv(uploaded_file)
    st.dataframe(df)
    if st.button('実行'):
        with st.spinner('Agent is running...'):
            result = run_agent(df)
            answer = json.dumps(result['output'],ensure_ascii=False).replace('"', '')
            print(answer)
            st.write("分析結果:"+answer)
        
        actions = result['intermediate_steps']
        actions_list = []
        for action, result in actions:
            text = f"""Tool: {action.tool}\n
                Input: {action.tool_input}\n
                Log: {action.log}\nResult: {result}\n
            """
            if action.log is not None:
                st.write(action.log)
            if result is not None:
                st.set_option('deprecation.showPyplotGlobalUse', False)
                if isinstance(result, matplotlib.collections.PathCollection):                    
                    st.pyplot()
                elif isinstance(result, matplotlib.axes.Axes):
                    st.pyplot()
                else:
                    st.write(result)

            
            text = re.sub(r'`[^`]+`', '', text)
            actions_list.append(text)
        
        with st.expander('ログ', expanded=False):
            st.write('\n'.join(actions_list))


        # st.write(answer)

```

なお、実行にはOpenAIから発行されたAPIキーを環境変数にセットする必要性があります。

## ポイント
### プロンプト
事前に与えるプロンプトは上記参考サイトをもとに日本語で改変しました。「どんなツールを使いたいのか」「そのツールを使ってどのようなことをしたいのか」を具体的に明示すると良さそうですが、改善の余地はありそうです。
分析ステップについては、ChatGPT(GPT-4)にきいて作成しました。

### 途中結果の取得
今回最も手こずったところです。LangChainにおいて最終結果は簡単に取得できるのですが、途中の思考過程を情報として取得し表示することがなかなか大変でした。
結論としては、`result['intermediate_steps']`で途中結果を取得し、それをfor文で取り出した上でそれぞれ`st.write(action.log)`という形でStreamlitに反映しています。

### グラフ描画
LangChainの結果が全て文字列であれば、上記のように`st.write(action.log)`でよいのですが、Streamlit上でグラフを描画する場合これではうまくいきません。
グラフを描画する場合は`st.pyplot()`という表現を使いますが、そのために途中結果を取得した際にオブジェクトの形式を判定した上で、pyplotするかwriteするか条件分岐をしています。
現行のコードだとmatplotlibのみしか対応していないので、seabornなど別の描画パッケージを使われると描画エラーが発生すると思われます。あらゆるケースに対応するように条件分岐を加えるか、プロンプト内で使用ツールを制限する表現を加えるかどちらかのアプローチが必要だと考えています。

### まとめ
上記完成物を見ていただくとわかるように、「なんとなく解析している」ようには見えますが、細かいところをみていくと若干？となる解釈結果が出ているようです。
自分が使用したAPIが「gpt-3.5」であるため、「gpt-4」にするともう少しマシな結果を得られるかもしれませんし、プロンプトをさらに具体的な表現にすることで改善するかもしれません。もしかしたらこういうデータ分析のすべてのステップを生成AIで自動化させるよりも、データ分析自体は決まった型に入れたプログラムで実行し、最後の解釈結果を自然言語化する際にこのようなものを使うというのが優れた使い方なのかもしれません。
取捨選択が必要なのか、そのあたりも踏まえながら**賢く**使っていきたいと思います。