2026/08/13

表のプロパティ ② - セルのマージン

表のプロパティを紹介するこのシリーズ、2 回目は表のマージンの設定に関して紹介します。


セルのマージンの設定

表のプロパティで[表のレイアウト]タブを開き、行の間隔と列の間隔で設定します。プロパティボックスに図式化されている通り、マージンはセルの境目(= 罫線)と文字などのコンテンツを書くエリアとの隙間の設定で、縦と横に対して別々に設定できます。

前回紹介したぎゅうぎゅうの表(図の左)は、マージンの設定が 0 です。ノーツで表を作成するとこの状態がデフォルトとなります。右は行の間隔を 0.2 cm、列の間隔を 0.5 cm に設定しています。これで、表が一気に見やすくなりましたね。

この設定の特徴は、表の全体(= すべてのセル)に一括で設定されます。設定や修正が簡単で効率的に設定できます。

なお、0.2 cm、0.5 cm と設定したのですが、プロパティボックスでは 0.199 cm、0.499 cm と表示されています。これは内部的には inch 管理なので、変換により誤差が出るのだと思います。cm 環境では切りのよい数値が設定できないことがあることを覚えておきましょう。


文字のマージン

セルにマージンを設定するにはもう一つ別の方法があります。文字のプロパティを使った設定です。設定するセルにカーソルを入れ[段落の余白]タブを開き、余白を設定します。

右のマージンも設定できますが、段落の余白では固定値を設定する「絶対位置」か比率で設定する「相対位置(%)」しか設定できません。ウィンドウサイズに合わせて伸縮するセルでは、セルのマージンのように罫線から一定には設定できませんのでご注意ください。

上下のマージンは[段落整列]タブの行間で設定することができます。

お気づきの通り、文字のプロパティは表に特化した機能ではなく、表の外側でも設定できます。設定の単位は段落と言って、ノーツ的には改行までの1行分を表します。ですので、セルのそれぞれで独立して設定でき、セル内に複数行(段落)あれば、1行ごとに設定できます。

複数のセルに対して一括で設定したい場合は設定するセルの範囲を選択してから、プロパティを書き換えると一括で設定することができます。


マージン設定の使い分け

文字のプロパティで行いマージン設定は個別に設定できるという柔軟性はある文設定に手間がかかります。また、行の追加を行った際、その行にマージンの設定が反映されない問題があります。

行を追加するたびに上の行の設定を確認し、同じ設定をするのは面倒ですね。

表のプロパティで設定するマージン設定はそれ一つで表全体に設定されるので、行や列を追加しても維持されます。このような背景から、表のマージン設定は、まず、表のプロパティで行い、個別に設定する理由がある場合に限り、文字のプロパティを利用することをおススメします。


ちなみに表のプロパティで行うマージン設定は、記憶が正しければ Notes R5 で登場した機能と記憶しています。それまでは、文字のプロパティで行うしかなかったため、古い DB 程利用している可能性があります。長く Notes/Domino をお使いの方は、発見したら、表のプロパティのマージン設定に移行しておきましょう。


2026/08/12

表のプロパティ ① - 表の幅

ノーツで文章やフォームを作成していて、体裁よく情報を整理するのに表は欠かせないですよね。この表には、さまざまな機能があり、こんな機能もあるの? そこまでいるんか? と驚くような設定も存在します。ほとんどの設定は、表のプロパティを開くことで指定できますが、プロパティには複数のタブがあり設定の多さに驚かされます。

今回の『表のプロパティ』シリーズでは、奥深い表の機能を順に紹介していきます。


表幅の設定

まずは、メールの本文などリッチテキストフィールドに表を書きます。サンプルとして以下のような 6 行 4 列の表を作成しました。

この状態で、Notes クライアントのウィンドウ幅を変えるとそれに応じて表の幅(各列の幅)が伸縮します。これは、表のプロパティで幅の設定が「マージンに合わせる」になっているからです。

幅の設定で「ウィンドウに合わせる」を選択すると表の左右の隙間(マージン)がなくなり画面の幅いっぱいの表になります。

「固定幅」はその名の通り表の幅を固定にします。画面幅に合わせると列幅が広くなりすぎて間延びするなど、固定したほうが見た目がいい場合に使用するとよいでしょう。


マージンの設定

幅の設定で「マージンに合わせる」を指定した場合の表の幅を詳しく確認してみましょう。ルーラを確認すると 2.5 cm 程度になっています。

正確にはプロパティボックスで確認できます。表のプロパティの右から 3 つ目[表の余白]タブの左余白がその値となります。

初期値は 2.54 cm です。中途半端に見えるのは cm 表記のためですね(1 inch = 2.54 cm)。この幅を 4 cm に修正すると一段下がった表にすることができます。


列幅の設定

表の幅をマージンに合わせる設定にした場合、特定の列の幅を固定することができます。

幅を固定したい列にカーソルを入れ、表のプロパティを開きます。最初のタブ[表のレイアウト]の『固定幅』にチェックを入れることで設定します。

幅に希望する値をセットするとその幅で固定されます。これで、ウィンドウの幅を変更してもその列の幅は変化しません。

たとえば、今回の表では 4 列目以外の幅は大きく変化しません。そこで 1 ~ 3 列目だけ固定幅にすると、画面の幅に合わせて最後の列だけ伸縮する表にできます。


自動サイズ

『固定幅』の横にある『自動サイズ調整』にチェックを入れると列の幅をコンテンツに合わせて自動調整してくれる機能があります。例えば 1 列目で設定するとその列で一番長い行に合わせて幅が設定されます。

その後、行を追加して長い名前を入力するとその幅に応じて列幅が変化します。

この例では 2 ~ 4 列目は固定幅にはしていなかったので、同じ比率で幅が縮小しています。もし、固定幅の列がある場合は、それ以外の列で幅の調整が行われます。


メニューの自動サイズ

先ほど紹介した自動サイズに近い機能がもう一つあります。表にカーソルがあるときだけ表示される[表]メニューに[自動サイズ]があります。

まず、わかりやすい例として固定幅の表で実行してみます。次のようにコンテンツの幅に合わせてすべての列の幅がセットされます。

先ほど紹介したプロパティの『自動サイズ調整』チェックと違うのは、メニューを選択したタイミングのみ効果があるということです。例えば、リッチテキストに書き換えると幅の調整が行われず、折り返しが発生します。

ノーツ文書で、コンテンツが出来上がってから実行して体裁を整える機能と思えばいいですね。

ちなみに、マージンに合わせる表では、固定幅の表で実行したときの比率でマージンの幅に伸長してセットされるようです。


まとめ

今回は、表や列の幅に関係する機能を紹介しました。知らない機能はありましたか?覚えておくと、美しい表を効率よく書けるようになるかもしれませんね。

ただ、今回紹介した表は、罫線と文字が近く、ぎゅうぎゅうで見ずらいですよね。次回は見やすい表を作るための設定をご紹介する予定です。


2026/07/30

複製と方向

しばらく間が空いてしまいましたが、久しぶりに 複製 に関する話です。今回は複製の方向についてまとめます。

複製(レプリケーション)は、複製関係にある2つの DB 間で同期を行う処理です。『同期なのに方向があるってどういうこと?』と思われるかもしれません。実はこの”方向”を理解すると、複製の仕組みがぐっとわかりやすくなります。


複製の方向

以下の画面は、サーバ上にある DB をローカルに複製した時のレプリケータの画面です。

複製結果欄の”受信”と”送信”が複製の方向になります。方向には、

  • 受信(Pull)
  • 送信(Push)
  • 送受信(Pull-Push)

の3種類があり、一般的な複製では送受信を使用します。ただ、Notes/Domino の仕様としてはそれぞれ独立した動作となっており、片方向だけを実行することも可能だということを覚えておきましょう。方向を表すには”どこから”という情報が必要となります。複製では、”複製を開始した側”を基準に方向が決まります。

初回の複製ではローカルに何も存在しないので、”受信”だけが記録されるという結果になります。

重要なのは、

受信(Pull) 複製相手の更新を受け取るだけ
送信(Push) 自分が持っている更新を複製相手に送るだけ

という点で、Pull-Push はそれぞれを連続して実行するという動作になります。片方向だけでは、”同期”ではなく、更新の片方向伝達になります。

また、サーバ管理における複製に関しては、よく Pull と Push という言葉が使われますので、覚えておきましょう。


方向の指定

複製の方向は『複製のオプション』画面の基本タブ、複製方法で指定します。通常は下図の通り、送受信にチェックが付いてます。

例えば、『複製相手へ文書を送る』チェックを外し、送信だけ停止するとレプリケータの設定欄の矢印は「←」のみになります。

レプリケータは、ローカル - サーバ間の複製を関るする画面に特化しており、左がローカルで右がサーバだと理解すればわかりやすいですね。


削除の反映

通常の複製は、文書の新規作成、更新、削除が対象となりますが、”削除”についてはより詳細な設定が可能となっています。

削除したことを他のレプリカに送信する設定は『複製のオプション』の送信タブにあります。

逆に他のレプリカでの削除を自分自身に反映する(受信)設定は、詳細タブにあります。


複製設定の利用例

今回紹介した設定を利用すると

  • サーバで作成/更新された文書は受け取るが、削除は受け取らない
  • ローカルでの更新(削除を含む)はサーバに反映しない

というような設定が可能です。

サーバ上のメールボックスを管理者が定期的に削除するような運用を行っても、ローカルレプリカ内のメールボックスには、メールが残り続けるという設定が可能になります。

また、掲示板のように期限が来たら消えていくようなアプリでも過去の履歴を保存しておくことができますね。


複製設定の注意

なお、『複製のオプション』を利用した場合に、思い通りに複製されないという問題がよく発生します。その原因を理解するときに重要になるのが、『複製のオプション』は DB 単位で存在するという点です。

例えば、ローカル側で複製を止めた場合、サーバ側でいくら送受信する設定になっていても、ローカルに複製されません。

逆にサーバ側で複製を止めた場合、ローカル側どのように設定しても複製は実現しません。


つまり、『複製のオプション』は各 DB が個別に持つ設定であり、この設定自体が複製されることはありません。

そのため、ローカルとサーバで異なる複製設定にして運用することができ、柔軟な設定ができます。一方で、意図した通りに複製されず、トラブルの原因になることもあります。

複製が期待どおりに動作しない場合は、双方の複製設定を確認することを心掛けましょう。


2026/07/26

つないでみよう:#35) Responses API - 画像生成

前回までで、一番単純な会話ができるようになったので、次は画像生成をさせてみましょう。

Responses API は、画像生成にも対応しているので、/images/generations など別の API をコールする必要はありません。例えば、次のような JSON を送信することで、Responses API に画像生成させることができます。

{
   "model": "gpt-5-nano",
   "input": "ビーチリゾートに行きたくなるような、白い砂浜とヤシの木の画像",
   "tools": [
      {
         "type": "image_generation",
         "model": "gpt-image-2",
         "size": "1024x1024",
         "output_format": "png",
         "quality": "low"
      }
   ]
}

重要なのは tools 配列を指定し、その中で type に image_generation を設定している点です。設定値からもわかるように、Responses API では画像生成も「ツール」の一つとして扱われます。次の model が画像生成を担当するモデルです。現在の推奨モデルは gpt-image-2 で、size、output_format、quality は画像生成時のオプションです。指定できる項目や設定値はモデルによって異なるため、必要に応じて公式ドキュメントを確認してください。

この例では、プロンプトの解釈を gpt-5-nano が担当し、実際の画像生成を gpt-image-2 が担当します。このように Responses API では、それぞれの役割に応じてモデルを組み合わせて利用できるのが特徴です。


画像生成機能の作成方針

前回までに紹介した会話のサンプルでは、新規と継続で別々のフォーム、エージェントで作成しました。今回の画像生成では、1つにまとめたいと思います。

新規か継続の判断は、文書に実行結果が入っているかで判断するものとします。具体的には、Result_ID に値が入っていれば、継続として処理します。


フォームの作成

画像生成用のフォームをサンプルアプリに追加します。

前回 #34 で作成した fGPT_Res02 フォーム(継続元ID フィールドがある)をコピペして、画像生成用フォーム fGPT_Res03 を作成します。

フィールドは基本的にはそのままなのですが、今回の返答が画像となります。そこで、実行結果のフィールドをリッチテキスト Result_Image に変更します。


エージェントの作成

フォームと同様にエージェントも前回作成のエージェントをコピペして作業を開始します。


◇ Initialize

基本的な処理の流れに変更はありません。ただ、新規と継続をまとめたことで中盤の太字の部分が大きく違います。また、この変更に関連して変数名や処理順を一部変更していますので、Initialize 全体を掲載しています。

Sub Initialize
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim ndb As NotesDatabase
   Dim ndCur As NotesDocument    'UI で開いている文書
   Dim ndReq As NotesDocument   '問い合わせに利用する文書

   Dim jnavRequest As NotesJSONNavigator
   Dim jnavResponse As NotesJSONNavigator

   Dim s As String

   '初期化
   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase

   '現在の文書(= 継続元の文書)
   Set nuid = nuiw.Currentdocument
   Set ndCur = nuid.Document    ' UI の文書は読み込みモードの前提

   ' 新規 or 継続の判定
   If ndCur.Result_ID(0) = "" Then
      '新規リクエスト(現在の文書で問い合わせ)
      Set ndReq = ndCur
   Else
      '継続したリクエスト(現在の文書を参照させる設定で新規作成)
      s = nuiw.Prompt(PROMPT_OKCANCELEDIT, "継続", "次のプロンプトを入力してください。")
      If s = "" Then Exit Sub

      '新規文書作成
      Set ndReq = ndb.CreateDocument()
      ndReq.Form = "fGPT_Res03"
      ndReq.Model = ndCur.Model
      ndReq.PrevID = ndCur.Result_ID    '現在の文書を参照
      ndReq.Prompt = s
   End If


   'Request Body(JSON) の作成
   Set jnavRequest = xGetRequest_JSON(ndReq)
   Call xSetJSON(ndReq, "Request", jnavRequest)    'Request Body の保存

   '問い合わせ実行
   Set jnavResponse = xAskResponses(jnavRequest)
   Call xSetJSON(ndReq, "Response", jnavResponse)    'Response の保存

   '返答を取得
   If xGetResponce(ndReq, jnavResponse) Then
      Call ndReq.Save(True, False)

      '文書を開きなおす
      Call nuid.Close()
      Call nuiw.EditDocument(False, ndReq)
   End If
End Sub


◇ xGetRequest_JSON

Responses API に送信する関数で、画像生成を指定する tools 配列を追加しています。今回はサンプルですので、model や size などには固定値を追加しています。

Sub Function xGetRequest_JSON(vnd As NotesDocument) As NotesJSONNavigator
   Dim jnav As NotesJSONNavigator
   Dim jaTol As NotesJSONArray
   Dim joTol As NotesJSONObject

   'RequestBody(JSON) の準備
   Set jnav = xns.CreateJSONNavigator("")

   '1.model
   Call jnav.AppendElement(vnd.Model(0),"model")

   '1.previous_response_id  継続の場合のみ設定
   If vnd.PrevID(0) <> "" Then
      Call jnav.AppendElement(vnd.PrevID(0),"previous_response_id")
   End If

   '1.input
   Call jnav.AppendElement(vnd.Prompt(0),"input")

   '1.tools 配列
   Set jaTol = jnav.AppendArray("tools")

   '2.image_generation
   Set joTol = jaTol.AppendObject()
   Call joTol.AppendElement("image_generation", "type")
   Call joTol.AppendElement("gpt-image-2", "model")
   Call joTol.AppendElement("1024x1024", "size")
   Call joTol.AppendElement("png", "output_format")
   Call joTol.AppendElement("low", "quality")


   Set xGetRequest_JSON = jnav
End Function


◇ xAskResponses

画像生成は会話より応答時間がかかります。そこで、タイムアウトを 450 秒に設定します(デフォルトは 30 秒)。ちなみに私が試した限り上記生成条件で、300 秒以内で返答が返ってきました。

Sub Function xAskResponses(vjnavRequest As NotesJSONNavigator) As NotesJSONNavigator
         ・・・(省略)・・・
   'HTTP ヘッダーの設定
   Call http.SetHeaderField("Content-Type", "application/json")
   Call http.SetHeaderField("Authorization", "Bearer " & xcsBearer)


   'API 実行
   http.TimeOutSec = 450    '画像生成は時間がかかるので
   http.PreferJSONNavigator = True
         ・・・(省略)・・・
End Function


◇ xGetResponce

API のレスポンスから返答を取得する処理も変化します。生成された画像は output 配列内の type が image_generation_call にあります。これを取り出し、result の値が画像データとなります。

Function xGetResponce(vnd As NotesDocument, vjnav As NotesJSONNavigator) As Boolean
   Dim je As NotesJSONElement
   Dim sBase64 As String

   Set je = vjnav.Getfirstelement()

   'id 取得
   vnd.Result_ID = GetPropertyValue(vjnav, "id")

   'output 取得してセット
   Set je = GetProperty(vjnav, "output")

   '"type": "image_generation_call" の要素を取得
   Set je = FindInArray(je, "type", "image_generation_call")

   '生成画像(result)を取得してセット
   sBase64 = GetPropertyValue(je, "result")
   Call xSetImageFile(vnd, "Result_Image", sBase64)


   xGetResponce = True
End Function

画像データは Base64 でエンコードされているので、以下の3つの関数を通して、デコードし、画像ファイルに変換します。


◇ xSetImageFile

Base64 の画像データをデコード、画像ファイル化し、文書に添付する関数です。

Private Function xSetImageFile(vnd As NotesDocument, ByVal vsFld As String, vsBase64 As String)
   Dim nsImg As NotesStream
   Dim nrti As NotesRichTextItem
   Dim sFP As String

   sFP = xns.GetEnvironmentString("Directory", True)
   sFP = sFP & "\" & Format(Now, "yyyymmddhhnnss") & ".png"

   On Error Resume Next
   Kill sFP    '存在してたら削除

   On Error GoTo Err_General
   Set nsImg = xns.CreateStream()
   Call Base64ToBinary(vsBase64, nsImg)   'デコード
   Call StreamToImageFile(nsImg, sFP)    '画像ファイルとして保存

   '画像ファイルを文書に添付
   Call vnd.RemoveItem(vsFld)
   Set nrti = vnd.CreateRichTextItem(vsFld)
   Call nrti.EmbedObject(EMBED_ATTACHMENT, "", sFP)

Exit_Proc:
   Exit Function

Err_General:
   MsgBox Error$
   Resume Exit_Proc
End Function


◇ Base64ToBinary / StreamToImageFile

これらの関数は  OpenNTFLotusScript Gold Collection プロジェクトから拝借した関数です。詳しくは #28 で紹介しているので必要に応じて参照ください。関数自体は下記に再掲載します。

%REM
   Sub Base64ToBinary
   Description: Given a string of base64-encoded data, write into a binary stream we are passed.
      This is done rather than creating the stream here and returning it, so that you can
   stream directly into a file if you choose.
%END REM

Sub Base64ToBinary(strBase64$, streamOut As NotesStream)
   ' Given a string of base64 encoded data, this routine decodes and writes the original binary data into a NotesStream
   Dim doc As NotesDocument
   Dim mime As NotesMIMEEntity
   Dim streamIn As NotesStream
   Dim db As NotesDatabase
   Dim session As New NotesSession

   Set db = session.CurrentDatabase
   Set doc = db.CreateDocument
   Set mime = doc.CreateMIMEEntity("Body") ' the mime classes already know how to do this conversion,
   Set streamIn = session.CreateStream
   Call streamIn.WriteText(strBase64)
   streamIn.Position = 0
   Call mime.SetContentFromText(streamIn, "binary", ENC_BASE64)
   Call mime.GetContentAsBytes(streamOut, True) ' decode as you stream out the data.
End Sub

Sub StreamToImageFile(vnstImage As NotesStream, ByVal vsFP As String)
   Dim nstOut As NotesStream

   On Error Resume Next
   Kill vsFP '存在してたら削除

   Set nstOut = xns.CreateStream()
   Call nstOut.Open(vsFP)

   vnstImage.Position = 0
   Do Until vnstImage.Position >= vnstImage.Bytes
      Call nstOut.Write(vnstImage.Read(16000))
   Loop
End Sub


想定外の改造

画像生成のサンプルとしては、上記までで完了のつもりでした。ただ、実行するとエラーが発生しました。

原因は、API のレスポンスが大きく、リッチテキストに書き込む処理で  リッチテキストの制限 に引っ掛かっていたためだと思われます(どちらも 64 KB まで)。

  • リッチテキストフィールドの単一段落の最大サイズ
  • 文書当たりのテキスト (要約) データの最大量

そこで、JSON のサイズが大きい場合、添付ファイルとして保存するように変更します。


◇ xSetJSON

JSON を文書に保存する関数で、サイズが大きい場合(10000 バイト)、後述する JSONToFile を通して、ファイルに保存します。

Private Function xSetJSON(vnd As NotesDocument, ByVal vsFld As String, vjnav As NotesJSONNavigator)
            ・・・(省略)・・・

   '日時の記録
   Call vnd.ReplaceItemValue(vsFld & "_DT", Now)

   'JSON の記録
   'いったん削除してからセット

   Dim sFP As String
   Call vnd.RemoveItem(vsFld & "_JSON")
   Set nrti = vnd.CreateRichTextItem(vsFld & "_JSON")

   If Len(sText) > 10000 Then
      '大きいのでファイルで保存
      sFP = JSONToFile(vjnav, "")    'ファイル名は指定なし
      Call nrti.EmbedObject(EMBED_ATTACHMENT, "", sFP)

      Kill sFP  
  '添付したのでファイルは削除
   Else
      'そのままテキストで保存
      Call nrti.AppendText(sText)
   End If
End Function


◇ JSONToFile

JSON をファイルに保存する関数です。この関数はほかでも使いそうなので、lsJSONNavi ライブラリに作成します。

Public Function JSONToFile(vjnav As NotesJSONNavigator, ByVal vsFileName As String) As String
   Dim ns As New NotesSession
   Dim nstOut As NotesStream
   Dim sFP As String

   sFP = ns.Getenvironmentstring("Directory", True)
   If vsFileName = "" Then
      sFP = sFP & "\" & Format(Now, "yyyymmddhhnnss") & ".json"
   Else
      sFP = sFP & "\" & vsFileName
   End If

   On Error Resume Next
   Kill sFP   '存在してたら削除

   On Error GoTo Err_General

   Set nstOut = ns.CreateStream()
   Call nstOut.Open(sFP)

   Call nstOut.WriteText(vjnav.Stringify())
   Call nstOut.Close()

   JSONToFile = sFP
Exit_Proc:
   Exit Function

Err_General:
   MsgBox Error$
   Resume Exit_Proc
End Function


動作検証

出来上がったら動作検証します。

最初のリクエストを書いた文書を開き、エージェントを実行します。しばらくすると文書が開きなおし、画像が添付された状態となります。API からの返答は大きくなるので、JSON は返答だけが添付ファイルになっているはずです。この状態から再度エージェントを実行すると作成された画像に対するリクエストが送信できます。

今日も朝から暑い中 Notes 三昧なので、『ビーチリゾートに行きたくなるような、白い砂浜とヤシの木の画像』ネタだけは休日仕様にしてみました。

結果は次のような感じでした。

青い海に真っ白な砂浜、デッキチェアにパラソル、イメージ通りの画像ですね。次のリクエストでは『この画像を夕景に変更してください。』と送信しました。構図は少々変わってしまってますが、見事な夕焼けですね。

あぁ、リゾートしたい...


前回 連載:つないでみよう


2026/07/20

つないでみよう:#34) Responses API - サンプルアプリ作成 ③

Responses API では過去のリクエストを参照させて、会話を継続する機能があります。仕組みは単純で、リクエストで送信する JSON に参照する過去のリクエストの id を previous_response_id に指定するだけでした。

{
   "model": "gpt-5-nano",
   "previous_response_id": "resp_xxxxx",
   "input": "そこの面積は?"
}

今回は、作成中のサンプルアプリに会話を継続する機能を追加します。


フォームの作成

前回作成したフォームをコピペして継続用のフォームを作成します。後述のプログラムで使用するので別名は fGPT_Res02 とします。

追加するのは参照するリクエストの id を指定する継続元 ID(PrevID)フィールドだけです。


エージェントの作成

エージェントも前回作成したものをコピペで作成し、変更を加える形で進めます。このエージェントの使い方は次の通りです。

  1. 継続元の文書を開く
  2. 今回のエージェント実行
  3. 次の問い合わせのプロンプトを入力
  4. Responses API に問い合わせ(会話の継続)
  5. 結果を記録して、UI に表示


エージェントの修正箇所を順に説明します。

◇ Initialize

メインルーチンでは、プロンプトの入力(青字)と会話の継続用の文書を新規作成(赤字)する処理を追加します。 それ以外に変更はありません。

Sub Initialize
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim ndb As NotesDatabase
   Dim nd As NotesDocument

   Dim jnavRequest As NotesJSONNavigator
   Dim jnavResponse As NotesJSONNavigator

   '初期化
   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase

   '次のプロンプトを入力
   Dim s As String
   s = nuiw.Prompt(PROMPT_OKCANCELEDIT, "継続", "次のプロンプトを入力してください。")
   If s = "" Then Exit Sub
     '未入力で処理終了

   '現在の文書(= 継続元の文書)
   Dim ndPrev As NotesDocument
   Set nuid = nuiw.Currentdocument
   Set ndPrev = nuid.Document

   '新規文書作成
   Set nd = ndb.CreateDocument()
   nd.Form = "fGPT_Res02"
   nd.Model = ndPrev.Model
   nd.PrevID = ndPrev.Result_ID     '継続元のID
   nd.Prompt = s          'プロンプト

   '① Request Body(JSON) の作成
   Set jnavRequest = xGetRequest_JSON(nd)
   Call xSetJSON(nd, "Request", jnavRequest) 'Request Body の保存

   '② 問い合わせ実行
   Set jnavResponse = xAskResponses(jnavRequest)
   Call xSetJSON(nd, "Response", jnavResponse) 'Response の保存

   '返答を取得
   If xGetResponce(nd, jnavResponse) Then
      Call nd.Save(True, False)

      '文書を開きなおす
      Call nuid.Close()
      Call nuiw.EditDocument(False, nd)
   End If
End Sub


◇ xGetRequest_JSON

送信する JSON を作成する処理では、previous_response_id の指定を追加します。会話の継続で一番重要な点なのですが、修正作業としては単純ですね...

Function xGetRequest_JSON(vnd As NotesDocument) As NotesJSONNavigator
   Dim jnav As NotesJSONNavigator

   'RequestBody(JSON) の準備
   Set jnav = xns.CreateJSONNavigator("")

   '1.model
   Call jnav.AppendElement(vnd.Model(0),"model")
  
   '1.previous_response_id
   If vnd.PrevID(0) <> "" Then
      Call jnav.AppendElement(vnd.PrevID(0),"previous_response_id")
   End If


   '1.input
   Call jnav.AppendElement(vnd.Prompt(0),"input")

   Set xGetRequest_JSON = jnav
End Function


動作検証

完成したら実行します。

前回(#33)で作成した文書を開き、今回作成したエージェントを実行します。『そこの面積は?』と聞いただけなのに、前回の「大阪の県庁所在地」を参照して、大阪市の面積に関して答えてくれました。


前回 連載:つないでみよう 次回


2026/07/19

つないでみよう:#33) Responses API - サンプルアプリ作成 ②

サンプルアプリ作成の2回目は、取得したレスポンス(JSON)から必要な値を取得する部分を作成します。

Responses API のレスポンスは #31 で紹介したように複雑な構造になっています。例えば API の返答が含まれる output ノードは配列となっており、要素の個数や順序が固定ではありません。その中から type が message となっているものを探し出す必要がありました。

{
   "id": "resp_xxxxx",
   "object": "response",
         ・・・(省略)・・・
   "moderation": null,
   "output": [
      {
         "id": "rs_xxxxx",
         "type": "reasoning",
         "content": [],
         "summary": []
      },
      {
         "id": "msg_xxxxx",
         "type": "message",
         "status": "completed",
         "content": [
            {
               "type": "output_text",
               "annotations": [],
               "logprobs": [],
               "text": "大阪府の県庁所在地は大阪市です。"
            }
         ],
         "role": "assistant"
      }
   ],
         ・・・(省略)・・・
}





← output は配列


← 1つ目の要素は回答ではない





← 2つ目の要素が回答






← 回答

実際にコーディングを行うと JSON の構造をたどるコードと Responses API 特有の意味を解釈するコードが混在し、煩雑で理解しがたいコードになります。

そこで、今回のサンプルでは JSON の汎用的な操作をライブラリに定義します。「JSON の構造をたどるコード」をライブラリに外出しにして、メインルーチンは「JSON の意味を解釈するコード」に特化させ、コードの可読性を上げようという算段です。


ライブラリの作成

今回利用する Responses API のレスポンス調査では、次のような JSON 操作が必要となります。

関数名 機能 使用例
GetProperty 名前を指定して、子ノードを取得します。 output ノードを取得
GetPropertyValue 名前を指定して、子ノードの値を取得します。 text ノードを探してその値を取得
FindInArray 配列からノードと値が一致した要素を返します。  output ノードから type = message の子ノードを取得

新規でスクリプトライブラリ lsJSONNavi を作成して、以下のコードを記述します。

Option Declare

%REM
名前を指定して、子ノードを取得します。

◆ 引数
1. voParent  Variant 取得もととなる親ノード
 NotesJSONElement の場合:値が NotesJSONObject であれば、子ノードを調査
 NotesJSONObject、NotesJSONNavigator の場合:その子ノードを調査
2. vsName String 取得するノード名

◆ データ型(戻り値)  NotesJSONElement
 一致したノードを返します。
 見つからない場合は Nothing を返します。
%END REM

Public Function GetProperty(voParent As Variant, ByVal vsName As String) As NotesJSONElement
   Dim jobj As NotesJSONObject
   Dim sType As String

   On Error GoTo Err_General

   sType = TypeName(voParent)

   If sType = "NOTESJSONELEMENT" Then
      Set jobj = voParent.Value
      Set GetProperty = jobj.GetElementByName(vsName)
   ElseIf sType = "NOTESJSONNAVIGATOR" Then
      Set GetProperty = voParent.GetElementByName(vsName)
   ElseIf sType = "NOTESJSONOBJECT" Then
      Set GetProperty = voParent.GetElementByName(vsName)
   End If

Exit_Proc:
   Exit Function

Err_General:
   Set GetProperty = Nothing
   Resume Exit_Proc
End Function

%REM
名前を指定して、子ノードの値を取得します。

◆ 引数
1. voParent  Variant 取得もととなる親ノード
 NotesJSONElement の場合:値が NotesJSONObject であれば、子ノードを調査
 NotesJSONObject、NotesJSONNavigator の場合:その子ノードを調査
2. vsName String 取得するノード名

◆ データ型(戻り値)  String
 一致したノードの値を返します。
 見つからない場合は Null を返します。
%END REM

Public Function GetPropertyValue(voParent As Variant, ByVal vsName As String) As String
   Dim je As NotesJSONElement
   Dim sType As String

   On Error GoTo Err_General

   sType = TypeName(voParent)

   If sType = "NOTESJSONELEMENT" Then
      Set je = GetProperty(voParent, vsName)
   ElseIf sType = "NOTESJSONNAVIGATOR" Then
      Set je = voParent.GetElementByName(vsName)
   ElseIf sType = "NOTESJSONOBJECT" Then
      Set je = voParent.GetElementByName(vsName)
   End If

   GetPropertyValue = je.Value

Exit_Proc:
   Exit Function

Err_General:
   GetPropertyValue = ""
   Resume Exit_Proc
End Function

%REM
配列の要素からノードと値が一致した要素を返します。

◆ 引数
1. voArray   Variant 調査する配列
 NotesJSONAarray の場合:その配列の各要素を調査
 NotesJSONElement の場合:値が配列の場合、各要素を調査
2. vsName String ノード名
3. vsValue String ノード名の値

◆ データ型(戻り値)  NotesJSONElement
 一致した要素(= ノード)を返します。
 見つからない場合は Nothing を返します。
%END REM

Public Function FindInArray(voArray As Variant, ByVal vsName As String, ByVal vsValue As String) As NotesJSONElement
   Dim je As NotesJSONElement
   Dim ja As NotesJSONArray
   Dim sType As String
   Dim sVal As String

   On Error GoTo Err_General

   sType = TypeName(voArray)

   If sType = "NOTESJSONARRAY" Then
      '配列なのでセット
      Set ja = voArray
   ElseIf sType = "NOTESJSONELEMENT" Then
      'Elemetの値が配列なら処理
      Set ja = voArray.Value
   End If

   If Not ja Is Nothing Then
      Set je = ja.GetFirstElement()

      Do Until je Is Nothing
         sVal = GetPropertyValue(je, vsName)

         If sVal = vsValue Then
            '発見
            Set FindInArray = je
            Exit Do
         End If

         Set je = ja.GetNextElement()
      Loop
   End If

Exit_Proc:
   Exit Function

Err_General:
   Set FindInArray = Nothing
   Resume Exit_Proc
End Function

要素の取得もとは、JSON ツリーのトップレベルである NotesJSONNavigator であっても、サブツリーを表す NotesJSONObject でも動作するようにします。また、NotesJSONElement であっても値が NotesJSONObject であればそこから取得するようにしています。関数の利用者は、JSON オブジェクトの型をあまり気にすることなく利用できるようにしてみました。

今のところ JSON の操作は手探りで、例外処理も不完全と思います。まだまだよい関数というには程遠いですが、いったん掲載しておきます。今後改善や機能追加することもあるので、その点はご了承ください。


 Responses API レスポンス取得

作成しているサンプルアプリに戻ります。

まず、取得した回答を記録するフィールドをフォームに作成します。

続いて、前回作成したエージェントにライブラリを組み込み、レスポンス取得の関数を追加します。取得するのは、レスポンスの最初の項目である id と返答です。

Use "lsJSONNavi"

Function xGetResponce(vnd As NotesDocument, vjnav As NotesJSONNavigator) As Boolean
   Dim je As NotesJSONElement

   Set je = vjnav.Getfirstelement()

   ' id の値を取得
   vnd.Result_ID = GetPropertyValue(vjnav, "id")

   ’ 返答を取得
   ' ① output ノードを取得

   Set je = GetProperty(vjnav, "output")

   ' ② "type": "message" のノードを取得
   Set je = FindInArray(je, "type", "message")

   ' ③ content ノードを取得
   Set je = GetProperty(je, "content")

   ' ④ "type": "output_text"
   Set je = FindInArray(je, "type", "output_text")

   ' ⑤ 返答を取得してセット
   vnd.Result_Content = GetPropertyValue(je, "text")

   xGetResponce = True
End Function


最後に作成した関数をメインルーチンに組み込んで完成です。

Sub Initialize
         ・・・(省略)・・・
   '問い合わせ実行
   Set jnavResponse = xAskResponses(jnavRequest)
   Call xSetJSON(nd, "Response", jnavResponse)   
'Response の保存

   '返答を取得
   If xGetResponce(nd, jnavResponse) Then
      Call nd.Save(True, False)

      '文書を開きなおす
      Call nuid.Close()
      Call nuiw.EditDocument(False, nd)
   End If
End Sub


実行して、ID と回答が取得できれば OK です。


まとめ

今回はレスポンス JSON から必要な情報を取得する部分を作成しました。

その過程で JSON から必要な情報を効率よく抽出するための関数群作成し、利用しました。xGetResponce 関数を見ていただけるとわかると思いますが、JSON をどのようにたどっているのか、一目瞭然になっています。これだけシンプルになっていれば、処理内容が一瞬で理解できます。

『動けばいい』でとどめず、後々のメンテナンス性まで考えてコーディングしたいですね。

内容的には、本題の Responses API からは少々外れてしまったような気もしますが ...


前回 連載:つないでみよう 次回


2026/07/18

つないでみよう:#32) Responses API ー サンプルアプリ作成 ①

今回からは Responses API で会話を行うサンプルアプリを作成します。

前回紹介したように Responses API と Chat Completion API は、エンドポイントと送受信する JSON が違うだけで、サンプルアプリの構造に特別なことはありません。以前作成した Chat Completion API のサンプルを改造して進めることも可能なのですが、記事が独立するよう、今回は初めから順に作成したいと思います。


フォームの作成

Responses API に送信する情報と結果を記録するためのフォームを作成します。

各フィールドの説明は詳細にはしませんが、『リクエスト設定』が Responses API に送信する内容の入力欄、『リクエスト』が送信した情報、『レスポンス』が Responses API の応答を記録するエリアです。

JSON を保存するリッチテキストフィールドはセクション内に配置し、展開/省略を次のように設定します。

画像の生成や解析を行う場合など JSON が大きくなると、文書を開くのが遅くなります。ただ、閉じたセクション内にフィールドが存在する場合は、文書を開くタイミングではなく、セクションを開くタイミングで遅くなります。

上記のように最初は閉じるようにしておくと、見たいときだけ待たされる構造にできます。フィールドサイズを表示しているので、待たされるかどうかもわかりますね。


エージェントの作成

上記フォームで作成された文書を使って、Responses API に問い合わせを行うエージェントを作成します。

まずは、メインルーチンです。① API に送信する JSON を作成、② API をコール、最後に、結果を保存して UI に再表示しています。

Option Declare

Private xns As NotesSession
Private Const xcsBearer = "sk-xxxxx"   'API キーをここにセット

Sub Initialize
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim ndb As NotesDatabase
   Dim nd As NotesDocument

   Dim jnavRequest As NotesJSONNavigator
   Dim jnavResponse As NotesJSONNavigator

   '初期化
   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase
   Set nuid = nuiw.Currentdocument
   Set nd = nuid.Document   ' UI の文書は読み込みモードの前提

   '① Request Body(JSON) の作成
   Set jnavRequest = xGetRequest_JSON(nd)
   Call xSetJSON(nd, "Request", jnavRequest)   'Request Body の保存

   '② 問い合わせ実行
   Set jnavResponse = xAskResponses(jnavRequest)
   Call xSetJSON(nd, "Response", jnavResponse)   'Response の保存

   '保存して開きなおす
   Call nd.Save(True, False)
   Call nuid.Close()
   Call nuiw.EditDocument(False, nd)
End Sub


作成する JSON は単純なものでした。

{
   "model": "gpt-5-nano",
   "input": "大阪府の県庁所在地は?"
}

なので、この JSON を作成する関数はいたってシンプルです。

Function xGetRequest_JSON(vnd As NotesDocument) As NotesJSONNavigator
   Dim jnav As NotesJSONNavigator

   'RequestBody(JSON) の準備
   Set jnav = xns.CreateJSONNavigator("")

   '1.model
   Call jnav.AppendElement(vnd.Model(0),"model")

   '1.input
   Call jnav.AppendElement(vnd.Prompt(0),"input")

   Set xGetRequest_JSON = jnav
End Function


続いて、Responses API に問い合わせを行う関数です。

Function xAskResponses(vjnavRequest As NotesJSONNavigator) As NotesJSONNavigator
   Dim sURL As String
   Dim http As NotesHTTPRequest
   Dim jnav As NotesJSONNavigator

   'HTTP リクエストの準備
   Set http = xns.CreateHTTPRequest()

   'HTTP ヘッダーの設定
   Call http.SetHeaderField("Content-Type", "application/json")
   Call http.SetHeaderField("Authorization", "Bearer " & xcsBearer)

   'API 実行
   http.PreferJSONNavigator = True
   sURL = "https://api.openai.com/v1/responses"      'エンドポイント
   Set jnav = http.Post(sURL, vjnavRequest.Stringify())

   'Response をセット
   Set xAskResponses = jnav
End Function

戻り値はレスポンスの JSON で、NotesJSONNavigator のオブジェクトを返します。


最後は送受信した JSON を文書に記録する関数です。引数の NotesJSONNavigator から JSON を抽出し、指定したフィールドにセットします。その際、JSON のサイズと時刻も記録しています。

Private Function xSetJSON(vnd As NotesDocument, ByVal vsFld As String, vjnav As NotesJSONNavigator)
   Dim nrti As NotesRichTextItem
   Dim sText As String

   On Error Resume Next

   'JSON(文字列)を取得 と サイズの記録
   If vjnav Is Nothing Then
      sText = "(Nothing)"
      Call vnd.ReplaceItemValue(vsFld & "_Size", "")
   Else
      sText = vjnav.Stringify()
      Call vnd.ReplaceItemValue(vsFld & "_Size", Len(sText))
   End If

   '日時の記録
   Call vnd.ReplaceItemValue(vsFld & "_DT", Now)

   'JSON の記録
   Call vnd.RemoveItem(vsFld & "_JSON")   'いったん削除してからセット
   Set nrti = vnd.CreateRichTextItem(vsFld & "_JSON")
   Call nrti.AppendText(sText)
End Function


動作検証

ここまで出来上がったらテスト実行します。

AI model に gpt-5-nano を指定し、プロンプトを記述します。文書を保存して、エージェントを実行します。

うまくいくと、次のように送受信の結果が記録されます。


まとめと今後の予定

今回は Responses API に問い合わせを行う基本的な部分を作成しました。ここまでは、以前の Chat Completion API のサンプルと大差がなく、あまり目新しさがありませんでしたが、今後はこれを拡張しながら、Responses API のさまざまな機能を確認していきます。

まず、次回は、レスポンスの JSON から必要な情報を抽出する部分を作成します。



前回 連載:つないでみよう 次回


2026/06/23

つないでみよう:#31) Responses API - 会話

今回から  Responses API を実際に使ってみましょう。まずは、会話から始めます。#2 で紹介した Chat Completion API と比較しながら進めたいと思います。


API のコールの仕方

まず、API の URL であるエンドポイントは以下の通りです。

Responses API Chat Completion API
https://api.openai.com/v1/responses https://api.openai.com/v1/chat/completions


HTTP ヘッダの指定方法には変化はないようです。どちらの API も以下の項目を指定します。

HTTP ヘッダ 設定値 補足
Content-Type application/json 送信するリクエストは JSON
Authorization Bearer (API キー) 別途取得した API キーを送信し認証


リクエストの記述は大きく変わっています。Chat Completion API では messages ノードに記述しましたが、Responses API では input ノードとなっています。

Chat Completion API では、構造がシンプルになっている点がありがたいですね。

Responses API Chat Completion API
{
   "model": "gpt-5-nano",
   "input": "大阪府の県庁所在地は?"
}
{
   "model": "gpt-5-nano",
   "messages": [
      {
         "role": "user",
         "content": "大阪府の県庁所在地は?"
      }
   ]
}

この事例では『大阪府の県庁所在地は?』とテキストで問い合わせるだけなのでシンプルな構造となっています。この input ノードの書き方で、テキストの問い合わせに画像を添えたり、柔軟に指定できます。結果的には、仕組みとしては messages ノードと大差ないと言えますね。


API コール

今回は、API の動作検証を兼ねて Postman でテスト実行してみます。

まず、”POST” を選択し、エンドポイントを入力します。ヘッダーには Authorization を追加し、API キーを指定します(Content-Type はデフォルトでセット済み)。最後に、ボディに送信する JSON を入力して完了です。

準備ができたら[送信]ボタンをクリックします。正しく送信できると、画面下部にレスポンスの JSON が表示されます。

Chat Completion API のレスポンスと比較しながら確認します。赤字の部分がリクエストに対する返答にあたる部分です。

Responses API Chat Completion API
{
   "id": "resp_xxxxx",
   "object": "response",
   "created_at": 1782125043,
   "status": "completed",
   "background": false,
   "billing": {
      "payer": "developer"
   },
   "completed_at": 1782125046,
   "error": null,
   "frequency_penalty": 0.0,
   "incomplete_details": null,
   "instructions": null,
   "max_output_tokens": null,
   "max_tool_calls": null,
   "model": "gpt-5-nano-2025-08-07",
   "moderation": null,
   "output": [
      {
         "id": "rs_xxxxx",
         "type": "reasoning",
         "content": [],
         "summary": []
      },
      {
         "id": "msg_xxxxx",
         "type": "message",
         "status": "completed",
         "content": [
            {
               "type": "output_text",
               "annotations": [],
               "logprobs": [],
               "text": "大阪府の県庁所在地は大阪市です。"
            }
         ],
         "role": "assistant"
      }
   ],
         ・・・(省略)・・・
}
{
   "id": "chatcmpl-xxxxx",
   "object": "chat.completion",
   "created": 1782126352,
   "model": "gpt-5-nano-2025-08-07",
   "choices": [
      {
         "index": 0,
         "message": {
            "role": "assistant",
            "content": "大阪府の県庁所在地は大阪市です。",
            "refusal": null,
            "annotations": []
         },
         "finish_reason": "stop"
      }
   ],
         ・・・(省略)・・・
}

Responses API は、Chat Completions API よりも汎用的かつ拡張性の高い API です。推論やツール実行などの情報を含めて返却するため、レスポンス JSON はより複雑な構造になっています。

output ノード内の要素で type が message となっているものが返答にとなります。output ノードは配列になっていて、リクエストに応じて要素数が可変となります。ですので、各要素の type を判定して返答を探す必要があります。

type にはさまざまな種類があり、抜粋すると次の通りです。

type 役割
message 最終回答
reasoning 内部推論の情報
function_call 関数(ツール)の呼び出し要求
function_call_output 関数実行結果
image_generation_call 画像生成の実行


会話の継続

Responses API では、会話の継続がサポートされています。

使い方は簡単で、previous_response_id に継続元のレスポンスを指定するだけです。具体的には、レスポンスの最初にある id(resp_ で始まる id)を指定するだけです。

Responses API Chat Completion API
{
   "model": "gpt-5-nano",
   "previous_response_id": "resp_xxxxx",
   "input": "そこの面積は?"
}
{
   "model": "gpt-5-nano",
   "messages": [
      {
         "role": "user",
         "content": "大阪府の県庁所在地は?"
      },
      {
         "role": "assistant",
         "content": "大阪府の県庁所在地は大阪市です。"
      }
,
      {
         "role": "user",
         "content": "そこの面積は?"
      }
   ]
}

Chat Completion API では、過去の会話をリクエストの JSON に含める必要がありました。会話が長くなると送信する JSON が膨大になります。

Responses API で会話を継続を使えば、課金額の節約にもなりますね。


前回 連載:つないでみよう 次回