diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.exe b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.exe new file mode 100644 index 0000000..51887f4 Binary files /dev/null and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.exe differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.exp index c3e412a..ceb6481 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.exp and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.exp differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.pdb index 7a5796d..a256fab 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.patch_0.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.patch_0.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.patch_0.pdb index 2c57992..af44870 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.patch_0.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.patch_0.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.dll b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.dll index 363b703..d1e7dd5 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.dll and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.dll differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.exp index 125ac61..c6e0196 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.exp and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.exp differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_0.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_0.pdb index 819a33a..fccb428 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_0.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_0.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.exe b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.exe deleted file mode 100644 index 62bd4b0..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.exe and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.exp deleted file mode 100644 index 0c8bf73..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.exp and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.lib b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.lib deleted file mode 100644 index 65f1b10..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.lib and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.pdb deleted file mode 100644 index 81e95c2..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_1.pdb and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.exe b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.exe deleted file mode 100644 index 9d25f72..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.exe and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.exp deleted file mode 100644 index 3688388..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.exp and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.lib b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.lib deleted file mode 100644 index dc1b3ca..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.lib and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.pdb deleted file mode 100644 index 12f39fd..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_2.pdb and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.exe b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.exe deleted file mode 100644 index 71ed61e..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.exe and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.exp deleted file mode 100644 index 9227fc0..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.exp and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.lib b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.lib deleted file mode 100644 index 56cf88c..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.lib and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.pdb deleted file mode 100644 index 38a9362..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_3.pdb and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.exe b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.exe deleted file mode 100644 index c95c710..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.exe and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.exp deleted file mode 100644 index 9e3c988..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.exp and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.lib b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.lib deleted file mode 100644 index fd75fcf..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.lib and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.pdb deleted file mode 100644 index ddfd92b..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.patch_4.pdb and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.pdb index eefdf0c..45f309e 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgent.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.dll b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.dll index db71779..a7c162c 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.dll and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.dll differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.exp index 8b6aa83..5bb8dcf 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.exp and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.exp differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_0.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_0.pdb index d80a317..34d23e0 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_0.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_0.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.exe b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.exe deleted file mode 100644 index 26edc13..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.exe and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.exp deleted file mode 100644 index 0db59a8..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.exp and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.lib b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.lib deleted file mode 100644 index 123017b..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.lib and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.pdb deleted file mode 100644 index e483e27..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_1.pdb and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.exe b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.exe deleted file mode 100644 index 75caa8f..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.exe and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.exp deleted file mode 100644 index 886366e..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.exp and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.lib b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.lib deleted file mode 100644 index 4f912de..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.lib and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.pdb deleted file mode 100644 index c6f08cf..0000000 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.patch_2.pdb and /dev/null differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.pdb index 8df8da0..987555c 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Binaries/Win64/UnrealEditor-PS_AI_ConvAgentEditor.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_AgentConfig_ElevenLabs.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_AgentConfig_ElevenLabs.h index 47615d5..dc5937d 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_AgentConfig_ElevenLabs.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_AgentConfig_ElevenLabs.h @@ -5,7 +5,7 @@ #include "CoreMinimal.h" #include "Engine/DataAsset.h" #include "PS_AI_ConvAgent_Definitions.h" -#include "PS_AI_ConvAgent_ActionSet_ElevenLabs.h" +#include "PS_AI_ConvAgent_Tool_ElevenLabs.h" #include "PS_AI_ConvAgent_AgentConfig_ElevenLabs.generated.h" /** @@ -206,36 +206,18 @@ public: meta = (ToolTip = "Start generating a response before confirming end-of-speech.\nReduces latency but may cause occasional false starts.\nDisable if the agent interrupts the user too often.")) bool bSpeculativeTurn = false; - // ── Emotion Tool ───────────────────────────────────────────────────────── + // ── Tools ──────────────────────────────────────────────────────────────── - /** Include the built-in "set_emotion" client tool in the agent configuration. - * Allows the LLM to set facial expressions (Joy, Sadness, Anger, etc.) - * that drive the FacialExpression component in real-time. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Emotion Tool", - meta = (ToolTip = "Include the set_emotion client tool.\nAllows the LLM to drive facial expressions.")) - bool bIncludeEmotionTool = true; - - /** System prompt fragment appended to CharacterPrompt when bIncludeEmotionTool is true. - * Pre-filled with the standard emotion instruction. Editable for customization. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Emotion Tool", - meta = (MultiLine = "true", EditCondition = "bIncludeEmotionTool", - ToolTip = "Prompt instructions for the emotion tool.\nAppended to CharacterPrompt when creating/updating the agent.")) - FString EmotionToolPromptFragment = TEXT( - "## Facial Expressions\n" - "You have a set_emotion tool to control your facial expression. " - "Use it whenever the emotional context changes:\n" - "- Call set_emotion with emotion=\"joy\" when happy, laughing, or excited\n" - "- Call set_emotion with emotion=\"sadness\" when empathetic or discussing sad topics\n" - "- Call set_emotion with emotion=\"anger\" when frustrated or discussing injustice\n" - "- Call set_emotion with emotion=\"surprise\" when reacting to unexpected information\n" - "- Call set_emotion with emotion=\"fear\" when discussing scary or worrying topics\n" - "- Call set_emotion with emotion=\"disgust\" when reacting to unpleasant things\n" - "- Call set_emotion with emotion=\"neutral\" to return to a calm expression\n\n" - "Use intensity to match the strength of the emotion:\n" - "- \"low\" for subtle hints (slight smile, mild concern)\n" - "- \"medium\" for normal expression (default)\n" - "- \"high\" for strong reactions (big laugh, deep sadness, shock)\n\n" - "Always return to neutral when the emotional moment passes."); + /** Standalone tools assigned to this agent. + * Each tool is a global resource on ElevenLabs (managed via its own Data Asset). + * The tool's PromptFragment is appended to CharacterPrompt on Create/Update. + * The tool's ToolID is sent in prompt.tool_ids to the ElevenLabs API. + * + * Drag Tool Data Assets here (e.g. set_emotion, perform_action, custom tools). + * Create tools in Content Browser: Miscellaneous > Data Asset > PS AI ConvAgent Tool. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tools", + meta = (ToolTip = "Standalone tools assigned to this agent.\nDrag Tool Data Assets here.\nEach tool's PromptFragment is appended to the system prompt.")) + TArray> Tools; // ── Expressive Mode (V3 Conversational) ───────────────────────────────── @@ -266,24 +248,6 @@ public: "Example: \"That's great to hear! [laughs] I'm glad we could sort that out for you.\"\n\n" "Use these tags naturally and sparingly to enhance expressiveness without overusing them."); - // ── Action Tool ───────────────────────────────────────────────────────── - - /** Include a configurable "perform_action" client tool in the agent configuration. - * Allows the LLM to trigger physical actions defined in the referenced ActionSet. - * Actions are handled via the OnAgentActionRequested event in Blueprint. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action Tool", - meta = (ToolTip = "Include the perform_action client tool.\nRequires an ActionSet Data Asset with at least one action.")) - bool bIncludeActionTool = false; - - /** Reference to a reusable Action Set Data Asset. - * Create one in Content Browser (Miscellaneous > Data Asset > PS AI ConvAgent Action Set), - * define your actions there, then drag it here. - * The same ActionSet can be shared by multiple agents. */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action Tool", - meta = (EditCondition = "bIncludeActionTool", - ToolTip = "Drag an ActionSet Data Asset here.\nDefines which actions the agent can trigger.")) - TObjectPtr ActionSet; - // ── Dynamic Variables ──────────────────────────────────────────────────── /** Key-value pairs sent as dynamic_variables at conversation start. diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_Definitions.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_Definitions.h index ba2f3f5..2271f49 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_Definitions.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_Definitions.h @@ -168,7 +168,7 @@ struct PS_AI_CONVAGENT_API FPS_AI_ConvAgent_ClientToolCall_ElevenLabs }; // ───────────────────────────────────────────────────────────────────────────── -// Agent action definition (used by ActionSet Data Asset) +// Agent action definition (used by ActionSet Data Asset — deprecated, kept for compat) // ───────────────────────────────────────────────────────────────────────────── /** Defines a single action that an agent can perform during conversation. */ USTRUCT(BlueprintType) @@ -184,3 +184,57 @@ struct PS_AI_CONVAGENT_API FPS_AI_ConvAgent_AgentAction_ElevenLabs UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|ElevenLabs") FString Description; }; + +// ───────────────────────────────────────────────────────────────────────────── +// Tool parameter data type (maps to JSON Schema "type") +// ───────────────────────────────────────────────────────────────────────────── +UENUM(BlueprintType) +enum class EPS_AI_ConvAgent_ToolParamType : uint8 +{ + String UMETA(DisplayName = "String"), + Integer UMETA(DisplayName = "Integer"), + Number UMETA(DisplayName = "Number"), + Boolean UMETA(DisplayName = "Boolean"), +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Tool parameter definition (used by Tool Data Asset) +// ───────────────────────────────────────────────────────────────────────────── +/** Defines a single parameter for a standalone ElevenLabs tool. + * Maps directly to the ElevenLabs tool parameter UI: + * Identifier, Data type, Required, Description (LLM prompt), Enum Values. */ +USTRUCT(BlueprintType) +struct PS_AI_CONVAGENT_API FPS_AI_ConvAgent_ToolParameter_ElevenLabs +{ + GENERATED_BODY() + + /** Parameter identifier (snake_case). Must match the name expected by the client code. + * Examples: "emotion", "intensity", "action", "target_name" */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|ElevenLabs", + meta = (ToolTip = "Parameter name (snake_case).\nThe key sent in the tool call JSON.")) + FString Name; + + /** Data type for this parameter. Maps to JSON Schema "type". */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|ElevenLabs", + meta = (ToolTip = "Data type: String, Integer, Number, or Boolean.")) + EPS_AI_ConvAgent_ToolParamType Type = EPS_AI_ConvAgent_ToolParamType::String; + + /** Whether this parameter is required in the tool call. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|ElevenLabs", + meta = (ToolTip = "If true, the LLM must provide this parameter.")) + bool bRequired = true; + + /** Description passed to the LLM explaining what this parameter is + * and how to extract or determine its value from the conversation. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|ElevenLabs", + meta = (MultiLine = "true", + ToolTip = "LLM prompt describing this parameter.\nGuides the LLM on what value to provide.")) + FString Description; + + /** Optional predefined values the LLM can choose from. + * If empty, the LLM can use any value of the specified type. + * Examples: ["joy","sadness","anger"] for emotion, ["low","medium","high"] for intensity. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI ConvAgent|ElevenLabs", + meta = (ToolTip = "Predefined enum values (optional).\nIf set, the LLM must choose from these.")) + TArray EnumValues; +}; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_Tool_ElevenLabs.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_Tool_ElevenLabs.h new file mode 100644 index 0000000..faf24d8 --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Public/PS_AI_ConvAgent_Tool_ElevenLabs.h @@ -0,0 +1,90 @@ +// Copyright ASTERION. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "PS_AI_ConvAgent_Definitions.h" +#include "PS_AI_ConvAgent_Tool_ElevenLabs.generated.h" + +/** + * Standalone tool definition for ElevenLabs Conversational AI agents. + * + * Each tool is a global resource on ElevenLabs, created once and shared + * by any number of agents. Assign tools to agents via the Tools array + * in the Agent Config data asset. + * + * Create via Content Browser > Miscellaneous > Data Asset > + * PS AI ConvAgent Tool (ElevenLabs). + * + * Examples: + * - set_emotion: drives facial expressions (emotion + intensity params) + * - perform_action: triggers physical actions (flee, draw_weapon, etc.) + * - Custom tools: any client-side tool the LLM can invoke + */ +UCLASS(BlueprintType, Blueprintable, + DisplayName = "PS AI ConvAgent Tool (ElevenLabs)") +class PS_AI_CONVAGENT_API UPS_AI_ConvAgent_Tool_ElevenLabs : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + // ── Identity ──────────────────────────────────────────────────────────── + + /** Tool name (snake_case). Must match the name used in the LLM tool call. + * Examples: "set_emotion", "perform_action", "open_door" */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tool", + meta = (ToolTip = "Tool name sent to/from the LLM (snake_case).")) + FString ToolName; + + /** ElevenLabs tool ID. Auto-populated when you click Create Tool. + * Leave empty to create a new tool; set to update an existing one. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Tool", + meta = (ToolTip = "ElevenLabs tool ID.\nAuto-populated on Create. Used for Update/Fetch.")) + FString ToolID; + + /** Short description of what this tool does and when the LLM should use it. + * Sent as the tool's 'description' field to ElevenLabs. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tool", + meta = (MultiLine = "true", + ToolTip = "Tool description for the LLM.\nExplains when and how to invoke this tool.")) + FString ToolDescription; + + // ── Prompt Fragment ───────────────────────────────────────────────────── + + /** System prompt fragment appended to the agent's CharacterPrompt + * when this tool is assigned. Provides detailed instructions + * for the LLM on how to use this tool in context. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompt", + meta = (MultiLine = "true", + ToolTip = "Prompt instructions appended to CharacterPrompt.\nDescribes when and how the agent should use this tool.")) + FString PromptFragment; + + // ── Parameters ────────────────────────────────────────────────────────── + + /** Tool parameters sent to the LLM. + * Each parameter defines an input the LLM must provide when calling this tool. + * Maps to the ElevenLabs tool parameter editor (Identifier, Data type, + * Required, Description, Enum Values). + * + * Examples: + * - set_emotion: param "emotion" (enum: joy,sadness,...) + param "intensity" (enum: low,medium,high) + * - perform_action: param "action" (enum: flee,draw_weapon,...) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters", + meta = (TitleProperty = "Name", + ToolTip = "Tool parameters.\nEach one is an input the LLM provides when calling this tool.")) + TArray Parameters; + + // ── Sync metadata ─────────────────────────────────────────────────────── + + /** ISO 8601 timestamp of the last successful sync with ElevenLabs. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Tool", + meta = (ToolTip = "Last sync timestamp (UTC).")) + FString LastSyncTimestamp; + + // UPrimaryDataAsset interface + virtual FPrimaryAssetId GetPrimaryAssetId() const override + { + return FPrimaryAssetId(TEXT("Tool_ElevenLabs"), GetFName()); + } +}; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgentEditorModule.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgentEditorModule.cpp index e3eb369..742a9a8 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgentEditorModule.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgentEditorModule.cpp @@ -6,6 +6,8 @@ #include "PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h" #include "PS_AI_ConvAgent_ActionSet_ElevenLabs.h" #include "PS_AI_ConvAgent_ActionSetCustomization_ElevenLabs.h" +#include "PS_AI_ConvAgent_Tool_ElevenLabs.h" +#include "PS_AI_ConvAgent_ToolCustomization_ElevenLabs.h" /** * Editor module for PS_AI_ConvAgent plugin. @@ -28,6 +30,11 @@ public: UPS_AI_ConvAgent_ActionSet_ElevenLabs::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic( &FPS_AI_ConvAgent_ActionSetCustomization_ElevenLabs::MakeInstance)); + + PropertyModule.RegisterCustomClassLayout( + UPS_AI_ConvAgent_Tool_ElevenLabs::StaticClass()->GetFName(), + FOnGetDetailCustomizationInstance::CreateStatic( + &FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::MakeInstance)); } virtual void ShutdownModule() override @@ -42,6 +49,9 @@ public: PropertyModule.UnregisterCustomClassLayout( UPS_AI_ConvAgent_ActionSet_ElevenLabs::StaticClass()->GetFName()); + + PropertyModule.UnregisterCustomClassLayout( + UPS_AI_ConvAgent_Tool_ElevenLabs::StaticClass()->GetFName()); } } }; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ActionSetCustomization_ElevenLabs.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ActionSetCustomization_ElevenLabs.cpp index 3fbe229..6a03d03 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ActionSetCustomization_ElevenLabs.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ActionSetCustomization_ElevenLabs.cpp @@ -2,24 +2,13 @@ #include "PS_AI_ConvAgent_ActionSetCustomization_ElevenLabs.h" #include "PS_AI_ConvAgent_ActionSet_ElevenLabs.h" -#include "PS_AI_ConvAgent_AgentConfig_ElevenLabs.h" -#include "PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h" #include "PS_AI_ConvAgent.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "DetailWidgetRow.h" -#include "Widgets/Input/SButton.h" #include "Widgets/Text/STextBlock.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "HttpModule.h" -#include "Interfaces/IHttpRequest.h" -#include "Interfaces/IHttpResponse.h" -#include "Dom/JsonObject.h" -#include "Serialization/JsonWriter.h" -#include "Serialization/JsonSerializer.h" - DEFINE_LOG_CATEGORY_STATIC(LogPS_AI_ActionSetEditor, Log, All); // ───────────────────────────────────────────────────────────────────────────── @@ -38,200 +27,37 @@ void FPS_AI_ConvAgent_ActionSetCustomization_ElevenLabs::CustomizeDetails( { DetailBuilder.GetObjectsBeingCustomized(SelectedObjects); - // ── Agent Sync category ───────────────────────────────────────────────── - IDetailCategoryBuilder& SyncCat = DetailBuilder.EditCategory( - TEXT("Agent Sync"), - FText::FromString(TEXT("Agent Sync")), + // ── Deprecation notice ────────────────────────────────────────────────── + IDetailCategoryBuilder& DeprecatedCat = DetailBuilder.EditCategory( + TEXT("Deprecated"), + FText::FromString(TEXT("Deprecated")), ECategoryPriority::Important); - SyncCat.AddCustomRow(FText::FromString(TEXT("Update All Agents"))) + DeprecatedCat.AddCustomRow(FText::FromString(TEXT("Deprecation Notice"))) .WholeRowContent() [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(0, 4) - [ - SNew(SButton) - .Text(FText::FromString(TEXT("Update All Agents"))) - .ToolTipText(FText::FromString( - TEXT("PATCH all AgentConfig assets that reference this ActionSet."))) - .OnClicked_Lambda([this]() - { - OnUpdateAllAgentsClicked(); - return FReply::Handled(); - }) - ] - + SVerticalBox::Slot() - .AutoHeight() - .Padding(0, 2) [ SAssignNew(StatusTextBlock, STextBlock) - .Text(FText::GetEmpty()) + .Text(FText::FromString( + TEXT("ActionSet is deprecated. Use Tool Data Assets instead.\n" + "Create a Tool (Miscellaneous > Data Asset > PS AI ConvAgent Tool),\n" + "add your actions there, then assign it to agents via the Tools array."))) .Font(IDetailLayoutBuilder::GetDetailFont()) - .ColorAndOpacity(FSlateColor(FLinearColor(0.3f, 0.7f, 1.0f))) + .ColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.7f, 0.2f))) // orange/warning ] ]; } // ───────────────────────────────────────────────────────────────────────────── -// Update All Agents +// OnUpdateAllAgentsClicked — Deprecated, no-op // ───────────────────────────────────────────────────────────────────────────── void FPS_AI_ConvAgent_ActionSetCustomization_ElevenLabs::OnUpdateAllAgentsClicked() { - const UPS_AI_ConvAgent_ActionSet_ElevenLabs* ActionSetAsset = GetEditedAsset(); - if (!ActionSetAsset) - { - SetStatusError(TEXT("No ActionSet asset selected.")); - return; - } - - const FString APIKey = GetAPIKey(); - if (APIKey.IsEmpty()) - { - SetStatusError(TEXT("API Key not set in Project Settings > PS AI ConvAgent - ElevenLabs.")); - return; - } - - // ── Scan all AgentConfig assets via Asset Registry ─────────────────────── - FAssetRegistryModule& ARModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - IAssetRegistry& AssetRegistry = ARModule.Get(); - - TArray AllAgentConfigs; - AssetRegistry.GetAssetsByClass( - UPS_AI_ConvAgent_AgentConfig_ElevenLabs::StaticClass()->GetClassPathName(), - AllAgentConfigs, true); - - // ── Filter: bIncludeActionTool && ActionSet == this asset && AgentID not empty ─ - TArray MatchingConfigs; - for (const FAssetData& AD : AllAgentConfigs) - { - UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Config = - Cast(AD.GetAsset()); - if (!Config) continue; - if (!Config->bIncludeActionTool) continue; - if (Config->ActionSet != ActionSetAsset) continue; - if (Config->AgentID.IsEmpty()) continue; - - MatchingConfigs.Add(Config); - } - - if (MatchingConfigs.Num() == 0) - { - SetStatusError(TEXT("No AgentConfig assets reference this ActionSet (with AgentID set).")); - return; - } - - SetStatusText(FString::Printf(TEXT("Updating %d agent(s)..."), MatchingConfigs.Num())); - - // ── Shared counter for async completion tracking ───────────────────────── - struct FBatchState - { - int32 Total = 0; - FThreadSafeCounter Succeeded; - FThreadSafeCounter Failed; - TArray Errors; - FCriticalSection ErrorLock; - }; - TSharedPtr State = MakeShareable(new FBatchState()); - State->Total = MatchingConfigs.Num(); - - TWeakPtr WeakSelf = - StaticCastSharedRef(this->AsShared()); - - for (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Config : MatchingConfigs) - { - TSharedPtr Payload = - FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::BuildAgentPayloadForAsset(Config); - - FString PayloadStr; - TSharedRef> Writer = TJsonWriterFactory<>::Create(&PayloadStr); - FJsonSerializer::Serialize(Payload.ToSharedRef(), Writer); - - const FString URL = FString::Printf( - TEXT("https://api.elevenlabs.io/v1/convai/agents/%s"), *Config->AgentID); - - TSharedRef Request = FHttpModule::Get().CreateRequest(); - Request->SetURL(URL); - Request->SetVerb(TEXT("PATCH")); - Request->SetHeader(TEXT("xi-api-key"), APIKey); - Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - Request->SetContentAsString(PayloadStr); - - // Capture Config as weak pointer for safety. - TWeakObjectPtr WeakConfig(Config); - FString AgentName = Config->AgentName.IsEmpty() ? Config->AgentID : Config->AgentName; - - Request->OnProcessRequestComplete().BindLambda( - [WeakSelf, State, WeakConfig, AgentName] - (FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) - { - bool bSuccess = false; - FString ErrorMsg; - - if (!bConnected || !Resp.IsValid()) - { - ErrorMsg = FString::Printf(TEXT("%s: connection failed"), *AgentName); - } - else if (Resp->GetResponseCode() != 200) - { - ErrorMsg = FString::Printf(TEXT("%s: HTTP %d"), - *AgentName, Resp->GetResponseCode()); - } - else - { - bSuccess = true; - // Update LastSyncTimestamp on the asset. - if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Cfg = WeakConfig.Get()) - { - Cfg->Modify(); - Cfg->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); - } - } - - if (bSuccess) - { - State->Succeeded.Increment(); - } - else - { - State->Failed.Increment(); - FScopeLock Lock(&State->ErrorLock); - State->Errors.Add(ErrorMsg); - } - - // Check if all requests are done. - const int32 Done = State->Succeeded.GetValue() + State->Failed.GetValue(); - if (Done >= State->Total) - { - auto Pinned = WeakSelf.Pin(); - if (!Pinned.IsValid()) return; - - if (State->Failed.GetValue() == 0) - { - Pinned->SetStatusSuccess(FString::Printf( - TEXT("Updated %d/%d agents successfully."), - State->Succeeded.GetValue(), State->Total)); - } - else - { - FString AllErrors; - { - FScopeLock Lock(&State->ErrorLock); - AllErrors = FString::Join(State->Errors, TEXT(", ")); - } - Pinned->SetStatusError(FString::Printf( - TEXT("Updated %d/%d agents. Failures: %s"), - State->Succeeded.GetValue(), State->Total, *AllErrors)); - } - } - }); - - Request->ProcessRequest(); - - UE_LOG(LogPS_AI_ActionSetEditor, Log, - TEXT(" → PATCH agent '%s' (ID: %s)"), *AgentName, *Config->AgentID); - } + SetStatusError(TEXT("ActionSet is deprecated. Use Tool Data Assets instead.")); } // ───────────────────────────────────────────────────────────────────────────── diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.cpp index 6f0a49e..08568de 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.cpp @@ -2,8 +2,11 @@ #include "PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h" #include "PS_AI_ConvAgent_AgentConfig_ElevenLabs.h" +#include "PS_AI_ConvAgent_Tool_ElevenLabs.h" #include "PS_AI_ConvAgent.h" +#include "AssetRegistry/AssetRegistryModule.h" + #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "DetailWidgetRow.h" @@ -540,108 +543,59 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnVoiceSelected( // ───────────────────────────────────────────────────────────────────────────── void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnFetchModelsClicked() { - const FString APIKey = GetAPIKey(); - if (APIKey.IsEmpty()) + // Conversational AI agents support a fixed set of TTS models. + // These are NOT returned by the general /v1/models endpoint + // (which lists speech-synthesis models, not agent TTS models). + // Source: ElevenLabs ConvAI API — PATCH /v1/convai/agents/{id} model_id enum. + struct FConvAIModel { - SetStatusError(TEXT("API Key not set in Project Settings > PS AI ConvAgent - ElevenLabs.")); - return; + const TCHAR* ID; + const TCHAR* DisplayName; + }; + + static const FConvAIModel ConvAIModels[] = { + { TEXT("eleven_v3_conversational"), TEXT("V3 Conversational (eleven_v3_conversational)") }, + { TEXT("eleven_turbo_v2_5"), TEXT("Turbo v2.5 (eleven_turbo_v2_5)") }, + { TEXT("eleven_flash_v2_5"), TEXT("Flash v2.5 (eleven_flash_v2_5)") }, + { TEXT("eleven_turbo_v2"), TEXT("Turbo v2 (eleven_turbo_v2)") }, + { TEXT("eleven_flash_v2"), TEXT("Flash v2 (eleven_flash_v2)") }, + { TEXT("eleven_multilingual_v2"), TEXT("Multilingual v2 (eleven_multilingual_v2)") }, + }; + + ModelDisplayNames.Reset(); + ModelIDs.Reset(); + + for (const auto& M : ConvAIModels) + { + ModelDisplayNames.Add(MakeShareable(new FString(M.DisplayName))); + ModelIDs.Add(M.ID); } - SetStatusText(TEXT("Fetching models...")); + // Pre-select the currently set TTSModelID. + if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = GetEditedAsset()) + { + int32 Idx = ModelIDs.IndexOfByKey(Asset->TTSModelID); - TSharedRef Request = FHttpModule::Get().CreateRequest(); - Request->SetURL(TEXT("https://api.elevenlabs.io/v1/models")); - Request->SetVerb(TEXT("GET")); - Request->SetHeader(TEXT("xi-api-key"), APIKey); - Request->SetHeader(TEXT("Accept"), TEXT("application/json")); - - TWeakPtr WeakSelf = - StaticCastSharedRef(this->AsShared()); - - Request->OnProcessRequestComplete().BindLambda( - [WeakSelf](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) + // Inject the asset's current model if it's not in our known list. + if (Idx == INDEX_NONE && !Asset->TTSModelID.IsEmpty()) { - auto Pinned = WeakSelf.Pin(); - if (!Pinned.IsValid()) return; + ModelDisplayNames.Add(MakeShareable(new FString(Asset->TTSModelID))); + ModelIDs.Add(Asset->TTSModelID); + Idx = ModelIDs.Num() - 1; + } - if (!bConnected || !Resp.IsValid()) - { - Pinned->SetStatusError(TEXT("Could not reach ElevenLabs API.")); - return; - } + if (Idx != INDEX_NONE && ModelComboBox.IsValid()) + { + ModelComboBox->SetSelectedItem(ModelDisplayNames[Idx]); + } + } - if (Resp->GetResponseCode() != 200) - { - Pinned->SetStatusError(ParseAPIError( - Resp->GetResponseCode(), Resp->GetContentAsString())); - return; - } + if (ModelComboBox.IsValid()) + { + ModelComboBox->RefreshOptions(); + } - // Response is a JSON array of model objects. - TArray> Models; - if (!FJsonSerializer::Deserialize( - TJsonReaderFactory<>::Create(Resp->GetContentAsString()), Models)) - { - Pinned->SetStatusError(TEXT("Failed to parse models JSON.")); - return; - } - - Pinned->ModelDisplayNames.Reset(); - Pinned->ModelIDs.Reset(); - - for (const auto& ModelVal : Models) - { - const TSharedPtr* ModelObj = nullptr; - if (!ModelVal->TryGetObject(ModelObj)) continue; - - FString Name, ID; - (*ModelObj)->TryGetStringField(TEXT("name"), Name); - (*ModelObj)->TryGetStringField(TEXT("model_id"), ID); - - // Only show TTS-capable models. - bool bCanTTS = false; - (*ModelObj)->TryGetBoolField(TEXT("can_do_text_to_speech"), bCanTTS); - if (!bCanTTS) continue; - - if (!ID.IsEmpty()) - { - FString DisplayStr = FString::Printf(TEXT("%s (%s)"), *Name, *ID); - Pinned->ModelDisplayNames.Add(MakeShareable(new FString(DisplayStr))); - Pinned->ModelIDs.Add(ID); - } - } - - // Pre-select the currently set TTSModelID if it exists in the list. - if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = Pinned->GetEditedAsset()) - { - int32 Idx = Pinned->ModelIDs.IndexOfByKey(Asset->TTSModelID); - - // Agent-only models (e.g. eleven_v3_conversational) may not appear - // in the general /v1/models list. Inject the asset's current model - // so the combo always reflects the actual value. - if (Idx == INDEX_NONE && !Asset->TTSModelID.IsEmpty()) - { - FString DisplayStr = FString::Printf(TEXT("%s"), *Asset->TTSModelID); - Pinned->ModelDisplayNames.Add(MakeShareable(new FString(DisplayStr))); - Pinned->ModelIDs.Add(Asset->TTSModelID); - Idx = Pinned->ModelIDs.Num() - 1; - } - - if (Idx != INDEX_NONE && Pinned->ModelComboBox.IsValid()) - { - Pinned->ModelComboBox->SetSelectedItem(Pinned->ModelDisplayNames[Idx]); - } - } - - if (Pinned->ModelComboBox.IsValid()) - { - Pinned->ModelComboBox->RefreshOptions(); - } - - Pinned->SetStatusSuccess(FString::Printf(TEXT("Fetched %d TTS models."), Pinned->ModelIDs.Num())); - }); - - Request->ProcessRequest(); + SetStatusSuccess(FString::Printf(TEXT("Loaded %d ConvAI TTS models."), ModelIDs.Num())); } void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnModelSelected( @@ -656,6 +610,16 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnModelSelected( { Asset->Modify(); Asset->TTSModelID = ModelIDs[Idx]; + + // Auto-sync Expressive Mode: V3 Conversational requires it, other models don't support it. + const bool bIsV3 = Asset->TTSModelID == TEXT("eleven_v3_conversational"); + if (Asset->bExpressiveMode != bIsV3) + { + Asset->bExpressiveMode = bIsV3; + UE_LOG(LogPS_AI_AgentConfigEditor, Log, + TEXT("TTS model changed to '%s' — %s Expressive Mode."), + *Asset->TTSModelID, bIsV3 ? TEXT("enabling") : TEXT("disabling")); + } } } @@ -937,7 +901,7 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnLanguageSelected( // ───────────────────────────────────────────────────────────────────────────── void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnCreateAgentClicked() { - const UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = GetEditedAsset(); + UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = GetEditedAsset(); if (!Asset) { SetStatusError(TEXT("No asset selected.")); @@ -959,6 +923,18 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnCreateAgentClicked( SetStatusText(TEXT("Creating agent...")); + // Validate that all assigned tools have been created on ElevenLabs + for (const auto& Tool : Asset->Tools) + { + if (Tool && Tool->ToolID.IsEmpty()) + { + SetStatusError(FString::Printf( + TEXT("Tool '%s' has no ToolID. Create it first via the Tool editor."), + *Tool->ToolName)); + return; + } + } + TSharedPtr Payload = BuildAgentPayload(); FString PayloadStr; TSharedRef> Writer = TJsonWriterFactory<>::Create(&PayloadStr); @@ -977,18 +953,18 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnCreateAgentClicked( Request->OnProcessRequestComplete().BindLambda( [WeakSelf](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) { - auto Pinned = WeakSelf.Pin(); - if (!Pinned.IsValid()) return; + auto P = WeakSelf.Pin(); + if (!P.IsValid()) return; if (!bConnected || !Resp.IsValid()) { - Pinned->SetStatusError(TEXT("Could not reach ElevenLabs API.")); + P->SetStatusError(TEXT("Could not reach ElevenLabs API.")); return; } if (Resp->GetResponseCode() != 200 && Resp->GetResponseCode() != 201) { - Pinned->SetStatusError(ParseAPIError( + P->SetStatusError(ParseAPIError( Resp->GetResponseCode(), Resp->GetContentAsString())); return; } @@ -997,26 +973,26 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnCreateAgentClicked( if (!FJsonSerializer::Deserialize( TJsonReaderFactory<>::Create(Resp->GetContentAsString()), Root) || !Root.IsValid()) { - Pinned->SetStatusError(TEXT("Failed to parse response.")); + P->SetStatusError(TEXT("Failed to parse response.")); return; } FString NewAgentID; if (!Root->TryGetStringField(TEXT("agent_id"), NewAgentID)) { - Pinned->SetStatusError(TEXT("No 'agent_id' in response.")); + P->SetStatusError(TEXT("No 'agent_id' in response.")); return; } - if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = Pinned->GetEditedAsset()) + if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* A = P->GetEditedAsset()) { - Asset->Modify(); - Asset->AgentID = NewAgentID; - Asset->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); - Asset->PostEditChange(); + A->Modify(); + A->AgentID = NewAgentID; + A->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); + A->PostEditChange(); } - Pinned->SetStatusSuccess(FString::Printf(TEXT("Agent created: %s"), *NewAgentID)); + P->SetStatusSuccess(FString::Printf(TEXT("Agent created: %s"), *NewAgentID)); }); Request->ProcessRequest(); @@ -1027,7 +1003,7 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnCreateAgentClicked( // ───────────────────────────────────────────────────────────────────────────── void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnUpdateAgentClicked() { - const UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = GetEditedAsset(); + UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = GetEditedAsset(); if (!Asset) { SetStatusError(TEXT("No asset selected.")); @@ -1047,6 +1023,18 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnUpdateAgentClicked( return; } + // Validate that all assigned tools have been created on ElevenLabs + for (const auto& Tool : Asset->Tools) + { + if (Tool && Tool->ToolID.IsEmpty()) + { + SetStatusError(FString::Printf( + TEXT("Tool '%s' has no ToolID. Create it first via the Tool editor."), + *Tool->ToolName)); + return; + } + } + SetStatusText(TEXT("Updating agent...")); TSharedPtr Payload = BuildAgentPayload(); @@ -1070,29 +1058,29 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnUpdateAgentClicked( Request->OnProcessRequestComplete().BindLambda( [WeakSelf](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) { - auto Pinned = WeakSelf.Pin(); - if (!Pinned.IsValid()) return; + auto P = WeakSelf.Pin(); + if (!P.IsValid()) return; if (!bConnected || !Resp.IsValid()) { - Pinned->SetStatusError(TEXT("Could not reach ElevenLabs API.")); + P->SetStatusError(TEXT("Could not reach ElevenLabs API.")); return; } if (Resp->GetResponseCode() != 200) { - Pinned->SetStatusError(ParseAPIError( + P->SetStatusError(ParseAPIError( Resp->GetResponseCode(), Resp->GetContentAsString())); return; } - if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset = Pinned->GetEditedAsset()) + if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* A = P->GetEditedAsset()) { - Asset->Modify(); - Asset->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); + A->Modify(); + A->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); } - Pinned->SetStatusSuccess(TEXT("Agent updated successfully.")); + P->SetStatusSuccess(TEXT("Agent updated successfully.")); }); Request->ProcessRequest(); @@ -1287,46 +1275,16 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnFetchAgentClicked() } } - // 3. Emotion tool fragment - if (!Asset->EmotionToolPromptFragment.IsEmpty()) + // 3. Tool PromptFragments — strip each assigned tool's fragment + for (const auto& Tool : Asset->Tools) { - int32 Idx = Prompt.Find(Asset->EmotionToolPromptFragment, + if (!Tool || Tool->PromptFragment.IsEmpty()) continue; + int32 Idx = Prompt.Find(Tool->PromptFragment, ESearchCase::CaseSensitive); if (Idx != INDEX_NONE) { Prompt.LeftInline(Idx); - } - else - { - const FString EmotionMarker = TEXT("\n\n## Facial Expressions"); - int32 MarkerIdx = Prompt.Find(EmotionMarker, - ESearchCase::CaseSensitive); - if (MarkerIdx != INDEX_NONE) - { - Prompt.LeftInline(MarkerIdx); - } - } - } - - // 4. Action tool fragment (from ActionSet) - if (Asset->ActionSet && !Asset->ActionSet->ActionToolPromptFragment.IsEmpty()) - { - int32 Idx = Prompt.Find(Asset->ActionSet->ActionToolPromptFragment, - ESearchCase::CaseSensitive); - if (Idx != INDEX_NONE) - { - Prompt.LeftInline(Idx); - } - else - { - // Fallback: strip by marker - const FString ActionMarker = TEXT("\n\n## Physical Actions"); - int32 MarkerIdx = Prompt.Find(ActionMarker, - ESearchCase::CaseSensitive); - if (MarkerIdx != INDEX_NONE) - { - Prompt.LeftInline(MarkerIdx); - } + break; // First match truncates everything after } } @@ -1388,7 +1346,7 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnFetchAgentClicked() Asset->MaxTurns = MaxTurns; } - // expressive_mode (V3 Conversational) + // expressive_mode — legacy location (agent level), kept for backwards compat bool bExpressive = false; if ((*AgentObj)->TryGetBoolField(TEXT("expressive_mode"), bExpressive)) { @@ -1411,11 +1369,15 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnFetchAgentClicked() { Asset->TTSModelID = ModelID; - // Auto-detect Expressive Mode from V3 Conversational model - if (ModelID == TEXT("eleven_v3_conversational")) - { - Asset->bExpressiveMode = true; - } + // Auto-sync Expressive Mode from model selection + Asset->bExpressiveMode = (ModelID == TEXT("eleven_v3_conversational")); + } + + // expressive_mode from tts block (authoritative, overrides agent-level) + bool bTTSExpressive = false; + if ((*TTSObj)->TryGetBoolField(TEXT("expressive_mode"), bTTSExpressive)) + { + Asset->bExpressiveMode = bTTSExpressive; } double Stability = 0.5; @@ -1452,6 +1414,74 @@ void FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::OnFetchAgentClicked() } } + // Resolve tool_ids to Tool DataAssets via Asset Registry lookup + { + const TArray>* ToolIDsArray = nullptr; + if (!Root->TryGetArrayField(TEXT("use_tool_ids"), ToolIDsArray)) + { + Root->TryGetArrayField(TEXT("tool_ids"), ToolIDsArray); + } + + if (ToolIDsArray && ToolIDsArray->Num() > 0) + { + // Gather all tool IDs from the agent + TArray FetchedToolIDs; + for (const auto& Val : *ToolIDsArray) + { + FString TID; + if (Val->TryGetString(TID) && !TID.IsEmpty()) + { + FetchedToolIDs.Add(TID); + } + } + + // Search all Tool DataAssets in the project by ToolID + FAssetRegistryModule& ARModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + IAssetRegistry& AssetRegistry = ARModule.Get(); + + TArray AllToolAssets; + AssetRegistry.GetAssetsByClass( + UPS_AI_ConvAgent_Tool_ElevenLabs::StaticClass()->GetClassPathName(), + AllToolAssets, true); + + TArray ResolvedTools; + for (const FString& TID : FetchedToolIDs) + { + bool bFound = false; + for (const FAssetData& AD : AllToolAssets) + { + UPS_AI_ConvAgent_Tool_ElevenLabs* ToolAsset = + Cast(AD.GetAsset()); + if (ToolAsset && ToolAsset->ToolID == TID) + { + ResolvedTools.Add(ToolAsset); + bFound = true; + UE_LOG(LogPS_AI_AgentConfigEditor, Log, + TEXT(" -> Resolved tool_id '%s' -> '%s'"), + *TID, *ToolAsset->ToolName); + break; + } + } + if (!bFound) + { + UE_LOG(LogPS_AI_AgentConfigEditor, Warning, + TEXT(" -> tool_id '%s' not found in any Tool DataAsset"), *TID); + } + } + + // Update agent's Tools array with resolved assets + if (ResolvedTools.Num() > 0) + { + Asset->Tools.Empty(); + for (auto* T : ResolvedTools) + { + Asset->Tools.Add(T); + } + } + } + } + Asset->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); // Refresh Language combo (static list, instant) @@ -1615,14 +1645,14 @@ TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::Bu UE_LOG(LogPS_AI_AgentConfigEditor, Log, TEXT("BuildAgentPayload: CharacterPrompt=%d chars, bMultilingual=%d, bAutoLangInstr=%d, Language='%s', " - "LangFragment=%d chars, MultiFragment=%d chars, bEmotionTool=%d, bExpressiveMode=%d"), + "LangFragment=%d chars, MultiFragment=%d chars, Tools=%d, bExpressiveMode=%d"), Asset->CharacterPrompt.Len(), Asset->bMultilingual, Asset->bAutoLanguageInstruction, *Asset->Language, Asset->LanguagePromptFragment.Len(), Asset->MultilingualPromptFragment.Len(), - Asset->bIncludeEmotionTool, + Asset->Tools.Num(), Asset->bExpressiveMode); // Language handling: multilingual mode vs fixed-language mode. @@ -1653,22 +1683,16 @@ TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::Bu UE_LOG(LogPS_AI_AgentConfigEditor, Log, TEXT(" → Appended LanguagePromptFragment for '%s'"), *DisplayLang); } - // Append emotion tool instructions. - if (Asset->bIncludeEmotionTool && !Asset->EmotionToolPromptFragment.IsEmpty()) + // Append each assigned tool's PromptFragment. + for (const auto& Tool : Asset->Tools) { - FullPrompt += TEXT("\n\n"); - FullPrompt += Asset->EmotionToolPromptFragment; - UE_LOG(LogPS_AI_AgentConfigEditor, Log, TEXT(" → Appended EmotionToolPromptFragment")); - } - - // Append action tool instructions from ActionSet. - if (Asset->bIncludeActionTool && Asset->ActionSet - && Asset->ActionSet->Actions.Num() > 0 - && !Asset->ActionSet->ActionToolPromptFragment.IsEmpty()) - { - FullPrompt += TEXT("\n\n"); - FullPrompt += Asset->ActionSet->ActionToolPromptFragment; - UE_LOG(LogPS_AI_AgentConfigEditor, Log, TEXT(" → Appended ActionToolPromptFragment from ActionSet")); + if (Tool && !Tool->PromptFragment.IsEmpty()) + { + FullPrompt += TEXT("\n\n"); + FullPrompt += Tool->PromptFragment; + UE_LOG(LogPS_AI_AgentConfigEditor, Log, TEXT(" -> Appended PromptFragment from tool '%s'"), + *Tool->ToolName); + } } // Append expressive mode instructions (V3 Conversational audio tags). @@ -1689,26 +1713,19 @@ TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::Bu PromptObj->SetStringField(TEXT("llm"), Asset->LLMModel); } - // Build tools array: emotion tool + action tool (API path: conversation_config.agent.prompt.tools) - TArray> Tools; - if (Asset->bIncludeEmotionTool) + // Tools are standalone resources managed via /v1/convai/tools. + // Reference them by ID in prompt.tool_ids. + // Each tool's ToolID is stored on its own Tool DataAsset. + TArray> ToolIDs; + for (const auto& Tool : Asset->Tools) { - TSharedPtr EmotionTool = BuildEmotionToolDefinition(); - Tools.Add(MakeShareable(new FJsonValueObject(EmotionTool))); - } - if (Asset->bIncludeActionTool && Asset->ActionSet - && Asset->ActionSet->Actions.Num() > 0) - { - TSharedPtr ActionTool = BuildActionToolDefinition(Asset); - if (ActionTool) + if (Tool && !Tool->ToolID.IsEmpty()) { - Tools.Add(MakeShareable(new FJsonValueObject(ActionTool))); + ToolIDs.Add(MakeShareable(new FJsonValueString(Tool->ToolID))); } } - if (Tools.Num() > 0) - { - PromptObj->SetArrayField(TEXT("tools"), Tools); - } + // Always set the array (empty = clear all tools from agent) + PromptObj->SetArrayField(TEXT("tool_ids"), ToolIDs); // agent TSharedPtr AgentObj = MakeShareable(new FJsonObject()); @@ -1725,11 +1742,6 @@ TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::Bu { AgentObj->SetNumberField(TEXT("max_tokens"), Asset->MaxTurns); } - if (Asset->bExpressiveMode) - { - AgentObj->SetBoolField(TEXT("expressive_mode"), true); - } - // tts TSharedPtr TTSObj = MakeShareable(new FJsonObject()); if (!Asset->VoiceID.IsEmpty()) @@ -1737,24 +1749,17 @@ TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::Bu TTSObj->SetStringField(TEXT("voice_id"), Asset->VoiceID); } + // Expressive mode lives in the TTS block per the ElevenLabs API. + // The model dropdown auto-syncs bExpressiveMode (V3 ↔ expressive), + // so no forced override is needed here. + TTSObj->SetBoolField(TEXT("expressive_mode"), Asset->bExpressiveMode); + // Resolve TTS model. // Multilingual and non-English agents require a multilingual-capable model: // eleven_multilingual_v2, eleven_turbo_v2_5, eleven_flash_v2_5 // Monolingual models (e.g. eleven_monolingual_v1) only support English. FString ResolvedModelID = Asset->TTSModelID; - // Expressive mode requires V3 Conversational — override if needed. - if (Asset->bExpressiveMode) - { - if (ResolvedModelID != TEXT("eleven_v3_conversational")) - { - UE_LOG(LogPS_AI_AgentConfigEditor, Warning, - TEXT("Expressive mode: overriding TTS model '%s' → eleven_v3_conversational (required for audio tags)."), - *ResolvedModelID); - ResolvedModelID = TEXT("eleven_v3_conversational"); - } - } - auto IsMultilingualModel = [](const FString& ModelID) -> bool { return ModelID.Contains(TEXT("multilingual")) @@ -1825,108 +1830,3 @@ TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::Bu return Root; } -TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::BuildEmotionToolDefinition() -{ - // Build the set_emotion client tool definition. - // Parameters: emotion (enum), intensity (enum). - - // emotion parameter - TSharedPtr EmotionParam = MakeShareable(new FJsonObject()); - EmotionParam->SetStringField(TEXT("type"), TEXT("string")); - EmotionParam->SetStringField(TEXT("description"), TEXT("The emotion to display.")); - TArray> EmotionEnum; - for (const FString& E : {TEXT("joy"), TEXT("sadness"), TEXT("anger"), TEXT("surprise"), - TEXT("fear"), TEXT("disgust"), TEXT("neutral")}) - { - EmotionEnum.Add(MakeShareable(new FJsonValueString(E))); - } - EmotionParam->SetArrayField(TEXT("enum"), EmotionEnum); - - // intensity parameter - TSharedPtr IntensityParam = MakeShareable(new FJsonObject()); - IntensityParam->SetStringField(TEXT("type"), TEXT("string")); - IntensityParam->SetStringField(TEXT("description"), TEXT("The intensity of the emotion.")); - TArray> IntensityEnum; - for (const FString& I : {TEXT("low"), TEXT("medium"), TEXT("high")}) - { - IntensityEnum.Add(MakeShareable(new FJsonValueString(I))); - } - IntensityParam->SetArrayField(TEXT("enum"), IntensityEnum); - - // properties - TSharedPtr Properties = MakeShareable(new FJsonObject()); - Properties->SetObjectField(TEXT("emotion"), EmotionParam); - Properties->SetObjectField(TEXT("intensity"), IntensityParam); - - // required - TArray> Required; - Required.Add(MakeShareable(new FJsonValueString(TEXT("emotion")))); - Required.Add(MakeShareable(new FJsonValueString(TEXT("intensity")))); - - // parameters - TSharedPtr Parameters = MakeShareable(new FJsonObject()); - Parameters->SetStringField(TEXT("type"), TEXT("object")); - Parameters->SetObjectField(TEXT("properties"), Properties); - Parameters->SetArrayField(TEXT("required"), Required); - - // Tool definition - TSharedPtr Tool = MakeShareable(new FJsonObject()); - Tool->SetStringField(TEXT("type"), TEXT("client")); - Tool->SetStringField(TEXT("name"), TEXT("set_emotion")); - Tool->SetStringField(TEXT("description"), - TEXT("Set the character's facial expression emotion and intensity.")); - Tool->SetObjectField(TEXT("parameters"), Parameters); - - return Tool; -} - -TSharedPtr FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::BuildActionToolDefinition( - const UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset) -{ - if (!Asset || !Asset->ActionSet || Asset->ActionSet->Actions.Num() == 0) - return nullptr; - - // Build description with action list from the referenced ActionSet. - FString ParamDesc = TEXT("The action to perform. Available actions:"); - TArray> ActionEnum; - for (const auto& Action : Asset->ActionSet->Actions) - { - if (!Action.Name.IsEmpty()) - { - ActionEnum.Add(MakeShareable(new FJsonValueString(Action.Name))); - if (!Action.Description.IsEmpty()) - { - ParamDesc += FString::Printf(TEXT("\n- %s: %s"), - *Action.Name, *Action.Description); - } - } - } - if (ActionEnum.Num() == 0) return nullptr; - - // action parameter - TSharedPtr ActionParam = MakeShareable(new FJsonObject()); - ActionParam->SetStringField(TEXT("type"), TEXT("string")); - ActionParam->SetStringField(TEXT("description"), ParamDesc); - ActionParam->SetArrayField(TEXT("enum"), ActionEnum); - - // properties + required - TSharedPtr Properties = MakeShareable(new FJsonObject()); - Properties->SetObjectField(TEXT("action"), ActionParam); - TArray> Required; - Required.Add(MakeShareable(new FJsonValueString(TEXT("action")))); - - TSharedPtr Parameters = MakeShareable(new FJsonObject()); - Parameters->SetStringField(TEXT("type"), TEXT("object")); - Parameters->SetObjectField(TEXT("properties"), Properties); - Parameters->SetArrayField(TEXT("required"), Required); - - // Tool definition - TSharedPtr Tool = MakeShareable(new FJsonObject()); - Tool->SetStringField(TEXT("type"), TEXT("client")); - Tool->SetStringField(TEXT("name"), TEXT("perform_action")); - Tool->SetStringField(TEXT("description"), - TEXT("Trigger a physical action or reaction for the character.")); - Tool->SetObjectField(TEXT("parameters"), Parameters); - - return Tool; -} diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h index 9cafaa6..6752642 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h @@ -53,12 +53,9 @@ private: public: /** Build the full ElevenLabs API payload for any AgentConfig asset. - * Static so it can be reused from other customizations (e.g. ActionSet batch update). */ + * Static so it can be reused from other customizations (e.g. Tool batch agent update). */ static TSharedPtr BuildAgentPayloadForAsset( const class UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset); - static TSharedPtr BuildEmotionToolDefinition(); - static TSharedPtr BuildActionToolDefinition( - const class UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Asset); private: diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolCustomization_ElevenLabs.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolCustomization_ElevenLabs.cpp new file mode 100644 index 0000000..3f8a54a --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolCustomization_ElevenLabs.cpp @@ -0,0 +1,899 @@ +// Copyright ASTERION. All Rights Reserved. + +#include "PS_AI_ConvAgent_ToolCustomization_ElevenLabs.h" +#include "PS_AI_ConvAgent_Tool_ElevenLabs.h" +#include "PS_AI_ConvAgent_AgentConfig_ElevenLabs.h" +#include "PS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs.h" +#include "PS_AI_ConvAgent.h" + +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Text/STextBlock.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "HttpModule.h" +#include "Interfaces/IHttpRequest.h" +#include "Interfaces/IHttpResponse.h" +#include "Dom/JsonObject.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonWriter.h" +#include "Serialization/JsonSerializer.h" + +DEFINE_LOG_CATEGORY_STATIC(LogPS_AI_ToolEditor, Log, All); + +// ───────────────────────────────────────────────────────────────────────────── +// Factory +// ───────────────────────────────────────────────────────────────────────────── +TSharedRef FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::MakeInstance() +{ + return MakeShareable(new FPS_AI_ConvAgent_ToolCustomization_ElevenLabs()); +} + +// ───────────────────────────────────────────────────────────────────────────── +// CustomizeDetails +// ───────────────────────────────────────────────────────────────────────────── +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::CustomizeDetails( + IDetailLayoutBuilder& DetailBuilder) +{ + DetailBuilder.GetObjectsBeingCustomized(SelectedObjects); + + // ── Tool Management category ──────────────────────────────────────────── + IDetailCategoryBuilder& ToolCat = DetailBuilder.EditCategory( + TEXT("Tool Management"), + FText::FromString(TEXT("Tool Management")), + ECategoryPriority::Important); + + ToolCat.AddCustomRow(FText::FromString(TEXT("Tool Actions"))) + .WholeRowContent() + [ + SNew(SVerticalBox) + // Create / Update / Fetch buttons on one row + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0, 4) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 4, 0) + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Create Tool"))) + .ToolTipText(FText::FromString( + TEXT("POST a new standalone tool to ElevenLabs.\nPopulates ToolID on success."))) + .OnClicked_Lambda([this]() + { + OnCreateToolClicked(); + return FReply::Handled(); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 4, 0) + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Update Tool"))) + .ToolTipText(FText::FromString( + TEXT("PATCH the existing tool on ElevenLabs with current settings.\nRequires a ToolID."))) + .OnClicked_Lambda([this]() + { + OnUpdateToolClicked(); + return FReply::Handled(); + }) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Fetch Tool"))) + .ToolTipText(FText::FromString( + TEXT("GET the tool definition from ElevenLabs and populate fields.\nRequires a ToolID."))) + .OnClicked_Lambda([this]() + { + OnFetchToolClicked(); + return FReply::Handled(); + }) + ] + ] + // Update All Agents button + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0, 4) + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Update All Agents"))) + .ToolTipText(FText::FromString( + TEXT("Re-PATCH all AgentConfig assets that reference this tool.\n" + "Use after changing the PromptFragment to update agent system prompts."))) + .OnClicked_Lambda([this]() + { + OnUpdateAllAgentsClicked(); + return FReply::Handled(); + }) + ] + // Status text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0, 2) + [ + SAssignNew(StatusTextBlock, STextBlock) + .Text(FText::GetEmpty()) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ColorAndOpacity(FSlateColor(FLinearColor(0.3f, 0.7f, 1.0f))) + ] + ]; +} + +// ───────────────────────────────────────────────────────────────────────────── +// BuildToolPayload — Generate the ElevenLabs tool definition JSON +// ───────────────────────────────────────────────────────────────────────────── +TSharedPtr FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::BuildToolPayload( + const UPS_AI_ConvAgent_Tool_ElevenLabs* Tool) +{ + if (!Tool || Tool->ToolName.IsEmpty()) return nullptr; + + // ElevenLabs tool API format: + // { "tool_config": { "type": "client", "name": "...", "parameters": { ... } } } + // BuildToolPayload returns the inner object (name, description, parameters). + // The caller adds "type": "client" and wraps in "tool_config". + + TSharedPtr ToolObj = MakeShareable(new FJsonObject()); + ToolObj->SetStringField(TEXT("name"), Tool->ToolName); + ToolObj->SetStringField(TEXT("description"), + Tool->ToolDescription.IsEmpty() + ? FString::Printf(TEXT("Client tool: %s"), *Tool->ToolName) + : Tool->ToolDescription); + + // ── Build parameters (JSON Schema format) ────────────────────────────── + // ElevenLabs uses standard JSON Schema for tool parameters: + // "emotion": { "type": "string", "description": "...", "enum": [...] } + if (Tool->Parameters.Num() > 0) + { + TSharedPtr Properties = MakeShareable(new FJsonObject()); + TArray> Required; + + for (const auto& Param : Tool->Parameters) + { + if (Param.Name.IsEmpty()) continue; + + // Resolve type string + FString TypeStr; + switch (Param.Type) + { + case EPS_AI_ConvAgent_ToolParamType::String: TypeStr = TEXT("string"); break; + case EPS_AI_ConvAgent_ToolParamType::Integer: TypeStr = TEXT("integer"); break; + case EPS_AI_ConvAgent_ToolParamType::Number: TypeStr = TEXT("number"); break; + case EPS_AI_ConvAgent_ToolParamType::Boolean: TypeStr = TEXT("boolean"); break; + default: TypeStr = TEXT("string"); break; + } + + // JSON Schema property: { "type": "string", "description": "...", "enum": [...] } + TSharedPtr ParamObj = MakeShareable(new FJsonObject()); + ParamObj->SetStringField(TEXT("type"), TypeStr); + + if (!Param.Description.IsEmpty()) + { + ParamObj->SetStringField(TEXT("description"), Param.Description); + } + + // Enum values (optional) + if (Param.EnumValues.Num() > 0) + { + TArray> EnumArr; + for (const FString& Val : Param.EnumValues) + { + if (!Val.IsEmpty()) + { + EnumArr.Add(MakeShareable(new FJsonValueString(Val))); + } + } + if (EnumArr.Num() > 0) + { + ParamObj->SetArrayField(TEXT("enum"), EnumArr); + } + } + + Properties->SetObjectField(Param.Name, ParamObj); + + if (Param.bRequired) + { + Required.Add(MakeShareable(new FJsonValueString(Param.Name))); + } + } + + TSharedPtr ParametersObj = MakeShareable(new FJsonObject()); + ParametersObj->SetStringField(TEXT("type"), TEXT("object")); + ParametersObj->SetObjectField(TEXT("properties"), Properties); + if (Required.Num() > 0) + { + ParametersObj->SetArrayField(TEXT("required"), Required); + } + + ToolObj->SetObjectField(TEXT("parameters"), ParametersObj); + } + + return ToolObj; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Create Tool +// ───────────────────────────────────────────────────────────────────────────── +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::OnCreateToolClicked() +{ + UPS_AI_ConvAgent_Tool_ElevenLabs* Asset = GetEditedAsset(); + if (!Asset) + { + SetStatusError(TEXT("No tool asset selected.")); + return; + } + if (Asset->ToolName.IsEmpty()) + { + SetStatusError(TEXT("ToolName is required.")); + return; + } + if (!Asset->ToolID.IsEmpty()) + { + SetStatusError(TEXT("Tool already has an ID. Use Update instead.")); + return; + } + + const FString APIKey = GetAPIKey(); + if (APIKey.IsEmpty()) + { + SetStatusError(TEXT("API Key not set in Project Settings.")); + return; + } + + TSharedPtr ToolPayload = BuildToolPayload(Asset); + if (!ToolPayload) + { + SetStatusError(TEXT("Failed to build tool payload.")); + return; + } + + SetStatusText(TEXT("Creating tool...")); + + // Wrap as: { "tool_config": { "type": "client", ...payload... } } + // The "type" field is a discriminator inside tool_config (not a nested key). + ToolPayload->SetStringField(TEXT("type"), TEXT("client")); + TSharedPtr Wrapped = MakeShareable(new FJsonObject()); + Wrapped->SetObjectField(TEXT("tool_config"), ToolPayload); + + FString PayloadStr; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&PayloadStr); + FJsonSerializer::Serialize(Wrapped.ToSharedRef(), Writer); + + UE_LOG(LogPS_AI_ToolEditor, Log, TEXT("Creating tool '%s': POST /v1/convai/tools"), + *Asset->ToolName); + UE_LOG(LogPS_AI_ToolEditor, Log, TEXT(" Payload: %s"), *PayloadStr); + + TSharedRef Request = FHttpModule::Get().CreateRequest(); + Request->SetURL(TEXT("https://api.elevenlabs.io/v1/convai/tools")); + Request->SetVerb(TEXT("POST")); + Request->SetHeader(TEXT("xi-api-key"), APIKey); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + Request->SetContentAsString(PayloadStr); + + TWeakPtr WeakSelf = + StaticCastSharedRef(this->AsShared()); + + Request->OnProcessRequestComplete().BindLambda( + [WeakSelf](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) + { + auto Pinned = WeakSelf.Pin(); + if (!Pinned.IsValid()) return; + + if (!bConnected || !Resp.IsValid()) + { + Pinned->SetStatusError(TEXT("Could not reach ElevenLabs API.")); + return; + } + + const int32 Code = Resp->GetResponseCode(); + if (Code != 200 && Code != 201) + { + Pinned->SetStatusError(ParseAPIError(Code, Resp->GetContentAsString())); + return; + } + + // Parse response for tool_id + TSharedPtr Root; + if (!FJsonSerializer::Deserialize( + TJsonReaderFactory<>::Create(Resp->GetContentAsString()), Root) || !Root.IsValid()) + { + Pinned->SetStatusError(TEXT("Failed to parse response.")); + return; + } + + FString NewToolID; + if (!Root->TryGetStringField(TEXT("tool_id"), NewToolID)) + { + Root->TryGetStringField(TEXT("id"), NewToolID); + } + + if (NewToolID.IsEmpty()) + { + Pinned->SetStatusError(TEXT("No tool_id in response.")); + return; + } + + if (UPS_AI_ConvAgent_Tool_ElevenLabs* A = Pinned->GetEditedAsset()) + { + A->Modify(); + A->ToolID = NewToolID; + A->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); + A->PostEditChange(); + } + + Pinned->SetStatusSuccess(FString::Printf(TEXT("Tool created: %s"), *NewToolID)); + }); + + Request->ProcessRequest(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Update Tool +// ───────────────────────────────────────────────────────────────────────────── +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::OnUpdateToolClicked() +{ + UPS_AI_ConvAgent_Tool_ElevenLabs* Asset = GetEditedAsset(); + if (!Asset) + { + SetStatusError(TEXT("No tool asset selected.")); + return; + } + if (Asset->ToolID.IsEmpty()) + { + SetStatusError(TEXT("No ToolID set. Use Create first.")); + return; + } + + const FString APIKey = GetAPIKey(); + if (APIKey.IsEmpty()) + { + SetStatusError(TEXT("API Key not set in Project Settings.")); + return; + } + + TSharedPtr ToolPayload = BuildToolPayload(Asset); + if (!ToolPayload) + { + SetStatusError(TEXT("Failed to build tool payload.")); + return; + } + + SetStatusText(TEXT("Updating tool...")); + + // Add discriminator field and wrap: { "tool_config": { "type": "client", ... } } + ToolPayload->SetStringField(TEXT("type"), TEXT("client")); + TSharedPtr Wrapped = MakeShareable(new FJsonObject()); + Wrapped->SetObjectField(TEXT("tool_config"), ToolPayload); + + FString PayloadStr; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&PayloadStr); + FJsonSerializer::Serialize(Wrapped.ToSharedRef(), Writer); + + const FString URL = FString::Printf( + TEXT("https://api.elevenlabs.io/v1/convai/tools/%s"), *Asset->ToolID); + + UE_LOG(LogPS_AI_ToolEditor, Log, TEXT("Updating tool '%s': PATCH %s"), + *Asset->ToolName, *URL); + UE_LOG(LogPS_AI_ToolEditor, Log, TEXT(" Payload: %s"), *PayloadStr); + + TSharedRef Request = FHttpModule::Get().CreateRequest(); + Request->SetURL(URL); + Request->SetVerb(TEXT("PATCH")); + Request->SetHeader(TEXT("xi-api-key"), APIKey); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + Request->SetContentAsString(PayloadStr); + + TWeakPtr WeakSelf = + StaticCastSharedRef(this->AsShared()); + + Request->OnProcessRequestComplete().BindLambda( + [WeakSelf](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) + { + auto Pinned = WeakSelf.Pin(); + if (!Pinned.IsValid()) return; + + if (!bConnected || !Resp.IsValid()) + { + Pinned->SetStatusError(TEXT("Could not reach ElevenLabs API.")); + return; + } + + if (Resp->GetResponseCode() != 200) + { + Pinned->SetStatusError(ParseAPIError( + Resp->GetResponseCode(), Resp->GetContentAsString())); + return; + } + + if (UPS_AI_ConvAgent_Tool_ElevenLabs* A = Pinned->GetEditedAsset()) + { + A->Modify(); + A->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); + } + + Pinned->SetStatusSuccess(TEXT("Tool updated successfully.")); + }); + + Request->ProcessRequest(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Fetch Tool +// ───────────────────────────────────────────────────────────────────────────── +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::OnFetchToolClicked() +{ + UPS_AI_ConvAgent_Tool_ElevenLabs* Asset = GetEditedAsset(); + if (!Asset) + { + SetStatusError(TEXT("No tool asset selected.")); + return; + } + if (Asset->ToolID.IsEmpty()) + { + SetStatusError(TEXT("No ToolID set. Enter one first or use Create.")); + return; + } + + const FString APIKey = GetAPIKey(); + if (APIKey.IsEmpty()) + { + SetStatusError(TEXT("API Key not set in Project Settings.")); + return; + } + + SetStatusText(TEXT("Fetching tool...")); + + const FString URL = FString::Printf( + TEXT("https://api.elevenlabs.io/v1/convai/tools/%s"), *Asset->ToolID); + + TSharedRef Request = FHttpModule::Get().CreateRequest(); + Request->SetURL(URL); + Request->SetVerb(TEXT("GET")); + Request->SetHeader(TEXT("xi-api-key"), APIKey); + Request->SetHeader(TEXT("Accept"), TEXT("application/json")); + + TWeakPtr WeakSelf = + StaticCastSharedRef(this->AsShared()); + + Request->OnProcessRequestComplete().BindLambda( + [WeakSelf](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) + { + auto Pinned = WeakSelf.Pin(); + if (!Pinned.IsValid()) return; + + if (!bConnected || !Resp.IsValid()) + { + Pinned->SetStatusError(TEXT("Could not reach ElevenLabs API.")); + return; + } + + if (Resp->GetResponseCode() != 200) + { + Pinned->SetStatusError(ParseAPIError( + Resp->GetResponseCode(), Resp->GetContentAsString())); + return; + } + + TSharedPtr Root; + if (!FJsonSerializer::Deserialize( + TJsonReaderFactory<>::Create(Resp->GetContentAsString()), Root) || !Root.IsValid()) + { + Pinned->SetStatusError(TEXT("Failed to parse response.")); + return; + } + + UPS_AI_ConvAgent_Tool_ElevenLabs* Asset = Pinned->GetEditedAsset(); + if (!Asset) return; + + Asset->Modify(); + + // The tool definition may be at root, inside "tool_config", + // or inside "tool_config.client" (ElevenLabs nested format). + TSharedPtr ToolDef = Root; + { + const TSharedPtr* ToolConfig = nullptr; + if (Root->TryGetObjectField(TEXT("tool_config"), ToolConfig)) + { + const TSharedPtr* ClientObj = nullptr; + if ((*ToolConfig)->TryGetObjectField(TEXT("client"), ClientObj)) + { + ToolDef = *ClientObj; + } + else + { + ToolDef = *ToolConfig; + } + } + } + + // Populate from response + FString Name; + if (ToolDef->TryGetStringField(TEXT("name"), Name)) + { + Asset->ToolName = Name; + } + + FString Description; + if (ToolDef->TryGetStringField(TEXT("description"), Description)) + { + Asset->ToolDescription = Description; + } + + // Parse parameters from the tool definition + const TSharedPtr* Params = nullptr; + if (ToolDef->TryGetObjectField(TEXT("parameters"), Params)) + { + const TSharedPtr* Props = nullptr; + if ((*Params)->TryGetObjectField(TEXT("properties"), Props)) + { + // Get required array for bRequired flag + TSet RequiredSet; + const TArray>* RequiredArr = nullptr; + if ((*Params)->TryGetArrayField(TEXT("required"), RequiredArr)) + { + for (const auto& Val : *RequiredArr) + { + FString ReqName; + if (Val->TryGetString(ReqName)) + { + RequiredSet.Add(ReqName); + } + } + } + + Asset->Parameters.Empty(); + + // Iterate all properties. + // ElevenLabs format: type is a KEY inside the property: + // "emotion": { "string": { "description": "...", "enum": [...] } } + // Also handle JSON Schema format as fallback: + // "emotion": { "type": "string", "description": "..." } + for (const auto& Pair : (*Props)->Values) + { + const TSharedPtr* PropObj = nullptr; + if (!Pair.Value->TryGetObject(PropObj)) continue; + + FPS_AI_ConvAgent_ToolParameter_ElevenLabs Param; + Param.Name = Pair.Key; + Param.bRequired = RequiredSet.Contains(Pair.Key); + + // Try ElevenLabs format: type-as-key + static const TArray> TypeKeys = { + {TEXT("string"), EPS_AI_ConvAgent_ToolParamType::String}, + {TEXT("integer"), EPS_AI_ConvAgent_ToolParamType::Integer}, + {TEXT("number"), EPS_AI_ConvAgent_ToolParamType::Number}, + {TEXT("boolean"), EPS_AI_ConvAgent_ToolParamType::Boolean}, + }; + + bool bFoundTypeKey = false; + for (const auto& TK : TypeKeys) + { + const TSharedPtr* TypeInner = nullptr; + if ((*PropObj)->TryGetObjectField(TK.Key, TypeInner)) + { + Param.Type = TK.Value; + (*TypeInner)->TryGetStringField(TEXT("description"), Param.Description); + + const TArray>* EnumArr = nullptr; + if ((*TypeInner)->TryGetArrayField(TEXT("enum"), EnumArr)) + { + for (const auto& EVal : *EnumArr) + { + FString EnumStr; + if (EVal->TryGetString(EnumStr)) + { + Param.EnumValues.Add(EnumStr); + } + } + } + bFoundTypeKey = true; + break; + } + } + + // Fallback: JSON Schema format + if (!bFoundTypeKey) + { + FString TypeStr; + if ((*PropObj)->TryGetStringField(TEXT("type"), TypeStr)) + { + if (TypeStr == TEXT("integer")) + Param.Type = EPS_AI_ConvAgent_ToolParamType::Integer; + else if (TypeStr == TEXT("number")) + Param.Type = EPS_AI_ConvAgent_ToolParamType::Number; + else if (TypeStr == TEXT("boolean")) + Param.Type = EPS_AI_ConvAgent_ToolParamType::Boolean; + else + Param.Type = EPS_AI_ConvAgent_ToolParamType::String; + } + (*PropObj)->TryGetStringField(TEXT("description"), Param.Description); + + const TArray>* EnumArr = nullptr; + if ((*PropObj)->TryGetArrayField(TEXT("enum"), EnumArr)) + { + for (const auto& EVal : *EnumArr) + { + FString EnumStr; + if (EVal->TryGetString(EnumStr)) + { + Param.EnumValues.Add(EnumStr); + } + } + } + } + + Asset->Parameters.Add(Param); + } + } + } + + Asset->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); + Asset->PostEditChange(); + + Pinned->SetStatusSuccess(FString::Printf(TEXT("Tool fetched: %s (%s)"), + *Asset->ToolName, *Asset->ToolID)); + }); + + Request->ProcessRequest(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Update All Agents — re-PATCH agents that reference this tool +// ───────────────────────────────────────────────────────────────────────────── +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::OnUpdateAllAgentsClicked() +{ + const UPS_AI_ConvAgent_Tool_ElevenLabs* ToolAsset = GetEditedAsset(); + if (!ToolAsset) + { + SetStatusError(TEXT("No tool asset selected.")); + return; + } + + const FString APIKey = GetAPIKey(); + if (APIKey.IsEmpty()) + { + SetStatusError(TEXT("API Key not set in Project Settings > PS AI ConvAgent - ElevenLabs.")); + return; + } + + // ── Scan all AgentConfig assets that reference this tool ───────────────── + FAssetRegistryModule& ARModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + IAssetRegistry& AssetRegistry = ARModule.Get(); + + TArray AllAgentConfigs; + AssetRegistry.GetAssetsByClass( + UPS_AI_ConvAgent_AgentConfig_ElevenLabs::StaticClass()->GetClassPathName(), + AllAgentConfigs, true); + + TArray MatchingConfigs; + for (const FAssetData& AD : AllAgentConfigs) + { + UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Config = + Cast(AD.GetAsset()); + if (!Config) continue; + if (Config->AgentID.IsEmpty()) continue; + + // Check if this agent's Tools array contains our tool + bool bReferencesTool = false; + for (const auto& T : Config->Tools) + { + if (T == ToolAsset) + { + bReferencesTool = true; + break; + } + } + if (bReferencesTool) + { + MatchingConfigs.Add(Config); + } + } + + if (MatchingConfigs.Num() == 0) + { + SetStatusError(TEXT("No AgentConfig assets reference this tool (with AgentID set).")); + return; + } + + SetStatusText(FString::Printf(TEXT("Updating %d agent(s)..."), MatchingConfigs.Num())); + + // ── Shared counter for async completion tracking ───────────────────────── + struct FBatchState + { + int32 Total = 0; + FThreadSafeCounter Succeeded; + FThreadSafeCounter Failed; + TArray Errors; + FCriticalSection ErrorLock; + }; + TSharedPtr State = MakeShareable(new FBatchState()); + State->Total = MatchingConfigs.Num(); + + TWeakPtr WeakSelf = + StaticCastSharedRef(this->AsShared()); + + for (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Config : MatchingConfigs) + { + TWeakObjectPtr WeakConfig(Config); + FString AgentName = Config->AgentName.IsEmpty() ? Config->AgentID : Config->AgentName; + FString ConfigAgentID = Config->AgentID; + + UE_LOG(LogPS_AI_ToolEditor, Log, + TEXT(" -> Updating agent '%s' (ID: %s)"), *AgentName, *ConfigAgentID); + + // Build payload and PATCH agent + TSharedPtr Payload = + FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::BuildAgentPayloadForAsset(Config); + + FString PayloadStr; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&PayloadStr); + FJsonSerializer::Serialize(Payload.ToSharedRef(), Writer); + + const FString URL = FString::Printf( + TEXT("https://api.elevenlabs.io/v1/convai/agents/%s"), *ConfigAgentID); + + TSharedRef Request = FHttpModule::Get().CreateRequest(); + Request->SetURL(URL); + Request->SetVerb(TEXT("PATCH")); + Request->SetHeader(TEXT("xi-api-key"), APIKey); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + Request->SetContentAsString(PayloadStr); + + Request->OnProcessRequestComplete().BindLambda( + [WeakSelf, State, WeakConfig, AgentName] + (FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bConnected) + { + bool bSuccess = false; + FString ErrorMsg; + + if (!bConnected || !Resp.IsValid()) + { + ErrorMsg = FString::Printf(TEXT("%s: connection failed"), *AgentName); + } + else if (Resp->GetResponseCode() != 200) + { + ErrorMsg = FString::Printf(TEXT("%s: HTTP %d"), + *AgentName, Resp->GetResponseCode()); + } + else + { + bSuccess = true; + if (UPS_AI_ConvAgent_AgentConfig_ElevenLabs* Cfg = WeakConfig.Get()) + { + Cfg->Modify(); + Cfg->LastSyncTimestamp = FDateTime::UtcNow().ToIso8601(); + } + } + + if (bSuccess) + { + State->Succeeded.Increment(); + } + else + { + State->Failed.Increment(); + FScopeLock Lock(&State->ErrorLock); + State->Errors.Add(ErrorMsg); + } + + const int32 Done = State->Succeeded.GetValue() + State->Failed.GetValue(); + if (Done >= State->Total) + { + auto Pinned = WeakSelf.Pin(); + if (!Pinned.IsValid()) return; + + if (State->Failed.GetValue() == 0) + { + Pinned->SetStatusSuccess(FString::Printf( + TEXT("Updated %d/%d agents successfully."), + State->Succeeded.GetValue(), State->Total)); + } + else + { + FString AllErrors; + { + FScopeLock Lock(&State->ErrorLock); + AllErrors = FString::Join(State->Errors, TEXT(", ")); + } + Pinned->SetStatusError(FString::Printf( + TEXT("Updated %d/%d agents. Failures: %s"), + State->Succeeded.GetValue(), State->Total, *AllErrors)); + } + } + }); + + Request->ProcessRequest(); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── +FString FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::GetAPIKey() const +{ + if (FPS_AI_ConvAgentModule::IsAvailable()) + { + if (const UPS_AI_ConvAgent_Settings_ElevenLabs* Settings = + FPS_AI_ConvAgentModule::Get().GetSettings()) + { + return Settings->API_Key; + } + } + return FString(); +} + +UPS_AI_ConvAgent_Tool_ElevenLabs* FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::GetEditedAsset() const +{ + for (const TWeakObjectPtr& Obj : SelectedObjects) + { + if (UPS_AI_ConvAgent_Tool_ElevenLabs* Asset = + Cast(Obj.Get())) + { + return Asset; + } + } + return nullptr; +} + +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::SetStatusText(const FString& Text) +{ + UE_LOG(LogPS_AI_ToolEditor, Log, TEXT("%s"), *Text); + if (StatusTextBlock.IsValid()) + { + StatusTextBlock->SetText(FText::FromString(Text)); + StatusTextBlock->SetColorAndOpacity(FSlateColor(FLinearColor(0.3f, 0.7f, 1.0f))); + } +} + +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::SetStatusError(const FString& Text) +{ + UE_LOG(LogPS_AI_ToolEditor, Error, TEXT("%s"), *Text); + if (StatusTextBlock.IsValid()) + { + StatusTextBlock->SetText(FText::FromString(Text)); + StatusTextBlock->SetColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.25f, 0.25f))); + } +} + +void FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::SetStatusSuccess(const FString& Text) +{ + UE_LOG(LogPS_AI_ToolEditor, Log, TEXT("%s"), *Text); + if (StatusTextBlock.IsValid()) + { + StatusTextBlock->SetText(FText::FromString(Text)); + StatusTextBlock->SetColorAndOpacity(FSlateColor(FLinearColor(0.2f, 0.9f, 0.3f))); + } +} + +FString FPS_AI_ConvAgent_ToolCustomization_ElevenLabs::ParseAPIError( + int32 HttpCode, const FString& ResponseBody) +{ + TSharedPtr Root; + if (FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(ResponseBody), Root) && Root.IsValid()) + { + const TSharedPtr* DetailObj = nullptr; + if (Root->TryGetObjectField(TEXT("detail"), DetailObj)) + { + FString Message; + if ((*DetailObj)->TryGetStringField(TEXT("message"), Message)) + { + return FString::Printf(TEXT("HTTP %d: %s"), HttpCode, *Message); + } + } + + FString DetailStr; + if (Root->TryGetStringField(TEXT("detail"), DetailStr)) + { + return FString::Printf(TEXT("HTTP %d: %s"), HttpCode, *DetailStr); + } + } + + return FString::Printf(TEXT("HTTP %d: %s"), HttpCode, *ResponseBody.Left(200)); +} diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolCustomization_ElevenLabs.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolCustomization_ElevenLabs.h new file mode 100644 index 0000000..7007a93 --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolCustomization_ElevenLabs.h @@ -0,0 +1,60 @@ +// Copyright ASTERION. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "IDetailCustomization.h" + +class IDetailLayoutBuilder; + +/** + * Detail Customization for UPS_AI_ConvAgent_Tool_ElevenLabs data assets. + * + * Provides: + * - Tool Management: "Create Tool" / "Update Tool" / "Fetch Tool" buttons + * Manages standalone tools on ElevenLabs via /v1/convai/tools API. + * - Agent Sync: "Update All Agents" button + * Re-PATCHes all AgentConfig assets that reference this tool + * (needed when PromptFragment changes, since it's baked into agent prompts). + */ +class FPS_AI_ConvAgent_ToolCustomization_ElevenLabs : public IDetailCustomization +{ +public: + static TSharedRef MakeInstance(); + + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + +private: + // ── Tool API ──────────────────────────────────────────────────────────── + void OnCreateToolClicked(); + void OnUpdateToolClicked(); + void OnFetchToolClicked(); + + // ── Agent Sync ────────────────────────────────────────────────────────── + void OnUpdateAllAgentsClicked(); + + // ── Helpers ───────────────────────────────────────────────────────────── + FString GetAPIKey() const; + + /** Retrieve the Tool data asset being edited (first selected object). */ + class UPS_AI_ConvAgent_Tool_ElevenLabs* GetEditedAsset() const; + + /** Build the ElevenLabs tool definition JSON from a Tool data asset. + * Returns the inner definition — caller wraps in {"tool_config": ...} for POST/PATCH. + * Handles set_emotion (hardcoded params), action-type tools (Actions array), + * and generic tools (no parameters). */ + static TSharedPtr BuildToolPayload( + const class UPS_AI_ConvAgent_Tool_ElevenLabs* Tool); + + /** Display a status message. Color: red for errors, green for success, cyan for info. */ + void SetStatusText(const FString& Text); + void SetStatusError(const FString& Text); + void SetStatusSuccess(const FString& Text); + + /** Parse ElevenLabs API error JSON and return a human-readable message. */ + static FString ParseAPIError(int32 HttpCode, const FString& ResponseBody); + + // ── Cached state ──────────────────────────────────────────────────────── + TArray> SelectedObjects; + TSharedPtr StatusTextBlock; +}; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolFactory_ElevenLabs.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolFactory_ElevenLabs.cpp new file mode 100644 index 0000000..06edf4f --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolFactory_ElevenLabs.cpp @@ -0,0 +1,29 @@ +// Copyright ASTERION. All Rights Reserved. + +#include "PS_AI_ConvAgent_ToolFactory_ElevenLabs.h" +#include "PS_AI_ConvAgent_Tool_ElevenLabs.h" +#include "AssetTypeCategories.h" + +UPS_AI_ConvAgent_ToolFactory_ElevenLabs::UPS_AI_ConvAgent_ToolFactory_ElevenLabs() +{ + SupportedClass = UPS_AI_ConvAgent_Tool_ElevenLabs::StaticClass(); + bCreateNew = true; + bEditAfterNew = true; +} + +UObject* UPS_AI_ConvAgent_ToolFactory_ElevenLabs::FactoryCreateNew( + UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, + UObject* Context, FFeedbackContext* Warn) +{ + return NewObject(InParent, Class, Name, Flags); +} + +FText UPS_AI_ConvAgent_ToolFactory_ElevenLabs::GetDisplayName() const +{ + return FText::FromString(TEXT("PS AI ConvAgent Tool (ElevenLabs)")); +} + +uint32 UPS_AI_ConvAgent_ToolFactory_ElevenLabs::GetMenuCategories() const +{ + return EAssetTypeCategories::Misc; +} diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolFactory_ElevenLabs.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolFactory_ElevenLabs.h new file mode 100644 index 0000000..53a9681 --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgentEditor/Private/PS_AI_ConvAgent_ToolFactory_ElevenLabs.h @@ -0,0 +1,27 @@ +// Copyright ASTERION. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "PS_AI_ConvAgent_ToolFactory_ElevenLabs.generated.h" + +/** + * Factory that lets users create PS_AI_ConvAgent_Tool_ElevenLabs assets + * directly from the Content Browser (right-click > Miscellaneous). + */ +UCLASS() +class UPS_AI_ConvAgent_ToolFactory_ElevenLabs : public UFactory +{ + GENERATED_BODY() + +public: + UPS_AI_ConvAgent_ToolFactory_ElevenLabs(); + + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, + FName Name, EObjectFlags Flags, UObject* Context, + FFeedbackContext* Warn) override; + + virtual FText GetDisplayName() const override; + virtual uint32 GetMenuCategories() const override; +}; diff --git a/tools.txt b/tools.txt new file mode 100644 index 0000000..1fd89eb --- /dev/null +++ b/tools.txt @@ -0,0 +1,26 @@ + + +## Facial Expressions +You have a set_emotion tool to control your facial expression. Use it whenever the emotional context changes: +- Call set_emotion with emotion="joy" when happy, laughing, or excited +- Call set_emotion with emotion="sadness" when empathetic or discussing sad topics +- Call set_emotion with emotion="anger" when frustrated or discussing injustice +- Call set_emotion with emotion="surprise" when reacting to unexpected information +- Call set_emotion with emotion="fear" when discussing scary or worrying topics +- Call set_emotion with emotion="disgust" when reacting to unpleasant things +- Call set_emotion with emotion="neutral" to return to a calm expression + +Use intensity to match the strength of the emotion: +- "low" for subtle hints (slight smile, mild concern) +- "medium" for normal expression (default) +- "high" for strong reactions (big laugh, deep sadness, shock) + +Always return to neutral when the emotional moment passes. + + +## Réactions physiques (perform_action) +Tu as un couteau sur toi. Tu es prêt à t'en servir si tu te sens menacé. +- Le flic te demande tes papiers ou de t'identifier → perform_action draw_weapon immédiatement +- Le flic essaie de te fouiller ou te touche → perform_action draw_weapon immédiatement +- Le flic essaie de vous séparer toi et ton pote → perform_action flee +- Si le flic sort son arme en premier → perform_action raise_hands \ No newline at end of file