※初出時から記事を修正しました。-2024/03/22(金)
改行を受け付けてくれない問題の対応
前編のLotusScriptでは質問に改行が入るとエラーが出るという問題がありました。利用者が改行なしに入力してくれれば良いですが、そうはいかないので改行を別の文字に置換する必要がありそうです。
案1:nullに置換する
→途中改行の場合は問題ない。2つの文章を区点(。)を入力せず、改行区切りで入力された場合、文章が繋がっていまい文意が伝わらない可能性がある。
案2:区点(。)に置換する
→区点の代わりに改行している場合、正しい文章になる。区点が入っている場合、区点が2つ付く。
案3:\n(改行コード)に置換する
→HTML+JavaScriptで作ってみたところ、改行は\nでPOSTしているようです。
今回は案3を採用し、POSTするJSONを作る前に改行を\nに変換します。
【LotusScriptコード-改行置換部分のみ】
sRequest = Replace(sRequest, Chr(13) & Chr(10), "\n") '改行を\nに変換
ソースコードを送信したい-追記しました
JavaScriptやPythonなど何らかのプログラムソースをPOSTしたい場合、改行コード問題と同じエラーが出ることがわかりました。エラーの原因は特殊文字にあるようです。
ひとまず問題がありそうなダブルコーテーションのエスケープ、TABの置換を行います。
【LotusScriptコード-置換部分のみ】
'ソースコード送信対策
sRequest = Replace(sRequest, """", "\""") 'ダブルコートをエスケープ
sRequest = Replace(sRequest, Chr(9), " ") 'TABを半角スペースに変換
残すボタンの実装
気に入った回答は残すボタンを使って履歴フォームの文書として保存します。履歴フォームは入力フォームと似た感じで作成し、「質問」フィールドなど変更させたくないフィールドは「作成時の計算結果」として作成して下さい。
【履歴フォーム-frmChatHistory】
前回の質問を引き継いで質問するために、入力フォームには「質問履歴」フィールドと「継続質問」フィールドを追加します。「質問履歴」フィールドは「複数値可能」で作成し、「残す」ボタンクリック時に現在の質問を追加して残していきます。「継続質問」フィールドはチェックボックスで作成し、選択肢に「継続質問 | 1」と設定し、チェックをつけた際に「1」が入力されるように設定します。
【入力フォーム-frmChatInput】
「残す」ボタンでは、入力フォームの値をコピーして履歴フォームに保存後、画面をリフレッシュすることで右に表示されたビューを更新します。また、このボタンで現在の質問を入力フォームの「質問履歴」フィールドに追加して残します。
※「質問」ボタンで「質問履歴」を残さず、「残す」ボタンを使う理由は、「質問」ボタンは気に入った回答が得られるまで複数クリックされる可能性があり、同じ質問が「質問履歴」に複数残ることを避けるためです。
【LotusScriptコード-残すボタン】
Sub Click(Source As Button)
'---------- ---------- ---------- ---------- ----------
'入力フォームの内容を履歴フォームにコピーして保存
'
'---------- ---------- ---------- ---------- ----------
'クラス・変数宣言
Dim session As New NotesSession
Dim db As NotesDatabase
Dim newdoc As NotesDocument
Dim ws As New NotesUIWorkspace
Dim uidoc As NotesUIDocument
Dim doc As NotesDocument
Dim item As NotesItem
Dim sRequest As String '入力した質問
'クラス・変数セット
Set db = session.CurrentDatabase
Set uidoc = ws.CurrentDocument
Set doc = uidoc.Document
Set item = doc.GetFirstItem("RequestList")
'入力チェック
If doc.Response(0) = "" Then
Msgbox "回答が登録されていません。",16,"エラー"
Exit Sub
End If
If doc.Category(0) = "" Then
Msgbox "カテゴリを入力してください。",16,"エラー"
Exit Sub
End If
'回答履歴文書を作成し、回答内容をコピー、文書を保存
Set newdoc = db.CreateDocument
newdoc.Form = "frmChatHistory"
newdoc.Request = doc.Request
newdoc.Response = doc.Response
newdoc.FactCheck = doc.FactCheck
newdoc.Category = doc.Category
newdoc.Quality = doc.Quality
Call newdoc.ComputeWithForm(True, True)
Call newdoc.Save(True, True)
'質問を質問履歴に書き込み
sRequest = doc.Request(0)
sRequest = Replace(sRequest, Chr(13) & Chr(10), "\n")
'ソースコード送信対策
sRequest = Replace(sRequest, """", "\""") 'ダブルコートをエスケープ
sRequest = Replace(sRequest, Chr(9), " ") 'TABを半角スペースに変換
Call item.AppendToTextList(sRequest)
'質問フォームの書き込み内容をクリア
doc.Request = ""
doc.Response = ""
doc.FactCheck = ""
doc.Category = ""
doc.Quality = "3"
'ビューを更新
Call ws.Viewrefresh()
Msgbox "done"
End Sub
前回の質問を引き継いで質問したい
前回の質問を引き継いで質問したい場合があります。例えば「ChatGPTに問合せを行うサンプルコードをJavaScriptで書いて下さい。」の後に「VBAに書き換えて下さい。」と質問したい場合です。過去の質問に関連して現在の質問を行いたい場合は、過去の質問と現在の質問を「\n」で連結してPOSTします。
【問合せJSON例】
{
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "user",
"content": "OpenAIのAPIを呼び出すサンプルコードをJavaScriptで書いてください。\n先ほどのコードをVBAに書き換えてください。"
}
]
}
LotusScriptでは、複数値可能に設定した「質問履歴」フィールドから取得したリストを「\n」区切りの文字列に変換後、新規の「質問」を連結し、POSTします。
【LotusScriptコード-送信ボタン】
Sub Click(Source As Button)
'---------- ---------- ---------- ---------- ----------
'OpenAI APIから取得したレスポンスを書込み
'
'---------- ---------- ---------- ---------- ----------
Const APIURL = "https://api.openai.com/v1/chat/completions"
Const APIKEY = "(取得したAPIKEY)"
Const MODEL = "gpt-3.5-turbo"
'クラス・変数宣言
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 sUrl As String '送信するURL
Dim sRequest As String '入力した質問
Dim sBodyJson As String '送信するJSON文字列
Dim sContent As String '回答本文
Dim sCarryOver As String '前回の質問引き継ぎフラグ
Dim vRequestList As Variant '質問履歴(リスト)
'クラス・変数セット
Set uidoc = ws.CurrentDocument
Set doc = uidoc.Document
'入力チェック
sRequest = doc.Request(0)
If sRequest = "" Then
Msgbox "質問を入力して下さい。",64,"確認"
Exit Sub
End If
'継続質問フラグ確認と送信JSONの作成
sCarryOver = doc.CarryOver(0) '質問引き継ぎフラグ取得
sRequest = Replace(sRequest, Chr(13) & Chr(10), "\n") '質問の改行を「\n」に変換(改行はエラーになる)
'ソースコード送信対策
sRequest = Replace(sRequest, """", "\""") 'ダブルコートをエスケープ
sRequest = Replace(sRequest, Chr(9), " ") 'TABを半角スペースに変換
If sCarryOver = "1" Then
vRequestList = doc.RequestList
sBodyJson = |{"model":"| & MODEL & |", "messages":[ {"role":"user", "content":"| & Join(vRequestList,"\n") & "\n" & sRequest & |"} ] }|
Else
sBodyJson = |{"model":"| & MODEL & |", "messages":[ {"role":"user", "content":"| & sRequest & |"} ] }|
End If
'Msgbox sBodyJson '< for debug>
'Getリクエストを実行し、結果を取得
Set vXml = CreateObject("MSXML2.XMLHTTP")
vXml.Open "POST", APIURL, False
vXml.setRequestHeader "Content-Type", "application/json"
vXml.setRequestHeader "Authorization", "Bearer " & APIKEY
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("/choices/0/message/content").Value
'回答をフォームに書込み
doc.HttpStatus = vXml.Status
doc.Response = sContent
End Sub
回答待ち状態への対応
ChatGPTのAPIはレスポンスまでに時間がかかります(Web版ChatGPTより遅い)。処理中であることを表示するため、WindowsAPIを使ってマウスカーソルを下記のように変化させます。
- 処理中-砂時計アイコン(コントロールパネルの設定でアイコンは変更できます)
- 処理終了-通常アイコン
【LotusScriptコード-送信ボタン(WindowsAPI宣言と関数)】
Declare Function LoadCursor Lib "user32" Alias "LoadCursorA" (Byval hInstance As Long, Byval lpCursorName As Long) As Long
Declare Function SetCursor Lib "user32" (Byval hCursor As Long) As Long
Sub ChangeCursorToHourglass()
'---------- ---------- ---------- ---------- ----------
' Windows API : 砂時計マークのカーソルをロード
'
'---------- ---------- ---------- ---------- ----------
Dim hCursor As Long
hCursor = LoadCursor(0, 32514) ' 32514 は砂時計マークのリソースID
SetCursor hCursor ' カーソルを設定
End Sub
Sub RestoreCursor()
'---------- ---------- ---------- ---------- ----------
' WindowsAPI : デフォルトのカーソルに戻す
'
'---------- ---------- ---------- ---------- ----------
SetCursor 0 ' カーソルを設定
End Sub
【LotusScriptコード-送信ボタン】
Sub Click(Source As Button)
'---------- ---------- ---------- ---------- ----------
'OpenAI APIから取得したレスポンスを書込み
'
'---------- ---------- ---------- ---------- ----------
Const APIURL = "https://api.openai.com/v1/chat/completions"
Const APIKEY = "(取得したAPIKEY)"
Const MODEL = "gpt-3.5-turbo"
'クラス・変数宣言
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 sUrl As String '送信するURL
Dim sRequest As String '入力した質問
Dim sBodyJson As String '送信するJSON文字列
Dim sContent As String '回答本文
Dim sCarryOver As String '前回の質問引き継ぎフラグ
Dim vRequestList As Variant '質問履歴(リスト)
'クラス・変数セット
Set uidoc = ws.CurrentDocument
Set doc = uidoc.Document
'入力チェック
sRequest = doc.Request(0)
If sRequest = "" Then
Msgbox "質問を入力して下さい。",64,"確認"
Exit Sub
End If
'継続質問フラグ確認と送信JSONの作成
sCarryOver = doc.CarryOver(0) '質問引き継ぎフラグ取得
sRequest = Replace(sRequest, Chr(13) & Chr(10), "\n") '質問の改行を「\n」に変換(改行はエラーになる)
'ソースコード送信対策
sRequest = Replace(sRequest, """", "\""") 'ダブルコートをエスケープ
sRequest = Replace(sRequest, Chr(9), " ") 'TABを半角スペースに変換
If sCarryOver = "1" Then
vRequestList = doc.RequestList '質問履歴フィールド取得
sBodyJson = |{"model":"| & MODEL & |", "messages":[ {"role":"user", "content":"| & Join(vRequestList,"\n") & "\n" & sRequest & |"} ] }|
Else
sBodyJson = |{"model":"| & MODEL & |", "messages":[ {"role":"user", "content":"| & sRequest & |"} ] }|
End If
'Msgbox sBodyJson '< for debug>
'Getリクエストを実行し、結果を取得
Call ChangeCursorToHourglass() 'マウスカーソルを砂時計に変更
Set vXml = CreateObject("MSXML2.XMLHTTP")
vXml.Open "POST", APIURL, False
vXml.setRequestHeader "Content-Type", "application/json"
vXml.setRequestHeader "Authorization", "Bearer " & APIKEY
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("/choices/0/message/content").Value
'回答をフォームに書込み
doc.HttpStatus = vXml.Status
doc.Response = sContent
Call RestoreCursor() 'マウスカーソルを元に戻す
End Sub
【完成イメージ】
設計公開NTFのダウンロード
今回のブログで紹介したコードを含む設計公開NTFをアップロードしましたので、必要に応じてご活用下さい。OpenAI APIのAPIKEYは含んでおりませんので、各自で取得し、設定文書に登録して頂く必要があります。
※自由に使っていただいて結構ですが、テンプレート販売や、自身のブログでの公開は禁止します。
【ダウンロードリンク】
最後に
ChatGPTの回答は必ずしも正しいとは限りませんが、下記のようなシーンでは活用できる可能性があります。
- 知らないことを学習するために興味のあることをどんどん質問する。
- 提示した文章を要約する。
- 他言語に翻訳する。
- プログラムのリファクタリング提案。
- 文章校正。などなど
生成AIのメリットは、人間と違って、疲れない、怒らないという点にあるのではないかと思います。ですので、どんどん言い回し、表現方法を変更してみたり、英語で質問してみたりすれば有効に使いこなせるはずです。今回のサンプルコードをベースに自社にあったソリューションを生み出して貰えればと思います。