Azure OpenAIのFunction CallingとLangChain Agentを使用して指定したURL記事の要約をしてくれるアプリを作ってみる


# ページ要約の利用イメージ
Streamlitで入力フォームを作成し、URLをコピペして要約ボタンを押下することで実行します。

![スクリーンショット 2023-10-15 15.23.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288299/b7e4aa9a-a557-8529-0a3a-f1d57610f7f9.png)

例えば、以下のYahooニュースのURLをコピペして実行すると、

https://news.yahoo.co.jp/pickup/6478503

![スクリーンショット 2023-10-15 15.26.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288299/541adc2a-2dde-8f19-37a6-df47c399d648.png)

以下のようにAgentから要約を返すようにします。これで記事の内容を全部読まずに要約だけ目を通して時短できるようにします。

![スクリーンショット 2023-10-15 15.26.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288299/1a5704b7-ab7b-8e01-8373-8505c208bb79.png)

# Function Callingとは
「gpt-35-turbo と gpt-4 の最新バージョンは、関数を操作するために微調整されており、関数を呼び出す必要があるタイミングと方法の両方を判断できます。 要求に 1 つ以上の関数が含まれている場合、モデルはプロンプトのコンテキストに基づいて、いずれかの関数を呼び出す必要があるかどうかを判断します。 モデルは、関数を呼び出す必要があると判断すると、その関数の引数を含む JSON オブジェクトで応答します。」　（MS AOAI Function callingの説明、下記より。）

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/how-to/function-calling

これは、Re-Act（LLMが何が必要か考え、実行するプロンプトエンジニアリング手法）を実現するための機能。あらかじめ関数を定義しておき、LLM側でこの関数の実行が必要だと判断された場合には適切な引数を設定して返してくれる。（LangChain Agentでこれを受けて関数として定義しているToolを実行し、またLLMに最終回答を生成させる）

# 入力フォーム(Streamlit)
```
    import streamlit as st

    st.title("URL記事要約 by function calling + PythonREPLTool")
    
    with st.form("my_form", clear_on_submit=True):
        text = st.text_input("URL")
        submitted = st.form_submit_button("要約")
```
Streamlitはpythonで簡単にWeb画面を作成してくれる超便利なライブラリ。flaskやDjangoなどのpythonのWebフレームワークはとても手軽だが、さらに簡単に手軽。（ただし、画面のリフレッシュタイミングなどクセもあるので、難しいことをする場合には注意。）
フォームを定義し、テキストボックス、ボタンを作成している。

# メインロジック
ボタン押下後の挙動を作成する。LangChainのLLM定義、Agent Toolの定義、エージェントの初期化、プロンプト指示を定義し、入力内容を元にエージェント実行する。
- AOAIのLLMを定義
- APIバージョンは、2023-07-01-previewを指定（Function Callingを使用するため。）
- エージェントはPythonREPLToolを指定した。対象ページをスクレイピング取得して内容を解析させる。

```
if submitted:
    try:
        with st.spinner("考え中..."):
            llm = AzureChatOpenAI(
                    temperature=0,
                    deployment_name=os.environ["AZURE_DEPLOYMENT_ID"],
                    openai_api_base=os.environ["OPENAI_API_HOST"],
                    openai_api_version=os.environ["OPENAI_API_VERSION"],
                    openai_api_key=os.environ["OPENAI_API_KEY"],
                    openai_api_type="azure",
                    streaming=True,
                    max_tokens=2000,
                    callback_manager=BaseCallbackManager(
                        [StreamlitCallbackHandler(st.container())],                        
                    ),
                )

            # カスタムツールの登録は従来のAgentと同様
            tools = [
                PythonREPLTool()
            ]
            # エージェントの準備
            agent = initialize_agent(
                tools,
                llm,
                agent=AgentType.OPENAI_FUNCTIONS,  # ここで、AgentType.OPENAI_FUNCTIONSを指定する
                verbose=True,
                return_intermediate_steps=True)

            text =  '''このURLの内容を取得するpythonコードを作成し、実行して、要約して。 
              なお、 article_body = soup.find('p', {'class': 'Article-Body'}).get_text() としてもArticle-Bodyが存在せず、
              エラー AttributeError("'NoneType' object has no attribute 'get_text'") となったので、タグ指定せず
              全体からテキストを取得して。functionとして実行し要約結果だけ表示して。
              URL: 
            ''' + text

            response = agent({"input": text})


    except Exception as e:
        response = str(e)
        if not response.startswith("Could not parse LLM output: `"):
            raise e
        response = response.removeprefix("Could not parse LLM output: `").removesuffix(
            "`"
        )
```

# LLM定義
ここはAOAIを使う場合のAzureChatOpenAIの定義。リソース設定は環境変数に設定してある前提としている。

# カスタムツール登録
```
            tools = [
                PythonREPLTool()
            ]
```
このPythonREPLTool()はLLMが必要と判断したpythonコードを生成して実行してくれるTool。（とても便利）

https://python.langchain.com/docs/integrations/toolkits/python

pipパッケージが足らない場合にはエラーとなるので、インストールしてから再実行する。（コンテナの場合にはDockerfileにて事前にインストールするなどしておく） openinterpreterではpipインストールのコマンドまで聞いてくれたはず。
（bash toolを入れればそこまでやってくれるかも？（未検証））

https://python.langchain.com/docs/integrations/tools/bash

# エージェントの準備
```
            agent = initialize_agent(
                tools,
                llm,
                agent=AgentType.OPENAI_FUNCTIONS,  # ここで、AgentType.OPENAI_FUNCTIONSを指定する
                verbose=True,
                return_intermediate_steps=True) 
```
エージェント初期化にて、Function Callingを使用する場合、agent=AgentType.OPENAI_FUNCTIONS を指定する。

https://python.langchain.com/docs/modules/agents/agent_types/openai_functions_agent


# プロンプト
```
 text =  '''このURLの内容を取得するpythonコードを作成し、実行して、要約して。 
              なお、 article_body = soup.find('p', {'class': 'Article-Body'}).get_text() としてもArticle-Bodyが存在せず、
              エラー AttributeError("'NoneType' object has no attribute 'get_text'") となったので、タグ指定せず
              全体からテキストを取得して。functionとして実行し要約結果だけ表示して。
              URL: 
            ''' + text
```
ここで挙動として、python toolを使用して、入力したURLの内容を取得し、「要約」するよう指示する。
指示のみで実行した場合、Beautiful soupを使用して特定タグを探す挙動をするが、それだと期待するタグがない場合に深掘りして深みにハマるケースがあったため、細かい挙動を指示している。


# 実行時のエージェント標準出力内容
![スクリーンショット 2023-10-15 15.27.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288299/0a975373-d983-bef1-5560-5fe14027c8e7.png)

PythonREPLToolによって作成・実行されたpythonコード、取得した記事のテキスト全文、要約した内容が確認できる。


# まとめ
FunctionCalling + AOAI + LangChainを使って簡易な要約ツールを作ってみました。
（実行に際しては要約対象サイトにリクエスト負荷が発生しますので、ご留意の上参考にしてください。）