🗂

TiDB Starter Data Service V3 (2) Chat2Query テスト用ウェブツール

に公開

https://zenn.dev/kameoncloud/articles/68eadd6a6afa58
前回の記事ではTiDB Starter のData App / Chat2Queryモードでv3のエンドポイントをテストしました。

おさらい

Chat2Queryは動的に自然言語ともとにSQLを作成し実行してくれるツールです。Data App で Chat2Queryモードを使うと動的にSQLが生成されじっこいうされます。
Standardモードだとあらかじめ保存しておいたSQLのみが実行されるため、Chat2Queryの方が多様なワークロードに対応しています。どういうSQLが生成されるかは実行前に事前確認が行えないため安全用にデータ操作系SQLは実行されないようになっています。

実行までの手順は以下です。

  1. Data Summary の取得 (スキーマが変更する都度実行が必要です)
  2. 自然言語でクエリの実行リクエス → クエリ生成/ジョブ生成
  3. クエリ生成ジョブステータス確認(ポーリング)
  4. ジョブの実行

Chat2Queryを実行するには3回curlコマンドを実行しないといけないため少し不便です。このため一気通貫で1,2,3を行い生成されたSQLやその実行結果を確認できるウェブツールを作りました

出力される内容

このウェブツールで出力される内容は生成されたSQLとその実行結果です。
実行結果は大きく3つに分かれます。
1.SQLの実行結果の出力
2.SQLは生成されたが実行が禁止された場合(データ操作系SQL)のエラー
3.その他汎用エラー / SQLの生成失敗

さっそくやってみる

1. 必要ファイルの作成

まずはserver.cjs,.env,index.html3つのファイルを作成し以下の内容をコピペします。

server.cjs
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const dotenv = require('dotenv');
const DigestFetch = require('digest-fetch').default;
dotenv.config();

const app = express();
const port = 3000;

const BASE_URL = 'https://us-west-2.data.tidbcloud.com/api/v1beta/app/chat2query-UfpVDXbp';
const CLUSTER_ID = '10080985057875672215';
const DATABASE = 'test';

const client = new DigestFetch(process.env.PUBLIC_KEY, process.env.PRIVATE_KEY, {
  algorithm: 'MD5',
  basic: false,
});

app.use(bodyParser.urlencoded({ extended: true }));

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'index.html'));
});

// 🔁 ポーリング処理:ジョブが完了するまで待機
async function waitForJob(jobId, maxRetries = 10, interval = 1500) {
  const jobUrl = `${BASE_URL}/endpoint/v2/jobs/${jobId}`;
  for (let i = 0; i < maxRetries; i++) {
    const resp = await client.fetch(jobUrl, {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' },
    });
    const text = await resp.text();
    console.log(`📨 GETレスポンス try=${i + 1}:`, text);

    const data = JSON.parse(text);
    const status = data?.result?.status;

    if (status === 'done') {
      return data;
    }
    if (status === 'failed') {
      throw new Error('❌ ジョブ失敗: ' + JSON.stringify(data, null, 2));
    }

    // running/init の場合は待つ
    await new Promise(r => setTimeout(r, interval));
  }
  throw new Error(`❌ ジョブが完了しませんでした(${maxRetries}回試行後)`);
}

// フォーム送信処理
app.post('/ask', async (req, res) => {
  const question = req.body.question;

  try {
    console.log('📤 質問:', question);

    // Step 1: Chat2Query 実行(POST)
    const postBody = { cluster_id: CLUSTER_ID, database: DATABASE, question };
    console.log('📦 POSTボディ:', JSON.stringify(postBody, null, 2));

    const postResponse = await client.fetch(`${BASE_URL}/endpoint/v3/chat2data`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(postBody),
    });

    const postData = await postResponse.json();
    const jobId = postData?.result?.job_id;
    if (!jobId) throw new Error('❌ job_id が取得できません: ' + JSON.stringify(postData, null, 2));
    console.log('✅ job_id:', jobId);

    // Step 2: job 完了まで待つ
    const getData = await waitForJob(jobId);

    const sql = getData?.result?.result?.sql || '(SQLなし)';
    const sqlError = getData?.result?.result?.sql_error || null;
    const rows = getData?.result?.result?.data?.rows || [];
    const columns = getData?.result?.result?.data?.columns || [];

    console.log('🧠 生成されたSQL:\n', sql);
    if (sqlError) console.error('🚨 SQLエラー:', sqlError);
    console.log('📊 行数:', rows.length, '列数:', columns.length);

    // テーブルHTML生成
    const table = rows.length > 0 ? `
      <table border="1" cellpadding="8" style="border-collapse: collapse;">
        <thead><tr>${columns.map(c => `<th>${c.col}</th>`).join('')}</tr></thead>
        <tbody>
          ${rows.map(row => `<tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`).join('')}
        </tbody>
      </table>
    ` : '<p>結果なし</p>';

    // HTMLレスポンス生成
    let content = `
      <h2>質問:</h2>
      <pre>${question}</pre>
      <h2>生成されたSQL:</h2>
      <pre>${sql}</pre>
    `;

    if (sqlError) {
      content += `<h2 style="color:red;">SQLエラー:</h2><pre>${sqlError}</pre>`;
    } else {
      content += `<h2>実行結果:</h2>${table}`;
    }

    res.send(content + '<br><a href="/">← 戻る</a>');

  } catch (err) {
    console.error('🚨 エラー内容:', err);
    res.send(`<h2>エラーが発生しました:</h2><pre>${err.message}</pre><a href="/">← 戻る</a>`);
  }
});

app.listen(port, () => {
  console.log(`✅ Server is running: http://localhost:${port}`);
});
.env
PUBLIC_KEY=xxJ7PJD0
PRIVATE_KEY=xxe13e7a-8b9b-4f98-a886-3d9ca5a58c6e
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>TiDB Chat2Query フォーム</title>
</head>
<body>
  <h1>TiDB Chat2Query 質問フォーム</h1>
  <form action="/ask" method="post">
    <label for="question">質問内容:</label><br>
    <input type="text" id="question" name="question" size="60" required><br><br>
    <input type="submit" value="送信">
  </form>
</body>
</html>

2.起動とテスト実行

node server.cjsで起動します。

node server.cjs
[dotenv@17.2.3] injecting env (2) from .env -- tip: 🔐 encrypt with Dotenvx: https://dotenvx.com
✅ Server is running: http://localhost:3000

http://localhost:3000にアクセスしてデータ分析指示を出します。


実行の許可がされないSQLが生成された場合は以下の通りエラーとなります。

Discussion