Notes開発者のためのXPagesデザインレシピ

簡単でCoolなXPagesアプリケーションを作るための情報を発信していきます

Notesクライアント 生成AI

話題のClaude3をNotesクライアントから使ってみる(後編)

画像をPOSTして、内容を解析して貰う

 Claude3のAPIは画像のPOSTに対応しています。画像をPOSTするにはどうしたらよいのでしょうか?APIリファレンスを見るとBase64でエンコードして、JSONのcontent属性の中にdata属性として埋め込むようです。

【JSONフォーマット】

{
    model : "claude-3-opus-20240229",
    max_tokens : 1024,
    messages: [
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": "/9j/4AAQSkZJRg...",
                    }
                },
                {
                    "type": "text",
                    "text": "What is in this image?"
                }
            ]
        }
    ]
}

 Notes上に実装するには画像を選択する必要がありますので、Notesフォームを拡張し、画像のファイルパスが入るフィールドと、画像を選択するためのボタンを作ります。

【Notesフォーム】

Base64にエンコードする関数を作る

 LotusScriptにはBase64に変換してくれる関数がないようなので自作します。またファイル拡張子からMediaTypeを判定する関数も作ります。

【LotusScriptソース-Base64エンコード関数】

Function Base64Encode(vImageData As Variant) As String
	'---------- ---------- ---------- ---------- ---------- 
	' 画像のBinaryをBase64エンコードした文字列に変換する
	' 引数	:vImageData - 画像Binary
	' 戻り値	:Base64エンコード済み画像
	'---------- ---------- ---------- ---------- ---------- 
	
	Dim objXml		As Variant
	Dim objElem	As Variant
	
	Set objXml = CreateObject("MSXML2.DOMDocument")
	
    ' Base64エンコードを行う要素を作成
	Set objElem = objXml.createElement("b64")
	objElem.DataType = "bin.base64"
	objElem.nodeTypedValue = vImageData
	
    ' Base64エンコードされた文字列を返す
	Base64Encode = Replace(objElem.Text, Chr(10), "")
	
	Set objElem = Nothing
	Set objXml = Nothing
	
End Function

【LotusScriptソース-MediaType判定関数】

Function GetMediaType(sFilePath As String) As String
	'---------- ---------- ---------- ---------- ---------- 
	' ファイルの拡張子からMediaTypeを決定する
	' 引数	:sFilePath - ファイルパス
	' 戻り値	:MediaType文字列(image/jpeg、image/png、image/gif、image/webp
	'---------- ---------- ---------- ---------- ---------- 
	Dim sExt			As String		'拡張子
	Dim sMediaType	As String		'メディアタイプ
	
	sExt	= Lcase(Strrightback(sFilePath , "."))
	
	Select Case sExt
	Case "jpg","jpeg":
		sMediaType		= "image/jpeg"
	Case "png":
		sMediaType		= "image/png"
	Case "gif":
		sMediaType		= "image/gif"
	Case "webp":
		sMediaType		= "image/webp"
	Case Else
		sMediaType		= "Unknown"
	End Select
	
	GetMediaType		= sMediaType
	
End Function

【LotusScriptソース-送信ボタン】

Sub Click(Source As Button)
	'---------- ---------- ---------- ---------- ---------- 
	'Claude3 APIから取得したレスポンスを書込み-画像
	'
	'---------- ---------- ---------- ---------- ---------- 	
	Const APIURL = "https://api.anthropic.com/v1/messages"
	Const APIKEY = "(取得したAPIKEY)"
	Const MODEL = "claude-3-opus-20240229"
	Const MAXTOKENS = 1024
	
	'クラス・変数宣言	
	Dim ws				As New NotesUIWorkspace
	Dim uidoc			As NotesUIDocument
	Dim doc			As NotesDocument
	Dim session		As New NotesSession
	Dim jsonNav		As NotesJSONNavigator
	
	Dim vXml			As Variant					'XMLオブジェクト	
	Dim sRequest		As String					'入力した質問
	Dim sBodyJson		As String					'送信するJSON文字列
	Dim sContent		As String					'回答本文
	Dim sImagePath	As String					'画像ファイルパス
	Dim sBase64Str	As String					'Base64変換後ファイル
	Dim vImageData	As Variant					'画像ファイルBinary
	Dim sMediaType	As String					'メディアタイプ(POSTで使用)
	Dim vFileStream	As Variant					'ADODB.Stream
	
	'クラス・変数セット
	Set uidoc		= ws.CurrentDocument
	Set doc		= uidoc.Document	
	
	'入力チェック
	sRequest		= doc.Request(0)
	sImagePath		= doc.ImagePath(0)
	If sRequest = "" Or sImagePath = "" Then
		Msgbox "質問を入力して下さい。"
		Exit Sub
	End If
	
	'ファイルの読み込み1-ADODB.Stream
	Set vFileStream = CreateObject("ADODB.Stream")
	vFileStream.Type = 1 						'バイナリ形式
	vFileStream.Open
	vFileStream.LoadFromFile sImagePath
	
    ' バイナリデータを変数に読み込む
	vImageData = vFileStream.Read	
	Set vFileStream = Nothing
	
	'Base64エンコード
	sBase64Str = Base64Encode(vImageData)	
	
	'画像ファイルのMediaType決定
	sMediaType		= GetMediaType(sImagePath)
	
	'Getリクエストを実行し、結果を取得
	Set vXml = CreateObject("MSXML2.XMLHTTP")	
	vXml.Open "POST", APIURL, False
	vXml.setRequestHeader "Content-Type", "application/json"
	vXml.setRequestHeader "x-api-key", APIKEY
	vXml.setRequestHeader "anthropic-version", "2023-06-01"
	
	sBodyJson		= |{"model":"| & MODEL _
	& |","max_tokens": | & MAXTOKENS _
	& |, "messages":[ |_
	& |{"role":"user", "content":[| _
	& | {"type":"image","source": { "type":"base64","media_type":"| & sMediaType & |","data":"| & sBase64Str & |"} },| _
	& | {"type":"text", "text": "| & sRequest & |"} | _
	& |] } | _
	& |] | _
	& |}|	
	vXml.send sBodyJson
	
	'エラーレスポンスチェック
	If vXml.Status <> 200 Then
		Msgbox  vXml.responseText , 16 , "Error : " & Cstr(vXml.Status)
		Exit Sub
	End If
	
	'レスポンスから回答を抽出
	Set jsonNav	= session.CreateJSONNavigator(vXml.responseText)
	sContent		= jsonNav.GetElementByPointer("/content/0/text").Value
	
	'回答をフォームに書込み
	doc.HttpStatus	= vXml.Status
	doc.Response	= sContent	
	
End Sub

【元画像】

【実行結果】

大きい画像を読み込むとVariantがエラーになる

 画像をPOSTし、分析する機能は実装できたのですが、大きな画像をPOSTするとNotesクライアントがエラーを出力するようです。デバッグモードで見るとファイルを読み込んでVariant型変数に代入するところでエラーになっています。Byte型の配列に書き換えてみましたが「64k制限:Click:(行数):I037 配列またはリストのリファレンスが不正です。(変数名)」エラーが出て、ソースの保存をさせて貰えません。下記エラーの「最大領域」というのは「64k制限」のことを言っているんでしょうね。
 VBAで実装したものを移植していますが、VBAでは正常に動きます。なぜかというとVBA6.0までは64k制限がありましたが、VBA7.0(Office2010)で2GBまで緩和され、大容量の変数を持つことができるようになったためです。

【Notesクライアントエラーメッセージ】

【デバックモードによるエラー箇所】

64k制限の回避とNotesの裏コマンド

 上記の制限についてNotesConsortium大阪研究会の浜さんに相談したところ、LotusScriptのNotesStreamクラスにはBase64にエンコードするための「裏コマンド、裏メソッド」があると言うことを教えて貰いました。このメソッドはDesignerHelpには掲載されていません。
 こういう裏コマンド、メソッドがあるなら、サポート対象内にして貰って、DesignerHelpも整備して欲しいですね。

【LotusScriptソース-NotesStreamクラス裏メソッド利用部分】

Function StreamToBase64(streamIn As NotesStream) As String
	On Error Goto theOldWay
	' ReadEncoded function is not documented. In case it doesn't work have a backup.
	StreamToBase64 = Replace(streamIn.ReadEncoded(ENC_BASE64, 76), Chr$(13), "") '←【裏メソッド】
	Exit Function
	
theOldWay:
	Dim session	As New NotesSession
	Dim db			As NotesDatabase
	Dim doc		As NotesDocument
	Dim mime		As NotesMIMEEntity
	
	Set db = session.CurrentDatabase
	Set doc = db.CreateDocument
	Set mime = doc.CreateMIMEEntity("Body")
	streamIn.Position = 0
	
	Call mime.SetContentFromBytes(streamIn, "image/gif", ENC_NONE)
	mime.EncodeContent(ENC_BASE64)
	StreamToBase64 = Replace(mime.ContentAsText, Chr$(13), "")
End Function

でも、動かない!なぜだ!?

 サクッと実装できるかと思いましたが、動きません。こういう場合は、動くものまで立ち返る必要があります。どうやらBase64でエンコードした後の文字列がおかしい感じがするので、エンコード後の文字列をPythonとLotusScriptでテキストファイルに出力して比較してみます。

【Notesクライアントエラーメッセージ】

【Python-エンコード後文字列】

【LotusScript-エンコード後文字列】

 どうやら改行コードが入っているようです。改行コードをReplace関数でNullに変換してPOSTすると正常に動作しました。NotesStreamクラスでBase64変換ができるのであれば、わざわざ「MSXML2.DOMDocument」を使ってBase64変換する必要はなかったですね。

【LotusScriptコード-送信ボタン】

Sub Click(Source As Button)
	'---------- ---------- ---------- ---------- ---------- 
	'Claude3 APIから取得したレスポンスを書込み-画像
	'
	'---------- ---------- ---------- ---------- ---------- 	
	Const APIURL = "https://api.anthropic.com/v1/messages"
	Const APIKEY = "(取得したAPIKEY)"
	Const MODEL = "claude-3-opus-20240229"
	Const MAXTOKENS = 1024
	
	'クラス・変数宣言	
	Dim ws				As New NotesUIWorkspace
	Dim uidoc			As NotesUIDocument
	Dim doc			As NotesDocument
	Dim session		As New NotesSession
	Dim jsonNav		As NotesJSONNavigator	'JSON解析
	Dim stream			As NotesStream			'画像ファイル読み込み'
	
	Dim vXml			As Variant					'XMLオブジェクト	
	Dim sRequest		As String					'入力した質問
	Dim sBodyJson		As String					'送信するJSON文字列
	Dim sContent		As String					'回答本文
	Dim sImagePath	As String					'画像ファイルパス
	Dim sBase64Str	As String					'Base64変換後ファイル
	Dim sMediaType	As String					'メディアタイプ(POSTで使用)
	
	'クラス・変数セット
	Set uidoc		= ws.CurrentDocument
	Set doc		= uidoc.Document	
	
	'入力チェック
	sRequest		= doc.Request(0)
	sImagePath		= doc.ImagePath(0)
	If sRequest = ""  Or sImagePath = "" Then
		Msgbox "質問を入力して下さい。"
		Exit Sub
	End If
	
	'画像ファイルの読み込みとBase64エンコード
	Set stream			= session.CreateStream()
	Call stream.Open(sImagePath, "binary")
	sBase64Str = Replace(stream.ReadEncoded(ENC_BASE64, 76), Chr(13) & Chr(10) , "")		'裏コード
	
	'画像ファイルのMediaType決定
	sMediaType		= GetMediaType(sImagePath)	
	
	'Getリクエストを実行し、結果を取得
	Set vXml = CreateObject("MSXML2.XMLHTTP")	
	vXml.Open "POST", APIURL, False
	vXml.setRequestHeader "Content-Type", "application/json"
	vXml.setRequestHeader "x-api-key", APIKEY
	vXml.setRequestHeader "anthropic-version", "2023-06-01"
	
	sBodyJson		= |{"model":"| & MODEL _
	& |","max_tokens": | & MAXTOKENS _
	& |, "messages":[ |_
	& |{"role":"user", "content":[| _
	& | {"type":"image","source": { "type":"base64","media_type":"| & sMediaType & |","data":"| & sBase64Str & |"} },| _
	& | {"type":"text", "text": "| & sRequest & |"} | _
	& |] } | _
	& |] | _
	& |}|	
	vXml.send sBodyJson
	
	'エラーレスポンスチェック
	If vXml.Status <> 200 Then
		Msgbox  vXml.responseText , 16 , "Error : " & Cstr(vXml.Status)
		Exit Sub
	End If
	
	'レスポンスから回答を抽出
	Set jsonNav	= session.CreateJSONNavigator(vXml.responseText)
	sContent		= jsonNav.GetElementByPointer("/content/0/text").Value
	
	'回答をフォームに書込み
	doc.HttpStatus	= vXml.Status
	doc.Response	= sContent	
	
End Sub

【実行結果】

最後に

 生成AIは今、一番ホットな話題でたくさん情報が出ています。最終的には複数社に収束していくのだと思いますが、情報がたくさんあり、みんなが試行錯誤している時に学んで実験しておくのが、一番楽しく、乗り遅れない方法だと思います。皆さんも生成AIがどのように活用できるか?実験してみて下さい。