top of page

最新記事リスト

Qlik Answersをqlik-embedで外部サイトに埋め込む

  • 執筆者の写真: Shin
    Shin
  • 6 時間前
  • 読了時間: 9分

今回は、Qlik Answersをqlik-embedで外部サイトに埋め込む方法をご紹介します。

以下の方法を用いることで、Qlik Answersの機能を搭載したチャットボットを、見た目を自由にカスタマイズして埋め込むことが可能です。


完成イメージ



Qlik Answersとは

Qlik AnswersはQlik Cloudに搭載されている生成AI機能です。 主な機能として、以下の2つが搭載されています。


・ナレッジベース:生成AIに学習させるデータを追加できる機能

・アシスタント:ナレッジベースに基づいたチャットボットを構築できる機能


上記の機能を用いることで、簡単にQlik Cloud上にRAGのチャットボットを構築することができます。



qlik-embedとは

qlik-embedを用いることで、Qlik上で作成したダッシュボードやチャートを、iframeタグを利用せずに外部のサイトに埋め込むことが可能です。


この機能は開発者向けツールとして公開されています。


詳細は以下のサイトをご覧ください。



作成手順

①データの準備

Qlik Cloudにアクセスし、ナレッジベースを作成します。



②アシスタントの作成

作成したナレッジベースを元にしたアシスタントを作成します。


※①②の詳細な手順に関しては以下の記事をご参照ください。



③Webサイトの構築

qlik-embedを用いて、アシスタントを埋め込んだWebサイトを構築します。


ファイル構成

│ 
├─node_modules
│      中身は省略
│                 
├─public
│        index.html
│        scripts.js
│        styles.css
│ 
├─index.js
│ 
├─package-lock.json
│ 
├─package.json
│ 
└─.gitignore

コード例

index.html

index.htmlで最初に表示されるメッセージや、質問入力ボックス、送信ボタンなどのチャット画面のUIを作成します。

<!doctype html>
<html lang="en">
    <head>
        <!-- ドキュメントのメタデータと文字エンコーディングを指定 -->
        <meta charset="UTF-8" />
        <!-- レスポンシブデザインのためのビューポート設定 -->
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <!-- ブラウザのタブに表示されるウェブページのタイトル -->
        <title>Qlik Assistant</title>
        <!-- ウェブページのスタイリングのための外部スタイルシートへのリンク -->
        <link rel="stylesheet" href="styles.css" />
    </head>
    <body>
        <!-- チャットインターフェースのメインコンテナ -->
        <div class="chat-container">
            <!-- チャットインターフェースのヘッダーセクション -->
            <div class="chat-header">
                <!-- チャットアシスタントのタイトル -->
                <h4>Qlikなんでもチャット</h4>
                <!-- アシスタントの機能の説明 -->
                <span class="header-span">Qlikについての質問に、AIが回答します。</span>
            </div>

            <!-- チャットメッセージが表示されるボディセクション -->
            <div class="chat-body" id="chat-body">
                <!-- アシスタントからのメッセージの例 -->
                <div class="message assistant">
                    <div class="bubble">
                        <!-- アシスタントの最初の挨拶メッセージ -->
                        <p>こんにちは!何でも聞いてね!</p>
                    </div>
                </div>
            </div>
            <!-- 入力フィールドと送信ボタンのあるフッターセクション -->
            <div class="chat-footer">
                <!-- ユーザーが質問を入力するためのテキスト入力フィールド -->
                <input
                    type="text"
                    id="chat-input"
                    placeholder="Qlikについて質問してみましょう…"
                />
                <!-- ユーザーの質問を送信するためのボタン -->
                <button id="send-btn">送信</button>
            </div>
        </div>
        <!-- インタラクティビティのための外部JavaScriptファイルへのリンク -->
        <script src="scripts.js"></script>
    </body>
</html>

scripts.js

scripts.jsでフロントエンドのユーザ操作の処理を記述します。

document.addEventListener("DOMContentLoaded", () => {
  // DOMツリーが完全に読み込み終わったことを検知
  const chatBody = document.getElementById("chat-body"); // チャットメッセージが表示されるボディ
  const chatInput = document.getElementById("chat-input"); // 質問を入力するためのテキスト入力フィールド
  const sendButton = document.getElementById("send-btn"); // 送信ボタン

  // ユーザーメッセージを即座に追加する関数
  function appendUserMessage(message) {
    const messageDiv = document.createElement("div"); // 新しいメッセージコンテナを作成
    messageDiv.classList.add("message", "user"); // クラスを付与してユーザーメッセージであることを示す
    const bubbleDiv = document.createElement("div"); // バブルコンテナを作成
    bubbleDiv.classList.add("bubble"); // クラスを付与
    bubbleDiv.innerHTML = `<p>${message}</p>`; // バブルコンテナにメッセージを挿入
    messageDiv.appendChild(bubbleDiv); // メッセージコンテナにバブルコンテナを追加
    chatBody.appendChild(messageDiv); // チャットボディにメッセージコンテナを追加
    chatBody.scrollTop = chatBody.scrollHeight; // すべてが表示されるようにスクロールを調整
  }

  // アシスタントのメッセージバブルを作成し、ストリーミングテキストを更新する関数
  function createAssistantBubble() {
    const messageDiv = document.createElement("div"); // 新しいメッセージコンテナを作成
    messageDiv.classList.add("message", "assistant"); // クラスを付与してアシスタントメッセージであることを示す
    const bubbleDiv = document.createElement("div"); // バブルコンテナを作成
    bubbleDiv.classList.add("bubble"); // クラスを付与
    bubbleDiv.innerHTML = "<p></p>"; // 空の段落要素を挿入
    messageDiv.appendChild(bubbleDiv); // メッセージコンテナにバブルコンテナを追加
    chatBody.appendChild(messageDiv); // チャットボディにメッセージコンテナを追加
    chatBody.scrollTop = chatBody.scrollHeight; // すべてが表示されるようにスクロールを調整
    return bubbleDiv.querySelector("p"); // 段落要素を返す
  }

  // 質問をバックエンドに送信し、ストリーミングで回答を受け取る関数
  function sendQuestion() {
    const question = chatInput.value.trim(); // 入力された質問をトリム(空白除去)して取得
    if (!question) return; // 質問が空なら何もしない

    // ユーザーメッセージを追加
    appendUserMessage(question);
    chatInput.value = ""; // 入力フィールドをクリア

    // アシスタントメッセージのバブルを作成
    const assistantTextElement = createAssistantBubble();

    // 回答をストリーミングで受け取るために接続を開く
    const eventSource = new EventSource(
      `/stream-answers?question=${encodeURIComponent(question)}`, // 質問をエンコードしてクエリパラメータとして送信
    );

    eventSource.onmessage = function (event) {
      if (event.data === "[DONE]") {
        eventSource.close(); // ストリーミングが終了したら接続を閉じる
      } else {
        assistantTextElement.innerHTML += event.data; // 応答データをアシスタントバブルに追加
        chatBody.scrollTop = chatBody.scrollHeight; // スクロールを調整
      }
    };

    eventSource.onerror = function (event) {
      console.error("EventSource error:", event); // エラー時の処理
      eventSource.close(); // 接続を閉じる
      assistantTextElement.innerHTML += " [Error receiving stream]"; // エラーメッセージを表示
    };
  }

  // 送信ボタンクリック時に質問を送信するイベントリスナーを追加
  sendButton.addEventListener("click", sendQuestion);

  // Enterキー押下時に質問を送信するイベントリスナーを追加
  chatInput.addEventListener("keydown", (event) => {
    if (event.key === "Enter") {
      event.preventDefault(); // デフォルトのEnterキー動作を無効化
      sendQuestion();
    }
  });
});

styles.css

style.cssでフロントエンドの見た目を記述します。

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
      Roboto, Helvetica, Arial, sans-serif;
  background-color: #f2f2f2;
  padding: 50px;
}
.chat-container {
  display: flex;
  flex-direction: column;
  height: 70vh;
  max-width: 600px;
  margin: 0 auto;
  border: 1px solid #ddd;
  background-color: #fff;
  border-radius: 20px;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.chat-header {
  padding: 1rem;
  background-color: #4caf50;
  color: #fff;
  text-align: center;
  font-size: 1.5rem;
  font-weight: bold;
}
.chat-body {
  flex: 1;
  padding: 1rem;
  overflow-y: auto;
  background-color: #e9e9e9;
}
.chat-footer {
  display: flex;
  padding: 0.5rem;
  border-top: 1px solid #ddd;
  background-color: #fff;
}
.chat-footer input[type="text"] {
  flex: 1;
  padding: 0.75rem;
  border: 1px solid #ccc;
  border-radius: 20px;
  outline: none;
  font-size: 1rem;
}
.chat-footer button {
  margin-left: 0.5rem;
  padding: 0.75rem 1rem;
  background-color: #4caf50;
  border: none;
  color: #fff;
  border-radius: 20px;
  cursor: pointer;
  font-size: 1rem;
}
.chat-footer button:hover {
  background-color: #45a049;
}
.message {
  margin-bottom: 1rem;
  display: flex;
  position: relative;
  max-width: 95%;
}
.message.assistant {
  justify-content: flex-start;
}
.message.user {
  justify-content: flex-end;
  align-self: flex-end;
}
.bubble {
  padding: 10px 15px;
  border-radius: 20px;
  position: relative;
  font-size: 1rem;
  line-height: 1.4;
  word-wrap: break-word;
}
/* アシスタントのバブル(受信メッセージ) */
.message.assistant .bubble {
  background-color: #e5e5ea;
  color: #000;
  border-top-left-radius: 0;
}
.message.assistant .bubble:before {
  content: "";
  position: absolute;
  bottom: 0;
  left: -10px;
  width: 0;
  height: 0;
  border-top: 10px solid #e5e5ea;
  border-right: 10px solid transparent;
}
/* ユーザーバブル(送信メッセージ) */
.message.user .bubble {
  background-color: #007aff;
  color: #fff;
  border-top-right-radius: 0;
}
.message.user .bubble:after {
  content: "";
  position: absolute;
  bottom: 0;
  right: -10px;
  width: 0;
  height: 0;
  border-top: 10px solid #007aff;
  border-left: 10px solid transparent;
}
h4 {
  margin: 0;
}
.header-span {
  font-size: 14px;
}

index.js

index.jsでバックエンドの処理を記述します。 この部分でQlik AnswersのAPIの呼び出しを行っています。

import express from "express"; // Expressライブラリのインポート
import fetch from "node-fetch"; // Node-fetchライブラリのインポート
import path from "path"; // パス操作用ライブラリのインポート
import { fileURLToPath } from "url"; // URL操作用ライブラリのインポート


// ESモジュールのための__dirnameセットアップ
const __filename = fileURLToPath(import.meta.url); // 現在のファイル名を取得
const __dirname = path.dirname(__filename); // ディレクトリ名を取得


// ポートの定義とExpressアプリの初期化
const PORT = process.env.PORT || 3000; // 環境変数PORTが存在する場合はそれを使用、存在しない場合は3000を使用
const app = express(); // Expressアプリの作成
app.use(express.static("public")); // 「public」ディレクトリを静的ファイルとして設定
app.use(express.json()); // JSONパースミドルウェアの使用


// フロントエンドの提供
app.get("/", (req, res) => { // ルートパスのGETリクエストに応答
    res.sendFile(path.join(__dirname, "public", "index.html")); // 「public/index.html」を返す
});


// Qlik Answers出力をストリームで返すエンドポイント
app.get("/stream-answers", async (req, res) => { // 「/stream-answers」パスのGETリクエストに応答
    const question = req.query.question; // 質問クエリパラメータの取得
    if (!question) { // 質問がない場合
        res.status(400).send("No question provided"); // 400エラーを返す
        return;
    }

    // ストリーミング応答のヘッダー設定
    res.writeHead(200, {
        "Content-Type": "text/event-stream", // コンテンツタイプをイベントストリームに設定
        "Cache-Control": "no-cache", // キャッシュを無効に設定
        Connection: "keep-alive", // 接続を保持する
    });

    const assistantId = "<Qlik AnswersのアシスタントID>"; 
    const baseUrl = "https://<テナントのURLから引用>.qlikcloud.com/api/v1/assistants/"; // 基本URLの定義
    const bearerToken = process.env["QlikApiKey"]; // 環境変数からBearerトークンを取得

    try {
        // 新しいスレッドを作成
        const createThreadUrl = `${baseUrl}${assistantId}/threads`; // スレッド作成URLの定義
        const threadResponse = await fetch(createThreadUrl, { // スレッド作成リクエストを送信
            method: "POST", // POSTメソッドの使用
            headers: { // リクエストヘッダーの設定
                "Content-Type": "application/json", // コンテンツタイプの設定
                Authorization: `Bearer ${bearerToken}`, // 認証ヘッダーの設定
            },
            body: JSON.stringify({ // リクエストボディの設定
                name: `Conversation for question: ${question}`, // スレッド名の設定
            }),
        });

        if (!threadResponse.ok) { // スレッド作成に失敗した場合
            const errorData = await threadResponse.text();
            res.write(`data: ${JSON.stringify({ error: errorData })}\n\n`); // エラーデータをストリームに出力
            res.end();
            return;
        }

        const threadData = await threadResponse.json(); // スレッド応答のJSONをパース
        const threadId = threadData.id; // スレッドIDの取得

        // Qlik Answersストリーミングエンドポイントを呼び出し
        const streamUrl = `${baseUrl}${assistantId}/threads/${threadId}/actions/stream`; // ストリーミングURLの定義
        const invokeResponse = await fetch(streamUrl, { // ストリーミングリクエストを送信
            method: "POST", // POSTメソッドの使用
            headers: { // リクエストヘッダーの設定
                "Content-Type": "application/json", // コンテンツタイプの設定
                Authorization: `Bearer ${bearerToken}`, // 認証ヘッダーの設定
            },
            body: JSON.stringify({ // リクエストボディの設定
                input: {
                    prompt: question, // 質問の設定
                    promptType: "thread", // プロンプトタイプの設定
                    includeText: true, // テキストを含める設定
                },
            }),
        });

        if (!invokeResponse.ok) { // ストリーミングに失敗した場合
            const errorData = await invokeResponse.text();
            res.write(`data: ${JSON.stringify({ error: errorData })}\n\n`); // エラーデータをストリームに出力
            res.end();
            return;
        }

        // 応答テキストの処理とストリーム出力
        const decoder = new TextDecoder(); // テキストデコーダーの設定
        for await (const chunk of invokeResponse.body) { // 応答ボディをストリームで処理
            let textChunk = decoder.decode(chunk); // チャンクをデコード
            let parts = textChunk.split(/(?<=\})(?=\{)/); // チャンクを分割
            for (const part of parts) {
                let trimmedPart = part.trim();
                if (!trimmedPart) continue;
                try {
                    const parsed = JSON.parse(trimmedPart); // パースを試みる
                    if (parsed.output && parsed.output.trim() !== "") {
                        res.write(`data: ${parsed.output}\n\n`); // メッセージ部分をストリームに出力
                    }
                } catch (e) {
                    if (trimmedPart && !trimmedPart.startsWith('{"sources"')) {
                        res.write(`data: ${trimmedPart}\n\n`); // 生のメッセージ部分をストリームに出力
                    }
                }
            }
        }
        res.write("data: [DONE]\n\n"); // 完了メッセージをストリームに出力
        res.end();
    } catch (error) {
        res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
        res.end();
    }
});


// バックエンドサーバーのスタート
app.listen(PORT, () => {
    console.log(`Backend running on port ${PORT}`);
});

このように、qlik-embedを用いて実装することで、UIや機能を様々にカスタマイズしながら、Qlik Answersの機能を活用することができます。


 

参考)


Comments


© 2023 by Kathy Schulders. Proudly created with Wix.com 

  • Grey Twitter Icon
bottom of page