2025/01/14

作ってみよう:#27)スマート名刺管理 - Nomad 用フレームセットの作成

前回までで Nomad 用の部品が完成したので、これらを組み合わせてアプリとして仕上げます。


メインフレームセットの作成

まず、Nomad 用アプリのメイン画面となるフレームセットを作成します。上下 2 分割とし、上にヘッダーとしてタイトルを表示します。


◇ フレームセットの設定

名前 mfsMain
タイトル @DbTitle


◇ フレームの設定

フレーム 名前 内容 補足
mfrmTitle pTitle ページ
mfrmForm m02.会社名別 フォーム

作成した各フレームのスクロールは ”オフ” にしておきます。


フォーム用のフレームセット作成

Nomad でフォームを開くと上下だけでなく、左右にもスクロールすることがあります。少々使いづらいので、フレームセットを使ってスクロールを止める設定を行います。

1 フレームだけのフレームセットを新規で作成します。


◇ フレームセットの設定

名前 mfsNameCard
タイトル @DbTitle


◇ フレームの設定

フレーム 名前 内容 補足

mfrmForm (設定不要) スクロールは ”オフ” 


Nomad 用フォームの修正

フォームを開くと、先ほど作成したフレームセット内に表示されるように設定します。

m01.名刺管理 フォームのプロパティ、[起動]タブの自動フレームを以下のように設定して保存してください。


UI 切り替えフレームセットの作成

今回 Nomad 用のフレームセット mfsMain を追加しました。ノーツクライアント用のフレームセットも存在します。ということは、アプリを開いたクライアントに応じて、開くフレームセットを切り替える必要があります。

この連載で前回のアプリ「お小遣い帳」を作成したときには、初期表示するページが開くタイミングで LotusScript を使って切り替える方法を紹介しました(#11)お小遣い帳 - Nomad とノーツクライアントの併用)。この記事に対して Miyo HCL Ambassador から『フレームセットの計算式でもできる』とステキな助言をいただきました。そこで、今回はこの方法にチャレンジします(Miyo-san の意図通りの方法かは確認はできていませんが...)。


新規で 1 フレームだけのフレームセットを作成し、フレームの内容を式で指定します。


◇ フレームセットの設定

名前 fsUISwitcher
タイトル @DbTitle


◇ フレームの設定

名前 frmSwitcher
内容 - 種類 名前付き設計要素
フレームセット
値(計算式) @If(@Contains(@Platform([Specific]); "iOS":"Android"); "mfsMain"; "fsMain")


起動画面の設定

最後にデータベースのプロパティを開き、起動画面を fsUISwitcher フレームセットに変更します。

これで、作業は完了です。


動作検証

Nomad から開いて、動作検証をします。以下の動画は、このシリーズの最初に添付したものですが、こんな感じで動作していたら成功です。

添付ファイルだけでなく、名刺の写真を撮影して画像で貼り付けたり、カメラロールから添付したり、さまざまな方法で試してみましょう。


トラブルの対処

最後に、今回のアプリの Nomad 実行時に起こりそうなトラブルについてまとめます。


◇ エージェントが実行されない

[名刺読込]ボタンを押してもエージェントが実行されないことがあります。これは、エージェントのプロパティのセキュリティレベルの設定で解決します。下図の通り 2 または 3 に設定しましょう。


◇ RunOnServer 実行時のエラー

[名刺読込]ボタンでは別のエラーが出ることがあります。

このエラーはエージェントの署名者(設計要素の最終更新者)がサーバでエージェントの実行権限がない場合に発生します。

対応には、サーバ文書のセキュリティ設定が必要です。サーバ管理者に連絡して権限を変更してもらうか、権限があるユーザ ID でエージェントを署名しましょう。


おわりに

『スマート名刺管理』いかがでしたか?

Nomad モバイルとノーツクライアントとのハイブリッドアプリを WebAPI 連係(GPT4o API)、DXL などノーツアプリ開発のさまざまな機能を組み合わせて実現しました。守備範囲が広く、自由度が高く、懐が深いところが Notes/Domino の魅力ですよね。

紹介したアプリは名刺管理としては基本機能しかありません。自社の要件に応じて、カスタマイズして、活用いただけたらと思います。

なお、今回の記事では、作成者フィールドを作成していません。よって、一般利用者の ACL は編集者以上となります。他のユーザの名刺情報の取り扱いについて制限したい場合には、作成者フィールドや読者フィールドを活用ください。

前回 作ってみよう


2025/01/13

作ってみよう:#26)スマート名刺管理 - Nomad 用エージェントの作成

現時点での Nomad は残念ながら DXL には未対応です。これが原因となり今回作成した名刺取り込みエージェント(リッチテキストに貼り付けられたインラインイメージを読み取る部分)は、そのまま Nomad では動作しません。そこで、今回は Nomad からでも名刺取り込みができるよう細工にをします。


Nomad 用エージェントの構成

前述の通り、Nomad で DXL は動作しません。そこで、DXL 部分の処理をサーバで実行するように変更します。具体的な対応としては、名刺読込の処理を 2 分割して、UI の処理を Nomad、それ以外をサーバで行うようにします。

UI 用エージェントの作成

新規でエージェントを作成します。

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

セキュリティ 実行時のセキュリティレベル
2. 制限された操作を許可する


Option Declare

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
   Dim na As NotesAgent

   Set xns = New NotesSession
   Set xndb = xns.CurrentDatabase

   '名刺を添付した文書を保存して文書ID取得
   Set nuid = nuiw.CurrentDocument
   Set nd = nuid.Document
   sID = nd.Noteid
   Set nd = Nothing '一旦文書開放(これがないと再取得できない)
   Call nuid.Close(True)

   'エージェントをサーバ実行
   Set na = xndb.GetAgent("ReadNameCard_OnServer")
   Call na.RunOnServer(sID)


   'エージェントで更新した文書を再取得して表示
   Set nd = xndb.GetDocumentByID(sID)
   nd.Form = "mfNameCard"

   Set nuid = nuiw.EditDocument(True, nd)
End Sub

RunOnServer メソッドを使用して、サーバでエージェントを実行させています。引数で渡した文書 ID でサーバエージェントでも現在の文書を取得できるようにしています。ただ、サーバ上で文書を取得するためには保存されている必要があるので、UI の文書を先に保存しています。

サーバエージェント実行後は、エージェントによる更新(サーバ上に保存)した結果を得るため、文書の再取得を行っています。また、再表示する際には、Nomad 用のフォームで開くよう、Form フィールドを書き換えています。


サーバ用エージェントの作成

新規でエージェントを作成します。

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

セキュリティ 実行時のセキュリティレベル
2. 制限された操作を許可する

Option Declare
Use "lsReadNameCard"

Private xns As NotesSession
Private xndb As NotesDatabase

Sub Initialize
   Dim na As NotesAgent
   Dim nd As NotesDocument

   Set xns = New NotesSession
   Set xndb = xns.Currentdatabase
   Set na = xns.CurrentAgent

   '呼び出し元の文書を取得
   Set nd = xGetPramDoc(na)

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

   '名刺管理フィールドセット
   nd.ExchangeDate = Today
   nd.Status = "3"

   '文書の保存(画面の表示は呼び出し元が担当)
   Call nd.Save(True, False)
End Sub

Private Function xGetPramDoc(vna As NotesAgent) As NotesDocument
   Dim nd As NotesDocument

   Set nd = xndb.GetDocumentByID(vna.ParameterDocID)
   Set xGetPramDoc = nd
End Function

名刺読み込みのメインルーチンはノーツクライアント用と共通で、スクリプトライブラリの ReadNameCard 関数をコールしているだけです。

サーバ用エージェントでは、名刺情報取得後、文書を保存しています。これにより、呼び出し元のエージェントで結果を取得できるようにしています。


Nomad 用フォームの修正

エージェントが完成したら Nomad 用フォームの[名刺読込]アクションで実行するエージェントを切り替えます。エージェントを実行する前に、@コマンドで文書を保存しています。

@Command([FileSave]);
@Command([ToolsRunMacro]; "Agent_Kicker")


次回の予定

今回のエージェント作成で Nomad 用の部品は完成しました。次回は、Nomad 用フレームセットを作成して、アプリとして仕上げます。




前回 作ってみよう 次回


2025/01/12

作ってみよう:#25)スマート名刺管理 - Nomad 用フォームの作成

今回は Nomad 用のフォームを作成します。


名刺管理フォームの作成

ノーツクライアント用の名刺管理フォームをコピペして作成します。

修正箇所は次の通りです。


◇ フォーム情報

フォーム名 m01.名刺管理
別名 mfNameCard | fNameCard

別名の mfNameCard は前回作成の埋め込み用ビューで、そのビューから文書を開くとこのフォームで開くという動作になります。文書を保存する際には、最後の別名が Form フィールドに保存されます。この設定により、Nomad で文書を作成してもノーツ用と区別なく保存されることになります。ハイブリッドアプリに便利な手法ですね。


◇ セクションの設定

Nomad アプリではじゃまになるので、管理用セクションを常時非表示となるように設定します。


◇ タブ表の設定

Nomad アプリでは画面幅いっぱいに使用したいので、[登録情報][名刺画像]タブの表の幅設定を「ウィンドウに合わせる」を選択します。


会社名別フォームの作成

Nomad で使いやすくするため、画面上部でカテゴリを選択し、選択したカテゴリ内の名刺データだけを表示する機能を作成します。フォーム内の埋め込みビューを利用して開発します。作成するフォームの動作イメージは次の通りです。

フォーム下部に埋め込みビューを配置し、その上部にカテゴリを指定する機能を配置します。カテゴリは、ア行、カ行など 50 音の ”行” の選択とその行のイニシャル(最大 5 音)を選択できるようにします。行だけを選ぶとその行に属する名刺、イニシャルを選ぶとその音で始まる名刺を表示します。

作成するフォームは次の通りです。


◇ フォーム情報

フォーム名 m02.会社名別
別名 mfViewByCompany
ウィンドウタイトル "会社名別"


◇ 常時非表示セクション

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

項目 フィールド名 種類 補足
保存制御 SaveOptions テキスト 編集可能 初期値: "0"
行リスト Keyword1_Lst テキスト 表示用の
計算結果
複数値も可
計算式は後述
イニシャル
リスト
Keyword2_Lst テキスト 表示用の
計算結果
複数値も可
計算式は後述

セクションは常時非表示とし、必ず閉じた状態になるように設定します。


・Keyword1_Lst の値(計算式)

REM{名刺データが存在する行を取得};

xLst := @DbColumn("":"NoCache"; "":""; "vSchKeyword1"; 1);
@If(@IsError(xLst); ""; @Unique(xLst))


・Keyword2_Lst の値(計算式)

REM{選択した行で名刺データが存在するイニシャルを取得};

REM{イニシャルを取得する行を決定};

xKey := @If(Keyword1 = ""; @Subset(Keyword1_Lst; 1); Keyword1);

REM{イニシャルを取得};
xLst := @DbLookup("":"NoCache"; "":""; "vSchKeyword1"; xKey; 2);
xNew := @If(@IsError(xLst); ""; @Unique(xLst));

REM{選択中のイニシャルが次の選択肢になければイニシャルの選択をクリア};
REM{選択肢を変えず画面更新したときにイニシャルを保持};

@If(@IsMember(Keyword2; xNew); @Success; @SetField("Keyword2"; ""));

REM{イニシャルの選択肢をセット};
xNew


◇ キーワード選択エリア

枠が非表示の表の中にフィールドを配置します。

フィールド名 種類 補足
Keyword1 コンボボックス 編集可能 初期値などの詳細設定は後述
Keyword2 ラジオボタン 編集可能 初期値などの詳細設定は後述


・Keyword1 の詳細設定

デフォルト値 xLst := @GetField(@ThisName + "_Lst");
@Subset(xLst; 1)

選択 式で選択肢を設定
@GetField(@ThisName + "_Lst")
オプション ☑ キーワードの変更時にフィールドを更新
☑ 文書の更新時に選択肢を更新


・Keyword2 の詳細設定

デフォルト値 ""

選択 式で選択肢を設定
@GetField(@ThisName + "_Lst")
オプション ☑ キーワードの変更時にフィールドを更新
☑ 文書の更新時に選択肢を更新


◇ 埋め込みビュー

フォームの下部に埋め込みビューを配置します。

メニューから [作成] - [埋め込み設計要素] - [ビュー] を選択し、前回作成した埋め込み用ビューを選択します。


作成した埋め込みビューを選択し、単一カテゴリの表示に次の式を設定します。

@If(Keyword2 = ""; Keyword1; Keyword2)


最後に埋め込みビューのプロパティでサイズを設定します。また、『スクロールバーを無効』チェックは外しておきましょう。件数が多い場合スクロールできなくなります。


◇ アクションボタン

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

ボタン名 Click 補足
閉じる @Command([FileCloseWindow])
新規作成 @Command([Compose]; "mfNameCard") Nomad 用フォーム


前回 作ってみよう 次回


2025/01/11

作ってみよう:#24)スマート名刺管理 - Nomad 用ビューの作成

名刺管理を Nomad で利用できるように機能を追加します。

今回は Nomad 用ビューの開発です。検索用のビューが中心なので地味な作業となりますが、Nomad アプリを使いやすくする要の設計と言えます。


埋め込み用ビューの作成

カード形式のデータベースのようにユーザの選択したキーワードに関連する名刺情報だけを表示するためのビューで、フォーム上に埋め込まれた単一カテゴリビューで利用します。

下図の通り、”ドミノ” という会社が、”タ行” と ”ト” のカテゴリで表示されるようになっており、2 通りの用途で使うハイブリッド構成になっています。

会社名順ビューをコピペして作成します。修正箇所は次の通りです。


◇ ビューの設定

ビュー名 (x.EmbByKeyword)
別名 mvEmbByKeyword
フォームの式 "mfNameCard"

なお、アクションボタンは不要なので削除します。


◇ 列式の修正

1 列目にあるカテゴリ列の式を以下の通り変更します。

xKana := CompanyName_Kn;

@If (xKana ="";@Return( "(不明)");@Success);

xIni := @Left(xKana; 1);

xFm := "イ":"ウ":"ヴ":"エ":"オ":"ガ":"キ":"ギ":"ク":"グ":"ケ":"ゲ":"コ":"ゴ":
"ザ":"シ":"ジ":"ス":"ズ":"セ":"ゼ":"ソ":"ゾ":"ダ":"チ":"ヂ":"ツ":"ヅ":"テ":"デ":"ト":"ド":
"ニ":"ヌ":"ネ":"ノ":"バ":"パ":"ヒ":"ビ":"ピ":"フ":"ブ":"プ":"ヘ":"ベ":"ペ":"ホ":"ボ":"ポ":
"ミ":"ム":"メ":"モ":"ユ":"ヨ":"ヲ";
xTo := "ア":"ア":"ア":"ア":"ア":"カ":"カ":"カ":"カ":"カ":"カ":"カ":"カ":"カ":
"サ":"サ":"サ":"サ":"サ":"サ":"サ":"サ":"サ":"タ":"タ":"タ":"タ":"タ":"タ":"タ":"タ":"タ":
"ナ":"ナ":"ナ":"ナ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":
"マ":"マ":"マ":"マ":"ヤ":"ヤ":"ワ";
xKeyword1 := @Replace ( xIni;xFm; xTo) +"行";

xFm := "ヴ":"ガ":"ギ":"グ":"ゲ":"ゴ":"ザ":"ジ":"ズ":"ゼ":"ゾ":"ダ":"ヂ":"ヅ":"デ":"ド":
"バ":"パ":"ビ":"ピ":"ブ":"プ":"ベ":"ペ":"ボ":"ポ";
xTo := "ウ":"カ":"キ":"ク":"ケ":"コ":"サ":"シ":"ス":"セ":"ソ":"タ":"チ":"ツ":"テ":"ト":
"ハ":"ハ":"ヒ":"ヒ":"フ":"フ":"ヘ":"ヘ":"ホ":"ホ";
xIni := @Left(xKana; 1);
xKeyword2 := @Replace(xIni; xFm; xTo);

xKeyword1:xKeyword2


カテゴリ検索ビュー

保存された名刺情報からキーワードを検索するためのビューです。


◇ ビューの設定

ビュー名 (x.SchKeyword1)
別名 vSchKeyword1
選択式 xStatus := @TextToNumber(Status);
xStatus := @If(@IsError(xStatus); 0; xStatus);
SELECT Form = "fNameCard" & xStatus >= 3


◇ 列

列名 列の値 補足
(後述)
昇順、カテゴリ別
イニシャル (後述) 昇順


・ ”行” の列式

xKana := CompanyName_Kn;

@If(xKana = ""; @Return("(不明)"); @Success);

xFm := "イ":"ウ":"ヴ":"エ":"オ":"ガ":"キ":"ギ":"ク":"グ":"ケ":"ゲ":"コ":"ゴ":
"ザ":"シ":"ジ":"ス":"ズ":"セ":"ゼ":"ソ":"ゾ":"ダ":"チ":"ヂ":"ツ":"ヅ":"テ":"デ":"ト":"ド":
"ニ":"ヌ":"ネ":"ノ":"バ":"パ":"ヒ":"ビ":"ピ":"フ":"ブ":"プ":"ヘ":"ベ":"ペ":"ホ":"ボ":"ポ":
"ミ":"ム":"メ":"モ":"ユ":"ヨ":"ヲ";
xTo := "ア":"ア":"ア":"ア":"ア":"カ":"カ":"カ":"カ":"カ":"カ":"カ":"カ":"カ":
"サ":"サ":"サ":"サ":"サ":"サ":"サ":"サ":"サ":"タ":"タ":"タ":"タ":"タ":"タ":"タ":"タ":"タ":
"ナ":"ナ":"ナ":"ナ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":"ハ":
"マ":"マ":"マ":"マ":"ヤ":"ヤ":"ワ";
xIni := @Left(xKana; 1);
@Replace(xIni; xFm; xTo) + "行"


・ ”イニシャル” の列式

xKana := CompanyName_Kn;

@If (xKana ="";@Return( "(不明)");@Success);

xIni := @Left(xKana; 1);

xFm := "ヴ":"ガ":"ギ":"グ":"ゲ":"ゴ":"ザ":"ジ":"ズ":"ゼ":"ゾ":"ダ":"ヂ":"ヅ":"デ":"ド":
"バ":"パ":"ビ":"ピ":"ブ":"プ":"ベ":"ペ":"ボ":"ポ";
xTo := "ウ":"カ":"キ":"ク":"ケ":"コ":"サ":"シ":"ス":"セ":"ソ":"タ":"チ":"ツ":"テ":"ト":
"ハ":"ハ":"ヒ":"ヒ":"フ":"フ":"ヘ":"ヘ":"ホ":"ホ";
xIni := @Left(xKana; 1);
xKeyword := @Replace(xIni; xFm; xTo);

xKeyword


次回の予定

次回は、作成したビューを使って、Nomad 用のフォームを作成します。


前回 作ってみよう 次回


2025/01/10

作ってみよう:#23)スマート名刺管理 - ノーツアプリとしての仕上げ

GPT4o API 連携機能が出来上がったので、ノーツアプリとして完成させます。


会社順ビュー

最初のビューは会社名順に表示するビューです。電話帳の索引のように 50 音でカテゴライズして表示します。

ビュー名 01.会社\50音順
別名 vByCompany
選択式 xStatus := @TextToNumber(Status);
xStatus := @If(@IsError(xStatus); 0; xStatus);
SELECT Form = "fNameCard" & xStatus >= 3

ビューに表示するのは AI に問い合わせ後の文書としています。


◇ 列

作成する列は次の通り。

列名 列の値 補足
(後述)
昇順、カテゴリ別
会社名カナ CompanyName_Kn 昇順、非表示
会社名 CompanyName
氏名カナ Name_Kn 昇順、非表示
氏名 Name
所属 Dept
 

1 列目は会社名カナの 1 文字目でカテゴライズするための項目です。

単純に作ると次のように ”ト” と ”ド” が別カテゴリとなります。これに対応するため 1, 2 列目の[ソート]タブで「濁点/半濁点を区別してソート」のチェックを外します。

ただ、この対応だとカテゴリ内の最初の文書が濁点付きの場合、カテゴリにも濁点が付いてしまい美しくありません。そこで、1 列目の列式を次のように設定します。

xKana := CompanyName_Kn;

@If (xKana ="";@Return( "(不明)");@Success);

xIni := @Left(xKana; 1);

xFm := "ヴ":"ガ":"ギ":"グ":"ゲ":"ゴ":"ザ":"ジ":"ズ":"ゼ":"ゾ":"ダ":"ヂ":"ヅ":"デ":"ド":
"バ":"パ":"ビ":"ピ":"ブ":"プ":"ベ":"ペ":"ボ":"ポ";
xTo := "ウ":"カ":"キ":"ク":"ケ":"コ":"サ":"シ":"ス":"セ":"ソ":"タ":"チ":"ツ":"テ":"ト":
"ハ":"ハ":"ヒ":"ヒ":"フ":"フ":"ヘ":"ヘ":"ホ":"ホ";
xIni := @Left(xKana; 1);
xKeyword := @Replace(xIni; xFm; xTo);

xKeyword


◇ アクションボタン

下書きビューと同じアクションボタンを配置します。

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


氏名順ビュー

会社順ビューをコピペして氏名別のビューを作成します。

ビュー名 02.氏名\50音順
別名 vByName
選択式
(変更なし)
xStatus := @TextToNumber(Status);
xStatus := @If(@IsError(xStatus); 0; xStatus);
SELECT Form = "fNameCard" & xStatus >= 3


列は、まず、会社名と氏名の位置を逆転させます(ソート列を含む)。次に 1 列目の列式のフィールド名を修正します。

xKana := Name_Kn;

@If (xKana ="";@Return( "(不明)");@Success);
   ・・・ 以下同じ


下書きビューの変更

テスト中はステータスにかかわらず表示していましたが、下書きのみ表示するように選択式を変更します。

SELECT Form = "fNameCard" & Status = "0"


アウトラインの修正

今回作成した 50 音別ビューをアウトラインに追加します。一般利用者向けのビューとなるので、管理メニューと同列に作成します。


フレームセットの修正

最後に、フレームセットに初期表示するビューを会社順に変更します。


これで、ノーツクライアント用アプリとしては作業完了です。

次回からは、このアプリを Namad から利用できるように修正します。

前回 作ってみよう 次回


2025/01/09

作ってみよう:#22)スマート名刺管理 - 名刺情報の取得

GPT4o API 連携の最後の仕上げです。前回の API からのレスポンスから名刺情報を抽出する部分を開発します。スクリプトライブラリに作成したメイン関数 ReadNameCard の以下の部分ですね。

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

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

End Function


名刺情報の JSON 取得

まず、レスポンスの JSON の構造を確認します。

赤線の部分が AI が認識した名刺情報です。choices(配列) > message > content の順でノードを取得すればよいということですね。

以下の xGetNameCard 関数の戻り値は、名刺情報の JSON を NotesJSONNavigator のオブジェクトで返します。関数内では choices 配列の 1 つ目の要素を取得し、その先を処理を別のサブ関数で行っています。

Private Function xGetNameCard(vnav As NotesJSONNavigator) As NotesJSONNavigator
   Dim je As NotesJSONElement
   Dim ja As NotesJSONArray
   Dim jeChoice As NotesJSONElement
   Dim sResponce As String

   Set je = xGetElementByName_Nav(vnav, "choices")

   If Not (je Is Nothing) Then
      'choices は配列
      Set ja = je.Value

      If ja.Size > 0 Then
         '1件目を利用
         Set jeChoice = ja.GetFirstElement()
         sResponce = xGetResponce_Content(jeChoice)

         'JSON(テキスト)を NotesJSONNavigator のオブジェクトに変換
         Set xGetNameCard = xns.CreateJsonNavigator(sResponce)
      End If
   End If
End Function


xGetResponce_Content 関数は choices の 1 配列要素から message > content を取得して、その値を文字列で返す処理を担当しています。

Private Function xGetResponce_Content(vjeChoice As NotesJSONElement) As String
   Dim jobj As NotesJSONObject
   Dim jeMsg As NotesJSONElement
   Dim je As NotesJSONElement

   Dim sRole As String
   Dim sContent As String

   'message を取得
   Set jobj = vjeChoice.Value
   Set je = xGetElementByName_Obj(jobj, "message")

   'role を取得(今回は未使用)
   Set jobj = je.Value
   Set je = xGetElementByName_Obj(jobj, "role")
   sRole = je.Value

   'content を取得
    Set je = xGetElementByName_Obj(jobj, "content")
   sContent = je.Value

   xGetResponce_Content = sContent
End Function


名刺情報を文書にセット

上記関数で取得した JSON の構造を確認します。当たり前ですが、StructuredOutputs で指定した構造通りとなっていますね。


この JSON を文書にセットするのが xSaveNameCard 関数です。

Private Sub xSaveNameCard(vjnav As NotesJSONNavigator, vnd As NotesDocument)
   If Not(vjnav Is Nothing) Then
      Call xSetFld(vnd, "CompanyName_Full", vjnav)
      Call xSetFld(vnd, "CompanyName", vjnav)
      Call xSetFld(vnd, "CompanyName_Kn", vjnav)
      Call xSetFld(vnd, "Name", vjnav)
      Call xSetFld(vnd, "Name_Kn", vjnav)
      Call xSetFld(vnd, "Name_En", vjnav)
      Call xSetFld(vnd, "Dept", vjnav)
      Call xSetFld(vnd, "Role", vjnav)
      Call xSetFld(vnd, "Zip", vjnav)
      Call xSetFld(vnd, "Address", vjnav)
      Call xSetFld(vnd, "Tel", vjnav)
      Call xSetFld(vnd, "Mobile", vjnav)
      Call xSetFld(vnd, "Fax", vjnav)
      Call xSetFld(vnd, "eMail", vjnav)
      Call xSetFld(vnd, "URL", vjnav)
   End If
End Sub


1 要素分の処理はサブ関数 xSetFld で行っています。関数では、引数の指示に従って JSON から値を取得して、フィールドにセットしています。

なお、フィールド名と JSON ノード名は一致する前提としています。

Private Function xSetFld(vnd As NotesDocument, ByVal vsFld As String, vjnav As NotesJSONNavigator)
   Dim s As String

   s = xGetValueByName_Nav(vjnav, LCase(vsFld))
   Call vnd.ReplaceItemValue(vsFld, s)
End Function


汎用関数

今回紹介した関数内には、JSON の処理に関する汎用的な関数を利用しています。これまでの他の記事で紹介した関数が多いのですが、このアプリ内で利用しているものをまとめておきます。

スクリプトライブラリ lsReadNameCard に追加してください。

◇ NotesJSONNavigator から指定した名前のノードを返す

Private Function xGetElementByName_Nav(vjnav As NotesJSONNavigator, ByVal vsName As String) As NotesJSONElement
   Dim je As NotesJSONElement

   Set je = Nothing

   On Error GoTo ErrProc
   Set je = vjnav.GetElementByName(vsName)

ExitProc:
   Set xGetElementByName_Nav = je
   Exit Function

ErrProc:
   Resume ExitProc
End Function

◇ NotesJSONObject から指定した名前のノードを返す

Private Function xGetElementByName_Obj(vjobj As NotesJSONObject, ByVal vsName As String) As NotesJSONElement
   Dim je As NotesJSONElement

   Set je = Nothing

   On Error GoTo ErrProc
   Set je = vjobj.GetElementByName(vsName)

ExitProc:
   Set xGetElementByName_Obj = je
   Exit Function

ErrProc:
   Resume ExitProc
End Function

◇ NotesJSONNavigator から指定した名前のノードの値を返す

Private Function xGetValueByName_Nav(vjnav As NotesJSONNavigator, ByVal vsName As String) As String
   Dim je As NotesJSONElement

   On Error GoTo Proc_Err

   Set je = xGetElementByName_Nav(vjnav, vsName)

   If Not (je Is Nothing) Then
      xGetValueByName_Nav = je.Value
   End If

Proc_Exit:
   Exit Function

Proc_Err:
   Resume Proc_Exit
End Function


動作検証

それでは GPT4o API 連携機能の最終テストを行います。ReadNameCard 関数で、今回開発した部分のコメントを外し、有効化します。

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 が出力されていれば成功です。あわせて登録情報タブの各項目が正しく表示されています。


次回の予定

これで GPT4o API 連携機能部分は完成です。次回は、ノーツアプリとしての仕上げ作業を行います。


前回 作ってみよう 次回


2025/01/08

作ってみよう:#21)スマート名刺管理 - GTP4o API のコール

送信する JSON の準備ができたので、GTP4o API のコールする部分を開発します。メイン関数 ReadNameCard の以下の部分ですね。

Public Function ReadNameCard(vnd As NotesDocument) As Boolean
         ・・・
   '2. API をコールし、結果の JSON を取得
   Set jnavResponce = xCallWebAPI(jnavRequest)
   Call xSetRT(vnd, "JSON_Responce", jnavResponce.Stringify)
         ・・・
End Function


API コール

スクリプトライブラリ lsReadNameCard を開き、GTP4o API のコールする関数  xCallWebAPI 関数を追加します。

NotesHTTPRequest のオブジェクトを準備して、HTTP ヘッダに OpenAI 社の API キーを使用するための Bearer をセットします。

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

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

   'HTTP ヘッダーの設定(Bearer を OpenAI API 設定から取得)
   Call http.SetHeaderField("Content-Type", "application/json")
   Call http.SetHeaderField("Authorization", "Bearer " & xndWAPI.Bearer(0))

   'API 実行(URL を OpenAI Chat Completion 設定から取得)
   http.PreferJSONNavigator = True
   sURL = xndChat.EndPoint(0)
   Set jnav = http.Post(sURL, vjnavRequest.Stringify)

   'API のレスポンスを戻り値にセット
   Set xCallWebAPI = jnav
End Function

API をコールするための URL(エンドポイント)に対して、Post メソッドを使って、作成した JSON を送信しています。

PreferJSONNavigator を True にセットしているので、結果は JSON(NotesJSONNavigator のオブジェクト)で返ります。それをそのまま関数の戻り値としています。


動作検証

ReadNameCard 関数を修正して、API コール部分のコメントを解除します。

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 が出力されていれば成功です。


次回の予定

これで WebAPI からレスポンスを受け取れるようになりました。次回はレスポンスの JSON から AI が読み取った名刺の情報を取得します。


前回 作ってみよう 次回


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 をコールする部分を作成します。


前回 作ってみよう 次回