2025/01/07

作ってみよう:#20)スマート名刺管理 - GTP4o API リクエストの作成 ④

これまでに完成した部分のテストを行います。

テストするために必要なエージェントなどの機能を追加します。テスト後も使用する機能となるので、作業をパスせず作成してください。


エージェントの作成

作成した関数をコールするエージェントを作成します。

名前 (ReadNameCard)
別名 ReadNameCard
トリガー イベント - エージェントリストの選択
対象 なし

Option Declare
Use "lsReadNameCard"

Private xns As NotesSession
Private xndb As NotesDatabase

Sub Initialize
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim nd As NotesDocument
   Dim sID As String

   Set xns = New NotesSession
   Set xndb = xns.CurrentDatabase

   'UI 文書を保存(リッチテキストを保存)
   Set nuid = nuiw.CurrentDocument
   Call nuid.Save()

   'ID 取得
   Set nd = nuid.Document
   sID = nd.Noteid

   'UI 文書を閉じる
   Set nd = Nothing '一旦文書開放
   Call nuid.Close(True)

   '保存した文書を再表示
   Set nd = xndb.GetDocumentByID(sID)

   'AI で名刺を読み込み
   Call ReadNameCard(nd)

   '名刺管理管理フィールドセット
   nd.ExchangeDate = Today
   nd.Status = "3"   '3 = AI 問い合わせ完了

   '文書の保存と画面の表示
   Call nd.Save(True, False)
   Set nuid = nuiw.Editdocument(True, nd)
End Sub

このエージェントでは、入力中の文書に貼り付けられた画像(リッチテキスト)をライブラリ内でバックエンド文書として確実に取得するため、UI からいったん保存して、UI を閉じたうえで、バックエンド文書を再取得しています。


名刺読込ボタン作成

名刺管理フォームを開き、[名刺読込]ボタンを追加します。

ボタン名 Click 補足
名刺読込 @Command([ToolsRunMacro];"(ReadNameCard)") 編集時と Status が 0 の時のみ表示
---
!(Status="0")


JSON 確認フィールドの追加

名刺管理フォームの管理者用セクションに JSON 確認用フィールドを作成します。

項目 フィールド名 種類 補足
認識した
名刺情報
JSON_NameCard リッチテキスト 編集可能
レスポンス JSON_Responce
リクエスト JSON_Request

ライブラリの調整

テスト実行のため送信する JSON を作成する部分だけ実行するように調整します。スクリプトライブラリを開き、現時点で完成していない部分をコメントアウトします。

Public Function ReadNameCard(vnd As NotesDocument) As Boolean
   Dim jnavRequest As NotesJSONNavigator
   Dim jnavResponce As NotesJSONNavigator
   Dim jnavNameCard As NotesJSONNavigator

   '1. API リクエスト時に送信する JSON を作成
   Set jnavRequest = xMakeRequest(vnd)
   Call xSetRT(vnd, "JSON_Request", jnavRequest.Stringify)

   '2. API をコールし、結果の JSON を取得
   'Set jnavResponce = xCallWebAPI(jnavRequest)
   'Call xSetRT(vnd, "JSON_Responce", jnavResponce.Stringify)

   '3. 結果の JSON 内から名刺情報部分だけの JSON を取得
   'Set jnavNameCard = xGetNameCard(jnavResponce)
   'Call xSetRT(vnd, "JSON_NameCard", jnavNameCard.Stringify)

   '4. 名刺情報を文書に保存
   'Call xSaveNameCard(jnavNameCard , vnd)

End Function

そして、作成した JSON をリッチテキストに出力する関数 xSetRT を有効化します。この関数はまだ作成していなかったので、ライブラリに追加します。

Private Function xSetRT(vnd As NotesDocument, ByVal vsFld As String, ByVal vsVal As String)
   Dim nrti As NotesRichTextItem

   Call vnd.RemoveItem(vsFld)
   Set nrti = vnd.CreateRichTextItem(vsFld)

   Call nrti.AppendText(vsVal)
End Function


動作検証

ノーツから文書を作成し、名刺の画像を添付し、[名刺読込] ボタンをクリックします。


クリック後、JSON が記録されていたら成功です。

画像データ(Base64 の文字列)が生成されていること、添付ファイルでもインラインイメージでも出力されていることを確認しましょう。

なお、確認が終われば xSetRT 関数のコールはコメントアウトしておきましょう。管理者用セクションの開閉が遅くなりますので...


◇ 『文書は保存されていません。』エラー

[名刺読込] ボタンをクリックすると、いきなり下記のエラーが表示されることがあります。エージェントのコードでは、Save メソッドで保存しているので、意味が分かりにくいですよね。

このエラーの原因はエージェントの設定です。対象が『すべての選択文書』などになっていると思います。これを『なし』にすると改善します。


次回の予定

今回でリクエストの作成部分は終了です。次回からは WebAPI をコールする部分を作成します。


前回 作ってみよう 次回


2025/01/06

作ってみよう:#19)スマート名刺管理 - GTP4o API リクエストの作成 ③

リクエスト時の送信する JSON の最後のパートは、response_format ノードを作成する部分です。

このシリーズの最初に説明した通り、GPT4o の Structured Outputs を利用します。この機能については、以下のリンクで紹介していますので、必要に応じてご確認ください。


response_format の指定

まず作成するノードの確認です。

図で省略している部分を含め、指定するスキーマ設定をまとめると次の通りです。

スキーマ名 readnamecard

◇ 指示する JSON ノードの定義

ノード 説明
companyname_full string 会社名
companyname 会社名(法人格なし)
companyname_kn カナ会社名(法人格なし)。記載がない場合は会社名から類推。
name 氏名(姓名の間は半角スペース)
name_kn カナ氏名(姓名の間は半角スペース)。記載がない場合は氏名から類推。
name_en 英字氏名(半角英字で姓名の間は半角スペース)。記載がない場合は氏名から類推。
dept 所属
role 役職・肩書
zip 郵便番号(半角数字、ハイフンなし)
address 住所
tel 電話番号
mobile 携帯電話番号
fax Fax番号
email eメールアドレス
url URL


response_format ノードの作成

response_format ノードを作成する関数は次の通りです。工夫が足りずだらだら長くなっていますがご容赦ください。

Private Function xMakeRequest_ResponseFormat(vjnav As NotesJSONNavigator) As Boolean
   Dim jo As NotesJSONObject

   '1) response_format
   Dim joResponseFormat As NotesJSONObject
   Set joResponseFormat = vjnav.AppendObject("response_format")

   Call joResponseFormat.AppendElement("json_schema", "type")

   '2) json_schema
   Dim joJsonSchema As NotesJSONObject
   Set joJsonSchema = joResponseFormat.AppendObject("json_schema")

   Call joJsonSchema.AppendElement("readnamecard", "name")
   Call joJsonSchema.AppendElement(True, "strict")

   '3) schema
   Dim joSchema As NotesJSONObject
   Set joSchema = joJsonSchema.AppendObject("schema")
   Call joSchema.AppendElement("object", "type")

   '4) properties
   Dim joProp As NotesJSONObject
   Set joProp = joSchema.AppendObject("properties")

   '5) companyname_full
   Set jo = joProp.AppendObject("companyname_full")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("会社名", "description")

   '5) companyname
   Set jo = joProp.AppendObject("companyname")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("会社名(法人格なし)", "description")

   '5) companyname_kn
   Set jo = joProp.AppendObject("companyname_kn")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("カナ会社名(法人格なし)。記載がない場合は会社名から類推。", "description")

   '5) name
   Set jo = joProp.AppendObject("name")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("氏名(姓名の間は半角スペース)", "description")

   '5) name_kn
   Set jo = joProp.AppendObject("name_kn")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("カナ氏名(姓名の間は半角スペース)。記載がない場合は氏名から類推。", "description")

   '5) name_en
   Set jo = joProp.AppendObject("name_en")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("英字氏名(半角英字で姓名の間は半角スペース)。記載がない場合は氏名から類推。", "description")

   '5) dept
   Set jo = joProp.AppendObject("dept")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("所属", "description")

   '5) role
   Set jo = joProp.AppendObject("role")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("役職・肩書", "description")

   '5) zip
   Set jo = joProp.AppendObject("zip")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("郵便番号(半角数字、ハイフンなし)", "description")

   '5) address
   Set jo = joProp.AppendObject("address")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("住所", "description")

   '5) tel
   Set jo = joProp.AppendObject("tel")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("電話番号", "description")

   '5) mobile
   Set jo = joProp.AppendObject("mobile")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("携帯電話番号", "description")

   '5) fax

   Set jo = joProp.AppendObject("fax")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("Fax番号", "description")

   '5) email
   Set jo = joProp.AppendObject("email")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("eメールアドレス", "description")

   '5) url
   Set jo = joProp.AppendObject("url")
   Call jo.AppendElement("string", "type")
   Call jo.AppendElement("URL", "description")

   '4) required
   Dim jaRequired As NotesJSONArray
   Set jaRequired = joSchema.AppendArray("required")

   Call jaRequired.AppendElement("companyname_full")
   Call jaRequired.AppendElement("companyname")
   Call jaRequired.AppendElement("companyname_kn")
   Call jaRequired.AppendElement("name")
   Call jaRequired.AppendElement("name_kn")
   Call jaRequired.AppendElement("name_en")
   Call jaRequired.AppendElement("dept")
   Call jaRequired.AppendElement("role")
   Call jaRequired.AppendElement("zip")
   Call jaRequired.AppendElement("address")
   Call jaRequired.AppendElement("tel")
   Call jaRequired.AppendElement("mobile")
   Call jaRequired.AppendElement("fax")
   Call jaRequired.AppendElement("email")
   Call jaRequired.AppendElement("url")

   '4) additionalProperties
   Call joSchema.AppendElement(False, "additionalProperties")
End Function

こういった階層構造が複雑な JSON を操作するプログラムの場合、得てして現在位置(=操作しているノードとレベル)を見失いがちです。開発している時点では理解できていても、しばらくたってコードを見るとつかみにくいです。その対応として、コメントのノード名の前にレベルをつけるようにしてチェックしやすくしています。


次回の予定

これで、リクエスト時の送信する JSON は完成です。次回はテスト実行して、JSON が正しくできるか確認します。


前回 作ってみよう 次回


2025/01/05

作ってみよう:#18)スマート名刺管理 - GTP4o API リクエストの作成 ②

GPT4o に画像を送信する JSON フォーマットで、赤線の部分が画像ファイルを Base64 でエンコードした文字列でした。この文字列を作成する部分を解説します。


画像データの取得方針

リッチテキストライトフィールドに Nomad から写真を添付する方法は、写真と添付ファイルの 2 種類の方法があります。写真を選択すると添付ファイルになります。カメラから撮影すると貼り付ける際にイメージの縮小が可能です。画像サイズを小さくしたいとき有効なのですが、この方法だとリッチテキストにインラインイメージ(リッチテキストに見える状態で貼り付けた画像)となります。

そこで今回は、添付ファイルとインラインイメージの双方に対応したいと思います。

ただ、インラインイメージは LotusScript の通常の NotesRichText*** クラスではアクセスできません。そこで、今回は DXL を使用して、リッチテキスト内のデータを取得します。

このシリーズにおいて、この記事だけは JSON ではなく DXL について記載しています。ご注意ください。なお、DXL を使用して、画像や添付ファイルを取得する方法については以下のリンクを参照ください。


画像取得関数

画像データを取得する関数は、大きく 2 つの処理に分かれています。まず、添付ファイルを探し、見つからない場合は、インラインイメージを取得しています。

戻り値は、取得した画像の Base64 文字列となります。

Private Function xGetImage_Base64(vnd As NotesDocument, ByVal vsFldName As String) As String
   '最初の添付ファイルを Base64 文字列で取得
   xGetImage_Base64 = xGetDXL_FirstAttachment64(vnd, "Body")

   If xGetImage_Base64 = "" Then
      '添付はない(= 最初のインラインイメージを取得)
      xGetImage_Base64 = xGetDXL_FirstInlineImage64(vnd, "Body")
   End If
End Function

なお、添付ファイルを先にチェックしているには理由があります。

添付ファイルを DXL に変換するとそこにアイコンの画像が含まれます。この画像がインラインイメージと同じ構造となっています。先にインラインイメージを検索すると添付ファイルのアイコン画像を取得してしまう可能性があるからです。


最初の添付ファイルの取得

DXL Step-by-Step:#46)添付ファイルの取得 ① で紹介した関数とほぼ同じです。

ただ、API に送信するのも Base64 エンコード、DXL から取得するのも同じです。リンクの記事の関数はデコードしてバイナリデータで返す仕様だったのですが、そこを省き、取得した Base64 文字列をそのまま戻り値として返すようにしています。

Private Function xGetDXL_FirstAttachment64(vnd As NotesDocument, ByVal vsFld As String) As String
   'DXL の準備
   Dim dprs As NotesDOMParser
   Set dprs = xGetDOMParser(vnd)

   'DOM ツリーのルートを取得
   Dim ddn As NotesDOMDocumentNode
   Set ddn = dprs.Document

   'リッチテキストの取得
   Dim denRT As NotesDOMElementNode
   Set denRT = xGetDXL_item(ddn, vsFld)

   '添付情報の取得
   Dim denRef As NotesDOMElementNode
   Set denRef = xGetDXL_FirstNodeByName(denRT, "attachmentref")

   If Not (denRef Is Nothing) Then
      'ファイル名取得
      Dim sName As String
      Dim sDisp As String
      sName = denRef.GetAttribute("name")
      sDisp = denRef.GetAttribute("displayname")

      '$FILE フィールド取得
      Dim denFile As NotesDOMElementNode
      Set denFile = xGetDXL_FileItem(ddn, sName) 'Notes 内部の名称で検索

      If Not (denFile Is Nothing) Then
         'ファイルの中身を取得
         Dim denData As NotesDOMElementNode
         Set denData = xGetDXL_FirstNodeByName(denFile, "filedata")

         If Not (denData Is Nothing) Then
            Dim dtn As NotesDOMTextNode
            Dim sB64 As String

            'エンコードされたファイルを取得
            Set dtn = denData.FirstChild
            sB64 = dtn.NodeValue

            '戻り値セット
            xGetDXL_FirstAttachment64 = sB64
         End If
      End If
   End If
End Function


最初のインラインイメージの取得

こちらも #42)インラインイメージの取得 で紹介した関数とほぼ同じなのですが、上記度同様に、取得した Base64 文字列をそのまま戻り値として返すようにしています。

Private Function xGetDXL_FirstInlineImage64(vnd As NotesDocument, ByVal vsFld As String) As String
   'DXL の準備
   Dim dprs As NotesDOMParser
   Set dprs = xGetDOMParser(vnd)

   'DOM ツリーのルートを取得
   Dim ddn As NotesDOMDocumentNode
   Set ddn = dprs.Document

   'リッチテキストの取得
   Dim denRT As NotesDOMElementNode
   Set denRT = xGetDXL_item(ddn, vsFld)

   '画像の取得
   Dim denPct As NotesDOMElementNode
   Set denPct = xGetDXL_FirstNodeByName(denRT, "picture")

   '画像ファイル取得
   Dim denImg As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
   Dim dtn As NotesDOMTextNode
   Dim sB64 As String

   Dim asTag(2) As String
   asTag(0) = "png"
   asTag(1) = "gif"
   asTag(2) = "jpeg"

   '画像データの捜索
   If Not(denPct Is Nothing) Then
      ForAll sTag In asTag
         Set denImg = xGetDXL_FirstNodeByName(denPct, sTag)
         If Not(denImg Is Nothing) Then
            'エンコードされた画像データ取得
            Set dtn = denImg.FirstChild
            sB64 = dtn.NodeValue

            '戻り値セット
            xGetDXL_FirstInlineImage64 = sB64
            Exit ForAll
         End If
      End ForAll
   End If
End Function


その他サブ関数

上記 2 関数からコールされている関数群を列挙します。こちらにまとめた関数は過去の記事の関数をそのまま再掲しています。

◇ 文書を DXL に変換

Private Function xGetDOMParser(vnd As NotesDocument) As NotesDOMParser
   'Dominoデータ を DXL に変換する準備
   Dim dexp As NotesDXLExporter
   Set dexp = xns.CreateDXLExporter()
   Call dexp.SetInput(vnd)

   'パーサーに変換する DXL をセット
   Dim dprs As NotesDOMParser
   Set dprs = xns.CreateDOMParser()
   Call dprs.SetInput(dexp)

   'DXL 変換を実行
   Call dexp.Process()

   Set xGetDOMParser = dprs
End Function

◇ 検索して最初のノードを取得

Private Function xGetDXL_FirstNodeByName(vdenParent As NotesDOMElementNode, ByVal vsName As String) As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList

   On Error Resume Next

   '配下のノードを検索
   Set dnl = vdenParent.GetElementsByTagName(vsName)
   If dnl.NumberOfEntries > 0 Then
      '最初のノードを戻り値にセット
      Set xGetDXL_FirstNodeByName = dnl.GetItem(1)
   End If
End Function

◇ フィールドを検索して取得

Private Function xGetDXL_item(vddn As NotesDOMDocumentNode, ByVal vsFld As String) As NotesDOMElementNode
   '文書のノードの取得
   Dim denDoc As NotesDOMElementNode
   Set denDoc = vddn.DocumentElement

   'itemノードをチェック
   Dim den As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
   Dim i As Integer
   Dim sFld As String

   Set dnl = denDoc.GetElementsByTagName("item")
   For i = 1 To dnl.NumberOfEntries
      Set den = dnl.GetItem(i)

      sFld = den.GetAttribute("name")
      If LCase(sFld) = LCase(vsFld) Then
         '戻り値セットしてループ脱出
         Set xGetDXL_item = den
         Exit For
      End If
   Next
End Function

◇ 添付ファイルを検索して取得

Private Function xGetDXL_FileItem(vddn As NotesDOMDocumentNode, ByVal vsName As String) As NotesDOMElementNode
   '文書のノードの取得
   Dim denDoc As NotesDOMElementNode
   Set denDoc = vddn.DocumentElement

   'itemノードをチェック
   Dim denItem As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
   Dim i As Integer
   Dim sFld As String
   Dim sName As String
   Dim den As NotesDOMElementNode

   Set dnl = denDoc.GetElementsByTagName("item")
   For i = 1 To dnl.NumberOfEntries
      Set denItem = dnl.GetItem(i)
      sFld = denItem.GetAttribute("name")
      If LCase(sFld) = LCase("$FILE") Then
         '添付ファイルなのでチェックを継続
         'object ノード取得
         Set den = xGetDXL_FirstNodeByName(denItem, "object")
         If Not (den Is Nothing) Then
            'file ノード取得
            Set den = xGetDXL_FirstNodeByName(den, "file")
            If Not (den Is Nothing) Then
               '添付ファイルなので Notes 内部のファイル名取得
               sName = den.GetAttribute("name")
               If LCase(sName) = LCase(vsName) Then
                  'ファイル名一致
                  '戻り値セットしてループ脱出

                  Set xGetDXL_FileItem = den 'file ノード
                  Exit For
               End If
            End If
         End If
      End If
   Next
End Function


前回 作ってみよう 次回


2025/01/04

作ってみよう:#17)スマート名刺管理 - GTP4o API リクエストの作成 ①

いよいよ、GPT4o に対して問い合わせを行うプログラムの開発を開始します。


スクリプトライブラリの作成

API 連携機能のコードをまとめて管理するため LotusScript のライブラリを作成します。

名前 lsReadNameCard

まずは、ライブラリ内で普遍的な値を Private 変数に登録し、初期化時点で取得します。このタイミングで、前回作成した 2 つのプロフィール文書を取得しておきます。


Option Declare

Private xns As NotesSession
Private xndb As NotesDatabase
Private xndWAPI As NotesDocument 'OpenAI API 設定
Private xndChat As NotesDocument 'Chat Completion 設定

Sub Initialize
   Set xns = New NotesSession
   Set xndb = xns.CurrentDatabase

   'プロフィール文書 取得
   Set xndWAPI = xndb.GetProfileDocument("fpStdCfg_OpenAI")
   Set xndChat = xndb.GetProfileDocument("fpStdCfg_OpenAI_ChatCompletion")
End Sub


以降の作業では、関数作成前に呼び出し側を記述する場合があります。そのため、デザイナー上で『N012 SUB または FUNCTION の名前ではありません。』エラーが出ます。このエラーについては気にせず作業を進めてください。完成時にはこのエラーはなくなります。


API コールを行うメイン関数

今回作成するメインの関数です。

ライブラリを利用するエージェントからコールされるので、Public 宣言されています。引数は、名刺管理の文書で、名刺画像が添付されている前提です。

Public Function ReadNameCard(vnd As NotesDocument) As Boolean
   Dim jnavRequest As NotesJSONNavigator
   Dim jnavResponce As NotesJSONNavigator
   Dim jnavNameCard As NotesJSONNavigator

   '1. API リクエスト時に送信する JSON を作成
   Set jnavRequest = xMakeRequest(vnd)
   'Call xSetRT(vnd, "JSON_Request", jnavRequest.Stringify)

   '2. API をコールし、結果の JSON を取得
   Set jnavResponce = xCallWebAPI(jnavRequest)
   'Call xSetRT(vnd, "JSON_Responce", jnavResponce.Stringify)

   '3. 結果の JSON 内から名刺情報部分だけの JSON を取得
   Set jnavNameCard = xGetNameCard(jnavResponce)
   'Call xSetRT(vnd, "JSON_NameCard", jnavNameCard.Stringify)

   '4. 名刺情報を文書に保存
   Call xSaveNameCard(jnavNameCard, vnd)
End Function

コメントに記載している通り、処理は大きく 4 つに分かれます。以降、順次作成します。

なお、xSetRT 関数は JSON をリッチテキストに保存する関数です。デバッグ時など値を確認したいときに有効にします。1 の JSON は画像を含むことから非常に大きくなります。フォームを開くのが遅くなるので必要な時以外は出力しないことをお薦めします。


送信する JSON の作成

API リクエスト時に送信する JSON の全体像は次の通りです。

この構造にそって、JSON を作成する関数が xMakeRequest です。ここでは、JSON 全体を作成することが目的で、3 つの各ノードを具体的に作成するのは、それぞれサブ関数が担当しています。

Private Function xMakeRequest(vnd As NotesDocument) As NotesJSONNavigator
   Dim jnav As NotesJSONNavigator

   '送信する JSON(RequestBody)の準備
   Set jnav = xns.CreateJSONNavigator("")

   '1) model
   Call xMakeRequest_Model(jnav)

   '1) messages
   Call xMakeRequest_Message(jnav, vnd)

   '1) response_format
   Call xMakeRequest_ResponseFormat(jnav)

   Set xMakeRequest = jnav
End Function


model ノードの作成

コールする API のモデルを指定する部分です。構造が単純なので関数も簡単です。

Private Function xMakeRequest_Model(vjnav As NotesJSONNavigator) As Boolean
   Dim sModel As String

   'Chat Completion 設定 からモデル指定を取得
   sModel = xndChat.Model(0)
   If sModel = "" Then
      sModel = "gpt-4o-2024-08-06"
   End If

   Call vjnav.AppendElement(sModel, "model")
End Function


messages ノードの作成

画像送信するパターンの messages ノードの指定については、別の連載『つないでみよう』の #16 でまとめています。必要に応じてご確認ください。

このノードの指定は大きく 2 つに分かれます。システムの役割を指定する設定とユーザのリクエストです。今回は、AI の役割は OCR であることを伝え、画像を送信するだけという単純な構造としています。


messages ノードを作成する関数 xMakeRequest_Message は次の通りです。

Private Function xMakeRequest_Message(vjnav As NotesJSONNavigator, vnd As NotesDocument) As Boolean
   Dim jaMsg As NotesJSONArray
   Dim joMsg As NotesJSONObject
   Dim jaCnt As NotesJSONArray
   Dim joCnt As NotesJSONObject
   Dim jo As NotesJSONObject
   Dim s As String
   Dim sSystem As String

   '1) messages
   Set jaMsg = vjnav.AppendArray("messages")

   'OpenAI Chat Completion 設定 から AI の役割を取得
   sSystem = xndChat.SystemRole(0)

   '2) system ロール
   If sSystem <> "" Then
      '指定があれば追加
      Set joMsg = jaMsg.AppendObject()
      Call joMsg.AppendElement("system", "role")
      Call joMsg.AppendElement(sSystem, "content")
   End If

   '2) user ロール
   Set joCnt = jaMsg.AppendObject()
   Call joCnt.AppendElement("user", "role")

   Set jaCnt = joCnt.AppendArray("content")

   '3) image_url 名刺画像
   Set joCnt = jaCnt.AppendObject()
   Call joCnt.AppendElement("image_url", "type")

   Set jo = joCnt.Appendobject("image_url")
   s = "data:image/jpeg;base64,{" & xGetImage_Base64(vnd, "Body") & "}"
   Call jo.AppendElement(s, "url")
End Function

GPT4o に画像を送信する JSON は以下のようなフォーマットで、赤線の部分が画像ファイルを Base64 でエンコードした文字列となります。文書内の添付ファイルからこの文字列を取得する関数が xGetImage_Base64 となります。この関数については次回紹介します。


前回 作ってみよう 次回


2025/01/03

作ってみよう:#16)スマート名刺管理 - GTP4o API 連携設定フォームの作成

今回は、GTP4o API を利用するために必要な設定を保存する機能を作成します。


OpenAI API 設定フォームの作成

このフォームは OpenAI 社の API を利用する際に共通で利用する設定を保存するためのフォームです。現時点では API キーの項目 1 つだけとなります。

フォームを新規作成します。

フォーム名 Std.API設定\11.OpenAI
別名 fpStdCfg_OpenAI
ウィンドウ
タイトル
"API 設定 - OpenAI"


作成するフィールドは次の通りです。

項目 フィールド名 種類 補足
Bearer Bearer テキスト 編集可能


◇ アクションボタン

フォームのアクションボタンは次の通りです。

ボタン名 Click 補足
閉じる @Command([FileCloseWindow])


OpenAI Chat Completion 設定フォームの作成

OpenAI 社の数ある API の中で今回利用する Chat Completion API 用の設定をご存するフォームです。

フォームを新規作成します。

フォーム名 Std.API設定\12.ChatCompletion
別名 fpStdCfg_OpenAI_ChatCompletion
ウィンドウ
タイトル
"API 設定 - ChatCompletion"


作成するフィールドは次の通りです。

項目 フィールド名 種類 補足
End Point EndPoint テキスト 編集可能
AI model Model テキスト 編集可能
AI の役割 SystemRole テキスト 編集可能


◇ アクションボタン

フォームのアクションボタンは次の通りです。

ボタン名 Click 補足
閉じる @Command([FileCloseWindow])


管理者メニューに追加

アウトライン olMain を開き、作成したフォームをプロフィール文書として開くメニューを追加します。

ラベル 内容
OpenAI 設定 アクション - 計算結果 @Command([EditProfile]; "fpStdCfg_OpenAI")
Chat 設定 アクション - 計算結果 @Command([EditProfile]; "fpStdCfg_OpenAI_ChatCompletion")


プロフィール文書の作成

フォームとメニューが完成したら、ノーツからプロフィール文書を作成、保存します。

[OpenAI 設定] ボタンをクリックし、OpenAI 社の API を使用するための API キーを入力します。キーの取得方法について不明な場合は以下の記事を参照ください。

つないでみよう:#1)新連載スタート!てはじめは今年はやった”アレ”


続いて [Chat 設定] を開き、ChatCompletion API に対する設定を記述します。これらの設定は、今後作成する機能が API をコールする際に参照します。

End Point https://api.openai.com/v1/chat/completions
AI model gpt-4o-2024-08-06
AI の役割 あなたはOCRです。ユーザは名刺の画像を送信します。名刺に記載された情報を読み取って、ユーザに返します。


前回 作ってみよう 次回


2025/01/02

作ってみよう:#15)スマート名刺管理 - アプリの器を準備

Nomad モバイル対応の前に、まずは通常のノーツアプリとして作成します。

機能は順次作成するので、今回はアプリの大枠を作成します。すべての手順や設定を書くと長くなるので、機能的に影響のない部分やデザイン的な部分は割愛しています。ご了承ください。


アプリケーションの作成

サーバ上に新規で空のアプリケーションを作成します。

ACL の画面を開いて [Admin] ロールを定義し、自分自身に [Admin] ロールを付与します。


名刺管理フォームの作成

まずは、メインの名刺管理フォームを作成します。

フォーム名 01.名刺管理
別名 fNameCard
ウィンドウ
タイトル
@If(@IsNewDoc; "新規作成"; Name + " 様")

作成するフィールドは次の通りです。


◇ 管理者([Admin] ロール)セクション

フォームにセクションを作成して、その中に管理用のフィールドを作成します。

項目 フィールド名 種類 補足
ステータス Status テキスト 編集可能 0 = 下書き(初期値)
3 = AI 問い合わせ完了

セクションの非表示式で [Admin] ロールを持っている人のみ表示させます。

!(
   @IsMember("[Admin]"; @UserRoles)
)

また、セクションは初期状態を閉じておきましょう。

これだけでロールを持たないユーザには、セクション丸ごと非表示にできます。複数のフィールドをまとめて非表示にできるので、メンテナンス性が上がり、見せたくないフィールドが見えてしまうバグが削減できます。


◇ [登録情報]タブ

タブ形式の表の 1 行目です。

項目 フィールド名 種類 補足
名刺交換日 ExchangeDate 日付 編集可能
メモ Memo テキスト
会社名 CompanyName_Full
法人格なし CompanyName
カナ CompanyName_Kn
氏名 Name
カナ Name_Kn
英字 Name_En
所属 Dept
役職・肩書 Role
郵便番号 Zip
住所 Address
電話番号 Tel
携帯番号 Mobile
Fax番号 Fax
メール eMail
URL URL

なお、このタブは Status が 3 以上の時だけ表示するように設定します。

xStatus := @TextToNumber(Status);
xStatus := @If(@IsError(xStatus); 0; xStatus);
!(xStatus >= 3)


◇ [名刺画像]タブ

項目 フィールド名 種類 補足
Body リッチテキスト
ライト
編集可能 写真、画像


◇ アクションボタン

フォームのアクションボタンは次の通りです。

ボタン名 Click 補足
閉じる @Command([FileCloseWindow])
編集 @Command([EditDocument]) 編集時非表示
保存 @Command([FileSave]) 読み込み時非表示


ビューの作成

次にビューを作成します。

ビュー名 00.下書き
別名 vDraft
選択式 SELECT Form = "fNameCard"


◇ 列

仮のビューなので、列は 1 列のみとします。

列名 列の値
最終更新日 シンプルアクション - 最終更新日


◇ アクションボタン

テストに使用するためのボタンを作成します。

ボタン名 Click 補足
閉じる @Command([FileCloseWindow])
新規作成 @Command([Compose]; "fNameCard")


フレームセットの準備

アプリとしての骨子となる、フレームセットと関連する設計要素を作成します。


◇ アウトライン

新規でアウトラインを作成し、先ほど作成したビューを指定したエントリを[管理]の下に配置します。

名前 olMain

[管理]エントリは、セクションの非表示式と同じ設定をして、[Admin] ロールを持っている人のみ表示させます。

!(
   @IsMember("[Admin]"; @UserRoles)
)


◇ メニューページ

新規でページを作成します。メニューから[作成]-[埋め込み設計要素]-[アウトライン]で、先ほどのアウトラインを呼び出します。

名前 pMenu_Main


埋め込みアウトラインのデザインは、お好みに合わせて設定してください。


◇ タイトルページ

フレームセットのタイトルに使用するページを追加します。

名前 pTitle

上図では、計算結果テキストを使って、@DbTitle でアプリケーション名を表示しています。なお、このページもアプリの機能には影響を与えないので、適当に設定してください。


◇ フレームセット

最後に、3 つにフレーム分けされたフレームセットを作成します。

名前 fsMain
タイトル @DbTitle

フレーム 名前 内容 補足
frmHeader pTitle ページ
frmMenu pMenu_Main ページ デフォルトターゲットは frmView
frmView 00.下書き ビュー


起動設定と確認

データベースのプロパティを開き、作成したフレームを初期表示するように設定します。

ここまでできたら、アプリをノーツで開き、動作確認します。フレームセットが表示され、[新規作成] ボタンでフォームが開けば OK です。


前回 作ってみよう 次回


2025/01/01

作ってみよう:#14)スマート名刺管理 - ChatGPT。お前はもう名刺リーダだ!

あけましておめでとうございます。本年もどうぞよろしくお願いいたします。

2024 年は初めて HCL Ambassador に任命いただいたこともあり、ノーツコンソーシアムの研究会、テクてく Lotus 夜会、DominoHub 2024 Tokyo/Osaka、ノーツしこくフェスタ(NSF)など、さまざまなイベントに参加させていただきました。

幸い 2025 年も HCL Ambassador に任命いただけました。今年はどんなイベントに参加して、どんな出会いがあるのか、ワクワク、ドキドキしております。

さて、年末年始のこの休暇、残念なことにノーツ以外することがありません。そこで、以前作成した ChatGPT を名刺リーダとして使うアプリの作り方を集中連載したいと思います。


アプリの機能

前回テーマ『お小遣い帳』に続き、今回も Nomad アプリです。

Nomad のカメラで撮影した名刺画像を ChatGPT に認識させ、会社名や所属、お名前などの情報をノーツに登録する機能を実現します。以下はデモ動画です。



このアプリ、ノーツコンソーシアム大阪地区研究会のアプリ第2弾として開発を開始して、DominoHub 大阪の展示会場で紹介した『スマート名刺管理』です。

チラシに記載の DXL 拡張ライブラリ dxlSuite for LotusScript を使用とありますが、今回作成するバージョンは、LotusScript 標準のクラス群だけで開発しています。また、DominoHub 大阪以降に習得した GPT4o の Structured Outputs を使用して構築しています。

Nomad モバイル、WebAPI 連係、OpenAI 社 GPT4o API など、最近気になる技術を使用します。そして、Nomad モバイルで作成した写真の取得では DXL を活用します。DXL は Nomad モバイル未対応なのですが、無理やり稼働させるテクニックも使用します。

単機能なアプリですが、技術的にはてんこ盛りです。これを機会に Notes/Domino の新しい一面に出会っていただければ幸いです!


次回以降、アプリの作り方を順次紹介します。


前回 作ってみよう 次回