生成AI（GitHub Copilot）にODC向けの非同期JavaScriptを書かせてみる

生成AI＋ODCの周辺開発の可能性を探るため、いくつかのパターンでプロンプトを書いて試してみる。
今回は、非同期JavaScript。

OutSystemsでClient Action内のJavaScript要素で動作する非同期処理を書くには条件がある。その条件を満たしたコードを書いてくれるか確認したい。

# 結論
In-context Learningを使えば可能ではある。
一般の（OutSystems＋JavaScriptの知識がさほど深くない）開発者に使ってもらうにはちょっと厳しいかもしれない。

# 環境情報
ODC Studio(Version 1.5.13)
VSCode (Version 1.97.1)
GitHub Copilot (VSCodeのExtension。1.277.0）

# 関連リソース
OutSystemsの非同期JavaScriptについては以下を参照。
（ちょっと古く、対象バージョンもOutSystems11の記述だが、基本的にODCでも同じ）

https://qiita.com/jyunji_watanabe/items/a26c2b9356c249e79193

OutSystems 11の時の公式ドキュメント（ODC版はこれを書いている時点では未公開）。
[Defining Asynchronous JavaScript Code](https://success.outsystems.com/documentation/11/integration_with_external_systems/javascript/defining_asynchronous_javascript_code/)

# テスト対象のセットアップ
時間のかかるExpose REST APIのMethodを用意する。
そのMethodをJavaScriptから呼び出し、非同期に結果を受け取る。
また、エラーを処理するコードを書いてくれるかも見たいので、入力によってはエラーが発生するようにする。

## 呼び出し対象のExpose REST API
テストだけが目的なので、人工的で実用を想定しない仕様。

Input Parameter: Integer型1つ
Output Parameter: Integer型1つ
処理：Input Parameterが0-10であれば100加算してOutput Parameterに設定。それ以外ならExceptionをRaiseする。終了前に10秒スリープさせる

![スクリーンショット 2025-03-23 10.20.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/41816f43-5955-4420-ba8b-c8f198ff3385.png)

## テスト用Screen
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/b4ed1c8c-9262-4c18-b17d-10f72e391923.png)

Inputを配置して、Integer型を入力できるようにする。
②のButtonクリックでJavaScript要素を配置したClient Actionを起動。
Client Actionは先頭と最後でそれぞれ実行時間を記録する（③）。これによって、Client Actionが非同期に動いているのかがわかる。

## JavaScript配置先Client Actionの実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/362ced23-4a8f-4e9d-be1c-85ddb59cbb03.png)

JavaScriptを実行し、結果の数値をExpressionに表示する（④）。エラーがあれば、Messageでポップアップ表示。

JavaScript要素は以下のParameterを持つ（InputとResultはInteger、ErrorMessageはText型）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/b9505ded-5363-4bd8-8a17-3acda2575974.png)

# 最初のトライ：特に工夫せず希望仕様をプロンプトに入力
上記Client Action内のJavaScript要素に配置するコードをGitHub Copilotに作ってもらう。
ODCにおけるJavaScript開発の知識をこちらからは提供せず、CopilotがOutSystemsの仕様をどの程度知っているのか確認してみた。

## プロンプト
``` 
OutSystems Developer Cloudにおいて、以下の仕様を満たすJavaScriptコードを作成してください。

- JavaScript要素内に記述する
- JavaScript要素は、Input Parameter Input (Integer型）を持つ
- JavaScript要素は、Output Parameter Result (Integer型）とErrorMessage（Text型）を持つ
- 非同期でREST API GenerativeAITest_JSを呼ぶ（相対パス：/JunjiWatanabeTest/rest/AITest/GenerativeAITest_JS）
- GenerativeAITest_JSのクエリストリングInputNumにInputを渡す
- GenerativeAITest_JSは、正常時には計算結果をInteger型として返すが、エラー時にはExceptionをRaiseする。その際にはその例外メッセージを取得し、ErrorMessageに設定しなければならない
```

## 出力コード
``` js
// Input Parameter: Input (Integer)
// Output Parameters: Result (Integer), ErrorMessage (Text)

// Clear the output parameters
$public.Result = null;
$public.ErrorMessage = null;

// Define an asynchronous function to call the REST API
async function callRestAPI() {
    try {
        // Construct the URL with the query string parameter
        const apiUrl = `/JunjiWatanabeTest/rest/AITest/GenerativeAITest_JS?InputNum=${encodeURIComponent($public.Input)}`;

        // Make the API call using fetch
        const response = await fetch(apiUrl, {
            method: 'GET',
        });

        // Check if the response is successful
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }

        // Parse the response as JSON
        const data = await response.json();

        // Set the Result output parameter with the response data
        $public.Result = data;
    } catch (error) {
        // If an exception occurs, set the ErrorMessage output parameter
        $public.ErrorMessage = error.message || "An unknown error occurred.";
    }
}

// Execute the asynchronous function
callRestAPI();
```
# 最初のトライで出力されたコードの問題点と改善のための情報源
## Parameterは$publicではなく、$parametersの下にある
これはシンプルな間違い。OutSystemsのJavaScript要素内では、\\$で始まるオブジェクトを経由してOutSystems要素にアクセスすることは知っているようだ。しかし、Input/Output Parameterにアクセスするときは、\\$publicではなく、\\$parametersオブジェクトを利用する。

つまり、
× \\$public.Result
○ \\$parameters.Resut

$publicの下にあるのは、Built-in Functionや (OutSystems提供の）JavaScript API。

[JavaScriptを使用してモバイルおよびリアクティブアプリを拡張する - OutSystems 11 ドキュメンテーション](https://success.outsystems.com/ja-jp/documentation/11/integration_with_external_systems/javascript/extend_your_mobile_and_reactive_apps_using_javascript/#javascript-1)

より以下の部分を参照。

> \\$parameters – 現在のJavaScript要素に定義された入出力パラメータが含まれています。
(中略)
$public – すべての公開JavaScript APIのクラスとモジュールをインスタンス化するオブジェクトが含まれています。すべての利用可能なモジュールと関数については、JavaScript APIリファレンスをご覧ください。

## Parameterをnullで初期化しない
以下のように、Parameterをnullで初期化すると、エラーになるようだ。

``` js
$public.Result = null;
$public.ErrorMessage = null;
```

このとき発生するエラー例。

> InvalidJavascriptType: Cannot assign a JavaScript object with value null to an output parameter of type Integer Check the JavaScript node 'JavaScript1' of the action 'RunJavaScript'.

そもそも、JavaScript要素のInput/Output ParameterはOutSystemsの型初期値で初期化されているようなので初期化不要。

## async/awaitキーワードでなく、OutSystemsが提供する\$resolve/$rejectを使う 
他の問題点を修正した上でテスト実行してみると、開始ログと終了ログが同じ時間で出力され、通信終了後もREST APIが返してくる結果を表示しない。

これは、OutSystemsが提供する\\$resolve()や\$reject()を使っていない為に、Client Actionが非同期実行されず、Expose REST APIが返ってくるのを待たないためと思われる。

関連リソースにあるリンク先の情報を参照。

# In-context learningを使ってプロンプトを改善
プロンプト中に参照すべき情報を記述しておくことによって、生成AIはその情報を踏まえた出力をしてくれるらしい。これをIn-context learningと呼ぶそうだ。

以下、このIn-context learningを使って、問題点に対応する情報をプロンプトに含めてみた。
「仕様」の部分だけを、要件ごとに書き換えることで使い回しをしたい、と言う意志を込めたプロンプト（実際にそうするにはもう少し磨いたほうが良さそうだが）。

## プロンプト
「即時実行関数式 (IIFE) を使わないこと」この部分は、試行錯誤中に追加した。OutSystemsのJavaScript要素は独自のスコープを持っていそうなので、IIFEを使わないように指示を追加した。

``` 
OutSystems Developer Cloudにおいて、以下の仕様と条件を満たすJavaScriptコードを作成してください。その際、「ドキュメント」に提供する情報を踏まえてください

# 仕様
- JavaScript要素は、Input Parameter Input (Integer型）を持つ
- JavaScript要素は、Output Parameter Result (Integer型）とErrorMessage（Text型）を持つ
- 非同期でREST API GenerativeAITest_JSを呼ぶ（相対パス：/JunjiWatanabeTest/rest/AITest/GenerativeAITest_JS）
- GenerativeAITest_JSのクエリストリングInputNumにInputを渡す
- GenerativeAITest_JSは、正常時には計算結果をInteger型として返すが、エラー時にはExceptionをRaiseする。その際にはその例外メッセージを取得し、ErrorMessageに設定しなければならない

# 条件
- JavaScript要素内に記述する
- 即時実行関数式 (IIFE) を使わないこと

# ドキュメント
## 各JavaScript要素で使用できるオブジェクト
OutSystems内の要素をJavaScriptで参照する場合は、以下のオブジェクトを経由します。
- $parameters – 現在のJavaScript要素に定義された入出力パラメータが含まれています。
- $actions – 現在のJavaScript要素のスコープから呼び出し可能なクライアントアクションへの参照が含まれています（現在のモジュールで定義されるか、システムアクションなどのように別のモジュールから参照されるクライアントアクション）
- $roles – 現在のモジュールでカスタム定義されたすべてのロールが含まれています。
- $public – すべての公開JavaScript APIのクラスとモジュールをインスタンス化するオブジェクトが含まれています。すべての利用可能なモジュールと関数については、JavaScript APIリファレンスをご覧ください。
## Input Parameter/Output Parameterの変数は初期化不要
OutSystemsがプラットフォームとしてその部分を担保するため。
また、制御を返す際も、Output Parameterに空の値を設定する、と言う意味の編集は不要。
## 非同期処理の実装方法
OutSystemsのJavaScript内での非同期処理にはasync/awaitキーワードではなく、OutSystemsが提供する$resolve/$rejectを使う。
JavaScript要素内で$resolve()または$reject()を使うか、サーバー呼び出しを含むクライアントアクションは非同期として扱われます。$resolve()は成功を、$reject()はエラーを通知し、$parametersオブジェクトで出力値を設定します。$reject()にはエラーオブジェクトを渡すことも可能です。
```

# 出力コード

``` js
// Input Parameter: Input (Integer)
// Output Parameters: Result (Integer), ErrorMessage (Text)

// REST APIの相対パスを構築
const apiUrl = `/JunjiWatanabeTest/rest/AITest/GenerativeAITest_JS?InputNum=${encodeURIComponent($parameters.Input)}`;

// fetchを使用してREST APIを呼び出す
fetch(apiUrl, {
    method: 'GET',
})
    .then((response) => {
        // HTTPステータスコードを確認
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json(); // レスポンスをJSONとしてパース
    })
    .then((data) => {
        // 正常時の処理：Resultに値を設定
        $parameters.Result = data;
        $parameters.ErrorMessage = ""; // エラーメッセージをクリア
        $resolve(); // 成功を通知
    })
    .catch((error) => {
        // エラー時の処理：ErrorMessageにエラーメッセージを設定
        $parameters.ErrorMessage = error.message || "An unknown error occurred.";
        $reject(error); // エラーを通知
    });
```

## 結果
このコードであれば、想定通りにExpose REST APIの終了を待ってからClient Actionが終了する。
また、このClient Actionを別のJavaScript要素から以下のように呼び、例外が発生するInput Parameter（例えば負の値）を入力すると、想定通りに.catchのパスに制御が移る（\$reject()の効果）。

``` js
$actions.RunJavaScript()
.then(function(result) {
  alert('then: ' + result);
})
.catch(function(error) {
  alert('catch: ' + error);
});
```