3 2 2026-11-02 (C) Questetra, Inc. (MIT License) This item sends a message to a Mistral AI model on Amazon Bedrock and stores the response in the specified data item. この工程は、Amazon Bedrock 上で動作する Mistral AI のモデルにメッセージを送信し、回答をデータ項目に保存します。 https://support.questetra.com/en/addons/aws-bedrock-mistral-ai-chat/ https://support.questetra.com/ja/addons/aws-bedrock-mistral-ai-chat/ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABE9JREFUWEfN l3tMlWUcxz/v4SCXA38gtyPTiXo2m8YdEp3cUzGxOdw0E5tRNqlsMDGlRRE5kFJjqUDLKEJsYuCN xD8qA5oMGeBhXpIdbiuRrORSkOKBt71vHuIcDnRU2un5893v8nl/z/f3e55HwMpLsDS/V1CQo6Pe MR6EKBECAG/A5b5/D9AhQBOI5weVg+VdDQ2DlsT+VwDvBZFqpXJkpyiwFbC3JChwRxAp0OsVOR1X v+uezGdSgLl+YVtB2CuAysLERmYiDICY2qatKZjIf0IAjX9EniiKSQ+T2NRHEIR83aWql83FMgug 8Q8vEUWenYrkhhiCwFHdpeqN4+BMP0zln1tSCaMKSHsuIORP5Z+bxhIRk8ZqYhRAUrvCdkT3sIKz FFoS5sg9hcbQHaMAGt/wD0SBZEsDPYqdIJKra65OkWLIANKQcdCrfjPt85zMXcTGRGCjtGFo6B6f lRwnfMkibv58i2073ubwwRw8Pdx4bUcGcbHRrIlbQfnpc2xcvwZnJxV37w6Rd7iYwuJSU947fyoH XKVhJQNo/MISRIRiU6uQQF96evvRtXXw8YE9eKk9adG147NwPum79/Hum9txUqnI2nuQVSuiUXu6 09f/OwqFgoQtycya6SWH/PGnrnEFExA36bQ1R+4DhH8iQqKp1aJgf57bsJaZXmpEUcTe3o6S0lO8 tHkDpScqeGp5FPrhYWrrGglbEkJ9YzNKpZLVK2PQtXVypvJrjpVXMDAwfioLUKjTVr8gA8zzC2/k 7/k+uubMnkVBbha9fX18efIsq2JjUHu48Xp6Nvuz07n1y69Ms53GjZvduLtNZ4bak4MfFVF2upLl 0WGsfXoli58I5JuqC6SkZZqTTFOrtjrQAHB7zMEiG0vO72Wmcb6mls+/KCd3z1s4ONjz4qs72ZWS xJLQICrOfUvXzW6eT1jHja5unkncxrKopVy+2iLrpCh/H38MDLA5KdUcQE+rtnq6AUA0Z7E7PZWV yyLR6/VcbNDi4e5Kbl4hPgvmy1tTeKSU1vZOMtJSqG/UytU58P47LF0cwjRbW2739PJhwaccP3nW bNO0aquFSQEepdUs8R0LMG4LUrdtoa5BS82Fi5bEMrKRxCkJVaqYnZ0dZacqydp3yDSO0RaME6HU AW9sf0Uuf+mJr2Q1P8haHx/HDLUHi4IDaL58jez9eabu/4hQ42e+DVUqRzJ2JbMseinDwyNcu66j SXuFKz+0UFNbP9pekl3Y4hCCA3zxffwx5s2ZTW9fP8fKzrDiyQhZB1Xf1xkBGLXhRIPI4KGZ603i pnVEhoXi6uKCjY3CbDGkyXdd1yYnlioWvzoWZ2cVRUfLJh9EE41ic1mkaRcaEoC/z0KcVI6yia69 k+strXLLWriMR7HkZNXDSAKw+nEsQVj1QjIquCm8jJrqwdzl9P93Kf0vKvHA13IDhFUfJgYIqz7N xorIao9TCyfbQ5v9BWbg5jDAzldXAAAAAElFTkSuQmCC { // 認証設定を作成し、指定 const keyAuth = httpClient.createAuthSettingToken('Access Key', key); configs.putObject('conf_AccessKey', keyAuth); const secretAuth = httpClient.createAuthSettingToken('Secret Access Key', secret); configs.putObject('conf_SecretKey', secretAuth); configs.put('conf_Region', region); configs.put('conf_Model', model); configs.put('conf_MaxTokens', maxTokens); configs.put('conf_Temperature', temperature); configs.put('conf_SystemPrompt', systemPrompt); configs.put('conf_Message1', message); // 回答を保存するデータ項目を作成し、指定 const answerDef = engine.createDataDefinition('回答', 1, 'q_response', 'STRING_TEXTAREA'); configs.putObject('conf_Answer1', answerDef); engine.setData(answerDef, '事前文字列'); return answerDef; }; /** * 異常系のテスト * @param errorMsg */ const assertError = (errorMsg) => { let failed = false; try { main(); } catch (e) { failed = true; expect(e.message).toEqual(errorMsg); } if (!failed) { fail('No error was thrown.'); } }; const MODEL_LARGE = 'mistral.mistral-large-3-675b-instruct'; const MODEL_14B = 'mistral.ministral-3-14b-instruct'; const MODEL_8B = 'mistral.ministral-3-8b-instruct'; // モデル選択肢にはないが、固定値で指定可能 const MODEL_3B = 'mistral.ministral-3-3b-instruct'; // モデル選択肢にはないが、固定値で指定可能 /** * リージョンコードの形式が不正 - ハイフンを含まない */ test('Region Code is invalid - no hyphens', () => { prepareConfigs('key-12345', 'secret-67890', 'invalidregioncode', MODEL_LARGE, '', '', '', 'Hello'); assertError('Region Code is invalid.'); }); /** * リージョンコードの形式が不正 - ハイフンの間の文字列が長すぎる */ test('Region Code is invalid - too many characters', () => { prepareConfigs('key-12345', 'secret-67890', 'eu-toomanycharacters-1', MODEL_LARGE, '', '', '', 'Hello'); assertError('Region Code is invalid.'); }); /** * モデルの指定が不正 - 不正な文字を含む */ test('Model is invalid - contains an invalid character', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', 'mistral.mistral-テスト', '', '', '', 'Hello'); assertError('Model is invalid. It contains an invalid character.'); }); /** * モデルの指定が不正 - mistral. で始まらない */ test('Model is invalid - does not start with the prefix', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', 'invalid-model-2.0:1', '', '', '', 'Hello'); assertError('Model is invalid. It must start with "mistral.".'); }); /** * トークン数の最大値が正の整数でない - 数字でない文字を含む場合 */ test('Maximum number of tokens must be a positive integer - includes a non-numeric character', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '1.1', '', '', 'Hello'); assertError('Maximum number of tokens for generating output must be a positive integer.'); }); /** * トークン数の最大値が正の整数でない - ゼロ */ test('Maximum number of tokens must be a positive integer - 0', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '0', '', '', 'Hello'); assertError('Maximum number of tokens for generating output must be a positive integer.'); }); /** * 温度が不正 - 数字、小数点でない文字を含む */ test('Invalid temperature - includes a non-numeric character', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '', '-0.1', '', 'Hello'); assertError('Temperature must be a number from 0 to 1.'); }); /** * 温度が不正 - 1 を超える */ test('Invalid temperature - bigger than 1', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '', '1.1', '', 'Hello'); assertError('Temperature must be a number from 0 to 1.'); }); /** * メッセージが空 */ test('Message is empty', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '', '', '', ''); assertError('User Message is empty.'); }); /** * 指定サイズのファイルを作成 * @param name * @param contentType * @param size * @return qfile */ const createQfile = (name, contentType, size) => { let text = ''; if (size >= 4000) { text = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'.repeat(100); // 40 * 100 = 4000 } while (text.length < size) { if (text.length !== 0 && text.length * 2 <= size) { text += text; } else if (text.length + 1000 <= size) { text += 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'.repeat(25); // 40 * 25 = 1000 } else { text += 'a'; } } return engine.createQfile(name, contentType, text); }; /** * 画像を添付 * @param {Array} files */ const attachFiles = (files) => { const filesDef = engine.createDataDefinition('添付画像', 2, 'q_files', 'FILE'); engine.setData(filesDef, files); configs.putObject('conf_Images1', filesDef); }; /** * 添付画像ファイルの数が多すぎる */ test('Too many images attached', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '', '', '', 'Describe the images.'); const images = []; for (let i = 0; i < MAX_IMAGE_NUM + 1; i++) { images.push(createQfile(`画像${i}.png`, 'image/png', 100)); } attachFiles(images); assertError(`Too many images attached. The maximum number is ${MAX_IMAGE_NUM}.`); }); /** * 添付画像ファイルのサイズが大きすぎる */ test('Attached image is too large', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '', '', '', 'Describe the images.'); const image1 = createQfile('画像1.png', 'image/png', 100); const image2 = createQfile('画像2.jpg', 'image/jpeg', MAX_IMAGE_SIZE + 1); attachFiles([image1, image2]); assertError(`Attached image file "画像2.jpg" is too large. The maximum size is ${MAX_IMAGE_SIZE} bytes.`); }); /** * 添付ファイルに許可されていない Content-Type のファイルを含む */ test('Content-Type of an attached file is not allowed', () => { prepareConfigs('key-12345', 'secret-67890', 'us-east-1', MODEL_LARGE, '', '', '', 'Describe the images.'); const image1 = createQfile('画像1.png', 'image/png;filename="画像1.png"', 100); const image2 = createQfile('画像2.jpg', 'image/jpeg', 200); const image3 = createQfile('画像3.gif', 'image/gif', 300); const image4 = createQfile('画像4.webp', 'image/webp', 100); const notAllowed = createQfile('doc.pdf', 'application/pdf', 200); attachFiles([image1, image2, image3, image4, notAllowed]); assertError('Content-Type of the attached file "doc.pdf" is not allowed.'); }); /** * API リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.contentType * @param request.body * @param {String} region * @param {String} model * @param {Number} maxTokens * @param {Number} temperature * @param {String} systemPrompt * @param {String} message * @param {Array} files */ const assertRequest = ({url, method, contentType, body}, region, model, maxTokens, temperature, systemPrompt, message, files = []) => { expect(url).toEqual(`https://bedrock-runtime.${region}.amazonaws.com/model/${model}/converse`); expect(method).toEqual('POST'); expect(contentType).toEqual('application/json'); const bodyObj = JSON.parse(body); if (maxTokens !== null) { expect(bodyObj.inferenceConfig.maxTokens).toEqual(maxTokens); } else { expect(bodyObj.inferenceConfig.maxTokens).toEqual(undefined); } if (temperature !== null) { expect(bodyObj.inferenceConfig.temperature).toEqual(temperature); } else { expect(bodyObj.inferenceConfig.temperature).toEqual(undefined); } if (systemPrompt !== '') { expect(bodyObj.system[0].text).toEqual(systemPrompt); } else { expect(bodyObj.system).toEqual(undefined); } expect(bodyObj.messages[0].role).toEqual('user'); expect(bodyObj.messages[0].content[0].text).toEqual(message); expect(bodyObj.messages[0].content.length).toEqual(files.length + 1); files.forEach((file, i) => { expect(bodyObj.messages[0].content[i + 1].image.format).toEqual(file.getContentType().split(';')[0].split('/')[1]); expect(bodyObj.messages[0].content[i + 1].image.source.bytes).toEqual(base64.encodeToString(fileRepository.readFile(file))); }); }; /** * API のレスポンスボディを作成 * @param {String} answerText * @returns {String} response */ const createResponse = (answerText) => { const responseObj = { stopReason: 'end_turn', output: { message: { content: [ {text: answerText} ] } }, usage: { inputTokens: 1024, outputTokens: 2048 } }; return JSON.stringify(responseObj); }; /** * API リクエストでエラー */ test('Fail to request', () => { const region = 'us-east-1'; const message = 'Hello'; prepareConfigs('key-12345', 'secret-67890', region, MODEL_LARGE, '1280', '', '', message); httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_LARGE, 1280, null, '', message); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); assertError('Failed to converse. status: 400'); }); /** * レスポンスに回答を含まないため、エラー - text フィールドを持つ content がない場合 */ test('No response content - no content with text field', () => { const region = 'us-east-1'; const message = 'Hello'; prepareConfigs('key-12345', 'secret-67890', region, MODEL_LARGE, '', '', '', message); const responseObj = { stopReason: 'max_tokens', output: { message: { content: [ {toolUse: {}} ] } }, usage: { inputTokens: 1024, outputTokens: 2048 } }; httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_LARGE, null, null, '', message); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(responseObj)); }); assertError('No response content generated. Stop Reason: max_tokens'); }); /** * レスポンスに回答を含まないため、エラー - text フィールドの値が空の場合 */ test('No response content - content with text field is empty', () => { const region = 'us-east-1'; const message = 'Hello'; prepareConfigs('key-12345', 'secret-67890', region, MODEL_14B, '', '', '', message); const responseObj = { stopReason: 'max_tokens', output: { message: { content: [ {text: ''} ] } }, usage: { inputTokens: 1024, outputTokens: 2048 } }; httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_14B, null, null, '', message); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(responseObj)); }); assertError('No response content generated. Stop Reason: max_tokens'); }); /** * 成功 - 必須項目のみ指定 * maxTokens, temperature は未指定のため inferenceConfig に含まれない */ test('Success - only with required configs', () => { const region = 'us-east-1'; const message = 'Hello'; const answerDef = prepareConfigs('key-12345', 'secret-67890', region, MODEL_LARGE, '', '', '', message); const answer = 'Hi there!'; httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_LARGE, null, null, '', message); return httpClient.createHttpResponse(200, 'application/json', createResponse(answer)); }); expect(main()).toEqual(undefined); expect(engine.findData(answerDef)).toEqual(answer); }); /** * 成功 - 最大トークン数、温度を指定 */ test('Success - with max tokens and temperature', () => { const region = 'us-east-1'; const message = 'Are you a robot?\n\nHow old are you?'; const answerDef = prepareConfigs('key-12345', 'secret-67890', region, MODEL_8B, '500', '0.5', '', message); const answer = "I'm an AI model. I don't have an age."; httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_8B, 500, 0.5, '', message); return httpClient.createHttpResponse(200, 'application/json', createResponse(answer)); }); expect(main()).toEqual(undefined); expect(engine.findData(answerDef)).toEqual(answer); }); /** * 成功 - 温度、システムプロンプトを指定 * 添付画像のデータ項目は指定しているが、添付ファイルなし */ test('Success - with temperature and system prompt, empty attached files', () => { const region = 'eu-central-2'; const systemPrompt = "You are a translator. Translate the user's input into Japanese."; const message = 'Hi. How are you?'; const answerDef = prepareConfigs('key-12345', 'secret-67890', region, MODEL_3B, '', '0', systemPrompt, message); attachFiles([]); const answer = 'こんにちは。お元気ですか?'; httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_3B, null, 0, systemPrompt, message); return httpClient.createHttpResponse(200, 'application/json', createResponse(answer)); }); expect(main()).toEqual(undefined); expect(engine.findData(answerDef)).toEqual(answer); }); /** * 成功 - 添付画像あり * 回答を保存するデータ項目に、文字型 Markdown を指定 */ test('Success - with attached images - Markdown', () => { const region = 'us-east-1'; const message = 'Describe each image attached.'; prepareConfigs('key-54321', 'secret-99999', region, MODEL_LARGE, '', '', '', message); // 回答を保存するデータ項目に、文字型 Markdown を指定 const answerDef = engine.createDataDefinition('回答', 1, 'q_answer', 'STRING_MARKDOWN'); engine.setData(answerDef, '事前文字列'); configs.putObject('conf_Answer1', answerDef); // 添付画像を指定 const files = []; files.push(createQfile('画像0.png', 'image/png', 100)); files.push(createQfile('画像1.jpg', 'image/jpeg', 200)); files.push(createQfile('画像2.gif', 'image/gif', 300)); files.push(createQfile('画像3.webp', 'image/webp', 100)); files.push(createQfile('最大サイズの画像.jpg', 'image/jpeg;filename="最大サイズの画像.jpg"', MAX_IMAGE_SIZE)); attachFiles(files); const answer = 'The 1st image shows a cat. The 2nd image shows a dog.'; httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_LARGE, null, null, '', message, files); return httpClient.createHttpResponse(200, 'application/json', createResponse(answer)); }); expect(main()).toEqual(undefined); expect(engine.findData(answerDef)).toEqual(answer); }); /** * 成功 - 温度 1.0 を指定 */ test('Success - temperature 1.0', () => { const region = 'us-east-1'; const message = 'Hello'; const answerDef = prepareConfigs('key-12345', 'secret-67890', region, MODEL_LARGE, '', '1', '', message); const answer = 'Hi!'; httpClient.setRequestHandler((request) => { assertRequest(request, region, MODEL_LARGE, null, 1, '', message); return httpClient.createHttpResponse(200, 'application/json', createResponse(answer)); }); expect(main()).toEqual(undefined); expect(engine.findData(answerDef)).toEqual(answer); }); ]]>