Memory対応したOpenAI Function Agentで実現する天気予報対応チャットアプリの実現方法


# はじめに

以前OpenAIの`Function Calling`を利用方法を確認した。今回は`Function　Calling`の機能をサポートしたChatGPTのようなチャットアプリの実現方法について検討した。

https://qiita.com/rairaii/items/b86704eac9f55e30eed2

# 結論

以下の3点からLangChainを利用したChatGPTのようなチャットアプリの実現方法について確認した。

1. ConversationBufferMemoryを使用してチャット履歴を管理する
LangChainでは、ConversationBufferMemoryを利用してチャット履歴を管理することができる。ユーザーとの対話の過去のやり取りを記憶し、チャットの流れを維持することが可能となる。

2. OpenAI Function AgentでOpenAI APIのFunction Calling機能を利用する
OpenAI Function Agentは、OpenAI APIのFunction Calling機能を利用することができる特殊なエージェントだ。これにより、カスタム関数を定義してOpenAI APIを呼び出すことが可能となる。例えば、天気予報を取得するカスタム関数を作成し、Function Agentを使用してOpenAI APIを介して呼び出すことができる。

3. OpenAI Function AgentにMemoryを追加する
OpenAI Function Agentは、さらにMemoryを追加することができます。このMemoryには、必要な情報を保存しておくことができる。例えば、天気予報を取得する前に、ユーザーの都市情報をMemoryに保存しておき、それを利用して天気予報の回答を行うことができる。

以上の3つの要素を組み合わせて、LangChainを使用したチャットアプリでOpenAI Function Agentを活用し、チャット履歴に対応した天気予報回答プログラムを実現した。ユーザーとの自然な対話が可能となり、過去のやり取りを覚えているかのような対話体験を提供することができる。




# ChatGPTのようなチャットアプリとは？

下記のサイトで紹介されているような過去のチャット履歴の文脈に基づいて回答が返ってくるChatGPTクローンのようなアプリのことだ。

https://eng-blog.iij.ad.jp/archives/18518

過去のチャット履歴を活用して回答する方法は、OpenAIのようなニューラルネットワークには記憶機能がないため、履歴データを別途提供する必要がある。これは少し力技のように見えるが、学習中でない限り、ニューラルネットワーク自体は情報を覚えることができないためだ。

履歴データを手動で管理するのは簡単ではないため、LangChainはMemory機能を活用して実装する。特にConversationBufferMemoryという機能は、チャットに特化しており、ChatGPTのようなアプリケーションをより簡単に開発できるようになっている。これを利用することで、過去の会話を効果的に活用しながらよりスムーズな対話型のAIアプリを作成することができる。

https://zenn.dev/umi_mori/books/prompt-engineer/viewer/langchain_memory#3.-buffer-memory%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9

https://zenn.dev/umi_mori/books/prompt-engineer/viewer/chatgpt_webapp_langchain_streamlit


# LangChain AgentでFunction Callingを利用する

LangChainを利用するのであれば、OpenAI APIのFunction Calling機能もLangChainから利用するのが良いだろう。LangChainにはFunction Callingのような機能を独自に実現する`Agent`がある。Function CallingがOpenAIでサポートされたのに合わせて[OpenAI Functions Agent](https://python.langchain.com/docs/modules/agents/how_to/custom-functions-with-openai-functions-agent)が実装された。

https://python.langchain.com/docs/modules/agents/how_to/custom-functions-with-openai-functions-agent

OpenAI Functions Agentを利用するには最新のLangChainのライブラリが必要になる。ライブラリのアップデートをしておきたい。

```terminal
$ pip install -U langchain
```


# AgentにMemoryを追加する
OpenAI Functions AgentへのMemoryの追加方法は、これまでのAgentへのMemoryの追加とは手法が異なるため注意が必要だ。

## Custom Agentを使ってAgentにMemoryを追加する
AgentへMemoryを追加する場合、通常は`Custom Agent`を作成して実現する。しかし、この方法はOpenAI Functions Agentには適用できない。

:::note alert
OpenAI Functions Agentには`Custom Agent`を作成する手法が利用できない
:::

https://python.langchain.com/docs/modules/memory/agent_with_memory


## OpenAI Functions AgentにMemoryを追加する

OpenAI Functions AgentにMemoryを追加するには下記のように行う。
1. エージェントの設定情報を格納するagent_kwargsにConversationBufferMemoryへの参照を設定
1. チャット履歴を管理するConversationBufferMemoryを作成する
1. AgentType.OPENAI_FUNCTIONSを指定してエージェントを初期化

```python
agent_kwargs = {
    "extra_prompt_messages": [MessagesPlaceholder(variable_name="memory")],
}

memory = ConversationBufferMemory(memory_key="memory", return_messages=True)

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True,
    agent_kwargs=agent_kwargs,
    memory=memory,
)
agent.run("hi")
```

https://python.langchain.com/docs/modules/agents/how_to/add_memory_openai_functions


# チャットで天気予報を回答させる
LangChainを使って、履歴管理とFunction Callingを組み合わせて利用する方法を確認した。ここでは、実際にチャットで天気予報を回答させるプログラムを実行して結果を確認する。

## 確認用プログラム
```terminal: Google Colab
!pip install langchain
!pip install openai
```

```python
import os
os.environ["OPENAI_API_KEY"] = "<Your OpenAI API KEY>"

from langchain.agents import Tool
from langchain.chains.conversation.memory import ConversationBufferMemory
from langchain import OpenAI
from langchain.utilities import GoogleSearchAPIWrapper
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.chat_models import ChatOpenAI
from langchain.schema import SystemMessage
from langchain import OpenAI, LLMChain, PromptTemplate

# 気象データをapi.open-meteo.comから取得する
import requests
import json
def get_weather_info(latitude, longitude):
    base_url = "https://api.open-meteo.com/v1/forecast"
    parameters = {
        "latitude": latitude,
        "longitude": longitude,
        #        "current_weather": "true",
        "hourly": "temperature_2m,relativehumidity_2m",
        "forecast_days": "3",
        "timezone": "Asia/Tokyo"
    }
    response = requests.get(base_url, params=parameters)
    if response.status_code == 200:
        data = response.json()
        return json.dumps(data)
    else:
        return None

#
# OpenAI Functions Agent　に get_weather_info の使い方を教える
#
from typing import Type
from pydantic import BaseModel, Field
from langchain.tools import BaseTool

class WeatherInfoInput(BaseModel):
  latitude: int = Field(descption = "latitude")
  longitude: int = Field(descption = "longitude")

class WeatherInfo(BaseTool):
  name = 'get_weather_info'
  description = "This is useful when you want to know the weather forecast. Enter longitude in the latitude field and latitude in the longitude field."
  args_schema: Type[BaseModel] = WeatherInfoInput
  def _run(self, latitude: int, longitude: int):
    return get_weather_info(latitude, longitude)
  def _arun(self, ticker: str):
    raise NotImplementedError("get_stock_performance does not support async")

#
# MemoryとFunction Callingに対応した Agent を用意する
#
from langchain.prompts import MessagesPlaceholder
from langchain.memory import ConversationBufferMemory

llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-16k")

tools = [
  WeatherInfo(),
]

def OpenAIFunctionsAgent(tools= tools, llm = llm, verbose=False):
  agent_kwargs = {
      "extra_prompt_messages": [MessagesPlaceholder(variable_name="memory")],
  }
  memory = ConversationBufferMemory(memory_key="memory", return_messages=True)
  # Add the user's prompt to the memory if need
  prompt = """
  The following is a friendly conversation between a human and an AI.
  The AI is talkative and provides lots of specific details from its context.
  If the AI does not know the answer to a question, it truthfully says it does not know.
  """
  memory.save_context({"input": prompt}, {"ouput": "understand!"})
  return initialize_agent(
      tools=tools,
      llm=llm,
      agent=AgentType.OPENAI_FUNCTIONS,
      verbose=True,
      agent_kwargs=agent_kwargs,
      memory=memory,
  )

agent_chain = OpenAIFunctionsAgent(tools = tools, llm = llm, verbose = True)
```

## プログラムの実行結果
チャット履歴機能が上手に働いている事を確認するために、挨拶から開始する。

```python
agent_chain.run(input="こんにちは")
> Entering new AgentExecutor chain...
こんにちは！お元気ですか？何かお手伝いできますか？

> Finished chain.
```

AIアシスタントのように挨拶した

```python
agent_chain.run(input="わたしの名前はシャンシャンです")
> Entering new AgentExecutor chain...
こんにちは、シャンシャンさん！素敵な名前ですね。私はAIアシスタントです。どのようにお手伝いできますか？

> Finished chain.
こんにちは、シャンシャンさん！素敵な名前ですね。私はAIアシスタントです。どのようにお手伝いできますか？
```

名前を名乗ってみた。AIアシスタントは名前を覚えただろうか？

```python
agent_chain.run("私の名前がわかりますか？")
 new AgentExecutor chain...
はい、先ほどおっしゃった通り、あなたの名前はシャンシャンさんですね。私は会話の中で情報を覚えていますので、お名前も覚えていますよ。どのようなお話をしましょうか？

> Finished chain.
はい、先ほどおっしゃった通り、あなたの名前はシャンシャンさんですね。私は会話の中で情報を覚えていますので、お名前も覚えていますよ。どのようなお話をしましょうか？
```

名前を回答できている。

```python
print(agent_chain.run(input="横浜の今日の天気は？人がどのように感じる天気なのかと、ドライブしているユーザーが今日どのように過ごすと良いか提案を含めてアナウンサー口調で答えよ"))
> Entering new AgentExecutor chain...
Invoking: `get_weather_info` with `{'latitude': 35.4437, 'longitude': 139.638}`
{"latitude": 35.0, "longitude": 139.0, "generationtime_ms": 0.10704994201660156, "utc_offset_seconds": 32400, "timezone": "Asia/Tokyo", "timezone_abbreviation": "JST", "elevation": 234.0, "hourly_units": {"time": "iso8601", "temperature_2m": "\u00b0C", "relativehumidity_2m": "%"}, "hourly": {"time": ["2023-07-29T00:00", "2023-07-29T01:00", "2023-07-29T02:00", "2023-07-29T03:00", "2023-07-29T04:00", "2023-07-29T05:00", "2023-07-29T06:00", "2023-07-29T07:00", "2023-07-29T08:00", "2023-07-29T09:00", "2023-07-29T10:00", "2023-07-29T11:00", "2023-07-29T12:00", "2023-07-29T13:00", "2023-07-29T14:00", "2023-07-29T15:00", "2023-07-29T16:00", "2023-07-29T17:00", "2023-07-29T18:00", "2023-07-29T19:00", "2023-07-29T20:00", "2023-07-29T21:00", "2023-07-29T22:00", "2023-07-29T23:00", "2023-07-30T00:00", "2023-07-30T01:00", "2023-07-30T02:00", "2023-07-30T03:00", "2023-07-30T04:00", "2023-07-30T05:00", "2023-07-30T06:00", "2023-07-30T07:00", "2023-07-30T08:00", "2023-07-30T09:00", "2023-07-30T10:00", "2023-07-30T11:00", "2023-07-30T12:00", "2023-07-30T13:00", "2023-07-30T14:00", "2023-07-30T15:00", "2023-07-30T16:00", "2023-07-30T17:00", "2023-07-30T18:00", "2023-07-30T19:00", "2023-07-30T20:00", "2023-07-30T21:00", "2023-07-30T22:00", "2023-07-30T23:00", "2023-07-31T00:00", "2023-07-31T01:00", "2023-07-31T02:00", "2023-07-31T03:00", "2023-07-31T04:00", "2023-07-31T05:00", "2023-07-31T06:00", "2023-07-31T07:00", "2023-07-31T08:00", "2023-07-31T09:00", "2023-07-31T10:00", "2023-07-31T11:00", "2023-07-31T12:00", "2023-07-31T13:00", "2023-07-31T14:00", "2023-07-31T15:00", "2023-07-31T16:00", "2023-07-31T17:00", "2023-07-31T18:00", "2023-07-31T19:00", "2023-07-31T20:00", "2023-07-31T21:00", "2023-07-31T22:00", "2023-07-31T23:00"], "temperature_2m": [23.3, 23.5, 22.8, 22.4, 22.0, 21.6, 22.9, 24.9, 26.6, 28.3, 29.6, 30.6, 30.5, 30.8, 30.7, 30.4, 29.9, 28.7, 26.9, 25.2, 24.4, 23.6, 23.2, 22.8, 22.5, 22.5, 22.2, 22.1, 22.3, 22.0, 22.7, 24.5, 26.4, 28.0, 29.4, 31.0, 31.8, 32.0, 32.1, 31.6, 30.8, 29.2, 27.5, 26.3, 25.7, 25.1, 24.7, 24.3, 24.1, 24.2, 24.0, 23.6, 23.5, 23.4, 23.9, 25.3, 26.8, 28.0, 29.0, 29.4, 30.8, 30.7, 29.4, 29.7, 28.1, 27.4, 26.8, 25.9, 25.3, 25.2, 25.1, 25.0], "relativehumidity_2m": [92, 88, 90, 89, 91, 92, 90, 85, 81, 70, 62, 59, 64, 63, 64, 62, 60, 62, 70, 82, 82, 88, 90, 92, 94, 93, 92, 92, 90, 90, 89, 84, 75, 68, 62, 55, 54, 55, 55, 58, 61, 68, 76, 82, 87, 91, 90, 90, 91, 90, 91, 92, 94, 93, 92, 87, 79, 73, 69, 66, 57, 54, 60, 61, 71, 77, 81, 85, 90, 89, 87, 85]}}横浜の今日の天気は、気温は23.3℃から32.0℃の間で変動します。朝の気温は23.3℃で、昼間に最高気温の32.0℃になります。湿度は92%から54%の範囲で変動し、朝は92%の湿度がありますが、昼間には54%まで下がります。
このような天気の中、ドライブを楽しむには、朝の涼しい時間帯や夕方の日が傾く頃がおすすめです。気温が高くなる昼間は、エアコンを効かせた車内で快適に過ごすことができます。また、日差しを避けるために帽子や日焼け止めを使用することも大切です。
横浜の天気は変わりやすいので、天気予報をチェックしながら計画を立てることをおすすめします。楽しいドライブをお楽しみください！

> Finished chain.
横浜の今日の天気は、気温は23.3℃から32.0℃の間で変動します。朝の気温は23.3℃で、昼間に最高気温の32.0℃になります。湿度は92%から54%の範囲で変動し、朝は92%の湿度がありますが、昼間には54%まで下がります。
このような天気の中、ドライブを楽しむには、朝の涼しい時間帯や夕方の日が傾く頃がおすすめです。気温が高くなる昼間は、エアコンを効かせた車内で快適に過ごすことができます。また、日差しを避けるために帽子や日焼け止めを使用することも大切です。
横浜の天気は変わりやすいので、天気予報をチェックしながら計画を立てることをおすすめします。楽しいドライブをお楽しみください！
```

横浜の天気について問い合わせたところ、get_weather_info関数を呼び出して気象データを入手し天気予報を回答した。

```python
print(agent_chain.run(input="北海道は？"))
> Entering new AgentExecutor chain...
[省略...]

> Finished chain.
北海道の今日の天気は、気温は19.3℃から24.8℃の間で変動します。朝の気温は19.3℃で、昼間に最高気温の24.8℃になります。湿度は96%から72%の範囲で変動し、朝は96%の湿度がありますが、昼間には72%まで下がります。
このような天気の中、ドライブを楽しむには、涼しい時間帯や湿度が低い時間帯がおすすめです。朝の涼しい時間帯や夕方の日が傾く頃にドライブをすると、快適に過ごすことができます。また、北海道の景色を楽しむために、美しい自然や観光地を訪れるのも良いでしょう。
天気は変わりやすいので、天気予報をチェックしながら計画を立てることをおすすめします。楽しいドライブをお楽しみください！
```

「北海道は？」という問いに対して、「北海道の天気は？」という問いだと予測して北海道の天気を回答した。チャット履歴とFunction Callingの組み合わせに対応したChatGPTのようなチャットアプリの実現できそうだ。