## モジュール 2: Text2SQL デモアプリケーションの実行、体験、および学習

次のデモでは、別の種類の GenAI アプリケーションについて掘り下げます。このアプリケーションは TiDB Cloud Starter クラスタを SQL 検証ツールとして使用します。次のことを行います：

1. クエリから SQL を生成します。
2. TiDB Cloud Starter クラスタで実行します。
3. TiDB Cloud Starter クラスタからフィードバックまたはエラーメッセージを取得します。
4. 確実な結果が得られるまで自己修正を行います。

### 手順 1. Text2SQL Streamlit サービスを実行する

新しいターミナルを作成します：

  ![90%](https://pingcap-edu-us-west-2.s3.us-west-2.amazonaws.com/quick-demo/demo-421/421-text2sql-new-terminal-0514.jpg)

次に、`streamlit` コマンドラインツールを使用してこのスクリプトを実行します：

   ```bash
   streamlit run bedrock/text2sql.py --server.port 8502
   ```

   ```output
   Collecting usage statistics. To deactivate, set browser.gatherUsageStats to false.

   You can now view your Streamlit app in your browser.

   Local URL: http://localhost:8502
   Network URL: http://172.17.0.2:8502
   External URL: http://${LAB:HOST_MONITOR1_PUBLIC_IP}:8502
   ```

### 手順 2. Text2SQL デモウェブページを使用する

- [Text2SQL デモウェブページ](http://${LAB:HOST_MONITOR1_PUBLIC_IP}:8502/)を開きます。

  ![90%](https://public-pingcap-innovation.s3.us-west-2.amazonaws.com/demo-409/409-text2sql-frontend-0427.jpg)

- 次に、このデータセットについて質問することができます。例えば：

  ```text
  How many tables in the database?
  ```

- <kbd>Enter</kbd> キーをクリックして SQL を生成し、TiDB Cloud Starter クラスタで実行します。

  ![90%](https://pingcap-edu-us-west-2.s3.us-west-2.amazonaws.com/quick-demo/demo-421/421-text2sql-query-0514.jpg)

- または、次のような別の質問を試すこともできます：

  ```text
  What is the table name in the database?
  ```

  ![text2SQL-result.png 85%](https://lab-static.pingcap.com/images/2026/1/14/3a4d7b5d1a68de898647b0841a8ca98cc11a67b5.png)

- これは、このデモを開始する前にデータベースにテーブル `TEXT2SQL` を作成したためです。
- このデータセットについて質問したいことを自由に試してみてください。

### 手順 3. Text2SQL デモコードの読み取り

再度、[VS Code](http://${LAB:HOST_MONITOR1_PUBLIC_IP}:3000/?folder=/root/tidb-lab-ai) に戻り、コードファイル `bedrock/text2sql.py` をクリックします。

1. スクリプトは必要なライブラリをインポートします：環境変数用の `os`、AWS サービス（特に Bedrock）と対話するための `boto3`、UI 用の `streamlit`、そしてデータベース接続用の `pytidb` から `TiDBClient` をインポートします。

    ```python
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import os

    import boto3
    import streamlit as st
    from pytidb import TiDBClient
    ```

2. Streamlitアプリケーションページのタイトル `## 📖 Text2SQL` が設定されています。

    ```python
    st.markdown("## 📖 Text2SQL")
    ```

3. `TiDBClient` 接続が TiDB Cloud Starter クラスタに確立されます。接続の詳細（ホスト、ポート、ユーザー、パスワード、データベース）は環境変数から読み取られます。接続には SSL が有効になっています。

    ```python
    db = TiDBClient.connect(
        host=os.getenv("SERVERLESS_CLUSTER_HOST"),
        port=int(os.getenv("SERVERLESS_CLUSTER_PORT")),
        username=os.getenv("SERVERLESS_CLUSTER_USERNAME"),
        password=os.getenv("SERVERLESS_CLUSTER_PASSWORD"),
        database=os.getenv("SERVERLESS_CLUSTER_DATABASE_NAME"),
        enable_ssl=True,
    )
    ```

4. Amazon Bedrock ランタイム用の `boto3` クライアントが初期化され、特に `us-west-2` リージョンに設定されています。このクライアントは Bedrock モデルを呼び出すために使用されます。

    ```python
    client = boto3.client(
        service_name="bedrock-runtime",
        region_name="us-west-2"
    )
    ```

5. Streamlit のセッション状態がチャット履歴を保存するために初期化されます。`generated`（アシスタントのメッセージ用）と `past`（ユーザーのメッセージ用）という2つのリストが、まだ存在しない場合に作成されます。

    ```python
    for item in ["generated", "past"]:
        if item not in st.session_state:
            st.session_state[item] = []
    ```

6. テーブル定義が接続されたTiDBデータベースから取得されます。スクリプトはすべてのテーブル名を反復処理し、それぞれに対して`SHOW CREATE TABLE`を実行し、DDLステートメントを`table_definitions`リストに格納します。`current_database`名も取得されます。この情報はLLMにスキーマのコンテキストを提供します。

    ```python
    table_definitions = []
    current_database = db._db_engine.url.database
    for table_name in db.table_names():
        table_definitions.append(db.query(f"SHOW CREATE TABLE `{table_name}`").to_rows()[0])
    ```

7. ヘルパー関数 `format_answer` が定義されています。この関数は、LLMのレスポンスから先頭や末尾のコードブロックマーカー（\`\`\`sql や \`\`\` など）を削除して、SQLやマークダウンが整理された状態になるようにします。

    ```python
    def format_answer(answer: str) -> str:
        # Handle ```.* specifically
        if answer.startswith("```"):
            # Find the first newline after the opening ```
            first_newline = answer.find("\n")
            if first_newline != -1:
                # Remove everything from start to the newline
                answer = answer[first_newline:].strip()

        # Remove closing ``` if present
        if answer.endswith("```"):
            answer = answer[:-len("```")].strip()

        return answer
    ```

8. `on_submit` 関数が定義されています。この関数はユーザーが質問を送信したときに呼び出されます。
    - セッション状態から `user_input` を取得します。
    - Amazon Bedrock モデル `us.amazon.nova-pro-v1:0` を `client.converse` を使用して呼び出し、SQL クエリを生成します。システムプロンプトはモデルに上級 DBA として振る舞うよう指示し、提供されたテーブル定義とデータベース名を使用して、余分なテキストなしで直接 SQL で応答するよう指示します。
    - 生成された SQL はレスポンスから抽出され、`format_answer` を使用してクリーンアップされます。
    - ユーザーの入力は `st.session_state.past` リストに追加されます。

    ```python
    def on_submit():
        user_input = st.session_state.user_input
        if user_input:
            generated_sql = (
                client.converse(
                    modelId="us.amazon.nova-pro-v1:0",
                    messages=[
                        {"role": "user", "content": [{"text": f"Question: {user_input}\n Respond in SQL directly, without any other text and format."}]},
                    ],
                    system=[
                        {"text": f"""You are a very senior database administrator who can write SQL very well,
                            please write MySQL SQL to answer user question,
                            Use backticks to quote table names and column names,
                            here are some table definitions in database,
                            the database name is {current_database}\n\n"""
                            + "\n".join("|".join(t) for t in table_definitions)}
                    ],
                )["output"]["message"]["content"][0]["text"]
            )

            generated_sql = format_answer(generated_sql)
            st.session_state.past.append(user_input)
    ```

9. `generated_sql` に対して安全性チェックが実行されます。もし `INSERT` または `UPDATE`（大文字小文字を区別しない）が含まれている場合、エラーメッセージが表示され、スクリプトは停止します。これにより、潜在的に有害なDMLステートメントの実行を防止します。

    ```python
            if "insert" in generated_sql.lower() or "update" in generated_sql.lower():
                st.error(
                    "The generated SQL is not a SELECT statement, please check it carefully before running it."
                )
                st.stop()
    ```

10. スクリプトは `generated_sql` を TiDB データベースに対して実行しようとします：
    - `try-except` ブロックがクエリ実行中の潜在的なエラーを処理します。
    - 成功した場合、データベースクエリの結果（`rows`）が取得され、文字列 `sql_result` に整形されます。
    - Bedrock モデル `us.amazon.nova-pro-v1:0` への別の呼び出しが行われます。今回、LLM はマークダウンフォーマッターとして機能するよう指示されます。元の質問、生成された SQL、および `sql_result` を入力として受け取り、データのマークダウンテーブルを含む読みやすいマークダウンレスポンスに整形するよう求められます。また、コードブロックなどの余分な書式を避けるよう指示されています。
    - LLM の整形された回答は `format_answer` を使用してクリーンアップされ、`st.session_state.generated` に追加されます。
    - このプロセス中に例外が発生した場合、エラーメッセージが `st.session_state.generated` に追加されます。

    ```python
            # Execute the SQL query and set the result
            answer = None
            try:
                rows = db.query(generated_sql).to_rows()
                sql_result = "\n".join(str(row) for row in rows)

                answer = (
                    client.converse(
                        modelId="us.amazon.nova-pro-v1:0",
                        messages=[
                            {"role": "user", "content": [{"text": f"""
                            Question: {user_input}\n\n
                            SQL: {generated_sql}\n\n
                            Result: {sql_result}"""}]},
                        ],
                        system=[
                            {"text": """You are a markdown formatter,
                                format the Question and SQL to markdown text,
                                format the data row into markdown tables.
                                NOTE: Without any other format like ``` etc.
                                """}
                        ],
                    )["output"]["message"]["content"][0]["text"]
                )
                answer = format_answer(answer)
                st.session_state.generated.append(answer)
            except Exception as e:
                st.session_state.generated.append(f"Error: {e}")
    ```

11. ユーザー操作のためのStreamlit UIがセットアップされます：
    - サブヘッダー `##### User Query` が表示されます。
    - `st.container` がチャット入力とチャット表示エリアをグループ化します。
    - `st.chat_input` はユーザーの質問用のテキスト入力フィールドを作成します。`key="user_input"` はセッション状態にバインドし、`on_submit=on_submit` はコールバック関数にリンクします。
    - `st.empty()` はチャット履歴が表示されるプレースホルダー（`chat_placeholder`）を作成します。

    ```python
    st.markdown("##### User Query")
    with st.container():
        st.chat_input(
            "Input your question, e.g. how many tables in the database?",
            key="user_input",
            on_submit=on_submit,
        )

        chat_placeholder = st.empty()
    ```

12. チャット履歴は `chat_placeholder` コンテナ内にレンダリングされます：

    - `st.session_state["generated"]`（アシスタントの応答）と `st.session_state["past"]`（ユーザーの入力）リストを逆時系列順（最新のものが最初）に繰り返し処理します。
    - `st.chat_message("user")` と `st.chat_message("assistant")` は、適切なスタイルで各メッセージを表示するために使用されます。

    ```python
        with chat_placeholder.container():
            for i in range(len(st.session_state["generated"]) - 1, -1, -1):
                with st.chat_message("user"):
                    st.write(st.session_state["past"][i])
                with st.chat_message("assistant"):
                    st.write(st.session_state["generated"][i])
    ```

### 手順 4. TiDB Cloud コンソールにアクセスして独自の旅を始めましょう

+ 独自のAWSアカウントの作成とAmazon Bedrockの設定は、この演習の範囲外です。詳細については、[Amazon Bedrockのドキュメント](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html)を参照してください。

+ TiDBとそのコンパニオンGenAI SDK「pytidb」は、GenAIアプリケーション用の統合データストアを提供することで開発を簡素化します。[こちら](https://tidbcloud.com/clusters)から TiDB Cloud の旅を始めることができます。この演習で自分の無料のクラスタを使用した場合は、同じページに表示されています。クラスタ名は ${LAB:SERVERLESS_CLUSTER_NAME} です。
