LlamaIndexとOpenAIを用いて自分のエージェントを構築する

こちらをウォークスルーします。途中エラーに遭遇したのでコードを修正しています。

https://docs.llamaindex.ai/en/stable/examples/agent/openai_agent.html

こちらのFunction Calling APIを使っているのですね。

https://openai.com/blog/function-calling-and-other-api-updates

# 自分のOpenAIエージェントを構築する

関数呼び出しをサポートする[新たなOpenAI API](https://openai.com/blog/function-calling-and-other-api-updates)によって、自分のエージェントの構築がかつてないほど簡単になっています！

このノートブックチュートリアルでは、**50行未満のコード**(!)でご自身のOpenAIエージェントをどのように記述するのかを説明します。これは最低限のものですが、(会話を進め、ツールを使用する)機能は完全なものです。

# 初期セットアップ

いくつかのシンプルなビルディングブロックをインポートするところからスタートしましょう。

主に必要になるのは:
1. (`llama_index` LLMクラスを使った)OpenAI API 
1. 会話履歴を保持する場所
1. 我々のエージェントが利用できるツールの定義

```py
%pip install llama-index-agent-openai
%pip install llama-index-llms-openai
%pip install llama-index
dbutils.library.restartPython()
```

```py
# OpenAIのセットアップ
import os
import openai

OPENAI_API_TOKEN = dbutils.secrets.get("demo-token-takaaki.yayoi", "openai_api_key")
os.environ["OPENAI_API_KEY"] = OPENAI_API_TOKEN
```

```py
import json
from typing import Sequence, List

from llama_index.llms.openai import OpenAI
from llama_index.core.llms import ChatMessage
from llama_index.core.tools import BaseTool, FunctionTool

import nest_asyncio

nest_asyncio.apply()
```

我々のエージェントのためにいくつかの非常にシンプルな計算機を定義しましょう。

```py
def multiply(a: int, b: int) -> int:
    """2つの整数を掛け算し、結果の整数を返却"""
    return a * b


multiply_tool = FunctionTool.from_defaults(fn=multiply)
```

```py
def add(a: int, b: int) -> int:
    """2つの整数を足し算し、結果の整数を返却"""
    return a + b


add_tool = FunctionTool.from_defaults(fn=add)
```

# エージェントの定義

それでは、**50行未満のコード**で、会話を保持し、ツールを呼び出す能力を持つ、我々のエージェントを定義します。

エージェントのロジックの肝となるのは、`chat`メソッドです。ハイレベルでは、3つのステップがあります:
1. (存在する場合)どのツールを呼び出すのか、どの引数を渡すのかを特定するためにOpenAIを呼び出し
1. アウトプットを取得するために、引数を用いてツールを呼び出し
1. 会話のコンテキストとツールのアウトプットからのレスポンスの調和を取るためにOpenAIを呼び出し

`reset`メソッドはシンプルに会話のコンテキストをリセットするので、別の会話をスタートすることができます。

```py
class YourOpenAIAgent:
    def __init__(
        self,
        tools: Sequence[BaseTool] = [],
        llm: OpenAI = OpenAI(temperature=0, model="gpt-3.5-turbo-0613"),
        chat_history: List[ChatMessage] = [],
    ) -> None:
        self._llm = llm
        self._tools = {tool.metadata.name: tool for tool in tools}
        self._chat_history = chat_history

    def reset(self) -> None:
        self._chat_history = []

    def chat(self, message: str) -> str:
        chat_history = self._chat_history
        chat_history.append(ChatMessage(role="user", content=message))
        tools = [
            tool.metadata.to_openai_tool() for _, tool in self._tools.items()
        ]

        ai_message = self._llm.chat(chat_history, tools=tools).message
        additional_kwargs = ai_message.additional_kwargs
        chat_history.append(ai_message)

        tool_calls = ai_message.additional_kwargs.get("tool_calls", None)
        # 並列関数呼び出しがサポートされました
        if tool_calls is not None:
            for tool_call in tool_calls:
                function_message = self._call_function(tool_call)
                chat_history.append(function_message)
                ai_message = self._llm.chat(chat_history).message
                chat_history.append(ai_message)

        return ai_message.content

    def _call_function(self, tool_call: dict) -> ChatMessage:
        id_ = tool_call.id
        function_call = tool_call.function
        tool = self._tools[function_call.name]
        output = tool(**json.loads(function_call.arguments))
        return ChatMessage(
            name=function_call.name,
            content=str(output),
            role="tool",
            additional_kwargs={
                "tool_call_id": id_,
                "name": function_call.name,
            },
        )
```

:::note info
**注意**
原文のコードですとここで[こちらのエラー](https://github.com/openai/openai-python/issues/774)に遭遇します。上のコードは修正済みです。
:::

# トライしてみましょう！

```py
agent = YourOpenAIAgent(tools=[multiply_tool, add_tool])
```
```py
agent.chat("こんにちは")
```

> 'こんにちは！なにかお手伝いできますか？'

```py
agent.chat("2123 * 215123はいくつですか")
```

> '2123 * 215123は456706129です。'

おおー。

# LlamaIndexの (少々優れている) `OpenAIAgent` 実装

LlamaIndexでは(少々優れている) `OpenAIAgent`実装を提供しており、以下のように直接使うことができます。

上述の簡素化されたバージョンと比較して:
* `BaseChatEngine` と `BaseQueryEngine`インタフェースを実装しており、LlamaIndexフレームワークで、よりシームレスに活用することができます。
* 会話のターンごとの複数関数の呼び出しをサポート
* ストリーミングのサポート
* 非同期エンドポイントのサポート
* コールバックとトレーシングのサポート

```py
from llama_index.agent.openai import OpenAIAgent
from llama_index.llms.openai import OpenAI
```
```py
llm = OpenAI(model="gpt-3.5-turbo-0613")
agent = OpenAIAgent.from_tools(
    [multiply_tool, add_tool], llm=llm, verbose=True
)
```

## チャット

```py
response = agent.chat("(121 * 3) + 42 はいくつですか？")
print(str(response))
```

どの関数を呼び出しているのかが確認できて良いですね。

```
Added user message to memory: (121 * 3) + 42 はいくつですか？
=== Calling Function ===
Calling function: multiply with args: {
  "a": 121,
  "b": 3
}
Got output: 363
========================

=== Calling Function ===
Calling function: add with args: {
  "a": 363,
  "b": 42
}
Got output: 405
========================

(121 * 3) + 42 は405です。
```

```py
# ソースの調査
print(response.sources)
```
```
[ToolOutput(content='363', tool_name='multiply', raw_input={'args': (), 'kwargs': {'a': 121, 'b': 3}}, raw_output=363), ToolOutput(content='405', tool_name='add', raw_input={'args': (), 'kwargs': {'a': 363, 'b': 42}}, raw_output=405)]
```

## 非同期チャット

```py
response = await agent.achat("121 * 3 はいくつですか？")
print(str(response))
```

ここでは分かりませんが非同期で動作します。

```
Added user message to memory: 121 * 3 はいくつですか？
=== Calling Function ===
Calling function: multiply with args: {
  "a": 121,
  "b": 3
}
Got output: 363
========================

121 * 3 は363です。
```

## ストリーミングチャット

ここではすべてのLLMレスポンスはジェネレータとして返却されます。すべてのインクリメンタルなステップをストリームしたり、最後のレスポンスのみをストリームすることができます。

```py
response = agent.stream_chat(
    "121 * 2 はいくつですか？ 答えがわかったら、ネズミのグループに関する物語を書く中でその数を使ってください。"
)

response_gen = response.response_gen

for token in response_gen:
    print(token, end="")
```

こちらもこれでは分かりませんが、ストリーミング処理され逐次表示されます。

```
Added user message to memory: 121 * 2 はいくつですか？ 答えがわかったら、ネズミのグループに関する物語を書く中でその数を使ってください。
=== Calling Function ===
Calling function: multiply with args: {
  "a": 121,
  "b": 2
}
Got output: 242
========================

121 * 2 は242です。

ある日、ネズミのグループが森の中を探検していました。彼らは食べ物を探しに行く途中で、242個のおいしいチーズを見つけました。ネズミたちは大喜びでチーズを分け合い、おなかいっぱいになりました。その後、彼らは楽しく遊びながら帰路につきました。
```

## 非同期ストリーミングチャット

```py
response = await agent.astream_chat(
    "121 * 2 はいくつですか？ 答えがわかったら、ネズミのグループに関する物語を書く中でその数を使ってください。"
)

response_gen = response.response_gen

async for token in response.async_response_gen():
    print(token, end="")
```
```
Added user message to memory: 121 * 2 はいくつですか？ 答えがわかったら、ネズミのグループに関する物語を書く中でその数を使ってください。
=== Calling Function ===
Calling function: multiply with args: {
  "a": 121,
  "b": 2
}
Got output: 242
========================

121 * 2 は242です。

ある日、ネズミのグループが森の中を探検していました。彼らは242匹のネズミで構成されており、一緒に冒険することで困難な状況を乗り越えることができました。彼らは協力し合い、242匹のネズミの力を合わせて、大きな岩を動かしたり、高い木に登ったりすることができました。242匹のネズミたちは団結力を持ち、困難な状況でもお互いを助け合って進んでいきました。
```

## 個性のあるエージェント

```py
from llama_index.agent.openai import OpenAIAgent
from llama_index.llms.openai import OpenAI
from llama_index.core.prompts.system import SHAKESPEARE_WRITING_ASSISTANT
```
```py
llm = OpenAI(model="gpt-3.5-turbo-0613")

agent = OpenAIAgent.from_tools(
    [multiply_tool, add_tool],
    llm=llm,
    verbose=True,
    system_prompt=SHAKESPEARE_WRITING_ASSISTANT,
)
```
```py
response = agent.chat("はい")
print(response)
```
```
Added user message to memory: はい
おっしゃっていただきありがとうございます！どのようなお手伝いができますでしょうか？
```

口調が変わってます。

```py
response = agent.chat(
    "121 * 2 はいくつですか？ 答えがわかったら、ネズミのグループに関する物語を書く中でその数を使ってください。"
)

print(str(response))
```
```
121 * 2 は 242 です。

さて、242匹のネズミのグループに関する物語をお作りいたします。

昔々、ある森の中に小さな村がありました。この村にはたくさんのネズミが住んでいました。彼らは仲良く暮らし、食べ物を探し、楽しく過ごしていました。

ある日、村のネズミたちは大切な任務を与えられました。彼らは村の食料庫にある穀物を集めるために、242匹の大きなグループに分かれて出発しました。

最初のグループは小川を渡り、青々とした草原を進みました。彼らは元気に走り回りながら、穀物を集めることができました。

次のグループは高い山を登りました。彼らは困難な道のりでしたが、助け合いながら頂上に到達しました。そこからの眺めは壮大で、彼らは自分たちの勇気に感動しました。

さらに別のグループは広大な森を進みました。彼らは様々な動物たちと出会い、友情を深めました。彼らは一緒に穀物を集め、楽しい時間を過ごしました。

最後のグループは広大な畑を駆け抜けました。彼らは風になびく穀物の中で飛び跳ね、自由を感じました。彼らは穀物を見つけるだけでなく、自分たちの存在を喜びました。

結局、242匹のネズミたちはそれぞれの任務から戻り、集まりました。彼らはお互いの成果を分かち合い、喜びと感謝の気持ちでいっぱいでした。

この物語は、242匹のネズミたちが困難を乗り越え、協力と努力の大切さを学ぶ旅を描いています。彼らの任務は、村の人々に食料を提供し、村全体の繁栄に貢献することでした。

どうでしょうか、お楽しみいただけましたでしょうか？もし他にご要望がありましたら、お知らせください。
```

面白い。チャットbotやQ&A botとは違ったユースケースが期待できそうです。もう少し色々いじってみます。

### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)