LangChainとLlamaindexそしてStreamlitでYoutube動画の要約・対話アプリを作ってみた

こんにちは！LangChain使ってますか？やばいですよね。楽しすぎますよね。。奥が深すぎるのでまずは触りながら学ぶために、LlamaIndexとStreamlitを用いて、YouTube動画の要点をまとめるシンプルなWebアプリ「ChatTube」を作成しました。
https://chattube.streamlit.app/
![chattube.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53055/681b5851-d09d-b395-e6fb-659741f1a618.png)


# やっていること
１．OpenAI APIを用いた自然言語処理
２．YouTube動画の情報の取得
３．会話スタイルのインターフェース

## 先に言い訳。。
今回はとりあえず動くことを目的としたので作りは粗いです。とくにLangChainのYoutubeLoaderがうまく動かなかったのもあり、結局途中からLlamaIndexも導入することになりました。
下記については認識している制約です。
- 英語字幕（自動字幕不可）がついているもののみ　＊試す際はTEDの動画などが字幕しっかりしてるのでおすすめです。
- LLMはGPT3（つまりChatGPTじゃありません。過去の履歴は読んでる雰囲気ありますが、読んでません。）

字幕に関してはWhisperを使って、LLMはGPT4にするっていうだけですが今回はそこまで手が回りませんでした。

# ツールについて
LangChain、LlamaIndex、およびStreamlitは、Pythonプログラミングにおいて異なる目的で使用されるライブラリです。それぞれの概要を以下に説明します。

## [LangChain](https://python.langchain.com/en/latest/index.html): 
LangChainは、自然言語処理（NLP）タスクを簡単に実行できるように設計されたPythonライブラリです。LangChainでは、会話モデルや文書モデルを使って、様々なタスク（例：質問応答、要約、文書生成）を行うことができます。このライブラリは、OpenAI GPT-3などの大規模な言語モデルを簡単に扱えるように設計されています。このコードでは、ConversationChainとOpenAIクラスをインポートしています。

## [LlamaIndex](https://gpt-index.readthedocs.io/en/latest/index.html): 
LlamaIndexは、文書データのインデックス作成と検索を行うPythonライブラリです。このライブラリでは、文書のベクトル表現を作成し、類似性に基づいて検索を行うことができます。コード内では、GPTSimpleVectorIndexクラスをインポートし、YouTube動画のトランスクリプトをベクトル化してインデックスを作成しています。これにより、質問に対する回答を効率的に検索できます。

## [Streamlit](https://streamlit.io/): 
Streamlitは、Pythonでデータアプリケーションを簡単かつ迅速に作成するためのフレームワークです。Streamlitを使うことで、データサイエンティストや開発者は、複雑なWebアプリケーションやダッシュボードを短時間で構築できます。このコードでは、Streamlitを使って、YouTube動画の要点をまとめるシンプルなWebアプリを作成しています。Streamlitのウィジェット（例：テキスト入力、ボタン、動画表示）を使用して、インタラクティブなインターフェースを実現しています。

## ってことですが
上記説明はGPT生成です。こんなさらっと言ってますがそれぞれ超便利なので人間の僕が追記します！LangChainはLLMを自律的に動かすことができる（自問自答させる）ので、細かいプロンプトなく勝手に動かすことができます。しかもChatGPTのプラグインにできることは大体できます。PythonもBashもLLMに生成させて実行させることができます。やばくないですか。この自律的に動くプログラムをAgentと呼びますが、AgentはさらにAgentを生成することができ、分業できます。会社つくれますよ、会社。Agentだけで無人の。

LlamaIndexはプロンプトに頼らずに大きなコンテンツを読ませるのに必要です。Embeddingという方法ですが、ドメイン知識はファインチューニングではなくLlamaIndexなどでEmbedしたほうがコスト的にも信頼性的にも良いみたいです。

Streamlitは、あなたがフロントエンドエンジニアでなくPython使いなら確実に触った方が良いです。。超感動しました。Bootstrapがシンプルだと思っているあなた。Streamlitは圧倒的にシンプルです。HTML知識ゼロでOKです。

# コードの全体像
まずはコードの全体像を共有します。
https://github.com/yazoo1220/ChatTube/blob/main/main.py
```
"""Python file to serve as the frontend"""
import streamlit as st
from streamlit_chat import message
import os

from langchain.chains import ConversationChain
from langchain.llms import OpenAI


st.set_page_config(page_title="ChatTube", page_icon=":robot:")
st.header("ChatTube")

if "generated" not in st.session_state:
    st.session_state["generated"] = []

if "past" not in st.session_state:
    st.session_state["past"] = []


api_token = st.text_input('OpenAI API　Token',type="password")
submit_button = st.button('Submit')

if submit_button:
    if api_token:
        os.environ['OPENAI_API_KEY'] = api_token
        st.write('API token set successfully.')
    else:
        st.write('Please input a valid API token.')
else:
    st.write('Waiting for API token input...')

def load_chain():
    """Logic for loading the chain you want to use should go here."""
    llm = OpenAI(temperature=0)
    chain = ConversationChain(llm=llm)
    return chain

if os.environ['OPENAI_API_KEY']!="":
    try:
        chain = load_chain()
    except Exception as e:
        st.write("error loading data: " + str(e))
else:
    st.write("waiting for api token input...")

from llama_index import download_loader, GPTSimpleVectorIndex

video_url = st.text_input("your YouTube url here")
if video_url:
    st.video(video_url)
    YoutubeTranscriptReader = download_loader("YoutubeTranscriptReader")
    loader = YoutubeTranscriptReader()
    documents = loader.load_data(ytlinks=[video_url])    
else:
    st.video('https://youtu.be/L_Guz73e6fw')
    YoutubeTranscriptReader = download_loader("YoutubeTranscriptReader")
    loader = YoutubeTranscriptReader()
    documents = loader.load_data(ytlinks=['https://youtu.be/L_Guz73e6fw'])  
def get_text():
    input_text = st.text_input("You: ", "この動画の要点を3つまとめてください。回答は日本語でお願いします。", key="input")
    return input_text


user_input = get_text()
load_button = st.button('ask')

from langchain.document_loaders import YoutubeLoader
index = ""
if load_button:
    try:
        index = GPTSimpleVectorIndex.from_documents(documents)
    except Exception as e:
        st.write("error loading the video: "+ str(e))
else:
    st.write("waiting for Youtube video to be loaded")


if index == "":
    pass
else:
    with st.spinner('waiting for the answer...'):
        output = index.query(user_input)
        st.session_state.past.append(user_input)
        st.session_state.generated.append(output.response)


if st.session_state["generated"]:

    for i in range(len(st.session_state["generated"]) - 1, -1, -1):
        try:
            message(st.session_state["generated"][i], key=str(i))
            message(st.session_state["past"][i], is_user=True, key=str(i) + "_user")
        except:
            pass
```

# コードを部分部分で解説します

まず、必要なライブラリをインポートします。

```
import streamlit as st
from streamlit_chat import message
import os

from langchain.chains import ConversationChain
from langchain.llms import OpenAI
```
次に、ページのタイトルやアイコンを設定し、ヘッダーに「ChatTube」と表示させます。

```
st.set_page_config(page_title="ChatTube", page_icon=":robot:")
st.header("ChatTube")
```
ここで、セッションステートに関連する情報を初期化しています。

```
if "generated" not in st.session_state:
    st.session_state["generated"] = []

if "past" not in st.session_state:
    st.session_state["past"] = []
```
次に、OpenAI APIトークンの入力欄と送信ボタンを作成します。

```
api_token = st.text_input('OpenAI API　Token',type="password")
submit_button = st.button('Submit')
```

APIトークンが入力され、送信ボタンが押されると、そのトークンが環境変数に設定されます。
```
if submit_button:
    if api_token:
        os.environ['OPENAI_API_KEY'] = api_token
        st.write('API token set successfully.')
    else:
        st.write('Please input a valid API token.')
else:
    st.write('Waiting for API token input...')
```
APIトークンが設定されたら、load_chain()関数を使用して、OpenAIを使った会話チェーンをロードします。
```
def load_chain():
    """Logic for loading the chain you want to use should go here."""
    llm = OpenAI(temperature=0)
    chain = ConversationChain(llm=llm)
    return chain

if os.environ['OPENAI_API_KEY']!="":
    try:
        chain = load_chain()
    except Exception as e:
        st.write("error loading data: " + str(e))
else:
    st.write("waiting for api token input...")
```
次に、ユーザーがYouTube URLを入力できるようにします。そして、動画のトランスクリプトを取得します。
```
video_url = st.text_input("your YouTube url here")
```
URLが入力されると、その動画が表示され、トランスクリプトが読み込まれます。デフォルトではサンプル動画が表示されます。
```
if video_url:
    st.video(video_url)
    YoutubeTranscriptReader = download_loader("YoutubeTranscriptReader")
    loader = YoutubeTranscriptReader()
    documents = loader.load_data(ytlinks=[video_url])    
else:
    st.video('https://youtu.be/L_Guz73e6fw')
    YoutubeTranscriptReader = download_loader("YoutubeTranscriptReader")
    loader = YoutubeTranscriptReader()
    documents = loader.load_data(ytlinks=['https://youtu.be/L_Guz73e6fw']) 
```
次に、ユーザーが質問を入力できるテキストボックスを作成します。

```
def get_text():
    input_text = st.text_input("You: ", "この動画の要点を3つまとめてください。回答は日本語でお願いします。", key="input")
    return input_text

user_input = get_text()
load_button = st.button('ask')
```
質問が入力され、askボタンが押されると、トランスクリプトに基づいてインデックスが作成されます。
```
from langchain.document_loaders import YoutubeLoader
index = ""
if load_button:
    try:
        index = GPTSimpleVectorIndex.from_documents(documents)
    except Exception as e:
        st.write("error loading the video: "+ str(e))
else:
    st.write("waiting for Youtube video to be loaded")
```
インデックスが作成されると、質問に対する回答が生成されます。
```
if index == "":
    pass
else:
    with st.spinner('waiting for the answer...'):
        output = index.query(user_input)
        st.session_state.past.append(user_input)
        st.session_state.generated.append(output.response)
```
最後に、過去の質問と回答を表示します。

```
if st.session_state["generated"]:

    for i in range(len(st.session_state["generated"]) - 1, -1, -1):
        try:
            message(st.session_state["generated"][i], key=str(i))
            message(st.session_state["past"][i], is_user=True, key=str(i) + "_user")
        except:
            pass
```
これで完成です！ユーザーはYouTube動画のURLを入力し、動画の要点に関する質問を投げかけることができます。OpenAI APIを使って、その質問に対する回答が生成され、画面上に表示されます。

# 感想：いまどきのLLMアプリ、こんな簡単につくれていいんかいな。。
僕は非エンジニアで趣味でやってるくらいで、今回のアプリも子供が横で寝てる間に全てスマホでつくりました。（スマホだとifを勝手にIfにされたりして超イラつきました。今はPCで書いてます。PC最高。）
LangChainのドキュメントにあるものを切り貼りしたのがメインなのでそもそもあんまり書くことなかったですが、それでこういうアプリができちゃうんだからすごいなあと感じました。あと、Streamlitも素晴らしいですね。今回初めて使いましたが、たしかに柔軟性ないかもしれませんが圧倒的に簡単。そしてGithub更新したら速攻デプロイされてしまうお手軽さ。かなり好きになりました。

LangChainの面白さはToolsやAgentにあると思うんですが、今回は少しも触れませんでした。まだまだIndex周りをもう少しスムーズに使えるようにしたいなと思います。

＊この記事のコード解説の部分はChatGPTを使いました。