目次
・本日の成果・考え
・最後に
本日の成果
では、前回お伝えしたAPI通信クラス(DifyApiClient.java)の動作確認からやっていきたいと思います。
/**
*
*/
package app.test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import app.windowView.api.DifyApiClient;
import app.windowView.api.DifyRequestDto;
/**
*
*/
public class DifyApiClientTest {
//API URL
private String testAPI_URL;
//API key
private String testapiKey;
//リクエストDto
DifyRequestDto testDto;
@Test
public void test() {
// 待機用ラッチ(1回の完了を待つ)
CountDownLatch latch = new CountDownLatch(1);
//動作検証用API
testAPI_URL = "https://api.dify~~~~~~";
//API key
testapiKey = "app-~~~~~~~~~~~~~~~~~";
//リクエストDtoの生成
testDto = new DifyRequestDto("君の役割を答えてくれ");
//DifyAPIClientクラスのインスタンス準備
DifyApiClient testClient = new DifyApiClient(testAPI_URL, testapiKey);
try {
testClient.streamingMsg(
testDto,
chunk ->{
System.out.println("チャンク受信:"+chunk);
},
()->{
System.out.println("完了通知:チャット終了");
latch.countDown(); // 完了したら待機解除
});
// 非同期完了まで最大10秒待機(タイムアウトも検出可能)
boolean completed = latch.await(10, TimeUnit.SECONDS);
assert completed : "ストリーミングがタイムアウトしました";
} catch (Exception e) {
throw new IllegalStateException("何かしらのエラー:"+e);
}
}
}
このテスト実行時点でのビルドパス
新たに追加したのは以下です。
・kotlin-stdlib-1.9.0.jar
・okio-jvm-3.9.0.jar
「okio-3.9.0.jar」だとBufferedSourceが型解決できませんでした。
では、実行。
はい、エラーと。
以下、エラー内容
7月 05, 2025 10:44:27 午前 okhttp3.internal.platform.Platform log
情報: Callback failure for call to https://api.dify.ai/...
java.io.IOException: API通信ステータスエラー;400
at app.windowView.api.DifyApiClient$1.onResponse(DifyApiClient.java:60)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
HTTP 400エラー(Bad Request)は、リクエストの構造やパラメータに誤りがある場合に返されるということで、
JSONの構造がDifyの仕様に合っていない可能性が高いみたい。
この時点で以下が原因と想定
・リクエストヘッダー(APIのURLまたはAPIキー)が正しくない。
・リクエストボディのJsonの形式が正しくない。
では、それぞれを出力。
リクエストヘッダ
Request{method=POST, url=https://api.dify.ai/v1/chat-messages, headers=[Authorization:Bearer app-自分で作成したAPIキー]}
リクエストボディ
{"query":"君の役割を答えてくれ","response_mode":"streaming"}
以下を確認してみました。
ヘッダはおそらく正しそうです。
で、ボディですが
{
"inputs": {},
"query": "君の役割を答えてくれ",
"response_mode": "streaming",
"conversation_id": null,
"user": "test-user"
}
最低でも、"inputs","user"は追加する必要がありそう?
早速追加したリクエストDto
package app.windowView.api;
import java.util.HashMap;
import java.util.Map;
public class DifyRequestDto {
//ユーザーが入力したメッセージ
private final String query;
//レスポンスの通信設定。streaming:ストリーミング blocking:一括
private final String response_mode = "streaming";
//共通関数のPCMACアドレス取得メソッドから取得。
private final String usrMacAddress;
//ユーザーチャット履歴追跡用ID 追加機能予定のため未使用
private final String conversation_id;
//ユーザーリクエストをフォーム項目として格納する用。 7/7時点で未使用。
private final Map<String, Object> inputs;
//使用ユーザーID。後々は使用端末のMacアドレスを格納予定。
private final String user;
/**
*ユーザーメッセージを引数にするコンストラクタ 未使用項目(MACアドレス、チャット追跡ID、フォーム項目)あり。
* @param query ユーザーメッセージ、Jsonの項目名と同一
*/
public DifyRequestDto(String usrInputs) {
this.query = usrInputs;
//以下は空で用意。
this.usrMacAddress = null;
this.conversation_id = null;
this.inputs = new HashMap<String, Object>();
this.user = "test-user";
}
public String getUser() {
return user;
}
public String getQuery() {
return query;
}
public String getResponse_mode() {
return response_mode;
}
public String getUsrMacAddress() {
return usrMacAddress;
}
public String getConversation_id() {
return conversation_id;
}
public Map<String, Object> getInputs() {
return inputs;
}
}
では、再度実行
テスト自体は落ちてますが、API通信でチャットbotとやり取り自体はできてそうです。
落ちた理由としては、チャンク終了をキャッチできずにnullで落ちたみたいです。
調べてみた限りですが、
公式HPでストリーミング通信の終了時のJsonの状態について触れているページを見つけることができませんでした。
ちょっと、公式外になりますが以下のやり取りでJsonの内容について触れていました。
event: proxy bTOYc1
data: sage", ... "answer": "..."} [cite: 2]
data:
data: data: {"event": "message_end", ... } // Double 'data:' prefix [cite: 9]
data:
data:
どうしてdata→dataの2重構造になっているのかは不明ですが、
eventのmessage_endが終了を表しているみたいです。
では、以下の2つを修正(それぞれ修正箇所だけ載せます)
//チャンクのイベント(終了キャッチ用)
private String event;
public String getEvent() {
return event;
}
//チャンク毎にレスポンスの受け取り
try (BufferedSource source = responseBdoy.source()) {
while (!source.exhausted()) {
//1行ずつUTF-8形式で格納
String resline = source.readUtf8LineStrict();
if (resline != null && resline.startsWith("data:")) {
String data = resline.substring(6);
//受信チャンクが終了文かどうかチェック
// if ("[DONE]".equals(data)) {
// onComplete.run();
// break;
// }
DifyResponseDto chunk = gson.fromJson(data, DifyResponseDto.class);
//受信チャンクのイベントをチェックし、終了イベントなら処理終了。
String event = chunk.getEvent();
//nullでないなら、event.trim()、nullなら空文字を返す。
if("message_end".equals(event!= null ? event.trim():"")) {
onComplete.run();
System.out.println("チャンクの終了を確認");
break;
}
//メソッド引数のonChunkにセットされているメソッドの呼び出し。
onChunk.accept(chunk.getAnswer());
}
}
} catch (Exception e2) {
//チャンク読み込み中エラー
throw new IllegalStateException("チャンク読み込みエラー:" + e2.getMessage());
}
}
});
では、テスト実行
ちょっと2重で完了メッセージ出してますが、OKです。
最後に
かなり長くなりましたが、これで通信処理についてはOKです。
次のUTで色々とチェックは必要ですがひと段落しました。
ここまでやってみた感想としてはGsonライブラリが楽すぎますね
自分がPHPでJson触った時は、確かリスト構造みたいに要素の位置とキーを指定して中身取り出していた気がします。
Dtoに放り込んで、getterで取れるのは簡単で良いですね。
次に懸念点になりますが、処理のためにここまで色々とライブラリを追加してきました。
それが最終的に「デスクトップアプリで実行する環境だと無理です。」みたいになりそうで怖いです。
できる限りシンプルな環境を心がけたいです。
以上、お付き合いありがとうございます。