Sequences are now implemented in UE Wingman
This commit is contained in:
40
Plugins/UEWingman/Source/UEWingman/Handlers/Sequence.h
Normal file
40
Plugins/UEWingman/Source/UEWingman/Handlers/Sequence.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "WingServer.h"
|
||||
#include "WingBasics.h"
|
||||
#include "Sequence.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UWing_Sequence : public UWingHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, meta=(Description=
|
||||
"Array of subcommand JSON objects to execute in order. Each must contain 'command' and its parameters."))
|
||||
FWingJsonArray Subcommands;
|
||||
|
||||
virtual void Register() override
|
||||
{
|
||||
UWingServer::AddHandler(this,
|
||||
TEXT("Execute multiple commands in one request. Each subcommand produces its own content block in the response. "
|
||||
"Nested Sequence commands are not allowed."));
|
||||
}
|
||||
virtual void Handle() override
|
||||
{
|
||||
// The actual code that implements Sequence is hardwired into
|
||||
// WingServer. Because of that, this handler is never actually called
|
||||
// under normal conditions. The handler exists for two reasons: to
|
||||
// provide documentation, and also to catch the case where somebody
|
||||
// nests a sequence inside another sequence (WingServer doesn't catch
|
||||
// that).
|
||||
//
|
||||
WingOut::Stdout.Print(TEXT("ERROR: Sequence inside a Sequence is not allowed.\n"));
|
||||
}
|
||||
};
|
||||
@@ -170,6 +170,70 @@ TStatId UWingServer::GetStatId() const
|
||||
// ============================================================
|
||||
|
||||
FString UWingServer::HandleRequest(const FString& Line)
|
||||
{
|
||||
// Parse the request as JSON before doing anything else.
|
||||
TSharedPtr<FJsonValue> Value;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
||||
if (!FJsonSerializer::Deserialize(Reader, Value))
|
||||
return PackageResponses({TEXT("Invalid Json")});
|
||||
|
||||
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
|
||||
if (!Value->TryGetObject(RequestPtr))
|
||||
return PackageResponses({TEXT("Json must be an object")});
|
||||
TSharedPtr<FJsonObject> Request = *RequestPtr;
|
||||
|
||||
FString Command;
|
||||
Request->TryGetStringField(TEXT("command"), Command);
|
||||
if (Command == TEXT("Sequence"))
|
||||
{
|
||||
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
|
||||
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
|
||||
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
|
||||
|
||||
TArray<FString> Responses;
|
||||
Responses.Reserve(Subcommands->Num());
|
||||
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
|
||||
if (!Sub->TryGetObject(SubObjPtr))
|
||||
Responses.Add(TEXT("Subcommand must be a JSON object."));
|
||||
else
|
||||
Responses.Add(HandleJsonRequest(*SubObjPtr));
|
||||
}
|
||||
return PackageResponses(Responses);
|
||||
}
|
||||
|
||||
return PackageResponses({HandleJsonRequest(Request)});
|
||||
}
|
||||
|
||||
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Blocks;
|
||||
Blocks.Reserve(Responses.Num());
|
||||
for (const FString& Response : Responses)
|
||||
{
|
||||
// Unreal's JSON writer terminates string serialization at the first
|
||||
// embedded null byte rather than escaping it, which would silently
|
||||
// truncate output. Sanitize null bytes to spaces.
|
||||
FString Sanitized = Response;
|
||||
for (int32 i = 0; i < Sanitized.Len(); ++i)
|
||||
{
|
||||
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
|
||||
Block->SetStringField(TEXT("type"), TEXT("text"));
|
||||
Block->SetStringField(TEXT("text"), Sanitized);
|
||||
Blocks.Add(MakeShared<FJsonValueObject>(Block));
|
||||
}
|
||||
|
||||
FString OutJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
|
||||
FJsonSerializer::Serialize(Blocks, Writer);
|
||||
return OutJson;
|
||||
}
|
||||
|
||||
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
||||
{
|
||||
LogCapture.CapturedErrors.Empty();
|
||||
LogCapture.bEnabled = true;
|
||||
@@ -178,7 +242,7 @@ FString UWingServer::HandleRequest(const FString& Line)
|
||||
bSuggestHandlerHelp = false;
|
||||
LastHandler = nullptr;
|
||||
|
||||
TryCallHandler(Line);
|
||||
TryCallHandler(Request);
|
||||
|
||||
Notifier.SendNotifications();
|
||||
LogCapture.bEnabled = false;
|
||||
@@ -202,37 +266,11 @@ FString UWingServer::HandleRequest(const FString& Line)
|
||||
}
|
||||
FString Result = WingOut::StdoutBuffer.ToString();
|
||||
WingOut::StdoutBuffer.Reset();
|
||||
for (int32 i = 0; i < Result.Len(); ++i)
|
||||
{
|
||||
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
|
||||
return Result;
|
||||
}
|
||||
|
||||
// Wrap the text in an MCP content-block array: [{"type":"text","text":"..."}]
|
||||
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
|
||||
Block->SetStringField(TEXT("type"), TEXT("text"));
|
||||
Block->SetStringField(TEXT("text"), Result);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Blocks;
|
||||
Blocks.Add(MakeShared<FJsonValueObject>(Block));
|
||||
|
||||
FString OutJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
|
||||
FJsonSerializer::Serialize(Blocks, Writer);
|
||||
return OutJson;
|
||||
}
|
||||
|
||||
void UWingServer::TryCallHandler(const FString &Line)
|
||||
void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
|
||||
{
|
||||
// Turn the request string into a JSON tree.
|
||||
TSharedPtr<FJsonObject> Request;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
||||
FJsonSerializer::Deserialize(Reader, Request);
|
||||
if (!Request.IsValid())
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("Request is not valid JSON"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the command from the request.
|
||||
FString Command;
|
||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "WingServer.generated.h"
|
||||
|
||||
class FSocket;
|
||||
class FJsonObject;
|
||||
|
||||
/**
|
||||
* UWingServer — editor subsystem that listens on a TCP socket and dispatches
|
||||
@@ -61,6 +62,9 @@ public:
|
||||
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
|
||||
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
|
||||
|
||||
/** Package a list of response texts into a single serialized JSON content-block array. */
|
||||
static FString PackageResponses(const TArray<FString>& Responses);
|
||||
|
||||
private:
|
||||
static UWingServer* GWingServer;
|
||||
|
||||
@@ -78,7 +82,8 @@ private:
|
||||
|
||||
// Handle a complete JSON line and return the response JSON
|
||||
FString HandleRequest(const FString& Line);
|
||||
void TryCallHandler(const FString &Line);
|
||||
FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
|
||||
void TryCallHandler(TSharedPtr<FJsonObject> Request);
|
||||
|
||||
// ----- TCP server -----
|
||||
FSocket* ListenSocket = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user