生成AIでSQLデータベースの分析

本記事は日本オラクルが運営する下記Meetupで発表予定の内容になります。発表までに今後、内容は予告なく変更される可能性があることをあらかじめご了承ください。


https://oracle-code-tokyo-dev.connpass.com/event/325752/

※本記事は下記記事の補足記事となります。

https://qiita.com/ksonoda/items/f3c703fb7b689fd65868

※ミートアップ実施時の動画は↓こちら。

<iframe width="560" height="315" src="https://www.youtube.com/embed/XQCKTrnxO88?si=o7FeZB6h_no65YbO" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 自然言語によるSQLデータベースの分析
一般的に、データベースの分析はSQLで分析処理を実行したり、BIツールでその処理を行ったりしますが、それらに関する深い技術的知識がなくても、LLMを利用し、自然言語で質問を入力するだけでデータ分析が可能になるユースケースです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/9f7a37f4-e816-8675-d1fc-4e8635ca89eb.png)

# ユースケース
DBの分析に関連するアプリケーションは全てユースケースの対象となります。例えば以下のようなアプリケーションです。

| ユースケース                        | 説明                                                                                   |
|-------------------------------------|----------------------------------------------------------------------------------------|
| 営業レポートの自動生成              | 営業部門の方々が自然言語で質問し、リアルタイムで営業レポートを生成。迅速な意思決定が可能に。       |
| マーケティングキャンペーンの効果測定| キャンペーンの効果を自然言語で分析し、ROI評価や次の戦略調整に役立てる。                     |
| 人事分析                            | 自然言語で退職率などを分析し、社員満足度やエンゲージメント向上のインサイトを得る。          |
| 財務分析と予算管理                  | 自然言語で経費データを集計し、予算管理や支出計画の迅速な把握をサポート。                      |
| 在庫管理の最適化                    | 自然言語で在庫データを分析し、適時の補充や在庫管理の最適化を実現。                            |



# 仕組み
基本的な仕組みとしては下記のようになりいたってシンプルです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/a4b4bdbe-8fae-d420-c013-67713d1e6e00.png)

①ユーザーからの質問をLLMに入力
②LLMが質問をSQLに変換
③変換されたSQLでデータベースにクエリを実行
④クエリ結果と質問文をLLMに入力し回答を生成

という流れで処理が進むようなコードを実装します。予想通りという感じではないでしょうか。とは言えこれをスクラッチで実装すると実は意外と面倒なことになります。そこでLangChain(のSQL Database Agent)の登場です。LangChainがバックエンドで自動的に処理してくれる内容も含めてサンプルコードを概説します。



#コード概説
利用する各コンポーネントは下記の通りです。
- LLM : OpenAI
- SQL Database : SQLite
- Dataset : 著名な音楽アーティスト名とリリースアルバムのスキーマ

まずは必要なライブラリのインストールです。

```python
!pip install --upgrade --quiet  langchain langchain-community langchain-experimental langchain-openai
```

次にOpenAIとLangSmithのAPIキーを入力します。(LangSmithはオプションです。必須ではありません。)

```python
import getpass
import os

def _set_if_undefined(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"Please provide your {var}")

_set_if_undefined("OPENAI_API_KEY")
_set_if_undefined("LANGCHAIN_API_KEY")

# Optional, add tracing in LangSmith
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "Database Agent"
```

SQLデータベース(sqlite)に接続してクエリを実行しスキーマを確認します。

```python
from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db", sample_rows_in_table_info=3)
```
データベース内にある表を確認します。

```python
print(db.get_usable_table_names())

['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']
```
上記、11個の表があることがわかります。
表 Artist にクエリを実行します。

```python
print(db.run("SELECT * FROM Artist LIMIT 10;"))

[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]
```

著名なロックバンドのバンド名が入力されている表だということがわかります。表名の通りです。次に、表Albumにクエリを実行します。

```python
print(db.run("SELECT * FROM Album LIMIT 10;"))

[(1, 'For Those About To Rock We Salute You', 1), (2, 'Balls to the Wall', 2), (3, 'Restless and Wild', 2), (4, 'Let There Be Rock', 1), (5, 'Big Ones', 3), (6, 'Jagged Little Pill', 4), (7, 'Facelift', 5), (8, 'Warner 25 Anos', 6), (9, 'Plays Metallica By Four Cellos', 7), (10, 'Audioslave', 8)]
```

表名通り、こちらはアーティストのアルバム名です。
今回はこの2つの表を使って自然言語でクエリを実行してみたいと思います。

まず、LLMとしてOpenAIを定義します。

```python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-3.5-turbo")
```

次にAgentを定義します。次の一行がこの実装の肝になります。LangChainのAgentは様々なタイプのものがあると上述しましたが、ここではcreate_sql_agent関数によりSQLDatabaseAgent、つまりRDB用のAgentを定義しています。そして、Agentが対象とするtoolはLangChainで事前に用意されているopenai-toolsです。

```python
agent_executor = create_sql_agent(llm, db=db, agent_type="openai-tools", verbose=True)
```

この一行の定義だけで、ユーザーの質問をLLMに連携し、SQLに変換後、DBにクエリ実行、その後、質問文とSQLの結果をLLMに入力するという全体のパイプラインが定義されます。

具体的にどのようなパイプラインが定義されているのか、agent_executorの中身をのぞいてみます。

```python
print(agent_executor)

AgentExecutor(name='SQL Agent Executor', verbose=True, agent=RunnableMultiActionAgent(runnable=RunnableAssign(mapper={
  agent_scratchpad: RunnableLambda(lambda x: format_to_openai_tool_messages(x['intermediate_steps']))
})
| ChatPromptTemplate(input_variables=['agent_scratchpad', 'input'], input_types={'agent_scratchpad': typing.List[typing.Union[langchain_core.messages.ai.AIMessage, langchain_core.messages.human.HumanMessage, langchain_core.messages.chat.ChatMessage, langchain_core.messages.system.SystemMessage, langchain_core.messages.function.FunctionMessage, langchain_core.messages.tool.ToolMessage]]}, messages=[SystemMessage(content='You are an agent designed to interact with a SQL database.\nGiven an input question, create a syntactically correct sqlite query to run, then look at the results of the query and return the answer.\nUnless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 10 results.\nYou can order the results by a relevant column to return the most interesting examples in the database.\nNever query for all the columns from a specific table, only ask for the relevant columns given the question.\nYou have access to tools for interacting with the database.\nOnly use the below tools. Only use the information returned by the below tools to construct your final answer.\nYou MUST double check your query before executing it. If you get an error while executing a query, rewrite the query and try again.\n\nDO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\n\nIf the question does not seem related to the database, just return "I don\'t know" as the answer.\n'), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input'], template='{input}')), AIMessage(content='I should look at the tables in the database to see what I can query.  Then I should query the schema of the most relevant tables.'), MessagesPlaceholder(variable_name='agent_scratchpad')])
| RunnableBinding(bound=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x7ffa4aa815d0>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x7ffa4aa853d0>, root_client=<openai.OpenAI object at 0x7ffa4b263ed0>, root_async_client=<openai.AsyncOpenAI object at 0x7ffa4aa81790>, temperature=0.0, openai_api_key=SecretStr('**********'), openai_proxy=''), kwargs={'tools': [{'type': 'function', 'function': {'name': 'sql_db_query', 'description': "Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields.", 'parameters': {'type': 'object', 'properties': {'query': {'description': 'A detailed and correct SQL query.', 'type': 'string'}}, 'required': ['query']}}}, {'type': 'function', 'function': {'name': 'sql_db_schema', 'description': 'Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3', 'parameters': {'type': 'object', 'properties': {'table_names': {'description': "A comma-separated list of the table names for which to return the schema. Example input: 'table1, table2, table3'", 'type': 'string'}}, 'required': ['table_names']}}}, {'type': 'function', 'function': {'name': 'sql_db_list_tables', 'description': 'Input is an empty string, output is a comma-separated list of tables in the database.', 'parameters': {'type': 'object', 'properties': {'tool_input': {'description': 'An empty string', 'default': '', 'type': 'string'}}}}}, {'type': 'function', 'function': {'name': 'sql_db_query_checker', 'description': 'Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query!', 'parameters': {'type': 'object', 'properties': {'query': {'description': 'A detailed and SQL query to be checked.', 'type': 'string'}}, 'required': ['query']}}}]})
| OpenAIToolsAgentOutputParser(), input_keys_arg=['input'], return_keys_arg=['output'], stream_runnable=True), tools=[QuerySQLDataBaseTool(description="Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields.", db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x7ffa60b65350>), InfoSQLDatabaseTool(description='Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x7ffa60b65350>), ListSQLDatabaseTool(db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x7ffa60b65350>), QuerySQLCheckerTool(description='Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query!', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x7ffa60b65350>, llm=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x7ffa4aa815d0>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x7ffa4aa853d0>, root_client=<openai.OpenAI object at 0x7ffa4b263ed0>, root_async_client=<openai.AsyncOpenAI object at 0x7ffa4aa81790>, temperature=0.0, openai_api_key=SecretStr('**********'), openai_proxy=''), llm_chain=LLMChain(prompt=PromptTemplate(input_variables=['dialect', 'query'], template='\n{query}\nDouble check the {dialect} query above for common mistakes, including:\n- Using NOT IN with NULL values\n- Using UNION when UNION ALL should have been used\n- Using BETWEEN for exclusive ranges\n- Data type mismatch in predicates\n- Properly quoting identifiers\n- Using the correct number of arguments for functions\n- Casting to the correct data type\n- Using the proper columns for joins\n\nIf there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n\nOutput the final SQL query only.\n\nSQL Query: '), llm=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x7ffa4aa815d0>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x7ffa4aa853d0>, root_client=<openai.OpenAI object at 0x7ffa4b263ed0>, root_async_client=<openai.AsyncOpenAI object at 0x7ffa4aa81790>, temperature=0.0, openai_api_key=SecretStr('**********'), openai_proxy='')))])
```

見ての通り、結構複雑なパイプラインをcreate_sql_agent関数一つで定義できています。上記の定義はLangChain特有のチェーン定義そのものです。LangChainでは抽象化された様々な関数を上記のようにパイプ(|)で繋げてゆきパイプラインを定義します。

すなわち、Agentの中身はチェーン定義ということになり、create_sql_agent関数を使ってAgentを定義するとSQLデータベースとLLMを組み合わせて利用するためのチェーン定義が自動的に定義され、このAgentをSQLDatabaseAgentと呼んでいます。

チェーンの中で実行されている関数の説明は以下の通りです。

- AgentExecutor : エージェントの実行者で、SQL Agent Executor という名前が付けられています。エージェントの動作を管理し、タスクを実行します。verbose=True により、処理内容が詳細に表示されます。
- RunnableMultiActionAgent : 複数のアクションを実行するエージェント。自然言語の質問をSQLクエリに変換するために、RunnableAssign というマッピングツールを使用します。
- RunnableLambda : エージェントが内部の中間結果を形式化し、それをOpenAIツールメッセージに適した形式に変換するために使われます。
- ChatPromptTemplate : エージェントに与えられたプロンプト（命令文）を元に、入力された質問を処理します。このプロンプトには、エージェントがSQLクエリを作成し、データベースとやり取りする際の手順や制約が記述されています。
- RunnableBinding : LLM（大規模言語モデル）に対してバインディングを行い、SQLクエリの生成やチェックを担当します。特定のツールにアクセスしてSQLクエリを実行し、その結果を処理します。

各関数の中身を見てみると、各処理に最適なプロンプトが自動的に定義されていたり、様々な引数に値が入れられていることがわかります。一番わかりやすいChatPromptTemplateの指示文を和訳してみると以下の通りです。

「あなたは SQL データベースと対話するように設計されたエージェントです。入力質問が与えられた場合、実行する構文的に正しい SQLite クエリを作成し、クエリの結果を確認して答えを返します。ユーザーが特定の数の例を指定しない限り、取得したい場合は、常にクエリの結果を最大 10 件に制限してください。

関連する列ごとに結果を並べ替えることで、データベース内の最も興味深い例を返すことができます。特定のテーブルのすべての列に対してクエリを実行するのではなく、クエリのみを要求します。質問に関連する列。データベースと対話するためのツールにアクセスできます。以下のツールのみを使用してください。

以下のツールから返された情報は、最終的な回答を作成する場合にのみ使用してください。クエリを実行する前に、クエリを再確認する必要があります。クエリの実行中にエラーが発生した場合は、クエリを書き直して再試行してください。

データベースに対して DML ステートメント (INSERT、UPDATE、DELETE、DROP など) を作成しないでください。質問が正しくない場合は、データベースに関連する場合は、回答として「わかりません」を返してください。」

パイプラインに入っている指示文を読むだけでもなんとなく処理の流れがわかります。ということで実際に自然言語でクエリを実行してみます。

```python
agent_executor.invoke("アーティストは何人登録されていますか？日本語で回答してください。")
```

すると、下記のようにAgentにより考察が始まり、最終的にテキストが生成されます。

```python
> Entering new SQL Agent Executor chain...

Invoking: `sql_db_list_tables` with `{}`


Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track
Invoking: `sql_db_query` with `{'query': 'SELECT COUNT(*) AS total_artists FROM Artist'}`


[(275,)]データベースには275人のアーティストが登録されています。

> Finished chain.
{'input': 'アーティストは何人登録されていますか？日本語で回答してください。',
 'output': 'データベースには275人のアーティストが登録されています。'}
```

データベースのスキーマ情報から入力されたプロンプトに関係する表Artistを見つけ、人数をカウントするためのSQLを生成していることがわかります。SQLの結果([(275,)])が確認でき、最終的にoutputが生成されたテキストということになります。

念のため答え合わせをすると以下のように正解ということになりました。

```python
print(db.run("SELECT count(*) FROM artist;"))
[(275,)]
```

# ベクトルデータベースを使ったSQL生成精度の向上
一つ前の章で紹介した実装は非常に簡単なスキーマなのでLLMがSQL変換をミスする可能性は低いと思います。ただ、実システムでは膨大な表数、レコード数、表明、カラム名、表の関連性など非常に複雑なスキーマになり、このような場合、実はうまく目的のSQLに変換されず、最終的に正しい応答テキストが生成されないケースが多々発生します。

そこで少しでもSQLの変換精度を上げるために、予めよくある質問(や、複雑なSQLになりそうな質問)と、それに対応するSQLをexampleとして用意しておき、ユーザーからの質問が入力された際に、LLMがそのexampleを参照しながらSQLを生成するというアーキテクチャを実装することによりこの問題を解決します。下図の通り、赤枠で囲った部分の実装を追加するイメージです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/8c97066f-4b5a-0f79-df8e-9246ccdd869f.png)




これはもともとLLMが流行り始めた初期のころからあるFew-shot Promptingというtips(質問と回答のサンプルを予め準備しておく方法)をSQL変換に応用したパターンです。(※Few-shot Promptingは簡単に応答テキストの精度を大幅に向上させることができると認知されている手法です。)この際、もちろん、exampleには沢山のパターンが登録されている方が精度は上がりやすいです。ですが、多すぎると

- LLMに入力するトークン数が増える
- 処理が遅くなる
- 無駄に課金が増える
- コンテキストウィンドウに収まらなくなる可能性がある(特に古いLLM)
- 関連性の低いSQL exampleはハルシネーションを引き起こす可能性がある

などの問題が発生します。従って、できれば「最も可能性の高いSQL ExampleのみをLLMに入力したい」ところです。そこで登場するのがベクトルデータベースです。SQL Exampleをベクトルデータベースにロードしておき、いざSQL Exampleが必要になったときに、ベクトルデータベースの類似検索で質問内容と近しいSQL Exampleのみを抽出し、LLMに入力するという仕組みを追加で実装します。この実装により上述した問題が全て解決します。非常にスマートな類似検索の使い方だと思います。

# コード概説
構成としては一つ前の章の構成にベクトルデータベースを追加した実装になり、前半部分のコードは全く同じです。

- LLM : OpenAI
- SQL Database : SQLite
- ベクトルデータベース : FAISS
- Dataset : 著名な音楽アーティスト名とリリースアルバムのスキーマ

APIキーの定義

```python
import getpass
import os

def _set_if_undefined(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"Please provide your {var}")

_set_if_undefined("OPENAI_API_KEY")
_set_if_undefined("LANGCHAIN_API_KEY")

# Optional, add tracing in LangSmith
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "Database Agent"

```

DBの定義

```python
from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db", sample_rows_in_table_info=3)
```

エージェント(SQLDatabaseAgent)の定義

```python
from langchain_community.agent_toolkits import create_sql_agent
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
agent_executor = create_sql_agent(llm, db=db, agent_type="openai-tools", verbose=True)

```

ここまでのコードは前の章のものと全く同じです。

ここから下記のように7つのSQL例を作成します。

```python
examples = [
    {"input": "すべてのアーティストをリストアップしてください。", "query": "SELECT * FROM Artist;"},
    {
        "input": "アーティスト 'AC/DC' のすべてのアルバムを見つけてください。",
        "query": "SELECT * FROM Album WHERE ArtistId = (SELECT ArtistId FROM Artist WHERE Name = 'AC/DC');",
    },
    {
        "input": "ジャンルが 'Rock' のすべてのトラックをリストアップしてください。",
        "query": "SELECT * FROM Track WHERE GenreId = (SELECT GenreId FROM Genre WHERE Name = 'Rock');",
    },
    {
        "input": "すべてのトラックの総再生時間を求めてください。",
        "query": "SELECT SUM(Milliseconds) FROM Track;",
    },
    {
        "input": "IDが5のアルバムにはトラックが何曲ありますか？",
        "query": "SELECT COUNT(*) FROM Track WHERE AlbumId = 5;",
    },
    {
        "input": "5分以上のトラックをリストアップしてください。",
        "query": "SELECT * FROM Track WHERE Milliseconds > 300000;",
    },
    {
        "input": "2000年のアルバムはどれですか？",
        "query": "SELECT * FROM Album WHERE strftime('%Y', ReleaseDate) = '2000';",
    },
]


```

ベクトルデータベースを定義してゆきます。下記コードでは上記のexampleをOpenAIの埋め込みモデルでベクトルに変換し、FAISSにロードしています。入力質問文章の中の"input"に対応するフィールドをキーとして、類似検索をし、最も類似度の高い上位3件を結果として取得するようSemanticSimilarityExampleSelectorで定義しています。

```python
from langchain_community.vectorstores import FAISS
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddings

example_selector = SemanticSimilarityExampleSelector.from_examples(
    examples,
    OpenAIEmbeddings(),
    FAISS,
    k=3,
    input_keys=["input"],
)
```

ここから、質問文章をもとに効果的なプロンプト(プロンプトテンプレート)を構築するための様々なツールをインポートし、指示文を追加しながら最終的なプロンプトを定義しています。

```python
from langchain_core.prompts import (
    ChatPromptTemplate,
    FewShotPromptTemplate,
    MessagesPlaceholder,
    PromptTemplate,
    SystemMessagePromptTemplate,
)

system_prefix = """
You are an agent designed to interact with a SQL database. Given an input question, create a syntactically correct {dialect} query to run, then look at the results of the query and return the answer.
Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most {top_k} results. You can order the results by a relevant column to return the most interesting examples in the database.
Never query for all the columns from a specific table, only ask for the relevant columns given the question.
You have access to tools for interacting with the database. Only use the given tools. Only use the information returned by the tools to construct your final answer.
You MUST double check your query before executing it.
If you get an error while executing a query, rewrite the query and try again.
DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database. If the question does not seem related to the database, just return "I don't know" as the answer.
Here are some examples of user inputs and their corresponding SQL queries:
"""

few_shot_prompt = FewShotPromptTemplate(
    example_selector=example_selector,
    example_prompt=PromptTemplate.from_template(
        "User input: {input}\nSQL query: {query}"
    ),
    input_variables=["input", "dialect", "top_k"],
    prefix=system_prefix,
    suffix="",
)
```

インポートしているクラスの説明は下記の通りですが、この実装でキーになるのはベクトルデータベースにロード済のSQL例を含めてプロンプトを生成するFewShotPromptTemplateの部分です。

- ChatPromptTemplate : チャット形式のプロンプトを定義するためのテンプレートクラスです。ユーザーからの入力やシステムからのメッセージを含む一連のメッセージの流れを定義するために使用されます。
- FewShotPromptTemplate : Few-shot learning(このコードの場合SQL例)に基づくプロンプトテンプレートを作成するためのクラスです。
- MessagesPlaceholder : チャットの流れの中で動的に生成されるメッセージを格納するための場所を確保します。
- PromptTemplate : 一般的なプロンプトテンプレートを定義するためのクラスです。
- SystemMessagePromptTemplate : システムメッセージ用のプロンプトテンプレートを作成するためのクラスです。システムからユーザーへの指示や背景情報を含むメッセージを定義します。

そして、追加されている指示文(system_prefix)の和訳が以下になります。

--------------------
「あなたは、SQL データベースと対話するように設計されたエージェントです。入力質問が与えられた場合、構文的に正しい {dialect} クエリを作成して実行し、クエリの結果を確認して回答を返します。

ユーザーが取得したい例の特定の数を指定しない限り、クエリは常に最大 {top_k} 件の結果に制限してください。関連する列ごとに結果を並べ替えて、データベース内の最も興味深い例を返すことができます。特定のテーブルのすべての列をクエリするのではなく、質問に応じて関連する列のみを要求します。

データベースと対話するためのツールにアクセスできます。指定されたツールのみを使用してください。ツールから返された情報は、最終的な答えを作成するためにのみ使用してください。クエリを実行する前に、クエリを再確認する必要があります。クエリの実行中にエラーが発生した場合は、クエリを書き直して再試行してください。データベースに対して DML ステートメント (INSERT、UPDATE、DELETE、DROP など) を実行しないでください。

質問がデータベースに関連していないと思われる場合は、単に「わかりません」と答えてください。

ユーザー入力とそれに対応する SQL クエリの例をいくつか示します。」

--------------------

指示文最後の行の「ユーザー入力とそれに対応する SQLクエリの例をいくつか示します。」がベクトルデータベースから取得したSQLのexampleを参照する処理の指示です。

これに対応するように、事前に定義したexample_selectorを使ってSQL例をベクトルデータベースから取得しプロンプトに含める処理をFewShotPromptTemplateで定義しているというのが上記のコードの概要です。

そして、ここから最終的なプロンプトテンプレートの定義になります。

```python
full_prompt = ChatPromptTemplate.from_messages(
    [
        SystemMessagePromptTemplate(prompt=few_shot_prompt),
        ("human", "{input}"),
        MessagesPlaceholder("agent_scratchpad"),
    ]
)
```

上記コードは、ChatPromptTemplate を使用して、チャット形式のプロンプトを定義しています。具体的には、SystemMessagePromptTemplate、人間のメッセージ、そしてエージェントのスクラッチパッドメッセージのプレースホルダーを組み合わせて、チャットの流れを設計しているというお決まりのコードです。

ここまでで実装は完了ですので質問文章を入力したいと思います。

```python
agent.invoke({"input": "10枚以上のアルバムをリリースしているアーティストの名前をリストしてください。"})

> Entering new SQL Agent Executor chain...

Invoking: `sql_db_query` with `{'query': 'SELECT Artist.Name FROM Artist JOIN Album ON Artist.ArtistId = Album.ArtistId GROUP BY Artist.Name HAVING COUNT(Album.AlbumId) >= 10'}`


[('Deep Purple',), ('Iron Maiden',), ('Led Zeppelin',), ('Metallica',), ('U2',)]10枚以上のアルバムをリリースしているアーティストの名前は、Deep Purple、Iron Maiden、Led Zeppelin、Metallica、U2です。

> Finished chain.
{'input': '10枚以上のアルバムをリリースしているアーティストの名前をリストしてください。',
 'output': '10枚以上のアルバムをリリースしているアーティストの名前は、Deep Purple、Iron Maiden、Led Zeppelin、Metallica、U2です。'}
```

以上がベクトルデータベースと組み合わせたパターンの実装となります。

:::note info
因みに、この実装ではRDBはSQLite、ベクトルDBはFAISSを利用していますが、Oracle DB23aiではRDBとベクトルDBの両方ワークロードをサポートしているため、データベースを一つに纏められるというメリットがあります。
:::