(C) Questetra, Inc. (MIT License) 2025-12-19 3 2 https://support.questetra.com/bpmn-icons/service-task-openai-chatgpt-chat/ https://support.questetra.com/ja/bpmn-icons/service-task-openai-chatgpt-chat/ This item sends a message to OpenAI ChatGPT and stores the response in the specified data item. この工程は、OpenAI ChatGPT へメッセージを送信し、回答をデータ項目に保存します。 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 AAAAAElFTkSuQmCC { const authSetting = httpClient.createAuthSettingToken('ChatGPT', authKey); configs.putObject('conf_Auth', authSetting); configs.put('conf_OrganizationId', organizationId); configs.put('conf_Model', model); configs.put('conf_MaxTokens', maxTokens); configs.put('conf_ReasoningEffort', reasoningEffort); configs.put('conf_Verbosity', verbosity); configs.put('conf_Temperature', temperature); configs.put('conf_StopSequences', stopSequences); configs.put('conf_GPT_Role', gptRole); configs.put('conf_Message1', message); const answerDef = engine.createDataDefinition('回答', 1, 'q_answer', 'STRING_TEXTAREA'); engine.setData(answerDef, '事前文字列'); configs.putObject('conf_Answer1', 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.'); } }; /** * メッセージが空 */ test('Message is empty', () => { prepareConfigs('key', '', 'gpt-4', '', '', '', '', '', '', ''); assertError('User Message is empty.'); }); /** * トークン数の最大値が正の整数でない - 数字でない文字を含む場合 */ test('Maximum number of tokens must be a positive integer - includes a non-numeric character', () => { prepareConfigs('key', '', 'gpt-4', '1.1', '', '', '', '', '', 'こんにちは'); assertError('Maximum number of tokens must be a positive integer.'); }); /** * トークン数の最大値が正の整数でない - ゼロ */ test('Maximum number of tokens must be a positive integer - 0', () => { prepareConfigs('key', '', 'gpt-4', '0', '', '', '', '', '', 'こんにちは'); assertError('Maximum number of tokens must be a positive integer.'); }); /** * 温度が不正 - 数字、小数点でない文字を含む */ test('Invalid temperature - includes a non-numeric character', () => { prepareConfigs('key', '', 'gpt-4', '', '', '', '-1', '', '', 'こんにちは'); assertError('Temperature must be a number from 0 to 2.'); }); /** * 温度が不正 - 2 を超える */ test('Invalid temperature - bigger than 2', () => { prepareConfigs('key', '', 'gpt-4', '', '', '', '2.1', '', '', 'こんにちは'); assertError('Temperature must be a number from 0 to 2.'); }); /** * 停止シークエンスの数が多すぎる */ test('Too many stop sequences', () => { let stopSequences = '\n'; for (let i = 0; i < MAX_STOP_SEQUENCE_NUM + 1; i++) { stopSequences += `stop${i}\n`; } prepareConfigs('key', '', 'gpt-4', '', '', '', '', stopSequences, '', 'こんにちは'); assertError(`Too many stop sequences. The maximum number is ${MAX_STOP_SEQUENCE_NUM}.`); }); /** * 指定サイズのファイルを作成 * @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); }; /** * 添付画像のサイズが大きすぎる */ test('Image file is too large', () => { prepareConfigs('key', '', 'gpt-4-vision-preview', '150', '', '', '', '', '', 'こんにちは'); // 添付画像を指定 const image1 = createQfile('画像1.png', 'image/png', 100); const image2 = createQfile('画像2.jpg', 'image/jpeg', MAX_IMAGE_SIZE + 1); const imagesDef = engine.createDataDefinition('添付画像', 2, 'q_images', 'FILE'); engine.setData(imagesDef, [image1, image2]); configs.putObject('conf_Images1', imagesDef); assertError(`Attached image "画像2.jpg" is too large. Each file must be less than ${MAX_IMAGE_SIZE} bytes.`); }); /** * 添付画像の Content-Type がサポート外 */ test('Content-Type of image file is not supported', () => { prepareConfigs('key', '', 'gpt-4-vision-preview', '150', '', '', '', '', '', 'こんにちは'); // 添付画像を指定 const image1 = createQfile('画像1.png', 'image/png', 100); const image2 = createQfile('画像2.jpg', 'image/jpeg', 100); const image3 = createQfile('画像3.bmp', 'image/bmp', 100); // サポート外 const imagesDef = engine.createDataDefinition('添付画像', 2, 'q_images', 'FILE'); engine.setData(imagesDef, [image1, image2, image3]); configs.putObject('conf_Images1', imagesDef); assertError(`Content-Type of "画像3.bmp" is not supported. Supported types are: ${AVAILABLE_IMAGE_TYPES.join(', ')}.`); }); /** * API リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.contentType * @param request.headers * @param request.body * @param {String} authKey * @param {String} organizationId * @param {String} model * @param {Number} maxTokens * @param {String} reasoningEffort * @param {String} verbosity * @param {Number} temperature * @param {Array} stopSequences * @param {String} gptRole * @param {String} message * @param {Array} imageContentTypes * @param {String} imageDetail */ const assertRequest = ({ url, method, contentType, headers, body }, authKey, organizationId, model, maxTokens, reasoningEffort, verbosity, temperature, stopSequences, gptRole, message, imageContentTypes, imageDetail = undefined) => { expect(url).toEqual('https://api.openai.com/v1/chat/completions'); expect(method).toEqual('POST'); expect(contentType).toEqual('application/json'); expect(headers['Authorization']).toEqual(`Bearer ${authKey}`); if (organizationId !== '') { expect(headers['OpenAI-Organization']).toEqual(organizationId); } const bodyObj = JSON.parse(body); expect(bodyObj.safety_identifier).toEqual(`m${processInstance.getProcessModelInfoId().toString()}`); expect(bodyObj.model).toEqual(model); expect(bodyObj.n).toEqual(1); expect(bodyObj.max_completion_tokens).toEqual(maxTokens); expect(bodyObj.reasoning_effort).toEqual(reasoningEffort); expect(bodyObj.verbosity).toEqual(verbosity); expect(bodyObj.temperature).toEqual(temperature); if (gptRole === '') { expect(bodyObj.messages.length).toEqual(1); } else { expect(bodyObj.messages.length).toEqual(2); expect(bodyObj.messages[0]).toEqual({ role: 'system', content: gptRole }); } expect(bodyObj.stop).toEqual(stopSequences); const userMessage = bodyObj.messages[bodyObj.messages.length - 1]; expect(userMessage.role).toEqual('user'); expect(userMessage.content[0]).toEqual({ type: 'text', text: message }); imageContentTypes.forEach((contentType, index) => { assertAttachment(userMessage, index, contentType, imageDetail); }); }; /** * API リクエストの body 内のユーザメッセージオブジェクトに添付ファイルが含まれることをテスト * @param userMessage * @param index * @param fileName * @param contentType * @param imageDetail */ const assertAttachment = (userMessage, index, contentType, imageDetail) => { const imageObj = userMessage.content[index + 1]; // 0 番目は text なので、1 加算 expect(imageObj.type).toEqual('image_url'); const url = imageObj.image_url.url; expect(url.split(',')[0]).toEqual(`data:${contentType};base64`); // ファイルのバイナリデータが base64 エンコードされた部分の確認は省略 expect(imageObj.image_url.detail).toEqual(imageDetail); }; /** * API リクエストでエラー */ test('Fail to request', () => { prepareConfigs('key2', '', 'gpt-3.5-turbo', '', '', '', '', '', '', 'こんにちは'); httpClient.setRequestHandler((request) => { assertRequest(request, 'key2', '', 'gpt-3.5-turbo', MAX_TOKENS_DEFAULT, undefined, undefined, 1, undefined, '', 'こんにちは', []); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); assertError('Failed to request. status: 400'); }); /** * API リクエストが 200 レスポンスを返すが、トークン切れで回答が空の場合 */ test('No response content generated', () => { prepareConfigs('key2', '', 'chatgpt-4o-latest', '100', '', '', '', '', '', 'こんにちは'); const responseObj = { "id": "chatcmpl-123", "object": "chat.completion", "created": 1677652288, "choices": [{ "index": 0, "message": { "role": "assistant", "content": "", }, "finish_reason": "length" }], "usage": { "prompt_tokens": 25, "completion_tokens": 100, "completion_tokens_details": { "reasoning_tokens": 100 }, "total_tokens": 125 } }; httpClient.setRequestHandler((request) => { assertRequest(request, 'key2', '', 'chatgpt-4o-latest', 100, undefined, undefined, 1, undefined, '', 'こんにちは', []); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(responseObj)); }); assertError('No response content generated. Finish Reason: length'); }); /** * API のレスポンスボディを作成 * @param {String} answerText * @returns {String} response */ const createResponse = (answerText) => { const responseObj = { "id": "chatcmpl-123", "object": "chat.completion", "created": 1677652288, "choices": [{ "index": 0, "message": { "role": "assistant", "content": answerText, }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 9, "completion_tokens": 12, "completion_tokens_details": { "reasoning_tokens": 5 }, "total_tokens": 26 } } ; return JSON.stringify(responseObj); }; /** * 成功 */ test('Success to request', () => { const answerDef = prepareConfigs('key3', '', 'gpt-4.1', '', '', '', '', '', 'Robot', 'Hello World'); // 添付画像指定なし httpClient.setRequestHandler(request => { assertRequest(request, 'key3', '', 'gpt-4.1', MAX_TOKENS_DEFAULT, undefined, undefined, 1, undefined, 'Robot', 'Hello World', []); return httpClient.createHttpResponse(200, 'application/json', createResponse("\n\nHello there, how may I assist you today?")); }); main(); // データ項目の値をチェック expect(engine.findData(answerDef)).toEqual("\n\nHello there, how may I assist you today?"); }); /** * 成功 - 組織 ID を指定 * reasoningEffort を指定 * verbosity は指定なし * 回答を保存するデータ項目に、文字型 Markdown を指定 */ test('Success to request - with organization ID', () => { prepareConfigs('key4', 'org-1', 'gpt-5', '', 'minimal', '', '', '', '', 'Hi!'); // 回答を保存するデータ項目に、文字型 Markdown を指定 const answerDef = engine.createDataDefinition('回答', 1, 'q_answer', 'STRING_MARKDOWN'); engine.setData(answerDef, '事前文字列'); configs.putObject('conf_Answer1', answerDef); // 添付画像はデータ項目の指定はあるが、ファイル添付なし const imagesDef = engine.createDataDefinition('添付画像', 2, 'q_images', 'FILE'); configs.putObject('conf_Images1', imagesDef); httpClient.setRequestHandler(request => { assertRequest(request, 'key4', 'org-1', 'gpt-5', MAX_TOKENS_DEFAULT, 'minimal', undefined, 1, undefined, '', 'Hi!', []); return httpClient.createHttpResponse(200, 'application/json', createResponse("Hi! How can I help you?")); }); main(); // データ項目の値をチェック expect(engine.findData(answerDef)).toEqual("Hi! How can I help you?"); }); /** * 成功 - 最大トークン数を指定 * reasoning_effort は指定なし * verbosity を指定 */ test('Success to request - with max_tokens', () => { const answerDef = prepareConfigs('key5', '', 'gpt-5-mini', '100', '', 'high', '', '', 'Assistant', 'Hi! How\'s the weather in Kyoto today?'); httpClient.setRequestHandler(request => { assertRequest(request, 'key5', '', 'gpt-5-mini', 100, undefined, 'high', 1, undefined, 'Assistant', 'Hi! How\'s the weather in Kyoto today?', []); return httpClient.createHttpResponse(200, 'application/json', createResponse("Hi! It's sunny today.")); }); main(); // データ項目の値をチェック expect(engine.findData(answerDef)).toEqual("Hi! It's sunny today."); }); /** * 成功 - 温度を指定 * 回答を保存するデータ項目に、文字型 Markdown を指定 */ test('Success to request - with temperature', () => { prepareConfigs('key5', '', 'gpt-4.1-mini', '', '', '', '1', '', 'Assistant', 'Hi! How\'s the weather in Kyoto today?'); // 回答を保存するデータ項目に、文字型 Markdown を指定 const answerDef = engine.createDataDefinition('回答', 1, 'q_answer', 'STRING_MARKDOWN'); engine.setData(answerDef, '事前文字列'); configs.putObject('conf_Answer1', answerDef); httpClient.setRequestHandler(request => { assertRequest(request, 'key5', '', 'gpt-4.1-mini', MAX_TOKENS_DEFAULT, undefined, undefined,1, undefined, 'Assistant', 'Hi! How\'s the weather in Kyoto today?', []); return httpClient.createHttpResponse(200, 'application/json', createResponse("Hi! It's sunny today.")); }); main(); // データ項目の値をチェック expect(engine.findData(answerDef)).toEqual("Hi! It's sunny today."); }); /** * 成功 - 停止シーケンスを指定 */ test('Success to request - with stop', () => { const stopSequences = []; for (let i = 0; i < MAX_STOP_SEQUENCE_NUM; i++) { stopSequences.push(`stop${i}`); } const stopSequencesStr = stopSequences.join('\n') + '\n'; const answerDef = prepareConfigs('key5', '', 'gpt-4.1-nano', '', '', '', '', stopSequencesStr, 'Assistant', 'Hi! How\'s the weather in Kyoto today?'); httpClient.setRequestHandler(request => { assertRequest(request, 'key5', '', 'gpt-4.1-nano', MAX_TOKENS_DEFAULT, undefined, undefined, 1, stopSequences, 'Assistant', 'Hi! How\'s the weather in Kyoto today?', []); return httpClient.createHttpResponse(200, 'application/json', createResponse("Hi! It's sunny today.")); }); main(); // データ項目の値をチェック expect(engine.findData(answerDef)).toEqual("Hi! It's sunny today."); }); /** * 成功 - 画像を添付。detail 指定なし reasoningEffort verbosity を指定 */ test('Success to request - with attached images, no detail param', () => { const answerDef = prepareConfigs('key5', '', 'o3-mini', '', 'high', 'low', '', '', '', 'Describe the images attached.'); configs.put('conf_Images1Detail', undefined); // 添付画像を指定 const image1 = createQfile('画像1.png', 'image/png', 100); const image2 = createQfile('画像2.jpg', 'image/jpeg', MAX_IMAGE_SIZE); const image3 = createQfile('画像3.gif', 'image/gif', 100); const image4 = createQfile('画像4.webp', 'image/webp', 100); const imagesDef = engine.createDataDefinition('添付画像', 2, 'q_images', 'FILE'); engine.setData(imagesDef, [image1, image2, image3, image4]); configs.putObject('conf_Images1', imagesDef); const imageContentTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']; httpClient.setRequestHandler(request => { assertRequest(request, 'key5', '', 'o3-mini', MAX_TOKENS_DEFAULT, 'high', 'low', 1, undefined, '', 'Describe the images attached.', imageContentTypes, undefined); return httpClient.createHttpResponse(200, 'application/json', createResponse("The first one is a blue circle and the second one is a red square.")); }); main(); // データ項目の値をチェック expect(engine.findData(answerDef)).toEqual("The first one is a blue circle and the second one is a red square."); }); /** * 成功 - 画像を添付。detail を指定 reasoningEffort verbosity を指定 */ test('Success to request - with attached images, detail param set', () => { const answerDef = prepareConfigs('key5', '', 'gpt-5-nano', '', 'medium', 'medium', '', '', '', 'Describe the images attached.'); configs.put('conf_Images1Detail', 'low'); // 添付画像を指定 const image1 = createQfile('画像1.png', 'image/png', 100); const image2 = createQfile('画像2.jpg', 'image/jpeg', MAX_IMAGE_SIZE); const imagesDef = engine.createDataDefinition('添付画像', 2, 'q_images', 'FILE'); engine.setData(imagesDef, [image1, image2]); configs.putObject('conf_Images1', imagesDef); const imageContentTypes = ['image/png', 'image/jpeg']; httpClient.setRequestHandler(request => { assertRequest(request, 'key5', '', 'gpt-5-nano', MAX_TOKENS_DEFAULT, 'medium', 'medium', 1, undefined, '', 'Describe the images attached.', imageContentTypes, 'low'); return httpClient.createHttpResponse(200, 'application/json', createResponse("The first one is a blue circle and the second one is a red square.")); }); main(); // データ項目の値をチェック expect(engine.findData(answerDef)).toEqual("The first one is a blue circle and the second one is a red square."); }); ]]>