[]
        
(Showing Draft Content)

AIアシスタント

SpreadJS AIアドオンは、スプレッドシートのコンテキスト(セル構造、名前付き範囲、データ内容などの文脈情報)をAIモデルに提供するためのフレームワークを提供する、AIアドオン機能です。これにより、AIモデルはスプレッドシート特有の構造を理解し、より正確で実用的な応答を生成できるようになります。

インストールとセットアップ

AIアドオンの追加

SpreadJSでAI機能を有効にするには、プロジェクトに AIアドオンのスクリプトを含める必要があります。

ヘッダー参照の実装の場合

<script src="gc.spread.sheets.ai.x.x.x.min.js"></script>

モジュール実装の場合

import '@mescius/spread-sheets-ai-addon';

コンテキスト認識

SpreadJSは現在のワークシートのデータ(テーブル構造、名前付き範囲、セル内容など)を自動的に抽出し、AIモデルに適切なコンテキスト(文脈情報)として渡します。これにより、AIはシートの実際の構造に基づいた精度の高い応答を生成できます。

例えば、「売上合計を計算する数式を作成して」と指示した場合:

  • コンテキストなし: =SUM(A1:A10)(AI がデータ範囲を推測します)

  • コンテキストあり: =SUM(table1[sales])(AI が実際のテーブル構造を理解した数式を生成します)

AIモデルとの連携方法

SpreadJSはAIモデルとの連携方法として、複数の接続方法を提供します。セキュリティ要件やシステム構成に応じて最適な方法を選択できます。

1. セキュアなバックエンドプロキシ(本番環境向け推奨方法)

APIキーを公開したくない場合は、サーバー経由でAIへのリクエストやレスポンスを中継します。最もセキュアなアプローチ(API キーをサーバー側に保持します)です。

フロントエンド側の実装内容

const serverCallback = async (requestBody) => {
    requestBody.model = 'your model name';

    let response = await fetch('/api/queryAI', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(requestBody)
    });
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response;
}
workbook.injectAI(serverCallback);

バックエンド側の実装内容(Node.js)

import express from "express";
// import cors from "cors";
import { OpenAI } from "openai";
// Azure OpenAIの場合
// import { AzureOpenAI } from "openai";
import dotenv from "dotenv";
dotenv.config();
const app = express();
const port = process.env.PORT || 3000;
// app.use(cors())
app.use(express.json());
app.use(express.static("public"));
const openai = new OpenAI({
    apiKey: process.env.AI_API_KEY,
    baseURL: process.env.AI_SERVER_URL,
});
// Azure OpenAIの場合
// const endpoint = process.env.AI_SERVER_URL;
// const apiKey = process.env.AI_API_KEY;
// const deployment = "gpt-4.1";
// const apiVersion = "2025-01-01-preview";
// const options = { endpoint, apiKey, deployment, apiVersion }
// const openai = new AzureOpenAI(options);

app.post("/api/queryAI", async (req, res) => {
    try {
        const response = await openai.chat.completions.create(req.body);
        if (req.body.stream) {
            await handleStreamResponse(response, res);
        } else {
            handleNonStreamResponse(response, res);
        }
    } catch (error) {
        handleError(error, res, req.body.stream);
    }
});
async function handleStreamResponse(response, res) {
    res.setHeader("Content-Type", "text/event-stream");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    try {
        for await (const chunk of response) {
            res.write(`data: ${JSON.stringify(chunk)}\n\n`);
        }
        res.write("data: [DONE]\n\n");
        res.end();
    } catch (error) {
        res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
        res.end();
    }
}
function handleNonStreamResponse(response, res) {
    console.log("Handling non-stream response...");
    res.setHeader("Content-Type", "application/json");
    res.setHeader("Cache-Control", "no-store");
    res.status(200).json(response);
}
function handleError(error, res, isStream) {
    console.error("Error:", error);
    if (isStream) {
        res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
        res.end();
    } else {
        res.status(500).json({ error: error.message });
    }
}
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

2. API直接設定(本番環境では非推奨)

APIキーをクライアント側に埋め込む方法です。このオプションは OpenAI 互換エンドポイントでのみ動作します。OpenAIを使用して、手早くAIアシスタント機能を確認したい場合などに便利です。

// SpreadJS ワークブックを初期化する
const workbook = new GC.Spread.Sheets.Workbook('ss');

// AI サービスの資格情報を直接構成する
workbook.injectAI({
    model: 'gpt-4-turbo',  // 使用する AI モデルを指定する
    key: 'sk-your-api-key-here',  // API キー
    basePath: 'https://api.openai.com/v1',  // API エンドポイント
});

3. カスタムクライアントサイドハンドラー

APIキーを公開する場合でも、リクエスト内容の検査やデータのマスキング・クリーニングなどを行いたい場合に利用できます。

const httpCallback = async (requestBody) => {
    requestBody.model = 'your model name';
    // データのクリーニングを行う
    const response = await fetch('your base path', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${'your api key'}`,
            'Content-Type': 'application/json'
        },
        // Azure OpenAIの場合
        // headers: {
        //     'api-key': 'your api key',
        //     'Content-Type': 'application/json'
        // },
        body: JSON.stringify(requestBody)
    });

    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response;
};
var workbook = GC.Spread.Sheets.findControl('ss');
workbook.injectAI(httpCallback);

OpenAI以外との連携方法

SpreadJSのAI機能は OpenAI を標準として設計、開発されており、OpenAI形式のリクエストとレスポンスを処理します。

  • SpreadJSが送信するリクエスト形式

    カスタムコールバックを実装する際、requestBody は以下の OpenAI形式で渡されます。

    {
      "messages": [
          { "role": "system", "content": "You are a helpful assistant..." },
          { "role": "user", "content": "Analyze this data..." }
      ],
      "temperature": 0.7,
      "max_tokens": 4096,
      "stream": true
    }
  • SpreadJSが処理するレスポンス形式

    コールバックは以下の形式のレスポンスを返す必要があります。


    非ストリーミングの場合:

    {
      "choices": [{
          "message": { "content": "..." }
      }]
    }

    ストリーミングの場合(SSE 形式):

    data: {"choices":[{"delta":{"content":"..."}}]}
    data: {"choices":[{"delta":{"content":"..."}}]}
    data: [DONE]

注記:

詳細なコールバックのシグネチャとリクエスト フィールドについては、GC.Spread.Sheets.Workbook.injectAI の API リファレンス(AIRequestCallbackIAIConfig)を参照してください。

他のAIプロバイダー(例: Google Gemini)を使用する場合は、AIモデルによるリクエストやレスポンス形式の差分を、SpreadJSやAIモデルが対応する形式に整合させるために、ユーザー定義のカスタムアダプターが必要です。

OpenAI以外のAIモデルとの連携方法として、Google Gemini と連携する方法を以下に説明します。

Google Gemini のリクエスト・レスポンス形式は OpenAI と異なるため、サーバー側でフォーマット変換を行う必要があります。APIキーはサーバーの .env ファイルで管理し、フロントエンドに公開しないでください。

サーバー側のアダプターは次の 3 つを行います。

  • SpreadJSからの OpenAI形式リクエストを Gemini リクエストにマップします

  • Gemini API を呼び出します(非ストリーミングまたはストリーミング)

  • Gemini のレスポンスを OpenAI 互換のレスポンス(または SSE チャンク)にマップします

プロジェクト構成

project/
├── server.js
├── package.json
├── .env
└── public/
    ├── index.html
    └── app.js

依存関係の設定

package.json:

{
    "name": "spreadjs-ai-gemini",
    "version": "1.0.0",
    "type": "module",
    "scripts": {
        "start": "node server.js"
    },
    "dependencies": {
        "@google/genai": "^1.41.0",
        "dotenv": "^16.4.7",
        "express": "^4.21.2"
    }
}

.env:

AI_API_KEY=your-gemini-api-key
AI_MODEL=gemini-2.5-flash
PORT=3000

フロントエンド側の実装内容

const serverCallback = async (requestBody) => {
    const response = await fetch('/api/queryAI', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(requestBody),
    });
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response;
};
workbook.injectAI(serverCallback);

バックエンド側の実装内容(server.js)

注意: Gemini の systemInstruction パラメーターはテーブルデータなどの構造化データを正しく処理できません。システムメッセージは最初のユーザーメッセージに統合してください。

import express from "express";
// import cors from "cors";
import { GoogleGenAI } from "@google/genai";
import dotenv from "dotenv";
dotenv.config();

const app = express();
const port = process.env.PORT || 3000;
// app.use(cors())
app.use(express.json());
app.use(express.static("public"));

const ai = new GoogleGenAI({ apiKey: process.env.AI_API_KEY });
const defaultModel = process.env.AI_MODEL || "gemini-2.5-flash";

function convertMessages(messages) {
    const systemParts = [];
    const contents = [];

    for (const msg of messages) {
        if (msg.role === "system") {
            systemParts.push(msg.content);
        } else {
            contents.push({
                role: msg.role === "assistant" ? "model" : "user",
                parts: [{ text: msg.content }],
            });
        }
    }

    if (systemParts.length > 0 && contents.length > 0 && contents[0].role === "user") {
        contents[0].parts[0].text = systemParts.join("\n\n") + "\n\n" + contents[0].parts[0].text;
    }

    return { contents };
}

app.post("/api/queryAI", async (req, res) => {
    try {
        const body = req.body;
        const model = body.model || defaultModel;
        const { contents } = convertMessages(body.messages || []);

        const params = {
            model,
            contents,
            config: {
                temperature: body.temperature ?? 0.7,
                maxOutputTokens: body.max_tokens ?? 4096,
            },
        };

        if (body.stream) {
            await handleStream(params, res);
        } else {
            await handleNonStream(params, model, res);
        }
    } catch (error) {
        handleError(error, res, req.body.stream);
    }
});

async function handleStream(params, res) {
    res.setHeader("Content-Type", "text/event-stream");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    try {
        const response = await ai.models.generateContentStream(params);
        const created = Math.floor(Date.now() / 1000);
        const requestId = "gemini-" + Date.now();

        for await (const chunk of response) {
            const text = chunk.text || "";
            if (!text) continue;

            const openaiChunk = {
                id: requestId,
                object: "chat.completion.chunk",
                created,
                model: params.model,
                choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
            };
            res.write(`data: ${JSON.stringify(openaiChunk)}\n\n`);
        }

        res.write("data: [DONE]\n\n");
        res.end();
    } catch (error) {
        res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
        res.end();
    }
}

async function handleNonStream(params, model, res) {
    const response = await ai.models.generateContent(params);
    const text = response.text || "";

    res.setHeader("Content-Type", "application/json");
    res.setHeader("Cache-Control", "no-store");
    res.status(200).json({
        id: "gemini-" + Date.now(),
        object: "chat.completion",
        created: Math.floor(Date.now() / 1000),
        model,
        choices: [{
            index: 0,
            message: { role: "assistant", content: text },
            finish_reason: "stop",
        }],
        usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
    });
}

function handleError(error, res, isStream) {
    if (isStream) {
        res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
        res.end();
    } else {
        res.status(500).json({ error: error.message });
    }
}

app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
});

言語ローカライズ

SpreadJSは、ワークブックで設定されているカルチャを自動的に取得し、AIの応答言語に反映します。

let culture = GC.Spread.Common.CultureManager.culture(); // ja-jp
let language = GC.Spread.Common.CultureManager.getCultureInfo(culture).displayName // 'Japanese (Japan)'

// in prompts
// 'please return the answer by this language: ' + language;

セキュリティのベスト プラクティス

  1. ​データ保護​​:

  • 機密情報や機微なスプレッドシート データは常にサニタイズします

  • 必要に応じて、コールバックでのフィールドのマスキングを行います

  1. ​認証情報の管理​​:

  • APIキーをクライアントに直接埋め込まない

  • 本番環境ではサーバー プロキシを使用します

  1. ​検証​​:

  • AIが生成した数式/コンテンツは必ず検証します

  • 出力内容のサニタイズを実装します

AI生成機能に関する注意

1. 本機能の概要および外部AIサービスの利用

  • 本機能は、サーバーを介して外部のAI関連サービス、クラウドサービス、またはこれらに付随するAPI(以下、総称して「外部AIサービス」といいます)を利用し、お客様(外部AIサービスの利用者を含みます。以下同じ)の入力内容に基づいて、テキストや画像をテキスト、画像および分析結果等を生成する補助機能です。

  • 外部AIサービスにおけるデータの取扱いについては、各提供者が定める規約が適用されます。

2. 利用上の注意

  • AIの特性上、本機能により生成される内容(文章、回答、分析結果その他一切の内容を含むものとし、以下「生成結果」といいます)には、不正確な情報や不完全な表現、または誤解を招く内容が含まれる可能性があります。お客様は、生成結果をあくまで参考情報として取り扱い、当該結果のみに基づいて意思決定、判断、または行動を行わないものとします。

  • 生成結果を実際の業務や意思決定に利用する場合は、必ずお客様ご自身で内容をご確認・検証してください。

  • 本機能は、お客様の業務を補助する目的で提供されるものであり、業務上の最終的な判断およびその生成結果については、お客様ご自身の責任において行うものとします。

3. AI生成結果に関する免責事項

  • 本機能は、お客様が契約する第三者が提供する外部AIサービスを利用しており、当該サービスの仕様変更、利用制限、または技術的な問題等に起因して、生成結果の品質や提供状況に影響が生じる場合があります。

  • 本機能の生成結果は、人工知能による自動生成であり、その確実性、完全性、真実性、正確性、最新性、有用性、特定目的への適合性または合法性について、当社は一切保証しません。また、当該生成結果の利用に起因してお客様または第三者に生じた損害について、当社は一切の責任を負わないものとします。

4. 利用者の責任および法令遵守

  • お客様は、本機能に入力する情報について、適法に取得されたものであり、本機能に入力・利用するために必要な正当な権限を有していることを前提としてご利用ください。

  • 当社は、生成結果が、当社または第三者の知的財産権(著作権、特許権、商標権等を含みます)その他一切の権利を侵害しないことを保証しません。

  • 本機能の利用にあたっては、関係法令および第三者の権利を遵守してください。