
Google Vertex AI: Gemini: チャット
Google Vertex AI: Gemini: Chat
この工程は、Google Vertex AI 上で動作する Gemini のモデルにメッセージを送信し、回答をデータ項目に保存します。
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: サービスアカウント設定]を設定するには:
- Google Cloud コンソールでサービスアカウントを準備します
- ロール[Vertex AI ユーザ]が必要です
- サービスアカウントキーを作成またはアップロードします
- Google Cloud コンソール上でサービスアカウントキーを作成すると、必要な情報を含む JSON ファイルをダウンロードできます
- Questetra BPM Suite で OAuth2 JWT ベアラーフローの設定を作成し、C1 に設定します
- スコープ
https://www.googleapis.com/auth/cloud-platformが必要です - 以降の項目は、下表のとおり設定してください
- スコープ
- Google Cloud コンソールでサービスアカウントを準備します
| Questetra BPM Suite の設定項目 | 対応する Google Cloud サービスアカウントキーの情報 | JSON ファイルの項目名 | 設定必須かどうか |
|---|---|---|---|
| クライアント ID | OAuth2 クライアント ID | client_id | 任意 |
| 秘密鍵 ID | キー ID | private_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);
};