Azure OpenAIを使用してSQLに自然言語で問い合わせる(NL2SQL)

2024/08/25に公開

はじめに

  • OpenAIで公開されているcookbookを参考に日本語化しながら解説します
  • 最初に天気を取得する簡単なFunction Callingを実施し、データベースに対して自然言語で問い合わせる仕組みを紹介します
  • 自然言語からクエリを作成し、Function Callingで外部関数を読み出し、結果を取得することができます
  • 一般的には、NL2SQL(Natural Language to SQL Queries)で認識されています。
  • LangChainのSQL Database Toolkitも同様のことができますが、出力の速度が遅かった(数十秒)のでFunction Callingを使用した方法をご紹介します
  • Function Callingだと数秒で出力が返ってきます。

チャットモデルで関数を呼び出す方法

  • このノートブックでは、Chat Completions APIを使用して、外部関数と組み合わせることでGPTモデルの機能を拡張する方法を説明します。

  • toolsは、Chat Completion APIのオプションのパラメータであり、関数の仕様を提供するために使用されます。これにより、モデルが指定された仕様に従った関数引数を生成できるようになります。なお、API自体は実際の関数呼び出しを実行することはありません。関数呼び出しを実行するのは開発者の責任です。

  • toolsパラメータ内で、functionsパラメータが提供されると、デフォルトではモデルが適切なタイミングで関数を使用するかどうかを判断します。特定の関数を使用させるには、tool_choiceパラメータを{"type": "function", "function": {"name": "my_function"}}に設定します。

  • また、関数をまったく使用しないようにするには、tool_choiceパラメータを"none"に設定します。関数が使用されると、応答には"finish_reason": "tool_calls"が含まれ、関数の名前と生成された関数引数を含むtool_callsオブジェクトが含まれます。

概要

このノートブックには次の2つのセクションが含まれています:

  • 関数引数を生成する方法: 関数のセットを指定し、APIを使用して関数引数を生成する方法。
  • モデル生成の引数を使って関数を呼び出す方法: モデル生成の引数を使って実際に関数を実行する方法。

関数の引数を生成する方法

  • 必要なパッケージをインストール
!pip install scipy 
!pip install tenacity 
!pip install tiktoken 
!pip install termcolor 
!pip install openai 
import json
from openai import AzureOpenAI
from tenacity import retry, wait_random_exponential, stop_after_attempt
from termcolor import colored  
  • Azure OpenAIの設定情報を記載しclientを作成
azure_endpoint = "https://xxxxx.openai.azure.com/"
api_version = "2024-05-01-preview"
aoai_api_key = "xxxxx"
deployment_name = "xxxxx"
client = AzureOpenAI(api_key=aoai_api_key, api_version=api_version, azure_endpoint=azure_endpoint)

ユーティリティ

Chat Completions APIへの呼び出しを行い、会話の状態を維持・管理するためのユーティリティ関数をいくつか定義しましょう。

# チャットモデルへのリクエストを行い、その応答を取得する関数 chat_completion_request を定義しています。リクエストの実行に失敗した場合には、自動的にリトライする機能が組み込まれています。
# @retry: このデコレーターは、関数が失敗した場合に再試行するために使用されます。tenacity というライブラリが提供する機能です。
# wait=wait_random_exponential(multiplier=1, max=40): 再試行間の待機時間をランダムな指数関数的な増加で設定します。multiplier=1 は基本の待機時間を設定し、max=40 は待機時間の上限を秒単位で設定します。
# stop=stop_after_attempt(3): 最大3回の再試行を行った後、それ以上の再試行を行わないようにします。

# 関数 chat_completion_request
# messages: チャットモデルに送信するメッセージのリストです。このリストには、ユーザーの入力やシステムメッセージが含まれ、チャットのコンテキストを保持します。
# tools=None: 使用するツール(関数呼び出しのような追加機能)のリストです。デフォルトでは None となっていますが、必要に応じてリスト形式でツールを指定できます。
# tool_choice=None: モデルがどのツールを選択するかを制御する引数です。デフォルトでは None となっています。
# model=deployment_name: 使用するモデルの名前(またはデプロイメント名)を指定します。deployment_name という変数に、このモデルの名前が格納されていると仮定しています。

@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3))
def chat_completion_request(messages, tools=None, tool_choice=None, model=deployment_name):
    try:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools,
            tool_choice=tool_choice,
        )
        return response
    except Exception as e:
        print("ChatCompletionの応答を生成できませんでした。")
        print(f"例外: {e}")
        return e

#  pretty_print_conversation は、会話のメッセージリストをコンソールに色分けして表示するためのものです。
#  role_to_color: メッセージの role に応じて、表示する色を定義した辞書です。
# "system": システムメッセージは赤色 ("red") で表示されます。
# "user": ユーザーメッセージは緑色 ("green") で表示されます。
# "assistant": アシスタント(AI)からのメッセージは青色 ("blue") で表示されます。
# "function": 関数呼び出しの結果はマゼンタ色 ("magenta") で表示されます。

def pretty_print_conversation(messages):
    role_to_color = {
        "system": "red",
        "user": "green",
        "assistant": "blue",
        "function": "magenta",
    }
    
    for message in messages:
        if message["role"] == "system":
            print(colored(f"system: {message['content']}\n", role_to_color[message["role"]]))
        elif message["role"] == "user":
            print(colored(f"user: {message['content']}\n", role_to_color[message["role"]]))
        elif message["role"] == "assistant" and message.get("function_call"):
            print(colored(f"assistant: {message['function_call']}\n", role_to_color[message["role"]]))
        elif message["role"] == "assistant" and not message.get("function_call"):
            print(colored(f"assistant: {message['content']}\n", role_to_color[message["role"]]))
        elif message["role"] == "function":
            print(colored(f"function ({message['name']}): {message['content']}\n", role_to_color[message["role"]]))

基本的な概念

  • 仮想的な天気APIとインターフェースするための関数仕様を作成しましょう。これらの関数仕様をChat Completions APIに渡して、仕様に従った関数引数を生成します。

  • get_current_weather:
    現在の天気を取得する関数。
    必要なパラメータは「場所 (location)」と「温度の単位 (format)」です。

  • get_n_day_weather_forecast:
    N日間の天気予報を取得する関数。
    必要なパラメータは「場所 (location)」、「温度の単位 (format)」、および「予報日数 (num_days)」です。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "現在の天気を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "都市と州を指定します。例: サンフランシスコ, CA",
                    },
                    "format": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "使用する温度単位。ユーザーの場所から推測します。",
                    },
                },
                "required": ["location", "format"],
            },
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_n_day_weather_forecast",
            "description": "N日間の天気予報を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "都市と州を指定します。例: サンフランシスコ, CA",
                    },
                    "format": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "使用する温度単位。ユーザーの場所から推測します。",
                    },
                    "num_days": {
                        "type": "integer",
                        "description": "予報する日数",
                    }
                },
                "required": ["location", "format", "num_days"]
            },
        }
    },
]
  • Jsonを見やすくするための関数を定義
def show_json(obj):
    display(json.loads(obj.model_dump_json()))
  • モデルに現在の天気についてプロンプトを与えると、情報が足りないのでいくつかの確認質問を返してきます。
messages = []
messages.append({"role": "system", "content": "関数に入力する値について、推測をしないでください。ユーザーのリクエストが曖昧な場合は、明確化を求めてください。"})
messages.append({"role": "user", "content": "今日の天気はどうですか"})
chat_response = chat_completion_request(
    messages, tools=tools
)
assistant_message = chat_response.choices[0].message
messages.append(assistant_message)
assistant_message
  • 出力:
    ChatCompletionMessage(content='どの場所の天気情報をお知りになりたいか教えていただけますか?都市と州または国名を具体的にご指定ください。', refusal=None, role='assistant', function_call=None, tool_calls=None)

  • 次に、不足している情報を提供すると、適切な関数引数が生成されます。

  • locationも正確に認識してます。

messages.append({"role": "user", "content": "私は千葉県千葉市にいます"})
chat_response = chat_completion_request(
    messages, tools=tools
)
assistant_message = chat_response.choices[0].message
messages.append(assistant_message)
assistant_message
  • 出力
    ChatCompletionMessage(content=None, refusal=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_W3NCSy9JCisF6olGMGATQxUw', function=Function (arguments='{\n "location": "千葉市, 千葉県",\n "format": "celsius"\n}', name='get_current_weather'), type='function') ])
show_json(assistant_message)

show_jsonの関数を使って見やすく出力します。

{'content': None,
 'refusal': None,
 'role': 'assistant',
 'function_call': None,
 'tool_calls': [{'id': 'call_W3NCSy9JCisF6olGMGATQxUw',
   'function': {'arguments': '{\n  "location": "千葉市, 千葉県",\n  "format": "celsius"\n}',
    'name': 'get_current_weather'},
   'type': 'function'}]}
  • 異なる形でプロンプトを入力することで、指定した他の関数をターゲットにすることができます。
messages = []
messages.append({"role": "system", "content": "関数に入力する値について、推測をしないでください。ユーザーのリクエストが曖昧な場合は、明確化を求めてください。"})
messages.append({"role": "user", "content": "千葉県千葉市で今後 x 日間の天気はどうなりますか"})
chat_response = chat_completion_request(
    messages, tools=tools
)
assistant_message = chat_response.choices[0].message
messages.append(assistant_message)
assistant_message
  • 出力
    ChatCompletionMessage(content='「x 日間」の具体的な日数を教えてください。', refusal=None, role='assistant', function_call=None, tool_calls=None)

  • 再び、モデルは十分な情報がまだないため追加で聞いています。

  • この場合だと、既に予報の場所を知っていますが何日間の予報が必要かを知る必要があります。

messages.append({"role": "user", "content": "5 日間"})
chat_response = chat_completion_request(
    messages, tools=tools
)
chat_response.choices[0]
  • 出力
    Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content=None, refusal=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_TyWlwDIdR8wy3OCtyUQPk2ti', function=Function(arguments='{\n "location": "千葉県千葉市",\n "format": "celsius",\n "num_days": 5\n}', name='get_n_day_weather_forecast'), type='function')]), content_filter_results={})
show_json(chat_response)
  • 以下jsonでの出力
{'id': 'chatcmpl-9xyloMl9E0mvrR6noRY5bbTAGFZUA',
 'choices': [{'finish_reason': 'tool_calls',
   'index': 0,
   'logprobs': None,
   'message': {'content': None,
    'refusal': None,
    'role': 'assistant',
    'function_call': None,
    'tool_calls': [{'id': 'call_TyWlwDIdR8wy3OCtyUQPk2ti',
      'function': {'arguments': '{\n  "location": "千葉県千葉市",\n  "format": "celsius",\n  "num_days": 5\n}',
       'name': 'get_n_day_weather_forecast'},
      'type': 'function'}]},
   'content_filter_results': {}}],
 'created': 1724081980,
 'model': 'gpt-4o-2024-05-13',
 'object': 'chat.completion',
 'service_tier': None,
 'system_fingerprint': 'fp_abc28019ad',
 'usage': {'completion_tokens': 40, 'prompt_tokens': 273, 'total_tokens': 313},
 'prompt_filter_results': [{'prompt_index': 0,
   'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'},
    'jailbreak': {'filtered': False, 'detected': False},
    'self_harm': {'filtered': False, 'severity': 'safe'},
    'sexual': {'filtered': False, 'severity': 'safe'},
    'violence': {'filtered': False, 'severity': 'safe'}}}]}

特定の関数を使用させたり、使用しないように強制する

  • モデルに特定の関数、例えば get_n_day_weather_forecast を使用させるためには、tool_choice 引数を使用できます。
  • これにより、モデルがその関数の使用方法について推測することを強制できます。
# このセルでは、モデルに get_n_day_weather_forecast を使用させることを強制します。(tool_choice=・・・)
messages = []
messages.append({"role": "system", "content":"関数に入力する値について、推測をしないでください。ユーザーのリクエストが曖昧な場合は、明確化を求めてください。"})
messages.append({"role": "user", "content": "沖縄県那覇市の天気予報を教えて"})
chat_response = chat_completion_request(
    messages, tools=tools, tool_choice={"type": "function", "function": {"name": "get_n_day_weather_forecast"}}
)
chat_response.choices[0].message
  • 出力
    ChatCompletionMessage(content=None, refusal=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_QhlKy4ldnUCvBeBQ1lScZb6L', function=Function(arguments='{"location":"那覇市, 沖縄県","format":"celsius","num_days":1}', name='get_n_day_weather_forecast'), type='function')])
# モデルに get_n_day_weather_forecast を使用させることを強制しない場合、使用しない可能性があります。
messages = []
messages.append({"role": "system", "content": "関数に入力する値について、推測をしないでください。ユーザーのリクエストが曖昧な場合は、明確化を求めてください。"})
messages.append({"role": "user", "content": "沖縄県那覇市の天気予報を教えて"})
chat_response = chat_completion_request(
    messages, tools=tools
)
chat_response.choices[0].message
  • 出力
    ChatCompletionMessage(content='何日間の天気予報が必要か教えてください。また、温度の単位は摂氏 (celsius) と華氏 (fahrenheit) のどちらをご希望ですか?', refusal=None, role='assistant', function_call=None, tool_calls=None)

  • モデルに全く関数を使用させないように強制することもできます。(tool_choice="none")これにより、モデルが適切な関数呼び出しを生成するのを防ぐことができます

messages = []
messages.append({"role": "system", "content": "関数に入力する値について、推測をしないでください。ユーザーのリクエストが曖昧な場合は、明確化を求めてください。"})
messages.append({"role": "user", "content": "沖縄県那覇市の現在の天気を(摂氏で)教えてください。"})
chat_response = chat_completion_request(
    messages, tools=tools, tool_choice="none"
)
chat_response.choices[0].message
  • 出力
    ChatCompletionMessage(content='それでは、沖縄県那覇市の現在の天気を確認します。少々お待ちください。', refusal=None, role='assistant', function_call=None, tool_calls=None)

並列関数呼び出し

  • gpt-4ogpt-3.5-turbo のような新しいモデルは、一度に複数の関数を呼び出すことができます。
  • 一度に複数関数を呼び出すことでレスポンス高速化とトークン数を節約することができます。
messages = []
messages.append({"role": "system", "content": "関数に入力する値について、推測をしないでください。ユーザーのリクエストが曖昧な場合は、明確化を求めてください。"})
messages.append({"role": "user", "content": "千葉県千葉市と沖縄県那覇市の今後4日間の天気はどうなりますか?"})
chat_response = chat_completion_request(
    messages, tools=tools, model=deployment_name
)

assistant_message = chat_response.choices[0].message.tool_calls
assistant_message
  • 出力

  • [ChatCompletionMessageToolCall(id='call_9dEG7mA34Gmd8F3zZSjA0bSA', function=Function(arguments='{"location": "千葉市, 千葉県", "format": "celsius", "num_days": 4}', name='get_n_day_weather_forecast'), type='function'),

  • ChatCompletionMessageToolCall(id='call_E0NNdTJcRZg1vH6uRfctE5c8', function=Function(arguments='{"location": "那覇市, 沖縄県", "format": "celsius", "num_days": 4}', name='get_n_day_weather_forecast'), type='function')]

モデル生成の引数を使って関数を呼び出す方法

  • 次の例では、モデルが生成した入力を使用して関数を実行する方法を示し、これを使ってデータベースに関する質問に答えるエージェントを実装します。
  • シンプルにするために、Chinook sample database を使用します。
  • 注意: SQL生成は、モデルが完全に正しいSQLを生成するわけではないため、本番環境では高リスクとなる可能性があります。

SQLクエリを実行する関数の指定

  • まず、SQLiteデータベースからデータを抽出するための関数を定義します
  • ("data/Chinook.db")は適切なパスを設定してください
import sqlite3

conn = sqlite3.connect("data/Chinook.db")
print("データベースを正常に開きました")
  • 出力
    データベースを正常に開きました

関数設定

  • SQLiteデータベースに接続して、データベース内のテーブル名とそのカラム名を取得する3つの関数を定義しています
  • これらの関数を使うことで、データベースの構造情報を簡単に取得できます
#conn: SQLiteデータベースへの接続オブジェクトを受け取ります。
#conn.execute("SELECT name FROM sqlite_master WHERE type='table';"): このSQLクエリを使用して、データベース内のすべてのテーブル名を取得します。sqlite_masterはSQLiteの内部テーブルで、データベースのスキーマ情報を保持しています。
#tables.fetchall(): クエリの結果をすべて取得し、各テーブルの名前をリストに追加します。
#最後に、table_names にすべてのテーブル名を格納し、返します。

def get_table_names(conn):
    """テーブル名のリストを返します"""
    table_names = []
    tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table';")
    for table in tables.fetchall():
        table_names.append(table[0])
    return table_names

#conn: SQLiteデータベースへの接続オブジェクト。
#table_name: カラム名を取得したいテーブルの名前。
#conn.execute(f"PRAGMA table_info('{table_name}');"): このSQLクエリを使用して、指定されたテーブルのカラム情報を取得します。PRAGMA table_infoはSQLiteのメタデータを取得するための命令で、指定したテーブルのカラムに関する情報を返します。
#columns: 取得されたカラム情報のリスト。各カラムの情報には、カラム名、データ型、デフォルト値などが含まれていますが、カラム名は columns[i][1] でアクセスします。
#最後に、column_names にすべてのカラム名を格納し、返します。

def get_column_names(conn, table_name):
    """カラム名のリストを返します"""
    column_names = []
    columns = conn.execute(f"PRAGMA table_info('{table_name}');").fetchall()
    for col in columns:
        column_names.append(col[1])
    return column_names

#conn: SQLiteデータベースへの接続オブジェクト。
#get_table_names(conn): データベース内のすべてのテーブル名を取得します。
#各テーブル名について、get_column_names(conn, table_name) を呼び出して、そのテーブルのカラム名を取得します。
#table_dicts.append({"table_name": table_name, "column_names": columns_names}): それぞれのテーブル名とそのカラム名のリストを辞書形式で table_dicts に追加します。
#最後に、すべてのテーブル情報が格納された table_dicts を返します。
def get_database_info(conn):
    """データベース内の各テーブルのテーブル名とカラムを含む辞書のリストを返します"""
    table_dicts = []
    for table_name in get_table_names(conn):
        columns_names = get_column_names(conn, table_name)
        table_dicts.append({"table_name": table_name, "column_names": columns_names})
    return table_dicts
  • これらのユーティリティ関数を使用して、データベーススキーマの表現を抽出することができます。
database_schema_dict = get_database_info(conn)
database_schema_string = "\n".join(
    [
        f"Table: {table['table_name']}\nColumns: {', '.join(table['column_names'])}"
        for table in database_schema_dict
    ]
)
  • 取得したデータベースの内容を確認します。
print(database_schema_string)
  • 出力

Table: albums
Columns: AlbumId, Title, ArtistId
Table: sqlite_sequence
Columns: name, seq
Table: artists
Columns: ArtistId, Name
Table: customers
Columns: CustomerId, FirstName, LastName, Company, Address, City, State, Country, PostalCode, Phone, Fax, Email, SupportRepId
Table: employees
Columns: EmployeeId, LastName, FirstName, Title, ReportsTo, BirthDate, HireDate, Address, City, State, Country, PostalCode, Phone, Fax, Email
Table: genres
Columns: GenreId, Name
Table: invoices
Columns: InvoiceId, CustomerId, InvoiceDate, BillingAddress, BillingCity, BillingState, BillingCountry, BillingPostalCode, Total
Table: invoice_items
Columns: InvoiceLineId, InvoiceId, TrackId, UnitPrice, Quantity
Table: media_types
Columns: MediaTypeId, Name
Table: playlists
Columns: PlaylistId, Name
Table: playlist_track
Columns: PlaylistId, TrackId
Table: tracks
Columns: TrackId, Name, AlbumId, MediaTypeId, GenreId, Composer, Milliseconds, Bytes, UnitPrice
Table: sqlite_stat1
Columns: tbl, idx, stat

image.png

  • 以前と同様に、APIが引数を生成するための関数仕様ask_databaseを定義します。
  • ここで重要なのは、データベーススキーマを関数仕様に挿入している点です。これにより、モデルがこの情報を把握することができます。
  • descriptionに取得した{database_schema_string}を入力しています。
tools = [
    {
        "type": "function",
        "function": {
            "name": "ask_database",
            "description": "この関数を使用して、音楽に関するユーザーの質問に答えます。入力は完全に形成されたSQLクエリである必要があります。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": f"""
                                ユーザーの質問に答えるための情報を抽出するSQLクエリ。
                                SQLは次のデータベーススキーマを使用して記述される必要があります:
                                {database_schema_string}
                                クエリはJSONではなくプレーンテキストで返される必要があります。
                                """,
                    }
                },
                "required": ["query"],
            },
        }
    }
]

SQLクエリの実行

  • 次に、データベースに対して実際にクエリを実行する関数(ask_database)を実装します
def ask_database(conn, query):
    """提供されたSQLクエリを使用してSQLiteデータベースにクエリを実行する関数。"""
    try:
        results = str(conn.execute(query).fetchall())
    except Exception as e:
        results = f"クエリが次のエラーで失敗しました: {e}"
    return results

Chat Completions APIを使用して関数呼び出しを行う手順

Step 1: モデルが使用するツールを選択する可能性があるコンテンツを使ってモデルにプロンプトを送信します。ツールの説明、関数名やシグネチャなどは「Tools」リストで定義され、API呼び出し時にモデルに渡されます。選択された場合、関数名とパラメータが応答に含まれます。

Step 2: プログラム的にモデルが関数を呼び出そうとしたかどうかを確認します。これがTrueであれば、ステップ3に進みます。

Step 3: 応答から関数名とパラメータを抽出し、そのパラメータで関数を呼び出します。結果をメッセージに追加します。

Step 4: メッセージリストを使ってChat Completions APIを呼び出し、応答を得ます。

# Step #1: 関数呼び出しにつながるコンテンツを含むプロンプトを作成します。この場合、モデルはユーザーが要求した情報が、ツールの説明でモデルに渡されたデータベーススキーマに含まれている可能性があると認識できます。 
# messages リスト: このリストには、ユーザーからのリクエストメッセージが含まれています。この場合、ユーザーは「最も多くのトラックを持つアルバムの名前」を尋ねています。
# role: メッセージの役割を示します。この場合は user です。
# content: ユーザーの質問内容です。

messages = [{
    "role":"user", 
    "content": "最も多くのトラックを持つアルバムの名前は何ですか?"
}]

#client.chat.completions.create: チャットモデルにリクエストを送信して、ユーザーの質問に対する応答を生成します。
#model=deployment_name: 使用するモデルの名前(またはデプロイメント名)を指定します。
#messages=messages: ユーザーからのリクエストメッセージを渡します。
#tools=tools: 使用可能なツール(関数)のリストを渡します。ここでは、先ほど定義した ask_database 関数が含まれています。
#tool_choice="auto": モデルが自動的に最適なツールを選択して関数呼び出しを行います。
response = client.chat.completions.create(
    model=deployment_name, 
    messages=messages, 
    tools= tools, 
    tool_choice="auto"
)

# メッセージをメッセージリストに追加します。
# response.choices[0].message: APIからの応答の中から、最初の選択肢のメッセージ部分を取得します。このメッセージには、生成されたSQLクエリやその結果が含まれている可能性があります。
# messages.append(response_message): 取得した応答メッセージを messages リストに追加して、会話の履歴を維持します。
response_message = response.choices[0].message 
messages.append(response_message)

print(response_message)
  • 出力
    ChatCompletionMessage(content=None, refusal=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_WBCbm5JpMTpK0YnNKGpQe6GB', function=Function(arguments='{"query":"SELECT albums.Title FROM albums JOIN tracks ON albums.AlbumId = tracks.AlbumId GROUP BY albums.AlbumId ORDER BY COUNT(tracks.TrackId) DESC LIMIT 1;"}', name='ask_database'), type='function')])
show_json(response)
  • 以下jsonでの出力
{'id': 'chatcmpl-9yVZ9iRZvWcWLS3o7F9ZJ0GfvpNzF',
 'choices': [{'finish_reason': 'tool_calls',
   'index': 0,
   'logprobs': None,
   'message': {'content': None,
    'refusal': None,
    'role': 'assistant',
    'function_call': None,
    'tool_calls': [{'id': 'call_WBCbm5JpMTpK0YnNKGpQe6GB',
      'function': {'arguments': '{"query":"SELECT albums.Title FROM albums JOIN tracks ON albums.AlbumId = tracks.AlbumId GROUP BY albums.AlbumId ORDER BY COUNT(tracks.TrackId) DESC LIMIT 1;"}',
       'name': 'ask_database'},
      'type': 'function'}]},
   'content_filter_results': {}}],
 'created': 1724208047,
 'model': 'gpt-4o-2024-05-13',
 'object': 'chat.completion',
 'service_tier': None,
 'system_fingerprint': 'fp_abc28019ad',
 'usage': {'completion_tokens': 49, 'prompt_tokens': 446, 'total_tokens': 495},
 'prompt_filter_results': [{'prompt_index': 0,
   'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'},
    'jailbreak': {'filtered': False, 'detected': False},
    'self_harm': {'filtered': False, 'severity': 'safe'},
    'sexual': {'filtered': False, 'severity': 'safe'},
    'violence': {'filtered': False, 'severity': 'safe'}}}]}

# Step 2: モデルの応答にツール呼び出しが含まれているかどうかを確認します。   

#tool_calls: response_message に含まれる tool_calls プロパティを取得します。このプロパティには、モデルが呼び出そうとしているツールや関数に関する情報が含まれます。
#if tool_calls:: tool_calls が存在するかどうかを確認します。存在する場合、モデルはツールを呼び出すように設計されています。
#tool_call_id: 最初のツール呼び出しのIDを取得します。このIDは、ツール呼び出しを一意に識別するために使用されます。
#tool_function_name: 呼び出す関数の名前を取得します。
#tool_query_string: 関数に渡す引数を取得し、この場合はJSONからSQLクエリを抽出します。

tool_calls = response_message.tool_calls
if tool_calls:
    # Trueであれば、モデルは呼び出すツールや関数の名前と引数を返します。  
    tool_call_id = tool_calls[0].id
    tool_function_name = tool_calls[0].function.name
    tool_query_string = json.loads(tool_calls[0].function.arguments)['query']


# Step 3: 関数を呼び出して結果を取得します。結果をメッセージリストに追加します。 
#client.chat.completions.create: 更新された messages リストを使って、再度チャットモデルにリクエストを送信します。これにより、モデルはツール呼び出しの結果を考慮した新しい応答を生成します。
#print(model_response_with_function_call.choices[0].message.content): 新しい応答の内容を出力します。この内容は、ユーザーに最終的に返される答えになります。
#if tool_function_name == 'ask_database': モデルが呼び出そうとしている関数が ask_database であることを確認します。
#results = ask_database(conn, tool_query_string): ask_database 関数を呼び出し、データベースに対して生成されたSQLクエリ (tool_query_string) を実行し、その結果を results に保存します。
#messages.append({...}): results を新しいメッセージとして messages リストに追加します。このメッセージは、ツールからの応答を表し、後で再度モデルに渡されます。
     
    if tool_function_name == 'ask_database':
        results = ask_database(conn, tool_query_string)
        
        messages.append({
            "role":"tool", 
            "tool_call_id":tool_call_id, 
            "name": tool_function_name, 
            "content":results
        })

        # Step 4:関数の応答をメッセージリストに追加して、Chat Completions API を呼び出します 
        # 'tool' の役割を持つメッセージは、直前の 'tool_calls' を持つメッセージへの応答でなければならないことに注意してください。
        model_response_with_function_call = client.chat.completions.create(
            model=deployment_name,
            messages=messages,
        )  #モデルが関数の応答を確認できるように、新しい応答を取得します
        print(model_response_with_function_call.choices[0].message.content)
    else: 
        print(f"エラー: 関数 {tool_function_name} 存在しません")
else: 
    # モデルは呼び出すべき関数を特定しなかったため、結果をユーザーに返すことができます。 
    print(response_message.content) 
  • 出力
    最も多くのトラックを持つアルバムの名前は「Greatest Hits」です。
show_json(model_response_with_function_call)
  • 以下jsonでの出力
{'id': 'chatcmpl-9yVYlZ9xjqOTOEU8Buc3y7wFzm6kU',
 'choices': [{'finish_reason': 'stop',
   'index': 0,
   'logprobs': None,
   'message': {'content': '最も多くのトラックを持つアルバムの名前は「Greatest Hits」です。',
    'refusal': None,
    'role': 'assistant',
    'function_call': None,
    'tool_calls': None},
   'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'},
    'self_harm': {'filtered': False, 'severity': 'safe'},
    'sexual': {'filtered': False, 'severity': 'safe'},
    'violence': {'filtered': False, 'severity': 'safe'}}}],
 'created': 1724208023,
 'model': 'gpt-4o-2024-05-13',
 'object': 'chat.completion',
 'service_tier': None,
 'system_fingerprint': 'fp_abc28019ad',
 'usage': {'completion_tokens': 22, 'prompt_tokens': 91, 'total_tokens': 113},
 'prompt_filter_results': [{'prompt_index': 0,
   'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'},
    'jailbreak': {'filtered': False, 'detected': False},
    'self_harm': {'filtered': False, 'severity': 'safe'},
    'sexual': {'filtered': False, 'severity': 'safe'},
    'violence': {'filtered': False, 'severity': 'safe'}}}]}

まとめ

  • Azure OpenAIを使用して、自然言語からSQLクエリを生成し、データベースに問い合わせる方法を解説しました。
  • 天気情報を取得するFunction Callingの例を紹介し、その後、SQLクエリを生成してデータベースから情報を引き出しました。
  • SqliteのChinookサンプルデータベースを使用して、自然言語からSQL文を発行し回答を作成しました。SQL Databaseでも実装してみたいと思います。
  • 高速かつシンプルに実装できるのでおすすめです。

Discussion