Google Vertex AI: Gemini: チャット

Google Vertex AI: Gemini: チャット

Google Vertex AI: Gemini: Chat

この工程は、Google Vertex AI 上で動作する Gemini のモデルにメッセージを送信し、回答をデータ項目に保存します。

Auto Step icon
Basic Configs
工程名
メモ
Configs for this Auto Step
conf_Auth
C1: サービスアカウント設定 *
conf_Region
C2: リージョンコード *
conf_ProjectId
C3: プロジェクト ID *
conf_Model
C4: モデル *
conf_MaxTokens
C5: 使用するトークン数の上限(空白の場合、モデルのデフォルト値)
conf_Temperature
C6: 温度(0.0 〜 2.0)(空白の場合、モデルのデフォルト値)
conf_StopSequences
C7: 停止シーケンス(1 行に 1 つ、最大 5 つ)
conf_Message1
U1: ユーザメッセージ *#{EL}
conf_Images1
I1: ユーザメッセージに添付するファイル
conf_Answer1
A1: 回答を保存するデータ項目 *

Notes

  • [C1: サービスアカウント設定]を設定するには:
    1. Google Cloud コンソールでサービスアカウントを準備します
    2. Questetra BPM Suite で OAuth2 JWT ベアラーフローの設定を作成し、C1 に設定します
      • スコープ https://www.googleapis.com/auth/cloud-platform が必要です
      • 以降の項目は、下表のとおり設定してください
Questetra BPM Suite の設定項目対応する Google Cloud
サービスアカウントキーの情報
JSON ファイルの項目名設定必須かどうか
クライアント IDOAuth2 クライアント IDclient_id任意
秘密鍵 IDキー IDprivate_key_id必須
秘密鍵秘密鍵private_key必須
カスタム秘密情報 1メールアドレスclient_email必須

Capture

See Also

Script (click to open)
  • 次のスクリプトが記述されている XML ファイルをダウンロードできます
    • google-vertexai-gemini-chat.xml (C) Questetra, Inc. (MIT License)
    • Professional のワークフロー基盤では、ファイル内容を改変しオリジナルのアドオン自動工程として活用できます


const MAX_IMAGE_SIZE = 20971520; // Gemini のインラインファイルの制限。1 ファイルにつき 20 MB まで

function main() {
    ////// == 工程コンフィグ・ワークフローデータの参照 / Config & Data Retrieving ==
    const auth = configs.getObject('conf_Auth');

    const region = retrieveRegion();
    const projectId = configs.get('conf_ProjectId');
    const model = retrieveModel();
    const maxTokens = retrieveMaxTokens();
    const temperature = retrieveTemperature();
    const stopSequences = retrieveStopSequences();
    const message = configs.get('conf_Message1');
    if (message === '') {
        throw new Error('User Message is empty.');
    }
    const inlineImages = retrieveImages();

    ////// == 演算 / Calculating ==
    const answer = invokeModel(
        auth,
        region,
        projectId,
        model,
        maxTokens,
        temperature,
        stopSequences,
        message,
        inlineImages
    );

    ////// == ワークフローデータへの代入 / Data Updating ==
    saveData('conf_Answer1', answer);
}

/**
 * config からリージョンコードを読み出す
 * リージョンコードの形式として不正な場合はエラー
 * @return {String}
 */
function retrieveRegion() {
    const region = configs.get('conf_Region');
    // 今後リージョンが増えることも考えて、文字数には余裕をみている
    const reg = new RegExp('^[a-z]{2,20}-[a-z]{2,20}[1-9]{1,2}$');
    if (!reg.test(region)) {
        throw new Error('Region Code is invalid.');
    }
    return region;
}

/**
 * config からモデル ID を読み出す
 * モデル ID として不正な文字が含まれている場合はエラー
 * @return {String}
 */
function retrieveModel() {
    const model = configs.get('conf_Model');
    const reg = new RegExp('^[a-z0-9.-]+$');
    if (!reg.test(model)) {
        throw new Error('Model includes an invalid character.');
    }
    return model;
}

/**
 * config から最大トークン数を読み出す
 * 未定義の場合、null を返す
 * @returns {Number}
 */
const retrieveMaxTokens = () => {
    const maxTokens = configs.get('conf_MaxTokens');
    if (maxTokens === '') {
        return null;
    }
    const regExp = new RegExp(/^[1-9][0-9]*$/);
    if (!regExp.test(maxTokens)) {
        throw new Error('Maximum number of tokens must be a positive integer.');
    }
    return parseInt(maxTokens, 10);
};

/**
 * config から温度を読み出す
 * 未定義の場合、null を返す
 * @returns {Number}
 */
const retrieveTemperature = () => {
    const temperature = configs.get('conf_Temperature');
    if (temperature === '') {
        return null;
    }
    const regExp = /^([0-1](\.\d+)?|2(\.0+)?)$/;
    if (!regExp.test(temperature)) {
        throw new Error('Temperature must be a number from 0 to 2.');
    }
    return parseFloat(temperature);
};

/**
 * config から停止シーケンスを読み出す
 * @returns {Array<String>}
 */
const retrieveStopSequences = () => {
    const stopSequencesStr = configs.get('conf_StopSequences');
    if (stopSequencesStr === '') {
        return [];
    }
    const stopSequences = stopSequencesStr.split('\n').filter((s) => s !== '');
    if (stopSequences.length > 5) {
        throw new Error('Too many stop sequences. The maximum number is 5.');
    }
    return stopSequences;
};

/**
 * config から画像・動画を読み出す
 * 以下の場合はエラー
 * - 添付ファイルの総数が多すぎる場合
 * - ファイルサイズが大きすぎる場合
 * - 画像でも動画でもないファイルが添付されている場合
 * @returns {Array<Object>} インライン画像・動画オブジェクトの配列
 */
const retrieveImages = () => {
    const imagesDef = configs.getObject('conf_Images1');
    if (imagesDef === null) {
        return [];
    }
    const images = engine.findData(imagesDef);
    if (images === null) {
        return [];
    }
    const inlineImages = [];
    images.forEach((image) => {
        if (image.getLength() > MAX_IMAGE_SIZE) {
            throw new Error(
                `Attached file "${image.getName()}" is too large. Each file must be less than ${MAX_IMAGE_SIZE} bytes.`
            );
        }
        const contentType = image.getContentType();
        if (
            !contentType.startsWith('image/') &&
            !contentType.startsWith('video/') &&
            !contentType.startsWith('audio/') &&
            !contentType.startsWith('application/pdf') &&
            !contentType.startsWith('text/plain')
        ) {
            throw new Error(
                `Attached file "${image.getName()}" is neither an image, video, audio, PDF, nor text.`
            );
        }
        const inlineImage = {
            mimeType: image.getContentType(),
            data: base64.encodeToString(fileRepository.readFile(image)),
        };
        inlineImages.push(inlineImage);
    });
    return inlineImages;
};

const SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
const URL_TOKEN_REQUEST = 'https://oauth2.googleapis.com/token';

/**
 * @param auth HTTP 認証設定
 * @returns {any} アクセストークンを含むオブジェクト
 */
const getAccessToken = (auth) => {
    const privateKeyId = auth.getPrivateKeyId();
    const privateKey = auth.getPrivateKey();
    const serviceAccount = auth.getCustomSecret1();
    const scope = auth.getScope();
    if (scope === null || !scope.split(' ').includes(SCOPE)) {
        throw new Error(`Scope ${SCOPE} must be included in the scope.`);
    }
    if (privateKeyId === '') {
        throw new Error('Private Key ID is required.');
    }
    if (privateKey === '') {
        throw new Error('Private Key is required.');
    }
    if (serviceAccount === '') {
        throw new Error('Service Account must be set to Custom Secret 1.');
    }
    const header = {
        alg: 'RS256',
        typ: 'at+jwt',
        kid: privateKeyId,
    };
    const now = Math.floor(Date.now() / 1000);
    const payload = {
        iss: serviceAccount,
        aud: URL_TOKEN_REQUEST,
        sub: '',
        iat: now,
        exp: now + 3600,
        /**
         * https://developers.google.com/identity/protocols/oauth2/service-account#jwt-auth
         * "without OAuth" の話だが、OAuth でも 1 hour になるようだ。
         * 1 hour より長ければエラー。短ければ、1 hour のトークンが返ってくる。
         */
        scope,
    };
    const keyB = rsa.readKeyFromPkcs8(privateKey);
    const assertion = jwt.build(header, payload, keyB);

    const response = httpClient
        .begin()
        .formParam('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer')
        .formParam('assertion', assertion)
        .post(URL_TOKEN_REQUEST);
    const responseText = response.getResponseAsString();
    if (response.getStatusCode() !== 200) {
        engine.log(responseText);
        throw new Error(`Failed to get Access token. status: ${response.getStatusCode()}`);
    }
    const result = JSON.parse(response.getResponseAsString());
    if (result.access_token === undefined) {
        engine.log(responseText);
        throw new Error(`Failed to get Access token. access token not found.`);
    }
    return result;
};

/**
 * モデルの実行
 * @param region
 * @param projectId
 * @param model
 * @param maxTokens
 * @param temperature
 * @param stopSequences
 * @param message
 * @param inlineImages
 * @returns {String} answer
 */
const invokeModel = (
    auth,
    region,
    projectId,
    model,
    maxTokens,
    temperature,
    stopSequences,
    message,
    inlineImages
) => {
    const URL = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:streamGenerateContent`;

    const generation_config = {};
    if (maxTokens !== null) {
        generation_config.maxOutputTokens = maxTokens;
    }
    if (temperature !== null) {
        generation_config.temperature = temperature;
    }
    generation_config.stopSequences = stopSequences;
    const payload = {
        contents: {
            role: 'user',
            parts: [
                {
                    text: message,
                },
            ],
        },
        safety_settings: {
            category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
            threshold: 'BLOCK_LOW_AND_ABOVE',
        },
        generation_config,
    };
    inlineImages.forEach((inlineImage) => {
        payload.contents.parts.push({
            inlineData: inlineImage,
        });
    });

    const response = httpClient
        .begin()
        .oauth2JwtBearer(auth, () => getAccessToken(auth))
        .body(JSON.stringify(payload), 'application/json')
        .post(URL);

    const status = response.getStatusCode();
    const respTxt = response.getResponseAsString();
    if (status !== 200) {
        engine.log(respTxt);
        throw new Error(`Failed to invoke model. status: ${status}`);
    }
    const json = JSON.parse(respTxt);
    let answers = [];

    for (const { candidates, usageMetadata } of json) {
        if (candidates[0].content === undefined) {
            engine.log('No content in the candidate.');
        } else {
            answers.push(candidates[0].content.parts[0].text);
        }
        const finishReason = candidates[0].finishReason;
        if (finishReason !== undefined) {
            engine.log(`Finish Reason: ${finishReason}`);
        }
        if (usageMetadata !== undefined) {
            if (usageMetadata.promptTokenCount !== undefined) {
                engine.log(`Prompt Token Count: ${usageMetadata.promptTokenCount}`);
            }
            if (usageMetadata.candidatesTokenCount !== undefined) {
                engine.log(`Candidates Token Count: ${usageMetadata.candidatesTokenCount}`);
            }
        }
    }

    if (answers.length === 0 || answers.join('') === '') {
        throw new Error(`No response content generated.`);
    }

    return answers.join('');
};

/**
 * データ項目への保存
 * @param configName
 * @param data
 */
const saveData = (configName, data) => {
    const def = configs.getObject(configName);
    if (def === null) {
        return;
    }
    engine.setData(def, data);
};


上部へスクロール

Questetra Supportをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む