UnrealEngineでDifyとのAIチャットを実装してみたので内容をメモしておきます。
試した環境
- UE5.5.4
- Dify:1.7.2
試した内容
以下のようなBlueprintノードを用意して、質問してその回答をSSE(Server-Sent Events)で少しずつ受け取れるようにしました。
以下のC++クラスを用意してます。
ChatMessageFunction.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "HttpFwd.h"
#include "ChatMessageFunction.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FChatMessageProgress, bool, bResult, FString, ResponseMessage);
/**
*
*/
UCLASS()
class DIFYCLIENT_API UChatMessageFunction : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable, Category = "DifyClient")
FChatMessageProgress Progress;
UPROPERTY(BlueprintAssignable, Category = "DifyClient")
FChatMessageProgress Complete;
public:
UChatMessageFunction(const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable, Category = "DifyClient", meta = (worldContext = "WorldContextObject", BlueprintInternalUseOnly = "true"))
static UChatMessageFunction* SendChatMessage(UObject* WorldContextObject, const FString& Message);
virtual void Activate() override;
private:
void OnResponseReceivedComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
void OnProcessResponse(FHttpRequestPtr Request, uint64 ByteSent, uint64 ByteReceived);
private:
FString Message;
int ResponseDataCount;
};
ChatMessageFunction.cpp
#include "ChatMessageFunction.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "HttpModule.h"
#include "Misc/Guid.h" // UUID生成用
#include "DifyClientBPLibrary.h"
UChatMessageFunction::UChatMessageFunction(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer), ResponseDataCount(0)
{
}
UChatMessageFunction* UChatMessageFunction::SendChatMessage(UObject* WorldContextObject, const FString& Message)
{
UChatMessageFunction* Node = NewObject<UChatMessageFunction>();
Node->RegisterWithGameInstance(WorldContextObject);
Node->Message = Message;
return Node;
}
void UChatMessageFunction::Activate()
{
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();
HttpRequest->SetURL(UDifyClientBPLibrary::GetBaseURL() + TEXT("/chat-messages"));
HttpRequest->SetVerb(TEXT("POST"));
HttpRequest->SetHeader(TEXT("Authorization"), TEXT("Bearer ") + UDifyClientBPLibrary::GetApiKey());
HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
TSharedPtr<FJsonObject> JsonRoot = MakeShareable(new FJsonObject);
JsonRoot->SetArrayField(TEXT("inputs"), TArray<TSharedPtr<FJsonValue>>());
JsonRoot->SetStringField(TEXT("query"), Message);
JsonRoot->SetStringField(TEXT("response_mode"), TEXT("streaming"));
JsonRoot->SetStringField(TEXT("user"), TEXT("test"));
FGuid Guid = FGuid::NewGuid();
FString GuidString = Guid.ToString(EGuidFormats::DigitsWithHyphens);
JsonRoot->SetStringField(TEXT("conversation_id"), TEXT(""));
FString OutPutString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutPutString);
FJsonSerializer::Serialize(JsonRoot.ToSharedRef(), Writer);
HttpRequest->SetContentAsString(OutPutString);
HttpRequest->OnProcessRequestComplete().BindUObject(this, &UChatMessageFunction::OnResponseReceivedComplete);
HttpRequest->OnRequestProgress64().BindUObject(this, &UChatMessageFunction::OnProcessResponse);
if (!HttpRequest->ProcessRequest())
{
Complete.Broadcast(false, TEXT("Failed to ProcessRequest"));
}
}
void UChatMessageFunction::OnResponseReceivedComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (Response.IsValid())
{
bool bResult = Response->GetResponseCode() == 200;
Complete.Broadcast(bResult, bResult ? TEXT("") : Response->GetContentAsString());
}
else
{
Complete.Broadcast(bWasSuccessful, TEXT(""));
}
SetReadyToDestroy();
}
void UChatMessageFunction::OnProcessResponse(FHttpRequestPtr Request, uint64 ByteSent, uint64 ByteReceived)
{
FHttpResponsePtr Response = Request->GetResponse();
if (Response.IsValid()) {
FString ResponseStr = Response->GetContentAsString();
TArray<FString> ResponseArr;
const int32 ArrNum = ResponseStr.ParseIntoArray(ResponseArr, TEXT("\n\n"));
FString ProgressStr;
bool bReceive = false;
for (int i = ResponseDataCount; i < ArrNum; i++)
{
FString& Line = ResponseArr[i];
// "data:"を先頭から削除
if (Line.StartsWith(TEXT("data:")))
{
Line = Line.RightChop(5).TrimStart();
}
else
{
continue;
}
// JSONパース
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) {
continue;
}
FString Event = JsonObject->GetStringField("event");
if (Event == "agent_message") {
FString Answer = JsonObject->GetStringField("answer");
ProgressStr = ProgressStr + Answer;
bReceive = true;
UE_LOG(LogTemp, Log, TEXT("Progress:%s"), *Answer);
}
else
{
UE_LOG(LogTemp, Log, TEXT("Progress:%s"), *Line);
}
}
if (bReceive)
{
Progress.Broadcast(true, ProgressStr);
}
ResponseDataCount = ArrNum;
}
}