Apexからのプロンプトテンプレート呼び出しで、JSONモードを使う

Apexからプロンプトテンプレート呼び出し時、`JSONモード` を有効にできるか？試しました。

:::note
本記事ではOpenAIのJSONモードを使用します
https://platform.openai.com/docs/guides/json-mode
:::


## JSONモードとは
一般的にプロンプトにJSONを出力させようとすると、生成コンテンツ内にJSON以外の余計な文字列が含まれる可能性があります。

OpenAIの一部対応モデルでは、`JSONモード` を設定することで純粋にJSONのみ生成するよう制限することができ、後続パース処理でのエラー発生の軽減、パフォーマンス改善が期待できます。

https://platform.openai.com/docs/guides/json-mode

##### 対応モデル
現時点で、以下モデルが `JSONモード` に対応しているようです。（2024年7月18日時点）
 - gpt-4o
 - gpt-4-turbo
 - gpt-3.5-turbo

> To prevent these errors and improve model performance, when using **gpt-4o**, **gpt-4-turbo**, or **gpt-3.5-turbo**, you can set...
>
> https://platform.openai.com/docs/guides/json-mode

## プロンプトテンプレートでJSONモードを使うには

プロンプトテンプレートはApexから呼び出し可能です。Apexから呼び出す際、LLM側の設定を細かく指定することができます。また、LLMプロバイダー固有のパラメータを渡すことも可能です。

 - ConnectApi
     - EinsteinLlm​Additional​ConfigInput (LLM共通の設定)
         - **additionalParameters** (LLMプロバイダー固有の設定)

> #### ConnectApi.​EinsteinLlm​Additional​ConfigInput
> Additional configuration information for the LLM provider.
> | Property | Type | Description | Required or Optional |
> | ---- | ---- | ---- | ---- |
> | additionalParameters | Map\<String, ConnectApi.​WrappedValue\> | Map of parameters and values for the LLM provider. | Optional |
>
> https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_connectapi_input_einstein_llm_additional_config.htm

このプロパティを使い、`JSONモード`が有効になるようなOpenAI固有のパラメータを定義してLLMを呼び出します。


## 検証
フリーテキストの入力文章を指定フォーマットのJSONに変換するプロンプトを想定し、`1.プロンプトテンプレート` とそれを呼び出す `2.Apex` を作成します。

```mermaid
flowchart LR
    id1["入力文章"] .-> id3["プロンプト"]
    id2["JSONスキーマ"] .-> id3["プロンプト"]
    id3["プロンプトテンプレート"] -- "生成" --> id4["JSON"]
    id4["JSON"] .-> id5(["後続処理(JSON parse)"])
```

### 1. プロンプトテンプレートの作成
呼び出すプロンプトテンプレートを、プロンプトビルダーで作成します。

#### 設定
 - 種別
     - `Flex`
 - プロンプトテンプレート名
     - `フリーテキストをJSONに変換`
 - API参照名
     - `convertTextToJson`
 - リソース定義
     - #1
         - 名前: `フリーテキスト`
         - パラメータ名: `inputText`
         - データ型: `String`
     - #2
         - 名前: `JSONスキーマ`
         - パラメータ名: `jsonSchema`
         - データ型: `String`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/44121d49-64fc-82f8-51f1-16ee74194725.png)



#### プロンプトテンプレート

##### テンプレート本体
```text:テンプレート
以下JSON Schemaに厳密に従って
ユーザーの入力文章からデータを抽出し
JSONデータを返答します

JSON Schema:"""
{!$Input:jsonSchema}
"""

ユーザーの入力文章:"""
{!$Input:inputText}
"""

JSON:
```

##### モデル
 - OpenAI GPT 4 Omni

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112543/d8d8ecb8-56c0-6eff-9763-66afbf8d35a5.png)


### 2. Apexコードの作成

`JSONモード`でプロンプトテンプレートを呼び出すためのコード例です。

```apex:Apexコード
// テンプレートパラメータの設定
String promptTemplateName = 'convertTextToJson';
String jsonSchema = '';
String inputText = '';

// プロンプト呼び出しの設定
ConnectApi.EinsteinPromptTemplateGenerationsInput promptGenerationsInput = new ConnectApi.EinsteinPromptTemplateGenerationsInput();
promptGenerationsInput.isPreview = false;

// 入力パラメータの定義
ConnectApi.WrappedValue jsonSchemaWrappedValue = new ConnectApi.WrappedValue();
jsonSchemaWrappedValue.value = jsonSchema;
ConnectApi.WrappedValue inputTextWrappedValue = new ConnectApi.WrappedValue();
inputTextWrappedValue.value = inputText;
promptGenerationsInput.inputParams = new Map<String, ConnectApi.WrappedValue>{
  'Input:jsonSchema' => jsonSchemaWrappedValue,
  'Input:inputText' => inputTextWrappedValue
};

// LLM共通の設定
promptGenerationsInput.additionalConfig = new ConnectApi.EinsteinLlmAdditionalConfigInput();
promptGenerationsInput.additionalConfig.applicationName = 'PromptTemplateGenerationsInvocable';
promptGenerationsInput.additionalConfig.temperature = 0.0;

// OpenAI固有の設定 - JSONモード有効化
Map<String, String> jsonType = new Map<String, String>{
    'type' => 'json_object'
};
ConnectApi.WrappedValue responseFormatProperty = new ConnectApi.WrappedValue();
responseFormatProperty.value = jsonType;
promptGenerationsInput.additionalConfig.additionalParameters = new Map<String, ConnectApi.WrappedValue>{
    'response_format' => responseFormatProperty
};

// プロンプトテンプレートの呼び出し実行
ConnectApi.EinsteinPromptTemplateGenerationsRepresentation generationsOutput = ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate(promptTemplateName, promptGenerationsInput);
ConnectApi.EinsteinLLMGenerationItemOutput response = generationsOutput.generations[0];
System.debug(response.text);
```

#### 〜Apexコードの解説〜
##### [解説] テンプレートパラメータの設定
```apex
String promptTemplateName = 'convertTextToJson';
String jsonSchema = '';
String inputText = '';
```

 - promptTemplateName
     - 作成したプロンプトテンプレートのAPI参照名 `'convertTextToJson'` を指定
 - jsonSchema
     - 変換したいJSONオブジェクト構造をJSON Schema形式で記載
 - inputText
     - JSONに変換したい自然言語文章をここに記載

```apex
ConnectApi.WrappedValue jsonSchemaWrappedValue = new ConnectApi.WrappedValue();
jsonSchemaWrappedValue.value = jsonSchema;
ConnectApi.WrappedValue inputTextWrappedValue = new ConnectApi.WrappedValue();
inputTextWrappedValue.value = inputText;
promptGenerationsInput.inputParams = new Map<String, ConnectApi.WrappedValue>{
  'Input:jsonSchema' => jsonSchemaWrappedValue,
  'Input:inputText' => inputTextWrappedValue
};
```

 - inputParams
     - Input:jsonSchema
         - 設定したテンプレートパラメータ `jsonSchema`
     - Input:inputText
         - 設定したテンプレートパラメータ `inputText`


##### [解説] LLM共通の設定
LLM呼び出し時の設定を定義します。

```apex
// applicationName
promptGenerationsInput.additionalConfig.applicationName = 'PromptTemplateGenerationsInvocable';

// temperature
promptGenerationsInput.additionalConfig.temperature = 0.0;

// additionalParameters
promptGenerationsInput.additionalConfig.additionalParameters = new Map<String, ConnectApi.WrappedValue>();
```
 - application​Name
     - `'PromptTemplateGenerationsInvocable'` を指定
 - temperature
     - 今回は生成結果のランダム性を求めてないので `0.0` を指定 
 - **additional​Parameters**
     - MaxToken、temperature、num​Generations等以外のLLMプロバイダー固有のパラメータはここで定義


##### [解説] OpenAI固有の設定
プロンプトテンプレート呼び出し時のLLMプロバイダー固有（今回はOpenAI）の設定を定義します。

```apex
Map<String, String> jsonType = new Map<String, String>{
  'type' => 'json_object'
};
ConnectApi.WrappedValue responseFormatProperty = new ConnectApi.WrappedValue();
responseFormatProperty.value = jsonType;
promptGenerationsInput.additionalConfig.additionalParameters = new Map<String, ConnectApi.WrappedValue>{
  'response_format' => responseFormatProperty
};
```

今回は、ここにOpenAIが定める`JSONモード`を有効にするための設定として、`response_format` を指定しています。  

 - additional​Parameters
     - response_format
         - `{type: "json_object"}`

> you can set response_format to { "type": "json_object" } to enable JSON mode. When JSON mode is enabled, the model is...
>
> https://platform.openai.com/docs/guides/json-mode


##### [解説] プロンプトテンプレートの呼び出し実行
`promptTemplateName`と`promptGenerationsInput`を渡してメソッドを呼び出すことで、組み立てられたプロンプトがLLM側にリクエストされます。

```apex
ConnectApi.EinsteinPromptTemplateGenerationsRepresentation generationsOutput = ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate(promptTemplateName, promptGenerationsInput);
```

最終的な `promptTemplateName`, `promptGenerationsInput` は以下の通りです。
 - **promptTemplateName** = `'convertTextToJson'`
 - **promptGenerationsInput**
     - isPreview = `false`
     - inputParams
         - Input:jsonSchema = `jsonSchema`
         - Input:inputText = `inputText`
     - additionalConfig
         - applicationName = `'PromptTemplateGenerationsInvocable'`
         - temperature = `0.0`
         - additionalParameters
             - response_format
                 - type = `'json_object'`


### 3. プロンプト実行

以下の入力パラメータでプロンプトテンプレートを実行すると、

```apex:入力パラメータ
String jsonSchema = '{"$schema": "https://json-schema.org/draft/2020-12/schema","type": "object","properties": {"records": {"type": "array","items": {"type": "object","properties": {"activityDate": {"type": "string","format": "date","description": "営業活動の実施日"},"account": {"type": "string","description": "営業活動先企業"},"subject": {"type": "string","description": "営業活動の件名"},"detail": {"type": "string","description": "営業活動の詳細説明"}},"required": ["activityDate", "account", "subject", "detail"]}}},"required": ["records"]}';
String inputText = '2024年6月10日の活動です。午前、株式会社ABCに訪問。新商品の提案を実施し、新商品の特長や価格について説明。その後、株式会社XYZにも訪問。契約更新の再確認と更新に関して話し合いました。午後には株式会社123にメールで見積もりを送付しました。';
```


グラウンディング後のプロンプトは以下のようになります。

```text:プロンプト
以下JSON Schemaに厳密に従って
ユーザーの入力文章からデータを抽出し
JSONデータを返答します

JSON Schema:"""
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "records": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "activityDate": {
            "type": "string",
            "format": "date",
            "description": "営業活動の実施日"
          },
          "account": {
            "type": "string",
            "description": "営業活動先企業"
          },
          "subject": {
            "type": "string",
            "description": "営業活動の件名"
          },
          "detail": {
            "type": "string",
            "description": "営業活動の詳細説明"
          }
        },
        "required": ["activityDate", "account", "subject", "detail"]
      }
    }
  },
  "required": ["records"]
}
"""

ユーザーの入力文章:"""
2024年6月10日の活動です。午前、株式会社ABCに訪問。新商品の提案を実施し、新商品の特長や価格について説明。その後、株式会社XYZにも訪問。契約更新の再確認と更新に関して話し合いました。午後には株式会社123にメールで見積もりを送付しました。
"""

JSON:

```

#### 生成結果 (通常モードの場合)
JSONモードを使わず通常モードで出力すると、

````json:出力 (通常モード)
```json
{
  "records": [
    {
      "activityDate": "2024-06-10",
      "account": "株式会社ABC",
      "subject": "新商品の提案",
      "detail": "新商品の特長や価格について説明"
    },
    {
      "activityDate": "2024-06-10",
      "account": "株式会社XYZ",
      "subject": "契約更新の再確認",
      "detail": "契約更新に関して話し合い"
    },
    {
      "activityDate": "2024-06-10",
      "account": "株式会社123",
      "subject": "見積もり送付",
      "detail": "メールで見積もりを送付"
    }
  ]
}
```
````

文頭、文末に余計な文字 ```` ```json ````、 ```` ``` ```` が入ってしまいますが、

#### 生成結果 (JSONモードの場合)
JSONモードをONにすると、

```json:出力 (JSONモードON)
{
  "records": [
    {
      "activityDate": "2024-06-10",
      "account": "株式会社ABC",
      "subject": "新商品の提案",
      "detail": "新商品の特長や価格について説明"
    },
    {
      "activityDate": "2024-06-10",
      "account": "株式会社XYZ",
      "subject": "契約更新の再確認",
      "detail": "契約更新に関して話し合い"
    },
    {
      "activityDate": "2024-06-10",
      "account": "株式会社123",
      "subject": "見積もり送付",
      "detail": "メールで見積もりを送付"
    }
  ]
}
```

無事、JSONのみを取得することができました！

## おわりに

JSONモードを使うと、より正確で構造化されたデータを取得できるので便利です。ただ、油断は禁物です。適切なプロンプト設計が重要なのは変わらないので、プロンプトビルダーを活用して継続的にプロンプトを改善しましょう。


https://qiita.com/misu007/items/b239e2b61aa20bc6c597